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


什么是 REST

REST 是 Roy Thomas Fielding [[1]](#fn1) 在 2000 年他的博士論文 [[2]](#fn2) “架構風格以及基於網絡的軟件架構設計” 中提出來的一個概念。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]](#fn3)。

1

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]](#fn4) 中的主要特性,其中就包括了基於 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>

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

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 的方法。

 

 

原文鏈接
本文為雲棲社區原創內容,未經允許不得轉載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM