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

什麼是 REST

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

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

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

對於一個提供服務的動態網站來講,能夠按照相似的思路將其 RESTful 化:nginx

其中的思路是利用 HTTP 協議的標準方法 POST、DELETE、PUT、GET 來表達對於一個資源的增刪改查 (CRUD) 操做,利用 URL 來表示一個資源的惟一標識。對資源訪問的錯誤碼也複用 HTTP 協議的狀態碼。返回結果一般由 json 或 XML 來表示,若是其中包換了對關聯資源的訪問方式 (所謂的表現層狀態遷移) ,這種類型的 RESTful 應用能夠進一步的稱之爲 hypermedia as the engine of application state (HATEOAS) 應用 [[3]](#fn3)。git


 

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

這裏須要注意的是,按照 Roy 的定義,RESTful 架構風格與 HTTP 協議並無什麼強關聯關係。可是,基於 HTTP 的 RESTful 架構風格是實現起來最天然,也是目前使用最普遍的一種實現,咱們稱之爲 RESTful HTTP。一樣的,在下文中將會專一在 HTTP 的場景下介紹如何在 Dubbo 框架中將服務暴露成 Restful 架構。web

在 Dubbo 中使用 REST

背景

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

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

基本用法

在如下的例子中,展現瞭如何經過最傳統的 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>

2. 定義服務接口

定義一個服務接口 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 下的訪問形式:

  1. @Path("users") 定義了 UserService 經過 '/users' 來訪問
  2. 在類級別上定義 @Consumers 和 @Produces 來規定參數以及返回值的類型爲 XML 和 JSON。在類級別上定義以後,就能夠不用在方法級別上進一步定義了
  3. getUser 方法上經過 @GET 定義了接受的 HTTP 方法爲 GET,經過 @Path 來規定參數是來自於 URL 中的 path。'GET /users/1' 等同於調用 'getUser(1)'
  4. 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 的形式。

3. 實現服務接口

爲了簡潔,這裏給出的接口的實現只是簡單的返回了接口須要的類型的示例,在真實的系統中,邏輯可能會比較複雜。

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();
    }
}

4. 裝配服務

如上所述,本例展現的是如何經過傳統的 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 -->
  1. 定義了該應用的名字爲 rest-provider
  2. 定義了服務註冊經過 Zookeeper,而且 URL 爲 "zookeeper://127.0.0.1:2181"
  3. 在端口 8080 上以 REST 方式暴露服務,底層的傳輸使用的是 netty
  4. 在默認端口 20880 上以原生 Dubbo 方式暴露服務,底層的傳輸方式是 netty
  5. 將 ‘userService' 的 Spring bean (也就是 UserServiceImpl)暴露爲 UserService 服務,支持的協議既包括了 REST 也包括了 Dubbo
  6. 將 UserServiceImpl 註冊成 'userService' 的 Spring bean

5. 服務提供方的啓動類

簡單的經過 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();
    }
}

6. 啓動服務端

因爲本例依賴 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

7. 裝配調用端

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>
  1. 'userService' 配置的 protocol 爲 「rest",將經過 REST 協議調用服務端

須要特別指出的是,這裏顯示的指定 protocol="rest" 在一般狀況下不是必須的。這裏須要顯示指定的緣由是咱們例子中服務端同時暴露了多種協議,這裏指定使用 rest 是爲了確保調用方走 REST 協議。

8. 發起調用

簡單的經過 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
    @EnableDubbo(scanBasePackages = "org.apache.dubbo.samples.rest.impl") // #1
    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;
        }
    }
  1. 經過 @EnableDubbo 來指定須要掃描 Dubbo 服務的包名,在本例中,UserServiceImpl 在 "org.apache.dubbo.samples.rest.impl" 下
  2. 經過提供一個 ProtocolConfig 的 Spring Bean 來指定服務提供方按照 REST 來暴露服務
  3. 經過提供一個 RegistryConfig 的 Spring Bean 來指定服務提供方所使用的服務註冊機制

2. 使用 Service 來申明 Dubbo 服務

@Service // #1
public class UserServiceImpl implements UserService {
    ...
}
  1. 簡單的使用 @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();
    }
}

4. 使用 Java Configuration 來配置服務消費方的 registry、application

@Configuration
    @EnableDubbo(scanBasePackages = "org.apache.dubbo.samples.rest.comp") // #1
    @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;
        }
    }
  1. 經過 @EnableDubbo 來指定須要掃描 Dubbo 服務引用 @Reference 的包名。在本例中,UserService 的引用在 "org.apache.dubbo.samples.rest.comp" 下
  2. 經過 @ComponentScan 來指定須要掃描的 Spring Bean 的包名。在本例中,包含 UserService 引用的類 UserServiceComponent 自己須要是一個 Spring Bean,以方便調用,因此,這裏指定的包名也是 "org.apache.dubbo.samples.rest.comp"
  3. 經過提供一個 RegistryConfig 的 Spring Bean 來指定服務消費方所使用的服務發現機制

這裏提到的 UserServiceComponent 的 Spring Bean 定義以下:

@Component
public class UserServiceComponent implements UserService { // #1
    @Reference
    private UserService userService;


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

    @Override
    public Long registerUser(User user) {
        return userService.registerUser(user);
    }
}
  1. 這裏比較好的實踐是讓這個 Spring Bean 也繼承 UserService 接口,這樣在調用的時候也能夠面向接口編程

5. 服務調用方啓動類

經過使用 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>

2. 修改 rest-provider.xml

配置 "server" 爲 "servlet" 表示將使用外部的 servlet 容器。並配置 "contextpath" 爲 "",緣由是在使用外部 servlet 容器時,Dubbo 的 REST 支持須要知道被託管的 webapp 的 contextpath 是什麼。這裏咱們計劃經過 root context path 來部署應用,因此配置其爲 ""。

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

3. 配置 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>
  1. 配置 Dubbo 和 Spring 相關的 ContextListener,打開 Dubbo HTTP 支持,以及經過 rest-provider.xml 來裝配 Dubbo 服務
  2. 配置 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

2. 集成 Swagger UI

在 pom.xml 中繼續增長 swagger-ui 的依賴,這裏使用的是 webjars 的版本,從集成的角度來講更加簡潔。webjars 的工做機制能夠參閱 webjars 官網 [[5]](#fn5)

<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 頁面的展現:


 

經過 Swagger UI 能夠很方便的瀏覽當前服務器提供的 REST 服務的文檔信息,甚至能夠直接調用來作服務測試。以 '/api/users/{id}' 爲例,測試結果以下圖所示:


 

總結

本文主要關注了在 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 ↩︎

 

原文連接 更多技術乾貨 請關注阿里云云棲社區微信號 :yunqiinsight

相關文章
相關標籤/搜索