Dubbo 協議介紹

Dubbo 協議共九種,分別爲:

dubbo:// rmi:// hessian:// http:// webservice:// thrift:// memcached:// redis:// rest://

dubbo://

Dubbo 缺省協議採用單一長連接和 NIO 異步通訊,適合於小數據量大併發的服務調用,以及服務消費者機器數遠大於服務提供者機器數的情況。

反之,Dubbo 缺省協議不適合傳送大數據量的服務,比如傳文件,傳視頻等,除非請求量很低。

在這裏插入圖片描述

  • Transporter: mina, netty, grizzy
  • Serialization: dubbo, hessian2, java, json
  • Dispatcher: all, direct, message, execution, connection
  • ThreadPool: fixed, cached

特性

缺省協議,使用基於 mina 1.1.7 和 hessian 3.2.1 的 tbremoting 交互。

  • 連接個數:單連接
  • 連接方式:長連接
  • 傳輸協議:TCP
  • 傳輸方式:NIO 異步傳輸
  • 序列化:Hessian 二進制序列化
  • 適用範圍:傳入傳出參數數據包較小(建議小於100K),消費者比提供者個數多,單一消費者無法壓滿提供者,儘量不要用 dubbo 協議傳輸大文件或超大字符串。
  • 適用場景:常規遠程服務方法調用

約束

  • 參數及返回值需實現 Serializable 接口
  • 參數及返回值不能自定義實現 List, Map, Number, Date, Calendar 等接口,只能用 JDK 自帶的實現,因爲 hessian 會做特殊處理,自定義實現類中的屬性值都會丟失。
  • Hessian 序列化,只傳成員屬性值和值的類型,不傳方法或靜態變量,兼容情況 :
數據通訊 情況 結果
A->B 類A多一種 屬性(或者說類B少一種 屬性) 不拋異常,A多的那 個屬性的值,B沒有, 其他正常
A->B 枚舉A多一種 枚舉(或者說B少一種 枚舉),A使用多 出來的枚舉進行傳輸 拋異常
A->B 枚舉A多一種 枚舉(或者說B少一種 枚舉),A不使用 多出來的枚舉進行傳輸 不拋異常,B正常接收數據
A->B A和B的屬性名相同,但類型不相同 拋異常
A->B serialId 不相同 正常傳輸

接口增加方法,對客戶端無影響,如果該方法不是客戶端需要的,客戶端不需要重新部署。輸入參數和結果集中增加屬性,對客戶端無影響,如果客戶端並不需要新屬性,不用重新部署。

輸入參數和結果集屬性名變化,對客戶端序列化無影響,但是如果客戶端不重新部署,不管輸入還是輸出,屬性名變化的屬性值是獲取不到的。

總結:服務器端和客戶端對領域對象並不需要完全一致,而是按照最大匹配原則。

配置

配置協議:

<dubbo:protocol name="dubbo" port="20880" />

設置默認協議:

<dubbo:provider protocol="dubbo" />

設置服務協議:

<dubbo:service protocol="dubbo" />

多端口:

<dubbo:protocol id="dubbo1" name="dubbo" port="20880" />
<dubbo:protocol id="dubbo2" name="dubbo" port="20881" />

配置協議選項:

<dubbo:protocol name=“dubbo” port=“9090” server=“netty” client=“netty” codec=“dubbo” serialization=“hessian2” charset=“UTF-8” threadpool=“fixed” threads=“100” queues=“0” iothreads=“9” buffer=“8192” accepts=“1000” payload=“8388608” />

多連接配置:

Dubbo 協議缺省每服務每提供者每消費者使用單一長連接,如果數據量較大,可以使用多個連接。

<dubbo:service connections="1"/>
<dubbo:reference connections="1"/>
  • <dubbo:service connections="0"><dubbo:reference connections="0"> 表示該服務使用 JVM 共享長連接。(缺省)
  • <dubbo:service connections="1"><dubbo:reference connections="1"> 表示該服務使用獨立長連接。
  • <dubbo:service connections="2"><dubbo:reference connections="2"> 表示該服務使用獨立兩條長連接。

爲防止被大量連接撐掛,可在服務提供方限制大接收連接數,以實現服務提供方自我保護。

<dubbo:protocol name="dubbo" accepts="1000" />

dubbo.properties 配置:

dubbo.service.protocol=dubbo

常見問題

爲什麼要消費者比提供者個數多?

因 dubbo 協議採用單一長連接,假設網絡爲千兆網卡 ,根據測試經驗數據每條連接最多隻能壓滿 7MByte(不同的環境可能不一樣,供參考),理論上 1 個服務提供者需要 20 個服務消費者才能壓滿網卡。

爲什麼不能傳大包?

因 dubbo 協議採用單一長連接,如果每次請求的數據包大小爲 500KByte,假設網絡爲千兆網卡 ,每條連接最大 7MByte(不同的環境可能不一樣,供參考),單個服務提供者的 TPS(每秒處理事務數)最大爲:128MByte / 500KByte = 262。單個消費者調用單個服務提供者的 TPS(每秒處理事務數)最大爲:7MByte / 500KByte = 14。如果能接受,可以考慮使用,否則網絡將成爲瓶頸。

爲什麼採用異步單一長連接?

因爲服務的現狀大都是服務提供者少,通常只有幾臺機器,而服務的消費者多,可能整個網站都在訪問該服務,比如 Morgan 的提供者只有 6 臺提供者,卻有上百臺消費者,每天有 1.5 億次調用,如果採用常規的 hessian 服務,服務提供者很容易就被壓跨,通過單一連接,保證單一消費者不會壓死提供者,長連接,減少連接握手驗證等,並使用異步 IO,複用線程池,防止 C10K 問題。


rmi://

RMI 協議採用 JDK 標準的 java.rmi.* 實現,採用阻塞式短連接和 JDK 標準序列化方式。

注意:如果正在使用 RMI 提供服務給外部訪問 ,同時應用裏依賴了老的 common-collections 包的情況下,存在反序列化安全風險。

特性

  • 連接個數:多連接
  • 連接方式:短連接
  • 傳輸協議:TCP
  • 傳輸方式:同步傳輸
  • 序列化:Java 標準二進制序列化
  • 適用範圍:傳入傳出參數數據包大小混合,消費者與提供者個數差不多,可傳文件。
  • 適用場景:常規遠程服務方法調用,與原生RMI服務互操作

約束

  • 參數及返回值需實現 Serializable 接口
  • dubbo 配置中的超時時間對 RMI 無效,需使用 java 啓動參數設置:-Dsun.rmi.transport.tcp.responseTimeout=3000,參見下面的 RMI 配置

dubbo.properties 配置

dubbo.service.protocol=rmi

RMI配置

java -Dsun.rmi.transport.tcp.responseTimeout=3000

接口

如果服務接口繼承了 java.rmi.Remote 接口,可以和原生 RMI 互操作,即:

  • 提供者用 Dubbo 的 RMI 協議暴露服務,消費者直接用標準 RMI 接口調用,
  • 或者提供方用標準 RMI 暴露服務,消費方用 Dubbo 的 RMI 協議調用。

如果服務接口沒有繼承 java.rmi.Remote 接口:

  • 缺省 Dubbo 將自動生成一個 com.xxx.XxxService$Remote 的接口,並繼承 java.rmi.Remote 接口,並以此接口暴露服務,
  • 但如果設置了 <dubbo:protocol name="rmi" codec="spring" />,將不生成 $Remote 接口,而使用 Spring 的 RmiInvocationHandler 接口暴露服務,和 Spring 兼容。

配置

定義 RMI 協議:

<dubbo:protocol name="rmi" port="1099" />

設置默認協議:

<dubbo:provider protocol="rmi" />

設置服務協議:

<dubbo:service protocol="rmi" />

多端口:

<dubbo:protocol id="rmi1" name="rmi" port="1099" />
<dubbo:protocol id="rmi2" name="rmi" port="2099" />
 
<dubbo:service protocol="rmi1" />

Spring 兼容性:

<dubbo:protocol name="rmi" codec="spring" />

hessian://

Hessian協議用於集成 Hessian 的服務,Hessian 底層採用 Http 通訊,採用 Servlet 暴露服務,Dubbo 缺省內嵌 Jetty 作爲服務器實現。

Dubbo 的 Hessian 協議可以和原生 Hessian 服務互操作,即:

  • 提供者用 Dubbo 的 Hessian 協議暴露服務,消費者直接用標準 Hessian 接口調用
  • 或者提供方用標準 Hessian 暴露服務,消費方用 Dubbo 的 Hessian 協議調用。

特性

  • 連接個數:多連接
  • 連接方式:短連接
  • 傳輸協議:HTTP
  • 傳輸方式:同步傳輸
  • 序列化:Hessian二進制序列化
  • 適用範圍:傳入傳出參數數據包較大,提供者比消費者個數多,提供者壓力較大,可傳文件。
  • 適用場景:頁面傳輸,文件傳輸,或與原生hessian服務互操作

依賴

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.7</version>
</dependency>

約束

  • 參數及返回值需實現 Serializable 接口
  • 參數及返回值不能自定義實現 List, Map, Number, Date, Calendar 等接口,只能用 JDK 自帶的實現,因爲 hessian 會做特殊處理,自定義實現類中的屬性值都會丟失。

配置

定義 hessian 協議:

<dubbo:protocol name="hessian" port="8080" server="jetty" />

設置默認協議:

<dubbo:provider protocol="hessian" />

設置 service 協議:

<dubbo:service protocol="hessian" />

多端口:

<dubbo:protocol id="hessian1" name="hessian" port="8080" />
<dubbo:protocol id="hessian2" name="hessian" port="8081" />

直連:

<dubbo:reference id="helloService" interface="HelloWorld" url="hessian://10.20.153.10:8080/helloWorld" 

http://

基於 HTTP 表單的遠程調用協議,採用 Spring 的 HttpInvoker 實現

特性

  • 連接個數:多連接
  • 連接方式:短連接
  • 傳輸協議:HTTP
  • 傳輸方式:同步傳輸
  • 序列化:表單序列化
  • 適用範圍:傳入傳出參數數據包大小混合,提供者比消費者個數多,可用瀏覽器查看,可用表單或URL傳入參數,暫不支持傳文件。
  • 適用場景:需同時給應用程序和瀏覽器 JS 使用的服務。

約束

  • 參數及返回值需符合 Bean 規範

配置

配置協議:

<dubbo:protocol name="http" port="8080" />

配置 Jetty Server (默認):

<dubbo:protocol ... server="jetty" />

配置 Servlet Bridge Server (推薦使用):

<dubbo:protocol ... server="servlet" />

配置 DispatcherServlet:

<servlet>
         <servlet-name>dubbo</servlet-name>
         <servlet-class>org.apache.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
         <servlet-name>dubbo</servlet-name>
         <url-pattern>/*</url-pattern>
</servlet-mapping>

注意,如果使用 servlet 派發請求:

  • 協議的端口 <dubbo:protocol port="8080" /> 必須與 servlet 容器的端口相同,
  • 協議的上下文路徑 <dubbo:protocol contextpath="foo" /> 必須與 servlet 應用的上下文路徑相同。

webservice://

基於 WebService 的遠程調用協議,基於 Apache CXF 的 frontend-simpletransports-http 實現。

可以和原生 WebService 服務互操作,即:

  • 提供者用 Dubbo 的 WebService 協議暴露服務,消費者直接用標準 WebService 接口調用,
  • 或者提供方用標準 WebService 暴露服務,消費方用 Dubbo 的 WebService 協議調用。

依賴

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-frontend-simple</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-transports-http</artifactId>
    <version>2.6.1</version>
</dependency>

特性

  • 連接個數:多連接
  • 連接方式:短連接
  • 傳輸協議:HTTP
  • 傳輸方式:同步傳輸
  • 序列化:SOAP 文本序列化
  • 適用場景:系統集成,跨語言調用

約束

  • 參數及返回值需實現 Serializable 接口
  • 參數儘量使用基本類型和 POJO

配置

配置協議:

<dubbo:protocol name="webservice" port="8080" server="jetty" />

配置默認協議:

<dubbo:provider protocol="webservice" />

配置服務協議:

<dubbo:service protocol="webservice" />

多端口:

<dubbo:protocol id="webservice1" name="webservice" port="8080" />
<dubbo:protocol id="webservice2" name="webservice" port="8081" />

直連:

<dubbo:reference id="helloService" interface="HelloWorld" url="webservice://10.20.153.10:8080/com.foo.HelloWorld" />

WSDL:

http://10.20.153.10:8080/com.foo.HelloWorld?wsdl

Jetty Server (默認):

<dubbo:protocol ... server="jetty" />

Servlet Bridge Server (推薦):

<dubbo:protocol ... server="servlet" />

配置 DispatcherServlet:

<servlet>
         <servlet-name>dubbo</servlet-name>
         <servlet-class>org.apache.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
         <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
         <servlet-name>dubbo</servlet-name>
         <url-pattern>/*</url-pattern>
</servlet-mapping>

注意,如果使用 servlet 派發請求:

  • 協議的端口 <dubbo:protocol port="8080" /> 必須與 servlet 容器的端口相同,
  • 協議的上下文路徑 <dubbo:protocol contextpath="foo" /> 必須與 servlet 應用的上下文路徑相同。

thrift://

當前 dubbo 支持的 thrift 協議是對 thrift 原生協議 [2] 的擴展,在原生協議的基礎上添加了一些額外的頭信息,比如 service name,magic number 等。

使用 dubbo thrift 協議同樣需要使用 thrift 的 idl compiler 編譯生成相應的 java 代碼,後續版本中會在這方面做一些增強。

依賴

<dependency>
    <groupId>org.apache.thrift</groupId>
    <artifactId>libthrift</artifactId>
    <version>0.8.0</version>
</dependency>

配置

所有服務共用一個端口 :

<dubbo:protocol name="thrift" port="3030" />

memcached://

基於 memcached 實現的 RPC 協議 。

註冊 memcached 服務的地址

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("memcached://10.20.153.11/com.foo.BarService?category=providers&dynamic=false&application=foo&group=member&loadbalance=consistenthash"));

在客戶端引用

在客戶端使用:

<dubbo:reference id="cache" interface="java.util.Map" group="member" />

或者,點對點直連:

<dubbo:reference id="cache" interface="java.util.Map" url="memcached://10.20.153.10:11211" />

也可以使用自定義接口:

<dubbo:reference id="cache" interface="com.foo.CacheService" url="memcached://10.20.153.10:11211" />

方法名建議和 memcached 的標準方法名相同,即:get(key), set(key, value), delete(key)。

如果方法名和 memcached 的標準方法名不相同,則需要配置映射關係:

<dubbo:reference id="cache" interface="com.foo.CacheService" url="memcached://10.20.153.10:11211" p:set="

redis://

基於 Redis 實現的 RPC 協議 。

註冊 redis 服務的地址

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("redis://10.20.153.11/com.foo.BarService?category=providers&dynamic=false&application=foo&group=member&loadbalance=consistenthash"));

在客戶端引用

在客戶端使用:

<dubbo:reference id="store" interface="java.util.Map" group="member" />

或者,點對點直連:

<dubbo:reference id="store" interface="java.util.Map" url="redis://10.20.153.10:6379" />

也可以使用自定義接口:

<dubbo:reference id="store" interface="com.foo.StoreService" url="redis://10.20.153.10:6379" />

方法名建議和 redis 的標準方法名相同,即:get(key), set(key, value), delet(key)。

如果方法名和 redis 的標準方法名不相同,則需要配置映射關係:

<dubbo:reference id="cache" interface="com.foo.CacheService" url="redis://10.20.153.10:6379" p:set="putFo 

rest://

基於標準的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡寫)實現的REST調用支持

快速入門

在dubbo中開發一個REST風格的服務會比較簡單,下面以一個註冊用戶的簡單服務爲例說明。

這個服務要實現的功能是提供如下URL(注:這個URL不是完全符合REST的風格,但是更簡單實用):

http://localhost:8080/users/register

而任何客戶端都可以將包含用戶信息的JSON字符串POST到以上URL來完成用戶註冊。

首先,開發服務的接口:

public class UserService {    
   void registerUser(User user);
}

然後,開發服務的實現:

@Path("users")
public class UserServiceImpl implements UserService {
       
    @POST
    @Path("register")
    @Consumes({MediaType.APPLICATION_JSON})
    public void registerUser(User user) {
        // save the user...
    }
}

上面的實現非常簡單,但是由於該 REST 服務是要發佈到指定 URL 上,供任意語言的客戶端甚至瀏覽器來訪問,所以這裏額外添加了幾個 JAX-RS 的標準 annotation 來做相關的配置。

@Path(“users”):指定訪問UserService的URL相對路徑是/users,即http://localhost:8080/users

@Path(“register”):指定訪問registerUser()方法的URL相對路徑是/register,再結合上一個@Path爲UserService指定的路徑,則調用UserService.register()的完整路徑爲http://localhost:8080/users/register

@POST:指定訪問registerUser()用HTTP POST方法

@Consumes({MediaType.APPLICATION_JSON}):指定registerUser()接收JSON格式的數據。REST框架會自動將JSON數據反序列化爲User對象

最後,在spring配置文件中添加此服務,即完成所有服務開發工作:

<!-- 用rest協議在8080端口暴露服務 -->
<dubbo:protocol name="rest" port="8080"/>

<!-- 聲明需要暴露的服務接口 -->
<dubbo:service interface="xxx.UserService" ref="userService"/>

<!-- 和本地bean一樣實現服務 -->
<bean id="userService" class="xxx.UserServiceImpl" />

REST服務提供端詳解

下面我們擴充“快速入門”中的UserService,進一步展示在dubbo中REST服務提供端的開發要點。

HTTP POST/GET的實現

REST服務中雖然建議使用HTTP協議中四種標準方法POST、DELETE、PUT、GET來分別實現常見的“增刪改查”,但實際中,我們一般情況直接用POST來實現“增改”,GET來實現“刪查”即可(DELETE和PUT甚至會被一些防火牆阻擋)。

前面已經簡單演示了POST的實現,在此,我們爲UserService添加一個獲取註冊用戶資料的功能,來演示GET的實現。

這個功能就是要實現客戶端通過訪問如下不同URL來獲取不同ID的用戶資料:

http://localhost:8080/users/1001
http://localhost:8080/users/1002
http://localhost:8080/users/1003

當然,也可以通過其他形式的URL來訪問不同ID的用戶資料,例如:

http://localhost:8080/users/load?id=1001

JAX-RS本身可以支持所有這些形式。但是上面那種在URL路徑中包含查詢參數的形式(http://localhost:8080/users/1001) 更符合REST的一般習慣,所以更推薦大家來使用。下面我們就爲UserService添加一個getUser()方法來實現這種形式的URL訪問:

@GET
@Path("{id : \\d+}")
@Produces({MediaType.APPLICATION_JSON})
public User getUser(@PathParam("id") Long id) {
    // ...
}

@GET:指定用HTTP GET方法訪問

@Path("{id : \d+}"):根據上面的功能需求,訪問getUser()的URL應當是“http://localhost:8080/users + 任意數字",並且這個數字要被做爲參數傳入getUser()方法。 這裏的annotation配置中,@Path中間的{id: xxx}指定URL相對路徑中包含了名爲id參數,而它的值也將被自動傳遞給下面用@PathParam(“id”)修飾的方法參數id。{id:後面緊跟的\d+是一個正則表達式,指定了id參數必須是數字。

@Produces({MediaType.APPLICATION_JSON}):指定getUser()輸出JSON格式的數據。框架會自動將User對象序列化爲JSON數據。

Annotation放在接口類還是實現類

在Dubbo中開發REST服務主要都是通過JAX-RS的annotation來完成配置的,在上面的示例中,我們都是將annotation放在服務的實現類中。但其實,我們完全也可以將annotation放到服務的接口上,這兩種方式是完全等價的,例如:

@Path("users")
public interface UserService {
    
    @GET
    @Path("{id : \\d+}")
    @Produces({MediaType.APPLICATION_JSON})
    User getUser(@PathParam("id") Long id);
}

在一般應用中,我們建議將annotation放到服務實現類,這樣annotation和java實現代碼位置更接近,更便於開發和維護。另外更重要的是,我們一般傾向於避免對接口的污染,保持接口的純淨性和廣泛適用性。

但是,如後文所述,如果我們要用dubbo直接開發的消費端來訪問此服務,則annotation必須放到接口上。

如果接口和實現類都同時添加了annotation,則實現類的annotation配置會生效,接口上的annotation被直接忽略。

JSON、XML等多數據格式的支持

在dubbo中開發的REST服務可以同時支持傳輸多種格式的數據,以給客戶端提供最大的靈活性。其中我們目前對最常用的JSON和XML格式特別添加了額外的功能。

比如,我們要讓上例中的getUser()方法支持分別返回JSON和XML格式的數據,只需要在annotation中同時包含兩種格式即可:

@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
User getUser(@PathParam("id") Long id);

或者也可以直接用字符串(還支持通配符)表示MediaType:

@Produces({"application/json", "text/xml"})
User getUser(@PathParam("id") Long id);

如果所有方法都支持同樣類型的輸入輸出數據格式,則我們無需在每個方法上做配置,只需要在服務類上添加annotation即可:

@Path("users")
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
public class UserServiceImpl implements UserService {
    // ...
}

在一個REST服務同時對多種數據格式支持的情況下,根據JAX-RS標準,一般是通過HTTP中的MIME header(content-type和accept)來指定當前想用的是哪種格式的數據。

但是在dubbo中,我們還自動支持目前業界普遍使用的方式,即用一個URL後綴(.json和.xml)來指定想用的數據格式。例如,在添加上述annotation後,直接訪問http://localhost:8888/users/1001.json則表示用json格式,直接訪問http://localhost:8888/users/1002.xml則表示用xml格式,比用HTTP Header更簡單直觀。Twitter、微博等的REST API都是採用這種方式。

如果你既不加HTTP header,也不加後綴,則dubbo的REST會優先啓用在以上annotation定義中排位最靠前的那種數據格式。

注意:這裏要支持XML格式數據,在annotation中既可以用MediaType.TEXT_XML,也可以用MediaType.APPLICATION_XML,但是TEXT_XML是更常用的,並且如果要利用上述的URL後綴方式來指定數據格式,只能配置爲TEXT_XML才能生效。

中文字符支持

爲了在dubbo REST中正常輸出中文字符,和通常的Java web應用一樣,我們需要將HTTP響應的contentType設置爲UTF-8編碼。

基於JAX-RS的標準用法,我們只需要做如下annotation配置即可:

@Produces({"application/json; charset=UTF-8", "text/xml; charset=UTF-8"})
User getUser(@PathParam("id") Long id);

爲了方便用戶,我們在dubbo REST中直接添加了一個支持類,來定義以上的常量,可以直接使用,減少出錯的可能性。

@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
User getUser(@PathParam("id") Long id);

XML數據格式的額外要求

由於JAX-RS的實現一般都用標準的JAXB(Java API for XML Binding)來序列化和反序列化XML格式數據,所以我們需要爲每一個要用XML傳輸的對象添加一個類級別的JAXB annotation,否則序列化將報錯。例如爲getUser()中返回的User添加如下:

@XmlRootElement
public class User implements Serializable {
    // ...
}

此外,如果service方法中的返回值是Java的 primitive類型(如int,long,float,double等),最好爲它們添加一層wrapper對象,因爲JAXB不能直接序列化primitive類型。

例如,我們想讓前述的registerUser()方法返回服務器端爲用戶生成的ID號:

long registerUser(User user);

由於primitive類型不被JAXB序列化支持,所以添加一個wrapper對象:

@XmlRootElement
public class RegistrationResult implements Serializable {
    
    private Long id;
    
    public RegistrationResult() {
    }
    
    public RegistrationResult(Long id) {
        this.id = id;
    }
    
    public Long getId() {
        return id;
    }
    
    public void setId(Long id) {
        this.id = id;
    }
}

並修改service方法:

RegistrationResult registerUser(User user);

這樣不但能夠解決XML序列化的問題,而且使得返回的數據都符合XML和JSON的規範。例如,在JSON中,返回的將是如下形式:

{"id": 1001}

如果不加wrapper,JSON返回值將直接是

1001 	

而在XML中,加wrapper後返回值將是:

<registrationResult>
    <id>1002</id>
</registrationResult>

這種wrapper對象其實利用所謂Data Transfer Object(DTO)模式,採用DTO還能對傳輸數據做更多有用的定製。

定製序列化

如上所述,REST的底層實現會在service的對象和JSON/XML數據格式之間自動做序列化/反序列化。但有些場景下,如果覺得這種自動轉換不滿足要求,可以對其做定製。

Dubbo中的REST實現是用JAXB做XML序列化,用Jackson做JSON序列化,所以在對象上添加JAXB或Jackson的annotation即可以定製映射。

例如,定製對象屬性映射到XML元素的名字:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class User implements Serializable {
    
    @XmlElement(name="username") 
    private String name;  
}

定製對象屬性映射到JSON字段的名字:

public class User implements Serializable {
    
    @JsonProperty("username")
    private String name;
}

更多資料請參考JAXB和Jackson的官方文檔,或自行google。

配置REST Server的實現

目前在dubbo中,我們支持5種嵌入式rest server的實現,並同時支持採用外部應用服務器來做rest server的實現。rest server可以通過如下配置實現:

<dubbo:protocol name="rest" server="jetty"/>

以上配置選用了嵌入式的jetty來做rest server,同時,如果不配置server屬性,rest協議默認也是選用jetty。jetty是非常成熟的java servlet容器,並和dubbo已經有較好的集成(目前5種嵌入式server中只有jetty和後面所述的tomcat、tjws,與dubbo監控系統等完成了無縫的集成),所以,如果你的dubbo系統是單獨啓動的進程,你可以直接默認採用jetty即可。

<dubbo:protocol name="rest" server="tomcat"/>

以上配置選用了嵌入式的tomcat來做rest server。在嵌入式tomcat上,REST的性能比jetty上要好得多(參見後面的基準測試),建議在需要高性能的場景下采用tomcat。

<dubbo:protocol name="rest" server="netty"/>

以上配置選用嵌入式的netty來做rest server。(TODO more contents to add)

<dubbo:protocol name="rest" server="tjws"/> (tjws is now deprecated)
<dubbo:protocol name="rest" server="sunhttp"/>

以上配置選用嵌入式的tjws或Sun HTTP server來做rest server。這兩個server實現非常輕量級,非常方便在集成測試中快速啓動使用,當然也可以在負荷不高的生產環境中使用。 注:tjws目前已經被deprecated掉了,因爲它不能很好的和servlet 3.1 API工作。

如果你的dubbo系統不是單獨啓動的進程,而是部署到了Java應用服務器中,則建議你採用以下配置:

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

通過將server設置爲servlet,dubbo將採用外部應用服務器的servlet容器來做rest server。同時,還要在dubbo系統的web.xml中添加如下配置:

<web-app>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/classes/META-INF/spring/dubbo-demo-provider.xml</param-value>
    </context-param>
    
    <listener>
        <listener-class>org.apache.dubbo.remoting.http.servlet.BootstrapListener</listener-class>
    </listener>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.apache.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

即必須將dubbo的BootstrapListener和DispatherServlet添加到web.xml,以完成dubbo的REST功能與外部servlet容器的集成。

注意:如果你是用spring的ContextLoaderListener來加載spring,則必須保證BootstrapListener配置在ContextLoaderListener之前,否則dubbo初始化會出錯。

其實,這種場景下你依然可以堅持用嵌入式server,但外部應用服務器的servlet容器往往比嵌入式server更加強大(特別是如果你是部署到更健壯更可伸縮的WebLogic,WebSphere等),另外有時也便於在應用服務器做統一管理、監控等等。

獲取上下文(Context)信息

在遠程調用中,值得獲取的上下文信息可能有很多種,這裏特別以獲取客戶端IP爲例。

在dubbo的REST中,我們有兩種方式獲取客戶端IP。

第一種方式,用JAX-RS標準的@Context annotation:

public User getUser(@PathParam("id") Long id, @Context HttpServletRequest request) {
    System.out.println("Client address is " + request.getRemoteAddr());
} 

用Context修飾getUser()的一個方法參數後,就可以將當前的HttpServletRequest注入進來,然後直接調用servlet api獲取IP。

注意:這種方式只能在將server設置爲 tjws、tomcat、jetty 或者 servlet 的時候才能工作,因爲只有這幾種 server 的實現才提供了 servlet 容器。另外,標準的JAX-RS還支持用@Context修飾service類的一個實例字段來獲取HttpServletRequest,但在dubbo中我們沒有對此作出支持。

第二種方式,用dubbo中常用的RpcContext:

public User getUser(@PathParam("id") Long id) {
    System.out.println("Client address is " + RpcContext.getContext().getRemoteAddressString());
} 

注意:這種方式只能在設置server="jetty"或者server="tomcat"或者server="servlet"或者server="tjws"的時候才能工作。另外,目前dubbo的RpcContext是一種比較有侵入性的用法,未來我們很可能會做出重構。

如果你想保持你的項目對JAX-RS的兼容性,未來脫離dubbo也可以運行,請選擇第一種方式。如果你想要更優雅的服務接口定義,請選用第二種方式。

此外,在最新的dubbo rest中,還支持通過RpcContext來獲取HttpServletRequest和HttpServletResponse,以提供更大的靈活性來方便用戶實現某些複雜功能,比如在dubbo標準的filter中訪問HTTP Header。用法示例如下:

if (RpcContext.getContext().getRequest() != null && RpcContext.getContext().getRequest() instanceof HttpServletRequest) {
    System.out.println("Client address is " + ((HttpServletRequest) RpcContext.getContext().getRequest()).getRemoteAddr());
}

if (RpcContext.getContext().getResponse() != null && RpcContext.getContext().getResponse() instanceof HttpServletResponse) {
    System.out.println("Response object from RpcContext: " + RpcContext.getContext().getResponse());
}

注意:爲了保持協議的中立性,RpcContext.getRequest()和RpcContext.getResponse()返回的僅僅是一個Object類,而且可能爲null。所以,你必須自己做null和類型的檢查。

注意:只有在設置server="jetty"或者server="tomcat"或者server="servlet"的時候,你才能通過以上方法正確的得到HttpServletRequest和HttpServletResponse,因爲只有這幾種server實現了servlet容器。

爲了簡化編程,在此你也可以用泛型的方式來直接獲取特定類型的request/response:

if (RpcContext.getContext().getRequest(HttpServletRequest.class) != null) {
    System.out.println("Client address is " + RpcContext.getContext().getRequest(HttpServletRequest.class).getRemoteAddr());
}

if (RpcContext.getContext().getResponse(HttpServletResponse.class) != null) {
    System.out.println("Response object from RpcContext: " + RpcContext.getContext().getResponse(HttpServletResponse.class));
}

如果request/response不符合指定的類型,這裏也會返回null。

配置端口號和Context Path

dubbo中的rest協議默認將採用80端口,如果想修改端口,直接配置:

<dubbo:protocol name="rest" port="8888"/>

另外,如前所述,我們可以用@Path來配置單個rest服務的URL相對路徑。但其實,我們還可以設置一個所有rest服務都適用的基礎相對路徑,即java web應用中常說的context path。

只需要添加如下contextpath屬性即可:

<dubbo:protocol name="rest" port="8888" contextpath="services"/>

以前面代碼爲例:

@Path("users")
public class UserServiceImpl implements UserService {
       
    @POST
    @Path("register")
    @Consumes({MediaType.APPLICATION_JSON})
    public void registerUser(User user) {
        // save the user...
    }	
}

現在registerUser()的完整訪問路徑爲:

http://localhost:8888/services/users/register

注意:如果你是選用外部應用服務器做rest server,即配置:

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

則必須保證這裏設置的port、contextpath,與外部應用服務器的端口、DispatcherServlet的上下文路徑(即webapp path加上servlet url pattern)保持一致。例如,對於部署爲tomcat ROOT路徑的應用,這裏的contextpath必須與web.xml中DispacherServlet的<url-pattern/>完全一致:

<servlet-mapping>
     <servlet-name>dispatcher</servlet-name>
     <url-pattern>/services/*</url-pattern>
</servlet-mapping>

配置線程數和IO線程數

可以爲rest服務配置線程池大小:

<dubbo:protocol name="rest" threads="500"/>

注意:目前線程池的設置只有當server="netty"或者server="jetty"或者server="tomcat"的時候才能生效。另外,如果server=“servlet”,由於這時候啓用的是外部應用服務器做rest server,不受dubbo控制,所以這裏的線程池設置也無效。

如果是選用netty server,還可以配置Netty的IO worker線程數:

<dubbo:protocol name="rest" iothreads="5" threads="100"/>

配置長連接

Dubbo中的rest服務默認都是採用http長連接來訪問,如果想切換爲短連接,直接配置:

<dubbo:protocol name="rest" keepalive="false"/>

注意:這個配置目前只對server="netty"和server="tomcat"才能生效。

配置最大的HTTP連接數

可以配置服務器提供端所能同時接收的最大HTTP連接數,防止REST server被過多連接撐爆,以作爲一種最基本的自我保護機制:

<dubbo:protocol name="rest" accepts="500" server="tomcat/>

注意:這個配置目前只對server="tomcat"才能生效。

配置每個消費端的超時時間和HTTP連接數

如果rest服務的消費端也是dubbo系統,可以像其他dubbo RPC機制一樣,配置消費端調用此rest服務的最大超時時間以及每個消費端所能啓動的最大HTTP連接數。

<dubbo:service interface="xxx" ref="xxx" protocol="rest" timeout="2000" connections="10"/>

當然,由於這個配置針對消費端生效的,所以也可以在消費端配置:

<dubbo:reference id="xxx" interface="xxx" timeout="2000" connections="10"/>

但是,通常我們建議配置在服務提供端提供此類配置。按照dubbo官方文檔的說法:“Provider上儘量多配置Consumer端的屬性,讓Provider實現者一開始就思考Provider服務特點、服務質量的問題。”

注意:如果dubbo的REST服務是發佈給非dubbo的客戶端使用,則這裏<dubbo:service/>上的配置完全無效,因爲這種客戶端不受dubbo控制。

用Annotation取代部分Spring XML配置

以上所有的討論都是基於dubbo在spring中的xml配置。但是,dubbo/spring本身也支持用annotation來作配置,所以我們也可以按dubbo官方文檔中的步驟,把相關annotation加到REST服務的實現中,取代一些xml配置,例如:

@Service(protocol = "rest")
@Path("users")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;
       
    @POST
    @Path("register")
    @Consumes({MediaType.APPLICATION_JSON})
    public void registerUser(User user) {
        // save the user
        userRepository.save(user);
    }	
}

annotation的配置更簡單更精確,通常也更便於維護(當然現代IDE都可以在xml中支持比如類名重構,所以就這裏的特定用例而言,xml的維護性也很好)。而xml對代碼的侵入性更小一些,尤其有利於動態修改配置,特別是比如你要針對單個服務配置連接超時時間、每客戶端最大連接數、集羣策略、權重等等。另外,特別對複雜應用或者模塊來說,xml提供了一箇中心點來涵蓋的所有組件和配置,更一目瞭然,一般更便於項目長時期的維護。

當然,選擇哪種配置方式沒有絕對的優劣,和個人的偏好也不無關係。

添加自定義的Filter、Interceptor等

Dubbo的REST也支持JAX-RS標準的Filter和Interceptor,以方便對REST的請求與響應過程做定製化的攔截處理。

其中,Filter主要用於訪問和設置HTTP請求和響應的參數、URI等等。例如,設置HTTP響應的cache header:

public class CacheControlFilter implements ContainerResponseFilter {

    public void filter(ContainerRequestContext req, ContainerResponseContext res) {
        if (req.getMethod().equals("GET")) {
            res.getHeaders().add("Cache-Control", "someValue");
        }
    }
}

Interceptor主要用於訪問和修改輸入與輸出字節流,例如,手動添加GZIP壓縮:

public class GZIPWriterInterceptor implements WriterInterceptor {
 
    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
                    throws IOException, WebApplicationException {
        OutputStream outputStream = context.getOutputStream();
        context.setOutputStream(new GZIPOutputStream(outputStream));
        context.proceed();
    }
}

在標準JAX-RS應用中,我們一般是爲Filter和Interceptor添加@Provider annotation,然後JAX-RS runtime會自動發現並啓用它們。而在dubbo中,我們是通過添加XML配置的方式來註冊Filter和Interceptor:

<dubbo:protocol name="rest" port="8888" extension="xxx.TraceInterceptor, xxx.TraceFilter"/>

在此,我們可以將Filter、Interceptor和DynamicFeature這三種類型的對象都添加到extension屬性上,多個之間用逗號分隔。(DynamicFeature是另一個接口,可以方便我們更動態的啓用Filter和Interceptor,感興趣請自行google。)

當然,dubbo自身也支持Filter的概念,但我們這裏討論的Filter和Interceptor更加接近協議實現的底層,相比dubbo的filter,可以做更底層的定製化。

注:這裏的XML屬性叫extension,而不是叫interceptor或者filter,是因爲除了Interceptor和Filter,未來我們還會添加更多的擴展類型。

如果REST的消費端也是dubbo系統(參見下文的討論),則也可以用類似方式爲消費端配置Interceptor和Filter。但注意,JAX-RS中消費端的Filter和提供端的Filter是兩種不同的接口。例如前面例子中服務端是ContainerResponseFilter接口,而消費端對應的是ClientResponseFilter:

public class LoggingFilter implements ClientResponseFilter {
 
    public void filter(ClientRequestContext reqCtx, ClientResponseContext resCtx) throws IOException {
        System.out.println("status: " + resCtx.getStatus());
	    System.out.println("date: " + resCtx.getDate());
	    System.out.println("last-modified: " + resCtx.getLastModified());
	    System.out.println("location: " + resCtx.getLocation());
	    System.out.println("headers:");
	    for (Entry<String, List<String>> header : resCtx.getHeaders().entrySet()) {
     	    System.out.print("\t" + header.getKey() + " :");
	        for (String value : header.getValue()) {
	            System.out.print(value + ", ");
	        }
	        System.out.print("\n");
	    }
	    System.out.println("media-type: " + resCtx.getMediaType().getType());
    } 
}

添加自定義的Exception處理

Dubbo的REST也支持JAX-RS標準的ExceptionMapper,可以用來定製特定exception發生後應該返回的HTTP響應。

public class CustomExceptionMapper implements ExceptionMapper<NotFoundException> {

    public Response toResponse(NotFoundException e) {     
        return Response.status(Response.Status.NOT_FOUND).entity("Oops! the requested resource is not found!").type("text/plain").build();
    }
}

和Interceptor、Filter類似,將其添加到XML配置文件中即可啓用:

<dubbo:protocol name="rest" port="8888" extension="xxx.CustomExceptionMapper"/>

配置HTTP日誌輸出

Dubbo rest支持輸出所有HTTP請求/響應中的header字段和body消息體。

在XML配置中添加如下自帶的REST filter:

<dubbo:protocol name="rest" port="8888" extension="org.apache.dubbo.rpc.protocol.rest.support.LoggingFilter"/>

然後配置在logging配置中至少爲org.apache.dubbo.rpc.protocol.rest.support打開INFO級別日誌輸出,例如,在log4j.xml中配置:

<logger name="org.apache.dubbo.rpc.protocol.rest.support">
    <level value="INFO"/>
    <appender-ref ref="CONSOLE"/>
</logger>

當然,你也可以直接在ROOT logger打開INFO級別日誌輸出:

<root>
	<level value="INFO" />
	<appender-ref ref="CONSOLE"/>
</root>

然後在日誌中會有類似如下的內容輸出:

The HTTP headers are: 
accept: application/json;charset=UTF-8
accept-encoding: gzip, deflate
connection: Keep-Alive
content-length: 22
content-type: application/json
host: 192.168.1.100:8888
user-agent: Apache-HttpClient/4.2.1 (java 1.5)
The contents of request body is: 
{"id":1,"name":"dang"}

打開HTTP日誌輸出後,除了正常日誌輸出的性能開銷外,也會在比如HTTP請求解析時產生額外的開銷,因爲需要建立額外的內存緩衝區來爲日誌的輸出做數據準備。

輸入參數的校驗

dubbo的rest支持採用Java標準的bean validation annotation(JSR 303)來做輸入校驗http://beanvalidation.org/

爲了和其他dubbo遠程調用協議保持一致,在rest中作校驗的annotation必須放在服務的接口上,例如:

public interface UserService {
   
    User getUser(@Min(value=1L, message="User ID must be greater than 1") Long id);
}

當然,在很多其他的bean validation的應用場景都是將annotation放到實現類而不是接口上。把annotation放在接口上至少有一個好處是,dubbo的客戶端可以共享這個接口的信息,dubbo甚至不需要做遠程調用,在本地就可以完成輸入校驗。

然後按照dubbo的標準方式在XML配置中打開驗證:

<dubbo:service interface=xxx.UserService" ref="userService" protocol="rest" validation="true"/>

在dubbo的其他很多遠程調用協議中,如果輸入驗證出錯,是直接將RpcException拋向客戶端,而在rest中由於客戶端經常是非dubbo,甚至非java的系統,所以不便直接拋出Java異常。因此,目前我們將校驗錯誤以XML的格式返回:

<violationReport>
    <constraintViolations>
        <path>getUserArgument0</path>
        <message>User ID must be greater than 1</message>
        <value>0</value>
    </constraintViolations>
</violationReport>

稍後也會支持其他數據格式的返回值。至於如何對驗證錯誤消息作國際化處理,直接參考bean validation的相關文檔即可。

如果你認爲默認的校驗錯誤返回格式不符合你的要求,可以如上面章節所述,添加自定義的ExceptionMapper來自由的定製錯誤返回格式。需要注意的是,這個ExceptionMapper必須用泛型聲明來捕獲dubbo的RpcException,才能成功覆蓋dubbo rest默認的異常處理策略。爲了簡化操作,其實這裏最簡單的方式是直接繼承dubbo rest的RpcExceptionMapper,並覆蓋其中處理校驗異常的方法即可:

public class MyValidationExceptionMapper extends RpcExceptionMapper {

    protected Response handleConstraintViolationException(ConstraintViolationException cve) {
        ViolationReport report = new ViolationReport();
        for (ConstraintViolation cv : cve.getConstraintViolations()) {
            report.addConstraintViolation(new RestConstraintViolation(
                    cv.getPropertyPath().toString(),
                    cv.getMessage(),
                    cv.getInvalidValue() == null ? "null" : cv.getInvalidValue().toString()));
        }
        // 採用json輸出代替xml輸出
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(report).type(ContentType.APPLICATION_JSON_UTF_8).build();
    }
}

然後將這個ExceptionMapper添加到XML配置中即可:

<dubbo:protocol name="rest" port="8888" extension="xxx.MyValidationExceptionMapper"/>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章