1. 程式人生 > >在Dubbo中開發REST風格的遠端呼叫(RESTful Remoting)

在Dubbo中開發REST風格的遠端呼叫(RESTful Remoting)


dubbo支援多種遠端呼叫方式,例如dubbo RPC(二進位制序列化 + tcp協議)、http invoker(二進位制序列化 + http協議,至少在開源版本沒發現對文字序列化的支援)、hessian(二進位制序列化 + http協議)、WebServices (文字序列化 + http協議)等等,但缺乏對當今特別流行的REST風格遠端呼叫(文字序列化 + http協議)的支援。

有鑑於此,我們基於標準的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡寫),為dubbo提供了接近透明的REST呼叫支援。由於完全相容Java標準API,所以為dubbo開發的所有REST服務,未來脫離dubbo或者任何特定的REST底層實現一般也可以正常執行。

特別值得指出的是,我們並不需要完全嚴格遵守REST的原始定義和架構風格。即使著名的Twitter REST API也會根據情況做適度調整,而不是機械的遵守原始的REST風格。

附註:我們將這個功能稱之為REST風格的遠端呼叫,即RESTful Remoting(抽象的遠端處理或者呼叫),而不是叫RESTful 
RPC(具體的遠端“過程”呼叫),是因為REST和RPC本身可以被認為是兩種不同的風格。在dubbo的REST實現中,可以說有兩個面向,其一是提供或消費正常的REST服務,其二是將REST作為dubbo 
RPC體系中一種協議實現,而RESTful Remoting同時涵蓋了這個面向。

REST的優點

以下摘自維基百科:

  • 可更高效利用快取來提高響應速度
  • 通訊本身的無狀態性可以讓不同的伺服器的處理一系列請求中的不同請求,提高伺服器的擴充套件性
  • 瀏覽器即可作為客戶端,簡化軟體需求
  • 相對於其他疊加在HTTP協議之上的機制,REST的軟體依賴性更小
  • 不需要額外的資源發現機制
  • 在軟體技術演進中的長期的相容性更好

這裡我還想特別補充REST的顯著優點:基於簡單的文字格式訊息和通用的HTTP協議,使它具備極廣的適用性,幾乎所有語言和平臺都對它提供支援,同時其學習和使用的門檻也較低。

應用場景

正是由於REST在適用性方面的優點,所以在dubbo中支援REST,可以為當今多數主流的遠端呼叫場景都帶來(顯著)好處:

  1. 顯著簡化企業內部的異構系統之間的(跨語言)呼叫。此處主要針對這種場景:dubbo的系統做服務提供端,其他語言的系統(也包括某些不基於dubbo的java系統)做服務消費端,兩者通過HTTP和文字訊息進行通訊。即使相比Thrift、ProtoBuf等二進位制跨語言呼叫方案,REST也有自己獨特的優勢(詳見後面討論)

  2. 顯著簡化對外Open API(開放平臺)的開發。既可以用dubbo來開發專門的Open API應用,也可以將原內部使用的dubbo service直接“透明”釋出為對外的Open REST API(當然dubbo本身未來最好可以較透明的提供諸如許可權控制、頻次控制、計費等諸多功能)

  3. 顯著簡化手機(平板)APP或者PC桌面客戶端開發。類似於2,既可以用dubbo來開發專門針對無線或者桌面的伺服器端,也可以將原內部使用的dubbo service直接”透明“的暴露給手機APP或桌面程式。當然在有些專案中,手機或桌面程式也可以直接訪問以上場景2中所述的Open API。

  4. 顯著簡化瀏覽器AJAX應用的開發。類似於2,既可以用dubbo來開發專門的AJAX伺服器端,也可以將原內部使用的dubbo service直接”透明“的暴露給瀏覽器中JavaScript。當然,很多AJAX應用更適合與web框架協同工作,所以直接訪問dubbo service在很多web專案中未必是一種非常優雅的架構。

  5. 為企業內部的dubbo系統之間(即服務提供端和消費端都是基於dubbo的系統)提供一種基於文字的、易讀的遠端呼叫方式。

  6. 一定程度簡化dubbo系統對其它異構系統的呼叫。可以用類似dubbo的簡便方式“透明”的呼叫非dubbo系統提供的REST服務(不管服務提供端是在企業內部還是外部)

需要指出的是,我認為1~3是dubbo的REST呼叫最有價值的三種應用場景,並且我們為dubbo新增REST呼叫,其最主要到目的也是面向服務的提供端,即開發REST服務來提供給非dubbo的(異構)消費端。

歸納起來,所有應用場景如下圖所示: 
這裡寫圖片描述

借用Java過去最流行的宣傳語,為dubbo新增REST呼叫後,可以實現服務的”一次編寫,到處訪問“,理論上可以面向全世界開放,從而真正實現比較理想化的面向服務架構(SOA)。

當然,傳統的WebServices(WSDL/SOAP)也基本同樣能滿足以上場景(除了場景4)的要求(甚至還能滿足那些需要企業級特性的場景),但由於其複雜性等問題,現在已經越來越少被實際採用了。

快速入門

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

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

http://localhost:8080/users/register
  • 1

而任何客戶端都可以將包含使用者資訊的JSON字串POST到以上URL來完成使用者註冊。

首先,開發服務的介面:

public class UserService { 
   void registerUser(User user);
}
  • 1
  • 2
  • 3

然後,開發服務的實現:

@Path("users") 
public class UserServiceImpl implements UserService { 
    @POST 
    @Path("register") 
    @Consumes({MediaType.APPLICATION_JSON}) 
    public void registerUser(User user) {
       // save the user... 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面的服務實現程式碼非常簡單,但是由於REST服務是要被髮布到特定HTTP 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" />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

標準Java REST API:JAX-RS簡介

JAX-RS是標準的Java REST API,得到了業界的廣泛支援和應用,其著名的開源實現就有很多,包括Oracle的Jersey,RedHat的RestEasy,Apache的CXF和Wink,以及restlet等等。另外,所有支援JavaEE 6.0以上規範的商用JavaEE應用伺服器都對JAX-RS提供了支援。因此,JAX-RS是一種已經非常成熟的解決方案,並且採用它沒有任何所謂vendor lock-in的問題。

JAX-RS在網上的資料非常豐富,例如下面的入門教程:

更多的資料請自行google或者百度一下。就學習JAX-RS來說,一般主要掌握其各種annotation的用法即可。

注意:dubbo是基於JAX-RS 2.0版本的,有時候需要注意一下資料或REST實現所涉及的版本。

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
  • 1
  • 2
  • 3

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

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

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) { 
    // ... 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

@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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在一般應用中,我們建議將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);
  • 1
  • 2

或者也可以直接用字串(還支援萬用字元)表示MediaType:

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

如果所有方法都支援同樣型別的輸入輸出資料格式,則我們無需在每個方法上做配置,只需要在服務類上新增annotation即可:

@Path("users")
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML}) 
public class UserServiceImpl implements UserService { 
    // ... 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在一個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);
  • 1
  • 2

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

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

XML資料格式的額外要求

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

@XmlRootElement 
public class User implements Serializable {
  // ... 
}
  • 1
  • 2
  • 3
  • 4

此外,如果service方法中的返回值是Java的 primitive型別(如int,long,float,double等),最好為它們新增一層wrapper物件,因為JAXB不能直接序列化primitive型別。

例如,我們想讓前述的registerUser()方法返回伺服器端為使用者生成的ID號:

long registerUser(User user);
  • 1

由於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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

並修改service方法:

RegistrationResult registerUser(User user);
  • 1

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

{"id": 1001}
  • 1

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

1001
  • 1

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

<registrationResult>
    <id>1002</id>
</registrationResult>
  • 1
  • 2
  • 3

這種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;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

定製物件屬性對映到JSON欄位的名字:

public class User implements Serializable { 
    @JsonProperty("username") 
    private String name;
}
  • 1
  • 2
  • 3
  • 4

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

配置REST Server的實現

目前在dubbo中,我們支援5種嵌入式rest server的實現,並同時支援採用外部應用伺服器來做rest server的實現。rest server的實現是通過如下server這個XML屬性來選擇的:

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

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

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

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

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

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

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

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

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

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

通過將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>com.alibaba.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>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>/*</url-pattern>
    </servlet-mapping>
</web-app>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

即必須將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());
}
  • 1
  • 2

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

注意:這種方式只能在設定server=”tjws”或者server=”tomcat”或者server=”jetty”或者server=”servlet”的時候才能工作,因為只有這幾種REST 
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());
}
  • 1
  • 2
  • 3

注意:這種方式只能在設定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());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

注意:為了保持協議的中立性,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));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

配置埠號和Context Path

dubbo中的rest協議預設將採用80埠,如果想修改埠,直接配置:

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

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

只需要新增如下contextpath屬性即可:

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

以前面程式碼為例:

@Path("users") 
public class UserServiceImpl implements UserService { 
    @POST @Path("register") 
    @Consumes({MediaType.APPLICATION_JSON}) 
    public void registerUser(User user) { 
        // save the user... 
    }   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

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

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

則必須保證這裡設定的port、contextpath,與外部應用伺服器的埠、DispatcherServlet的上下文路徑(即webapp path加上servlet url pattern)保持一致。例如,對於部署為tomcat ROOT路徑的應用,這裡的contextpath必須與web.xml中DispacherServlet的完全一致:

<servlet-mapping>
     <servlet-name>dispatcher</servlet-name>
     <url-pattern>/services/*</url-pattern>
</servlet-mapping>
  • 1
  • 2
  • 3
  • 4

配置執行緒數和IO執行緒數

可以為rest服務配置執行緒池大小:

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

注意:目前執行緒池的設定只有當server=”netty”或者server=”jetty”或者server=”tomcat”的時候才能生效。另外,如果server=”servlet”,由於這時候啟用的是外部應用伺服器做rest server,不受dubbo控制,所以這裡的執行緒池設定也無效。

如果是選用netty server,還可以配置Netty的IO worker執行緒數:

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

配置長連線

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

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

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

配置最大的HTTP連線數

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

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

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

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

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

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

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

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

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

注意:如果dubbo的REST服務是釋出給非dubbo的客戶端使用,則這裡上的配置完全無效,因為這種客戶端不受dubbo控制。

GZIP資料壓縮

Dubbo的REST支援用GZIP壓縮請求和響應的資料,以減少網路傳輸時間和頻寬佔用,但這種方式會也增加CPU開銷。

TODO more contents to add

用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);
    }   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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

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

在此,我們可以將Filter、Interceptor和DynamicFuture這三種類型的物件都新增到extension屬性上,多個之間用逗號分隔。(DynamicFuture是另一個介面,可以方便我們更動態的啟用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());
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

新增自定義的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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

和Interceptor、Filter類似,將其新增到XML配置檔案中即可啟用: