1. 程式人生 > >服務化改造實踐 | 如何在 Dubbo 中支持 REST

服務化改造實踐 | 如何在 Dubbo 中支持 REST

parameter 而不是 odin 方法 borde rest 接口編程 rem 默認

什麽是 REST
REST 是 Roy Thomas Fielding [1] 在 2000 年他的博士論文 [2] “架構風格以及基於網絡的軟件架構設計” 中提出來的一個概念。REST 是 RESTransfer 的縮寫,翻譯過來就是 “表現層狀態轉化”。REST 就是 Roy 在這篇論文中提出的面向互聯網的軟件所應當具備的架構風格。

按照 REpresentational State Transfer 的字面意思,可以把應用看成是一個虛擬的狀態機,軟件提供的不是服務而是一系列的資源統一的操作**來訪問,而返回的結果代表了資源狀態的一次躍遷。REST 是一種架構風格,如果一個軟件架構符合 REST 風格,就可以稱之為 RESTful 架構。這個架構應當具備以下一些設計上的約束:資源具有唯一標示、資源之間有關聯關系、使用標準的方式來訪問、資源有多種表現形式、無狀態交互。

舉例來說,一個簡單的靜態 HTML 頁面的網站就很好的符合了 RESTful 架構風格。訪問 http://acme.com/accounts 返回一個包含所有賬號的頁面,選取其中一個鏈接 http://acme.com/accounts/1 又會返回包含用戶 1 的詳細信息。爬蟲軟件在這種場景下工作的很好,當知道了某個網站的首頁地址後,可以自舉發現這個網站上所有關聯的網頁。更重要的是,這種訪問形式不依賴網站提供的任何客戶端,而是僅僅通過 HTTP 標準的訪問方式完成的。可以說,HTML 這種超媒體文檔的組織形式就是資源的表現層狀態遷移的一種形式。

對於一個提供服務的動態網站來說,可以按照類似的思路將其 RESTful 化:

GET http://acme.com/accounts 返回所有賬號信息
POST http://acme.com/accounts 創建一個新的賬號
GET http://acme.com/accounts/1 返回賬號 1 的信息
DELETE http://acme.com/accounts/1 刪除賬號 1
PUT http://acme.com/accounts/1 更新賬號 1 信息
其中的思路是利用 HTTP 協議的標準方法 POST、DELETE、PUT、GET 來表達對於一個資源的增刪改查 (CRUD) 操作,利用 URL 來表示一個資源的唯一標識。對資源訪問的錯誤碼也復用 HTTP 協議的狀態碼。返回結果通常由 json 或 XML 來表示,如果其中包換了對關聯資源的訪問方式 (所謂的表現層狀態遷移) ,這種類型的 RESTful 應用可以進一步的稱之為 hypermedia as the engine of application state (HATEOAS) 應用 [3]。

技術分享圖片

source: https://www.nginx.com/wp-content/uploads/2016/04/micro-image.png

這裏需要註意的是,按照 Roy 的定義,RESTful 架構風格與 HTTP 協議並沒有什麽強關聯關系。但是,基於 HTTP 的 RESTful 架構風格是實現起來最自然,也是目前使用最廣泛的一種實現,我們稱之為 RESTful HTTP。同樣的,在下文中將會專註在 HTTP 的場景下介紹如何在 Dubbo 框架中將服務暴露成 Restful 架構。

在 Dubbo 中使用 REST
背景
隨著微服務的流行以及多語言互操作訴求日益增多,在 Dubbo 中暴露 REST 服務變成了一個不容忽視的訴求。為了在 Dubbo 中暴露 REST 服務,通常有兩種做法,一種是直接依賴 Sprng REST 或者其他 REST 框架來直接暴露,另一種是通過 Dubbo 框架內置的 REST 能力暴露。兩種做法各有優缺點,主要體現在前者與微服務體系中的服務發現組件能夠更好的工作,而後者可以無縫的享受到 Dubbo 體系中的服務發現以及服務治理的能力。本文關註的是如何使用後者來暴露 REST 服務。

自 2.6.0 開始,Dubbo 合並了當當網捐獻的 DubboX [4] 中的主要特性,其中就包括了基於 RESTeasy 3.0.19.Final 的 REST 支持,具備 JAXRS 2.0 規範中所有的能力。

基本用法
在以下的例子中,展示了如何通過最傳統的 Spring XML 配置的方式來快速的暴露和調用一個 REST 服務。其中底層的 server 使用的是 netty,服務註冊發現基於 Zookeeper。

註:本章節討論的示例可以通過 https://github.com/beiwei30/dubbo-rest-samples/tree/master/basic 來獲得

  1. Maven 依賴
    首先需要在項目中引入 dubbo all-in-one 的依賴以及 RESTEasy 相關的必要依賴。因為在本例中使用 Zookeeper 作為服務發現,還需要引入 Zookeeper client 相關的依賴。為了方便使用,第三方的依賴可以通過框架提供的 BOM 文件 dubbo-dependencies-bom 來引入。

<properties>
<dubbo.version>2.6.5</dubbo.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo-dependencies-bom</artifactId>
            <version>${dubbo.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo</artifactId>
        <version>${dubbo.version}</version>
    </dependency>

    <!-- REST support dependencies -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxrs</artifactId>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-netty4</artifactId>
    </dependency>

    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jackson-provider</artifactId>
    </dependency>

    <dependency>
        <groupId>org.jboss.resteasy</groupId>
        <artifactId>resteasy-jaxb-provider</artifactId>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>

    <!-- zookeeper client dependency -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
    </dependency>
</dependencies>
  1. 定義服務接口
    定義一個服務接口 UserService,該接口提供兩個功能,一個是獲取指定 User 的詳細信息,另一個是新註冊一個用戶。

@Path("users") // #1
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_XML}) // #2
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public interface UserService {
@GET // #3
@Path("{id: \d+}")
User getUser(@PathParam("id") Long id);

@POST // #4
@Path("register")
Long registerUser(User user);

}
通過在接口上用 JaxRS 標準的 annotation 來修飾,我們規定了該服務在 REST 下的訪問形式:

@Path("users") 定義了 UserService 通過 ‘/users‘ 來訪問
在類級別上定義 @Consumers 和 @Produces 來規定參數以及返回值的類型為 XML 和 JSON。在類級別上定義之後,就可以不用在方法級別上進一步定義了
getUser 方法上通過 @GET 定義了接受的 HTTP 方法為 GET,通過 @Path 來規定參數是來自於 URL 中的 path。‘GET /users/1‘ 等同於調用 ‘getUser(1)‘
registerUser 方法上通過 @POST 定義了接受的 HTTP 方法為 POST,通過將 JSON 或 XML 格式的 User 數據 POST 到 ‘/users/register‘ 上來創建一個 User
在 Dubbo 中,將 REST 相關的 annotation 定義在接口或者實現上都是可以的。這個在設計上是個權衡問題。Annotation 定義在實現類上可以保證接口的純凈,否則對於不需要通過 REST 方式調用的 Dubbo 調用方來說將需要強制依賴 JaxRS 的庫,但是同時,對於需要通過 REST 方式調用的 Dubbo 調用方來說,就需要自己來處理 REST 調用相關的細節了。Annotation 定義在接口上,框架會自動處理掉 REST 調用相關的細節,並和 Dubbo 的服務發現以及服務治理功能能夠很好的結合起來。在本例中采用了在接口上定義 JaxRS annotation 的形式。

  1. 實現服務接口
    為了簡潔,這裏給出的接口的實現只是簡單的返回了接口需要的類型的示例,在真實的系統中,邏輯可能會比較復雜。

public class UserServiceImpl implements UserService {
private final AtomicLong id = new AtomicLong();

public User getUser(Long id) {
    return new User(id, "username-" + id);
}

public Long registerUser(User user) {
    return id.incrementAndGet();
}

}

  1. 裝配服務
    如上所述,本例展示的是如何通過傳統的 Spring XML 的方式來裝配並暴露 Dubbo 服務。需要指出的是,這裏展示了如何同時暴露兩種不同的協議,一種是 REST,另一種是原生的 Dubbo 協議。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="rest-provider"/> <!-- #1 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/> <!-- #2 -->
<dubbo:protocol name="rest" port="8080" server="netty"/> <!-- #3 -->
<dubbo:protocol name="dubbo" server="netty4"/> <!-- #4 -->
<dubbo:service interface="org.apache.dubbo.samples.rest.api.UserService" protocol="rest,dubbo" ref="userService"/> <!-- #5 -->
<bean id="userService" class="org.apache.dubbo.samples.rest.impl.UserServiceImpl"/>
</beans> <!-- #6 -->
定義了該應用的名字為 rest-provider
定義了服務註冊通過 Zookeeper,並且 URL 為 "zookeeper://127.0.0.1:2181"
在端口 8080 上以 REST 方式暴露服務,底層的傳輸使用的是 netty
在默認端口 20880 上以原生 Dubbo 方式暴露服務,底層的傳輸方式是 netty
將 ‘userService‘ 的 Spring bean (也就是 UserServiceImpl)暴露為 UserService 服務,支持的協議既包括了 REST 也包括了 Dubbo
將 UserServiceImpl 註冊成 ‘userService‘ 的 Spring bean

  1. 服務提供方的啟動類
    簡單的通過 ClassPathXmlApplicationContext 來加載剛剛配置的 Spring XML 配置 ‘rest-provider.xml‘ 即可啟動 Dubbo 服務端

public class RestProvider {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/rest-provider.xml");
context.start();
System.in.read();
}
}

  1. 啟動服務端
    由於本例依賴 Zookeeper 做服務註冊發現,在啟動 RestProvider 之前,需要先啟動一個 Zookeeper 服務器。之後就可以直接運行 RestProvider 了。通過以下的輸出日誌,我們可以知道 UserService 以兩種方式對外暴露了同一個服務,其中:

REST: rest://192.168.2.132:8080/org.apache.dubbo.samples.rest.api.UserService
Dubbo: dubbo://192.168.2.132:20880/org.apache.dubbo.samples.rest.api.UserService
...
[01/01/19 07:18:56:056 CST] main INFO config.AbstractConfig: [DUBBO] Export dubbo service org.apache.dubbo.samples.rest.api.UserService to url rest://192.168.2.132:8080/org.apache.dubbo.samples.rest.api.UserService?anyhost=true&application=rest-provider&bean.name=org.apache.dubbo.samples.rest.api.UserService&bind.ip=192.168.2.132&bind.port=8080&dubbo=2.0.2&generic=false&interface=org.apache.dubbo.samples.rest.api.UserService&methods=getUser,registerUser&pid=27386&server=netty&side=provider&timestamp=1546341536194, dubbo version: 2.6.5, current host: 192.168.2.132
...
[01/01/19 07:18:57:057 CST] main INFO config.AbstractConfig: [DUBBO] Export dubbo service org.apache.dubbo.samples.rest.api.UserService to url dubbo://192.168.2.132:20880/org.apache.dubbo.samples.rest.api.UserService?anyhost=true&application=rest-provider&bean.name=org.apache.dubbo.samples.rest.api.UserService&bind.ip=192.168.2.132&bind.port=20880&dubbo=2.0.2&generic=false&interface=org.apache.dubbo.samples.rest.api.UserService&methods=getUser,registerUser&pid=27386&server=netty4&side=provider&timestamp=1546341537392, dubbo version: 2.6.5, current host: 192.168.2.132
...
也可以通過 zkCli 訪問 Zookeeper 服務器來驗證。‘/dubbo/org.apache.dubbo.samples.rest.api.UserService/providers‘ 路徑下返回了一個數組 [dubbo://..., rest:.//...]。數組的第一個元素是 ’dubbo‘ 打頭的,而第二個元素是 ‘rest‘ 打頭的。

[zk: localhost:2181(CONNECTED) 10] ls /dubbo/org.apache.dubbo.samples.rest.api.UserService/providers
[dubbo%3A%2F%2F192.168.2.132%3A20880%2Forg.apache.dubbo.samples.rest.api.UserService%3Fanyhost%3Dtrue%26application%3Drest-provider%26bean.name%3Dorg.apache.dubbo.samples.rest.api.UserService%26dubbo%3D2.0.2%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.samples.rest.api.UserService%26methods%3DgetUser%2CregisterUser%26pid%3D27386%26server%3Dnetty4%26side%3Dprovider%26timestamp%3D1546341537392, rest%3A%2F%2F192.168.2.132%3A8080%2Forg.apache.dubbo.samples.rest.api.UserService%3Fanyhost%3Dtrue%26application%3Drest-provider%26bean.name%3Dorg.apache.dubbo.samples.rest.api.UserService%26dubbo%3D2.0.2%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.samples.rest.api.UserService%26methods%3DgetUser%2CregisterUser%26pid%3D27386%26server%3Dnetty%26side%3Dprovider%26timestamp%3D1546341536194]
可以簡單的通過 ‘curl‘ 在命令行驗證剛才暴露出來的 REST 服務:

$ curl http://localhost:8080/users/1
{"id":1,"name":"username-1"}
$ curl -X POST -H "Content-Type: application/json" -d ‘{"id":1,"name":"Larry Page"}‘ http://localhost:8080/users/register
1

  1. 裝配調用端
    Dubbo 調用方只需要依賴服務的接口,通過以下方式裝配好 Dubbo Consumer,即可發起調用。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="rest-consumer"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:reference id="userService" interface="org.apache.dubbo.samples.rest.api.UserService" protocol="rest"/> <!-- #1 -->
</beans>
‘userService‘ 配置的 protocol 為 “rest",將通過 REST 協議調用服務端
需要特別指出的是,這裏顯示的指定 protocol="rest" 在通常情況下不是必須的。這裏需要顯示指定的原因是我們例子中服務端同時暴露了多種協議,這裏指定使用 rest 是為了確保調用方走 REST 協議。

  1. 發起調用
    簡單的通過 ClassPathXmlApplicationContext 來加載剛剛配置的 Spring XML 配置 ‘rest-consumer.xml‘ 即可發起對 RestProvider 所提供的 UserService 的 REST 服務的調用。

public class RestConsumer {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/rest-consumer.xml");
context.start();

    UserService userService = context.getBean("userService", UserService.class);
    System.out.println(">>> " + userService.getUser(1L));

    User user = new User(2L, "Larry Page");
    System.out.println(">>> " + userService.registerUser(user));
}

}
這裏分別展示了對 ‘getUser‘ 和 ‘registerUser‘ 的調用,輸出結果如下:

>> User{id=1, name=‘username-1‘}
>> 2
進階
A. 在 REST 中使用 Annotation
在 Dubbo 中使用 annotation 而不是 Spring XML 來暴露和引用服務,對於 REST 協議來說並沒有什麽不同。有關如何使用 annotation 更詳細的用法,請參閱《在 Dubbo 中使用註解》章節。這裏主要展示一下與上面基於 Spring XML 配置的例子不同之處。

註:本章節討論的示例可以通過 https://github.com/beiwei30/dubbo-rest-samples/tree/master/annotation 來獲得

  1. 使用 Java Configuration 來配置服務提供方的 protocol、registry、application@Configuration
    br/>@Configuration
    static class ProviderConfiguration {
    @Bean // #2
    public ProtocolConfig protocolConfig() {
    ProtocolConfig protocolConfig = new ProtocolConfig();
    protocolConfig.setName("rest");
    protocolConfig.setPort(8080);
    protocolConfig.setServer("netty");
    return protocolConfig;
    }

    @Bean // #3
    public RegistryConfig registryConfig() {
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setProtocol("zookeeper");
        registryConfig.setAddress("localhost");
        registryConfig.setPort(2181);
        return registryConfig;
    }
    
    @Bean
    public ApplicationConfig applicationConfig() {
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("rest-provider");
        return applicationConfig;
    }

    }
    通過 @EnableDubbo 來指定需要掃描 Dubbo 服務的包名,在本例中,UserServiceImpl 在 "org.apache.dubbo.samples.rest.impl" 下
    通過提供一個 ProtocolConfig 的 Spring Bean 來指定服務提供方按照 REST 來暴露服務
    通過提供一個 RegistryConfig 的 Spring Bean 來指定服務提供方所使用的服務註冊機制

  2. 使用 Service 來申明 Dubbo 服務
    @Service // #1
    public class UserServiceImpl implements UserService {
    ...
    }
    簡單的使用 @Service 或者 @Service(protocol = "rest") 修飾 "UserServiceImpl" 來申明一個 Dubbo 服務,這裏 protocol = "rest" 不是必須提供的,原因是通過 Java Configuration 只配置了一個 ProtocolConfig 的示例,在這種情況下,Dubbo 會自動裝配該協議到服務中
  3. 服務提供方啟動類
    通過使用 ProviderConfiguration 來初始化一個 AnnotationConfigApplicationContext 實例,就可以完全擺脫 Spring XML 的配置文件,完全借助 annotation 來裝配好一個 Dubbo 的服務提供方。

public class RestProvider {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProviderConfiguration.class);
context.start();
System.in.read();
}
}

  1. 使用 Java Configuration 來配置服務消費方的 registry、application@Configuration
    br/>@Configuration
    @ComponentScan({"org.apache.dubbo.samples.rest.comp"}) // #2
    static class ConsumerConfiguration {
    @Bean // #3
    public RegistryConfig registryConfig() {
    RegistryConfig registryConfig = new RegistryConfig();
    registryConfig.setProtocol("zookeeper");
    registryConfig.setAddress("localhost");
    registryConfig.setPort(2181);
    return registryConfig;
    }

    @Bean
    public ApplicationConfig applicationConfig() {
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("rest-consumer");
        return applicationConfig;
    }

    }
    通過 @EnableDubbo 來指定需要掃描 Dubbo 服務引用 @Reference 的包名。在本例中,UserService 的引用在 "org.apache.dubbo.samples.rest.comp" 下
    通過 @ComponentScan 來指定需要掃描的 Spring Bean 的包名。在本例中,包含 UserService 引用的類 UserServiceComponent 本身需要是一個 Spring Bean,以方便調用,所以,這裏指定的包名也是 "org.apache.dubbo.samples.rest.comp"
    通過提供一個 RegistryConfig 的 Spring Bean 來指定服務消費方所使用的服務發現機制
    這裏提到的 UserServiceComponent 的 Spring Bean 定義如下:

@Component
public class UserServiceComponent implements UserService { // #1@Reference
br/>@Reference

@Override
public User getUser(Long id) {
    return userService.getUser(id);
}

@Override
public Long registerUser(User user) {
    return userService.registerUser(user);
}

}
這裏比較好的實踐是讓這個 Spring Bean 也繼承 UserService 接口,這樣在調用的時候也可以面向接口編程

  1. 服務調用方啟動類
    通過使用 ConsumerConfiguration 來初始化一個 AnnotationConfigApplicationContext 實例,就可以完全擺脫 Spring XML 的配置文件,完全借助 annotation 來裝配好一個 Dubbo 的服務消費方。然後就可以通過查找 UserServiceComponent 類型的 Spring Bean 來發起遠程調用。

public class RestConsumer {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfiguration.class);
context.start();

    UserService userService = context.getBean(UserServiceComponent.class);
    System.out.println(">>> " + userService.getUser(1L));

    User user = new User(2L, "Larry Page");
    System.out.println(">>> " + userService.registerUser(user));
}

}
B. 讓協議跑在不同的服務器上
目前 REST 協議在 Dubbo 中可以跑在五種不同的 server 上,分別是:

"netty": 直接基於 netty 框架的 rest server,通過 <dubbo:protocol name="rest" server="netty"/> 來配置
"tomcat": 基於嵌入式 tomcat 的 rest server,通過 <dubbo:protocol name="rest" server="tomcat"/> 來配置
"jetty": 默認選項,基於嵌入式 jetty 的 rest server,通過 <dubbo:protocol name="rest" server="jetty"/> 來配置
"sunhttp": 使用 JDK 內置的 Sun HTTP server 作為 rest server,通過 <dubbo:protocol name="rest" server="sunhttp"/> 來配置,僅推薦在開發環境中使用
"servlet”: 采用外部應用服務器的 servlet 容器來做 rest server,這個時候,除了配置 <dubbo:protocol name="rest" server="servlet"/> 之外,還需要在 web.xml 中做額外的配置
由於以上的例子展示了 "netty" 作為 rest server,下面演示一下使用嵌入式 tomcat 的 rest server 的用法。

註:本章節討論的示例可以通過 https://github.com/beiwei30/dubbo-rest-samples/tree/master/tomcat 來獲得

  1. 增加 Tomcat 相關的依賴
    <dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    </dependency>
    <dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-logging-juli</artifactId>
    </dependency>
  2. 配置 protocol 使用 tomcat 作為 REST server
    <dubbo:protocol name="rest" port="8080" server="tomcat"/>
    啟動服務提供方之後,在以下的輸出將會出現與嵌入式 Tomcat 相關的日誌信息:

Jan 01, 2019 10:15:12 PM org.apache.catalina.core.StandardContext setPath
WARNING: A context path must either be an empty string or start with a ‘/‘ and do not end with a ‘/‘. The path [/] does not meet these criteria and has been changed to []
Jan 01, 2019 10:15:13 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
Jan 01, 2019 10:15:13 PM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
Jan 01, 2019 10:15:13 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
Jan 01, 2019 10:15:13 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet Engine: Apache Tomcat/8.5.31
Jan 01, 2019 10:15:13 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
C. 使用外部的 Servlet 容器
進一步的,還可以使用外部的 servlet 容器來啟動 Dubbo 的 REST 服務。

註:本章節討論的示例可以通過 https://github.com/beiwei30/dubbo-rest-samples/tree/master/servlet 來獲得

  1. 修改 pom.xml 改變打包方式
    因為使用的是外部的 servlet 容器,需要將打包方式修改為 "war"

<packaging>war</packaging>

  1. 修改 rest-provider.xml
    配置 "server" 為 "servlet" 表示將使用外部的 servlet 容器。並配置 "contextpath" 為 "",原因是在使用外部 servlet 容器時,Dubbo 的 REST 支持需要知道被托管的 webapp 的 contextpath 是什麽。這裏我們計劃通過 root context path 來部署應用,所以配置其為 ""。

<dubbo:protocol name="rest" port="8080" server="servlet" contextpath=""/>

  1. 配置 WEB-INF/web.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">
    <context-param> <!-- #1 -->
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/classes/spring/rest-provider.xml</param-value>
    </context-param>

    <listener>
    <listener-class>com.alibaba.dubbo.remoting.http.servlet.BootstrapListener</listener-class>
    </listener>

    <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet> <!-- #2 -->
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
    配置 Dubbo 和 Spring 相關的 ContextListener,打開 Dubbo HTTP 支持,以及通過 rest-provider.xml 來裝配 Dubbo 服務
    配置 Dubbo HTTP 所需的 DispatcherServlet
    這樣做之後,不再需要 RestProvider 來啟動 Dubbo 服務,可以將其從工程中刪掉。對應的,現在 Dubbo 的服務將會隨著 Servlet 容器的啟動而啟動。啟動完畢之後,可以通過類似 "http://localhost:8080/api/users/1" 來訪問暴露出的 REST 服務。需要註意的是,這個例子裏假定了服務提供方的 WAR 包部署在 root context path 上,所以當該應用通過 IDE 配置的 tomcat server 啟動時,需要指定 Application Context 為 "/"。

D. 增加 Swagger 支持
在上面使用外部 Servlet 容器的例子的基礎上,討論如何暴露 Swagger OpenApi 以及如何繼承 Swagger UI。

註:本章節討論的示例可以通過 https://github.com/beiwei30/dubbo-rest-samples/tree/master/servlet 來獲得

  1. 暴露 Swagger OpenApi
    增加 swagger 相關依賴,以便通過 "http://localhost:8080/openapi.json" 來訪問 REST 服務的描述

<properties>
<swagger.version>2.0.6</swagger.version>
</properties>

<dependencies> 
    <dependency>
        <groupId>io.swagger.core.v3</groupId>
        <artifactId>swagger-jaxrs2</artifactId>
        <version>${swagger.version}</version>
    </dependency>
    <dependency>
        <groupId>io.swagger.core.v3</groupId>
        <artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
        <version>${swagger.version}</version>
    </dependency>
</dependencies>

修改 WEB-INF/web.xml,增加 openapi servlet 的配置

<web-app>
...
<servlet> <!-- #3 -->
<servlet-name>openapi</servlet-name>
<servlet-class>io.swagger.v3.jaxrs2.integration.OpenApiServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>openapi</servlet-name>
<url-pattern>/openapi.json</url-pattern>
<url-pattern>/openapi.yaml</url-pattern>
</servlet-mapping>
</web-app>
重新啟動應用之後,可以通過訪問 "http://localhost:8080/openapi.json" 或者 "http://localhost:8080/openapi.yaml" 來訪問暴露出的 openapi 的契約,以下是 yaml 格式的表述:

openapi: 3.0.1
paths:
/api/users/{id}:
get:
operationId: getUser
parameters:

  • name: id
    in: path
    required: true
    schema:
    type: integer
    format: int64
    responses:
    default:
    description: default response
    content:
    application/json:
    schema:
    $ref: ‘#/components/schemas/User‘
    text/xml:
    schema:
    $ref: ‘#/components/schemas/User‘
    /api/users/register:
    post:
    operationId: registerUser
    requestBody:
    description: a user to register
    content:
    application/json:
    schema:
    $ref: ‘#/components/schemas/User‘
    text/xml:
    schema:
    $ref: ‘#/components/schemas/User‘
    responses:
    default:
    description: default response
    content:
    application/json:
    schema:
    type: integer
    format: int64
    text/xml:
    schema:
    type: integer
    format: int64
    components:
    schemas:
    User:
    type: object
    properties:
    id:
    type: integer
    format: int64
    name:
    type: string
    1. 集成 Swagger UI
      在 pom.xml 中繼續增加 swagger-ui 的依賴,這裏使用的是 webjars 的版本,從集成的角度來說更加簡潔。webjars 的工作機制可以參閱 webjars 官網 [5]

<properties>
<swagger.webjar.version>3.20.3</swagger.webjar.version>
</properties>
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>${swagger.webjar.version}</version>
</dependency>
</dependencies>
在工程的 webapp/WEB-INF 根目錄下增加一個 HTML 文件,內容如下。HTML 文件名可以為任何名字,沒有硬性要求,如果該文件被命名為 "swagger-ui.html",那麽你可以通過訪問 “http://localhost:8080/swagger-ui.html" 來訪問 swagger UI。本例為了演示方便起見,將其命名為 "index.html",這樣當訪問 "http://localhost:8080" 時,就可以很方便的得到 swagger UI 的頁面。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>API UI</title>
<link rel="stylesheet" type="text/css" href="webjars/swagger-ui/3.20.3/swagger-ui.css" >
<link rel="icon" type="image/png" href="webjars/swagger-ui/3.20.3/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="webjars/swagger-ui/3.20.3/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}

    *,
    *:before,
    *:after
    {
        box-sizing: inherit;
    }

    body
    {
        margin:0;
        background: #fafafa;
    }
</style>

</head>

<body>
<div id="swagger-ui"></div>

<script src="webjars/swagger-ui/3.20.3/swagger-ui-bundle.js"> </script>
<script src="webjars/swagger-ui/3.20.3/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function () {
window.ui = SwaggerUIBundle({
url: "openapi.json",
dom_id: ‘#swagger-ui‘,
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
};
</script>

</body>
</html>
再次重啟服務器,並訪問 "http://localhost:8080" 時,將會看到 swagger UI 頁面的展示:

2

通過 Swagger UI 可以很方便的瀏覽當前服務器提供的 REST 服務的文檔信息,甚至可以直接調用來做服務測試。以 ‘/api/users/{id}‘ 為例,測試結果如下圖所示:

3

總結
本文主要關註了在 Dubbo 中支持 REST 協議的情況。首先探索了 REST 概念的起源,澄清了 REST 是一種適合互聯網的軟件架構風格,進一步的說明了 REST 風格的架構可以與 HTTP 協議無關,但是 HTTP 協議的確是 REST 風格架構的最常用甚至是最佳的組合和搭檔。然後討論了如何在 Dubbo 中開發 REST HTTP 的幾種典型用法,其中包括了通過不同的配置,如傳統的 Spring XML,完全通過 annotation 來配置兩種典型的用法,本文中沒有涉及到的還有純 API 編程方式,Spring Boot 配置方式也是完全可以的,因為篇幅原因沒有提及;還討論了如何通過不同的 REST server 來暴露 REST HTTP 服務,包括了 embedded tomcat,netty,以及外置的 servlet 容器等幾種用法。最後,在外置的 servlet 容器的基礎上,進一步的討論了如何通過 Swagger 暴露 openAPI 以及集成 Swagger UI 的方法。

本文沒有涉及的內容包含但不限於國際化支持、Dubbo REST 更高階的註入擴展的用法、以及 Dubbo REST 支持未來的規劃。其中 Dubbo REST 擴展的支持可以參考 https://github.com/beiwei30/dubbo-rest-samples/tree/master/extensions 中的演示。以後有機會會開專門的篇幅來探討更高級的 Swagger 的支持、以及對未來的展望。

http://en.wikipedia.org/wiki/Roy_Fielding ??

http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm ??

https://martinfowler.com/articles/richardsonMaturityModel.html ??

https://github.com/dangdangdotcom/dubbox ??

https://www.webjars.org/documentation#servlet3 ??

服務化改造實踐 | 如何在 Dubbo 中支持 REST