什么是 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)。
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 下的訪問形式:
@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 的形式。
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 -->
- 定義了該應用的名字為
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
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×tamp=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×tamp=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>
- '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; } }
- 通過
@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(); } }
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; } }
- 通過
@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 private UserService userService; @Override public User getUser(Long id) { return userService.getUser(id); } @Override public Long registerUser(User user) { return userService.registerUser(user); } }
- 這里比較好的實踐是讓這個 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>
- 配置 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
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 的方法。
原文鏈接
本文為雲棲社區原創內容,未經允許不得轉載。