Dubbo3 開發(fā) REST 應(yīng)用

2022-04-24 16:41 更新

在 Dubbo 中開發(fā) REST 風(fēng)格的遠(yuǎn)程調(diào)用

作者:沈理

文檔版權(quán):Apache 2.0許可證 署名-禁止演繹

本文篇幅較長(zhǎng),因?yàn)镽EST本身涉及面較多。另外,本文參照 Spring 等的文檔風(fēng)格,不僅僅局限于框架用法的闡述,同時(shí)也努力呈現(xiàn)框架的設(shè)計(jì)理念和優(yōu)良應(yīng)用的架構(gòu)思想。 對(duì)于想粗略了解 dubbo 和 REST 的人,只需瀏覽 概述 至 標(biāo)準(zhǔn)Java REST API:JAX-RS簡(jiǎn)介 幾節(jié)即可。

目錄

  • 概述
  • REST的優(yōu)點(diǎn)
  • 應(yīng)用場(chǎng)景
  • 快速入門
  • 標(biāo)準(zhǔn)Java REST API:JAX-RS簡(jiǎn)介
  • REST服務(wù)提供端詳解HTTP POST/GET的實(shí)現(xiàn)Annotation放在接口類還是實(shí)現(xiàn)類JSON、XML等多數(shù)據(jù)格式的支持中文字符支持XML數(shù)據(jù)格式的額外要求定制序列化配置REST Server的實(shí)現(xiàn)獲取上下文(Context)信息配置端口號(hào)和Context Path配置線程數(shù)和IO線程數(shù)配置長(zhǎng)連接配置最大的HTTP連接數(shù)配置每個(gè)消費(fèi)端的超時(shí)時(shí)間和HTTP連接數(shù)GZIP數(shù)據(jù)壓縮用Annotation取代部分Spring XML配置添加自定義的Filter、Interceptor等添加自定義的Exception處理配置HTTP日志輸出輸入?yún)?shù)的校驗(yàn)是否應(yīng)該透明發(fā)布REST服務(wù)Dubbo的REST提供端在被調(diào)用時(shí)使用header
  • REST服務(wù)消費(fèi)端詳解場(chǎng)景1:非dubbo的消費(fèi)端調(diào)用dubbo的REST服務(wù)場(chǎng)景2:dubbo消費(fèi)端調(diào)用dubbo的REST服務(wù)場(chǎng)景3:dubbo的消費(fèi)端調(diào)用非dubbo的REST服務(wù)Dubbo的消費(fèi)端在調(diào)用REST服務(wù)時(shí)配置自定義header
  • Dubbo中JAX-RS的限制
  • REST常見問題解答(REST FAQ)Dubbo REST的服務(wù)能和Dubbo注冊(cè)中心、監(jiān)控中心集成嗎?Dubbo REST中如何實(shí)現(xiàn)負(fù)載均衡和容錯(cuò)(failover)?JAX-RS中重載的方法能夠映射到同一URL地址嗎?JAX-RS中作POST的方法能夠接收多個(gè)參數(shù)嗎?
  • Dubbo當(dāng)前體系可能的不足之處(與REST相關(guān)的)RpcContext的侵入性Protocol配置的局限性XML命名不符合spring規(guī)范
  • REST最佳實(shí)踐
  • 性能基準(zhǔn)測(cè)試測(cè)試環(huán)境測(cè)試腳本測(cè)試結(jié)果
  • 擴(kuò)展討論REST與Thrift、Protobuf等的對(duì)比REST與傳統(tǒng)WebServices的對(duì)比JAX-RS與Spring MVC的對(duì)比
  • 未來

概述

dubbo支持多種遠(yuǎn)程調(diào)用方式,例如dubbo RPC(二進(jìn)制序列化 + tcp協(xié)議)、http invoker(二進(jìn)制序列化 + http協(xié)議,至少在開源版本沒發(fā)現(xiàn)對(duì)文本序列化的支持)、hessian(二進(jìn)制序列化 + http協(xié)議)、WebServices (文本序列化 + http協(xié)議)等等,但缺乏對(duì)當(dāng)今特別流行的REST風(fēng)格遠(yuǎn)程調(diào)用(文本序列化 + http協(xié)議)的支持。

有鑒于此,我們基于標(biāo)準(zhǔn)的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡(jiǎn)寫),為dubbo提供了接近透明的REST調(diào)用支持。由于完全兼容Java標(biāo)準(zhǔn)API,所以為dubbo開發(fā)的所有REST服務(wù),未來脫離dubbo或者任何特定的REST底層實(shí)現(xiàn)一般也可以正常運(yùn)行。

特別值得指出的是,我們并不需要完全嚴(yán)格遵守REST的原始定義和架構(gòu)風(fēng)格。即使著名的Twitter REST API也會(huì)根據(jù)情況做適度調(diào)整,而不是機(jī)械的遵守原始的REST風(fēng)格。

附注:我們將這個(gè)功能稱之為REST風(fēng)格的遠(yuǎn)程調(diào)用,即RESTful Remoting(抽象的遠(yuǎn)程處理或者調(diào)用),而不是叫RESTful RPC(具體的遠(yuǎn)程“過程”調(diào)用),是因?yàn)镽EST和RPC本身可以被認(rèn)為是兩種不同的風(fēng)格。在dubbo的REST實(shí)現(xiàn)中,可以說有兩個(gè)面向,其一是提供或消費(fèi)正常的REST服務(wù),其二是將REST作為dubbo RPC體系中一種協(xié)議實(shí)現(xiàn),而RESTful Remoting同時(shí)涵蓋了這兩個(gè)面向。

REST的優(yōu)點(diǎn)

以下摘自維基百科:

  • 可更高效利用緩存來提高響應(yīng)速度
  • 通訊本身的無狀態(tài)性可以讓不同的服務(wù)器的處理一系列請(qǐng)求中的不同請(qǐng)求,提高服務(wù)器的擴(kuò)展性
  • 瀏覽器即可作為客戶端,簡(jiǎn)化軟件需求
  • 相對(duì)于其他疊加在HTTP協(xié)議之上的機(jī)制,REST的軟件依賴性更小
  • 不需要額外的資源發(fā)現(xiàn)機(jī)制
  • 在軟件技術(shù)演進(jìn)中的長(zhǎng)期的兼容性更好

這里我還想特別補(bǔ)充REST的顯著優(yōu)點(diǎn):基于簡(jiǎn)單的文本格式消息和通用的HTTP協(xié)議,使它具備極廣的適用性,幾乎所有語言和平臺(tái)都對(duì)它提供支持,同時(shí)其學(xué)習(xí)和使用的門檻也較低。

應(yīng)用場(chǎng)景

正是由于REST在適用性方面的優(yōu)點(diǎn),所以在dubbo中支持REST,可以為當(dāng)今多數(shù)主流的遠(yuǎn)程調(diào)用場(chǎng)景都帶來(顯著)好處:

  1. 顯著簡(jiǎn)化企業(yè)內(nèi)部的異構(gòu)系統(tǒng)之間的(跨語言)調(diào)用。此處主要針對(duì)這種場(chǎng)景:dubbo的系統(tǒng)做服務(wù)提供端,其他語言的系統(tǒng)(也包括某些不基于dubbo的java系統(tǒng))做服務(wù)消費(fèi)端,兩者通過HTTP和文本消息進(jìn)行通信。即使相比Thrift、ProtoBuf等二進(jìn)制跨語言調(diào)用方案,REST也有自己獨(dú)特的優(yōu)勢(shì)(詳見后面討論)
  2. 顯著簡(jiǎn)化對(duì)外Open API(開放平臺(tái))的開發(fā)。既可以用dubbo來開發(fā)專門的Open API應(yīng)用,也可以將原內(nèi)部使用的dubbo service直接“透明”發(fā)布為對(duì)外的Open REST API(當(dāng)然dubbo本身未來最好可以較透明的提供諸如權(quán)限控制、頻次控制、計(jì)費(fèi)等諸多功能)
  3. 顯著簡(jiǎn)化手機(jī)(平板)APP或者PC桌面客戶端開發(fā)。類似于2,既可以用dubbo來開發(fā)專門針對(duì)無線或者桌面的服務(wù)器端,也可以將原內(nèi)部使用的dubbo service直接”透明“的暴露給手機(jī)APP或桌面程序。當(dāng)然在有些項(xiàng)目中,手機(jī)或桌面程序也可以直接訪問以上場(chǎng)景2中所述的Open API。
  4. 顯著簡(jiǎn)化瀏覽器AJAX應(yīng)用的開發(fā)。類似于2,既可以用dubbo來開發(fā)專門的AJAX服務(wù)器端,也可以將原內(nèi)部使用的dubbo service直接”透明“的暴露給瀏覽器中JavaScript。當(dāng)然,很多AJAX應(yīng)用更適合與web框架協(xié)同工作,所以直接訪問dubbo service在很多web項(xiàng)目中未必是一種非常優(yōu)雅的架構(gòu)。
  5. 為企業(yè)內(nèi)部的dubbo系統(tǒng)之間(即服務(wù)提供端和消費(fèi)端都是基于dubbo的系統(tǒng))提供一種基于文本的、易讀的遠(yuǎn)程調(diào)用方式。
  6. 一定程度簡(jiǎn)化dubbo系統(tǒng)對(duì)其它異構(gòu)系統(tǒng)的調(diào)用??梢杂妙愃芼ubbo的簡(jiǎn)便方式“透明”的調(diào)用非dubbo系統(tǒng)提供的REST服務(wù)(不管服務(wù)提供端是在企業(yè)內(nèi)部還是外部)

需要指出的是,我認(rèn)為1~3是dubbo的REST調(diào)用最有價(jià)值的三種應(yīng)用場(chǎng)景,并且我們?yōu)閐ubbo添加REST調(diào)用,其最主要到目的也是面向服務(wù)的提供端,即開發(fā)REST服務(wù)來提供給非dubbo的(異構(gòu))消費(fèi)端。

歸納起來,所有應(yīng)用場(chǎng)景如下圖所示: rest

借用Java過去最流行的宣傳語,為dubbo添加REST調(diào)用后,可以實(shí)現(xiàn)服務(wù)的”一次編寫,到處訪問“,理論上可以面向全世界開放,從而真正實(shí)現(xiàn)比較理想化的面向服務(wù)架構(gòu)(SOA)。

當(dāng)然,傳統(tǒng)的WebServices(WSDL/SOAP)也基本同樣能滿足以上場(chǎng)景(除了場(chǎng)景4)的要求(甚至還能滿足那些需要企業(yè)級(jí)特性的場(chǎng)景),但由于其復(fù)雜性等問題,現(xiàn)在已經(jīng)越來越少被實(shí)際采用了。

快速入門

在dubbo中開發(fā)一個(gè)REST風(fēng)格的服務(wù)會(huì)比較簡(jiǎn)單,下面以一個(gè)注冊(cè)用戶的簡(jiǎn)單服務(wù)為例說明。

這個(gè)服務(wù)要實(shí)現(xiàn)的功能是提供如下URL(注:這個(gè)URL不是完全符合REST的風(fēng)格,但是更簡(jiǎn)單實(shí)用):

http://localhost:8080/users/register

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

首先,開發(fā)服務(wù)的接口:

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

然后,開發(fā)服務(wù)的實(shí)現(xiàn):

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

上面的服務(wù)實(shí)現(xiàn)代碼非常簡(jiǎn)單,但是由于REST服務(wù)是要被發(fā)布到特定HTTP URL,供任意語言客戶端甚至瀏覽器來訪問,所以這里要額外添加了幾個(gè)JAX-RS的標(biāo)準(zhǔn)annotation來做相關(guān)的配置:

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

@Path(“register”):指定訪問registerUser()方法的URL相對(duì)路徑是/register,再結(jié)合上一個(gè)@Path為UserService指定的路徑,則調(diào)用UserService.register()的完整路徑為http://localhost:8080/users/register

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

@Consumes({MediaType.APPLICATION_JSON}):指定registerUser()接收J(rèn)SON格式的數(shù)據(jù)。REST框架會(huì)自動(dòng)將JSON數(shù)據(jù)反序列化為User對(duì)象

最后,在spring配置文件中添加此服務(wù),即完成所有服務(wù)開發(fā)工作:

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

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

<!-- 和本地bean一樣實(shí)現(xiàn)服務(wù) -->
<bean id="userService" class="xxx.UserServiceImpl" />

標(biāo)準(zhǔn)Java REST API:JAX-RS簡(jiǎn)介

JAX-RS是標(biāo)準(zhǔn)的Java REST API,得到了業(yè)界的廣泛支持和應(yīng)用,其著名的開源實(shí)現(xiàn)就有很多,包括Oracle的Jersey,RedHat的RestEasy,Apache的CXF和Wink,以及restlet等等。另外,所有支持JavaEE 6.0以上規(guī)范的商用JavaEE應(yīng)用服務(wù)器都對(duì)JAX-RS提供了支持。因此,JAX-RS是一種已經(jīng)非常成熟的解決方案,并且采用它沒有任何所謂vendor lock-in的問題。

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

  • Oracle官方的tutorial:http://docs.oracle.com/javaee/7/tutorial/doc/jaxrs.htm
  • IBM developerWorks中國站文章:http://www.ibm.com/developerworks/cn/java/j-lo-jaxrs/

更多的資料請(qǐng)自行g(shù)oogle或者百度一下。就學(xué)習(xí)JAX-RS來說,一般主要掌握其各種annotation的用法即可。

注意:dubbo是基于JAX-RS 2.0版本的,有時(shí)候需要注意一下資料或REST實(shí)現(xiàn)所涉及的版本。

REST服務(wù)提供端詳解

下面我們擴(kuò)充“快速入門”中的UserService,進(jìn)一步展示在dubbo中REST服務(wù)提供端的開發(fā)要點(diǎn)。

HTTP POST/GET的實(shí)現(xiàn)

REST服務(wù)中雖然建議使用HTTP協(xié)議中四種標(biāo)準(zhǔn)方法POST、DELETE、PUT、GET來分別實(shí)現(xiàn)常見的“增刪改查”,但實(shí)際中,我們一般情況直接用POST來實(shí)現(xiàn)“增改”,GET來實(shí)現(xiàn)“刪查”即可(DELETE和PUT甚至?xí)灰恍┓阑饓ψ钃酰?/p>

前面已經(jīng)簡(jiǎn)單演示了POST的實(shí)現(xiàn),在此,我們?yōu)閁serService添加一個(gè)獲取注冊(cè)用戶資料的功能,來演示GET的實(shí)現(xiàn)。

這個(gè)功能就是要實(shí)現(xiàn)客戶端通過訪問如下不同URL來獲取不同ID的用戶資料:

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

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

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

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

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

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

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

@Produces({MediaType.APPLICATION_JSON}):指定getUser()輸出JSON格式的數(shù)據(jù)??蚣軙?huì)自動(dòng)將User對(duì)象序列化為JSON數(shù)據(jù)。

Annotation放在接口類還是實(shí)現(xiàn)類

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

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

在一般應(yīng)用中,我們建議將annotation放到服務(wù)實(shí)現(xiàn)類,這樣annotation和java實(shí)現(xiàn)代碼位置更接近,更便于開發(fā)和維護(hù)。另外更重要的是,我們一般傾向于避免對(duì)接口的污染,保持接口的純凈性和廣泛適用性。

但是,如后文所述,如果我們要用dubbo直接開發(fā)的消費(fèi)端來訪問此服務(wù),則annotation必須放到接口上。

如果接口和實(shí)現(xiàn)類都同時(shí)添加了annotation,則實(shí)現(xiàn)類的annotation配置會(huì)生效,接口上的annotation被直接忽略。

JSON、XML等多數(shù)據(jù)格式的支持

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

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

@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);

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

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

在一個(gè)REST服務(wù)同時(shí)對(duì)多種數(shù)據(jù)格式支持的情況下,根據(jù)JAX-RS標(biāo)準(zhǔn),一般是通過HTTP中的MIME header(content-type和accept)來指定當(dāng)前想用的是哪種格式的數(shù)據(jù)。

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

如果你既不加HTTP header,也不加后綴,則dubbo的REST會(huì)優(yōu)先啟用在以上annotation定義中排位最靠前的那種數(shù)據(jù)格式。

注意:這里要支持XML格式數(shù)據(jù),在annotation中既可以用MediaType.TEXT_XML,也可以用MediaType.APPLICATION_XML,但是TEXT_XML是更常用的,并且如果要利用上述的URL后綴方式來指定數(shù)據(jù)格式,只能配置為TEXT_XML才能生效。

中文字符支持

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

基于JAX-RS的標(biāo)準(zhǔn)用法,我們只需要做如下annotation配置即可:

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

為了方便用戶,我們?cè)赿ubbo REST中直接添加了一個(gè)支持類,來定義以上的常量,可以直接使用,減少出錯(cuò)的可能性。

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

XML數(shù)據(jù)格式的額外要求

由于JAX-RS的實(shí)現(xiàn)一般都用標(biāo)準(zhǔn)的JAXB(Java API for XML Binding)來序列化和反序列化XML格式數(shù)據(jù),所以我們需要為每一個(gè)要用XML傳輸?shù)膶?duì)象添加一個(gè)類級(jí)別的JAXB annotation,否則序列化將報(bào)錯(cuò)。例如為getUser()中返回的User添加如下:

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

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

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

long registerUser(User user);

由于primitive類型不被JAXB序列化支持,所以添加一個(gè)wrapper對(duì)象:

@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序列化的問題,而且使得返回的數(shù)據(jù)都符合XML和JSON的規(guī)范。例如,在JSON中,返回的將是如下形式:

{"id": 1001}

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

1001 	

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

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

這種wrapper對(duì)象其實(shí)利用所謂Data Transfer Object(DTO)模式,采用DTO還能對(duì)傳輸數(shù)據(jù)做更多有用的定制。

定制序列化

如上所述,REST的底層實(shí)現(xiàn)會(huì)在service的對(duì)象和JSON/XML數(shù)據(jù)格式之間自動(dòng)做序列化/反序列化。但有些場(chǎng)景下,如果覺得這種自動(dòng)轉(zhuǎn)換不滿足要求,可以對(duì)其做定制。

Dubbo中的REST實(shí)現(xiàn)是用JAXB做XML序列化,用Jackson做JSON序列化,所以在對(duì)象上添加JAXB或Jackson的annotation即可以定制映射。

例如,定制對(duì)象屬性映射到XML元素的名字:

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

定制對(duì)象屬性映射到JSON字段的名字:

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

更多資料請(qǐng)參考JAXB和Jackson的官方文檔,或自行g(shù)oogle。

配置REST Server的實(shí)現(xiàn)

目前在dubbo中,我們支持5種嵌入式rest server的實(shí)現(xiàn),并同時(shí)支持采用外部應(yīng)用服務(wù)器來做rest server的實(shí)現(xiàn)。rest server的實(shí)現(xiàn)是通過如下server這個(gè)XML屬性來選擇的:

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

以上配置選用了嵌入式的jetty來做rest server,同時(shí),如果不配置server屬性,rest協(xié)議默認(rèn)也是選用jetty。jetty是非常成熟的java servlet容器,并和dubbo已經(jīng)有較好的集成(目前5種嵌入式server中只有jetty和后面所述的tomcat、tjws,與dubbo監(jiān)控系統(tǒng)等完成了無縫的集成),所以,如果你的dubbo系統(tǒng)是單獨(dú)啟動(dòng)的進(jìn)程,你可以直接默認(rèn)采用jetty即可。

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

以上配置選用了嵌入式的tomcat來做rest server。在嵌入式tomcat上,REST的性能比jetty上要好得多(參見后面的基準(zhǔn)測(cè)試),建議在需要高性能的場(chǎng)景下采用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。這兩個(gè)server實(shí)現(xiàn)非常輕量級(jí),非常方便在集成測(cè)試中快速啟動(dòng)使用,當(dāng)然也可以在負(fù)荷不高的生產(chǎn)環(huán)境中使用。 注:tjws目前已經(jīng)被deprecated掉了,因?yàn)樗荒芎芎玫暮蛃ervlet 3.1 API工作。

如果你的dubbo系統(tǒng)不是單獨(dú)啟動(dòng)的進(jìn)程,而是部署到了Java應(yīng)用服務(wù)器中,則建議你采用以下配置:

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

通過將server設(shè)置為servlet,dubbo將采用外部應(yīng)用服務(wù)器的servlet容器來做rest server。同時(shí),還要在dubbo系統(tǒng)的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初始化會(huì)出錯(cuò)。

其實(shí),這種場(chǎng)景下你依然可以堅(jiān)持用嵌入式server,但外部應(yīng)用服務(wù)器的servlet容器往往比嵌入式server更加強(qiáng)大(特別是如果你是部署到更健壯更可伸縮的WebLogic,WebSphere等),另外有時(shí)也便于在應(yīng)用服務(wù)器做統(tǒng)一管理、監(jiān)控等等。

獲取上下文(Context)信息

在遠(yuǎn)程調(diào)用中,值得獲取的上下文信息可能有很多種,這里特別以獲取客戶端IP為例。

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

第一種方式,用JAX-RS標(biāo)準(zhǔn)的@Context annotation:

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

用Context修飾getUser()的一個(gè)方法參數(shù)后,就可以將當(dāng)前的HttpServletRequest注入進(jìn)來,然后直接調(diào)用servlet api獲取IP。

注意:這種方式只能在設(shè)置server=“tjws"或者server=“tomcat"或者server=“jetty"或者server=“servlet"的時(shí)候才能工作,因?yàn)橹挥羞@幾種REST server的實(shí)現(xiàn)才提供了servlet容器。另外,標(biāo)準(zhǔn)的JAX-RS還支持用@Context修飾service類的一個(gè)實(shí)例字段來獲取HttpServletRequest,但在dubbo中我們沒有對(duì)此作出支持。

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

public User getUser(@PathParam("id") Long id) {
    System.out.println("Client address is " + RpcContext.getContext().getRemoteAddressString());
} 
注意:這種方式只能在設(shè)置server=“jetty"或者server=“tomcat"或者server=“servlet"或者server=“tjws"的時(shí)候才能工作。另外,目前dubbo的RpcContext是一種比較有侵入性的用法,未來我們很可能會(huì)做出重構(gòu)。

如果你想保持你的項(xiàng)目對(duì)JAX-RS的兼容性,未來脫離dubbo也可以運(yùn)行,請(qǐng)選擇第一種方式。如果你想要更優(yōu)雅的服務(wù)接口定義,請(qǐng)選用第二種方式。

此外,在最新的dubbo rest中,還支持通過RpcContext來獲取HttpServletRequest和HttpServletResponse,以提供更大的靈活性來方便用戶實(shí)現(xiàn)某些復(fù)雜功能,比如在dubbo標(biāo)準(zhǔn)的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());
}
注意:為了保持協(xié)議的中立性,RpcContext.getRequest()和RpcContext.getResponse()返回的僅僅是一個(gè)Object類,而且可能為null。所以,你必須自己做null和類型的檢查。
注意:只有在設(shè)置server=“jetty"或者server=“tomcat"或者server=“servlet"的時(shí)候,你才能通過以上方法正確的得到HttpServletRequest和HttpServletResponse,因?yàn)橹挥羞@幾種server實(shí)現(xiàn)了servlet容器。

為了簡(jiǎn)化編程,在此你也可以用泛型的方式來直接獲取特定類型的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不符合指定的類型,這里也會(huì)返回null。

配置端口號(hào)和Context Path

dubbo中的rest協(xié)議默認(rèn)將采用80端口,如果想修改端口,直接配置:

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

另外,如前所述,我們可以用@Path來配置單個(gè)rest服務(wù)的URL相對(duì)路徑。但其實(shí),我們還可以設(shè)置一個(gè)所有rest服務(wù)都適用的基礎(chǔ)相對(duì)路徑,即java web應(yīng)用中常說的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...
    }	
}

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

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

注意:如果你是選用外部應(yīng)用服務(wù)器做rest server,即配置:

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

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

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

配置線程數(shù)和IO線程數(shù)

可以為rest服務(wù)配置線程池大?。?/p>

<dubbo:protocol name="rest" threads="500"/>
注意:目前線程池的設(shè)置只有當(dāng)server=“netty"或者server=“jetty"或者server=“tomcat"的時(shí)候才能生效。另外,如果server=“servlet”,由于這時(shí)候啟用的是外部應(yīng)用服務(wù)器做rest server,不受dubbo控制,所以這里的線程池設(shè)置也無效。

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

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

配置長(zhǎng)連接

Dubbo中的rest服務(wù)默認(rèn)都是采用http長(zhǎng)連接來訪問,如果想切換為短連接,直接配置:

<dubbo:protocol name="rest" keepalive="false"/>
注意:這個(gè)配置目前只對(duì)server=“netty"和server=“tomcat"才能生效。

配置最大的HTTP連接數(shù)

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

<dubbo:protocol name="rest" accepts="500" server="tomcat/>
注意:這個(gè)配置目前只對(duì)server=“tomcat"才能生效。

配置每個(gè)消費(fèi)端的超時(shí)時(shí)間和HTTP連接數(shù)

如果rest服務(wù)的消費(fèi)端也是dubbo系統(tǒng),可以像其他dubbo RPC機(jī)制一樣,配置消費(fèi)端調(diào)用此rest服務(wù)的最大超時(shí)時(shí)間以及每個(gè)消費(fèi)端所能啟動(dòng)的最大HTTP連接數(shù)。

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

當(dāng)然,由于這個(gè)配置針對(duì)消費(fèi)端生效的,所以也可以在消費(fèi)端配置:

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

但是,通常我們建議配置在服務(wù)提供端提供此類配置。按照dubbo官方文檔的說法:“Provider上盡量多配置Consumer端的屬性,讓Provider實(shí)現(xiàn)者一開始就思考Provider服務(wù)特點(diǎn)、服務(wù)質(zhì)量的問題。”

注意:如果dubbo的REST服務(wù)是發(fā)布給非dubbo的客戶端使用,則這里上的配置完全無效,因?yàn)檫@種客戶端不受dubbo控制。

GZIP數(shù)據(jù)壓縮

Dubbo的REST支持用GZIP壓縮請(qǐng)求和響應(yīng)的數(shù)據(jù),以減少網(wǎng)絡(luò)傳輸時(shí)間和帶寬占用,但這種方式會(huì)也增加CPU開銷。

TODO more contents to add

用Annotation取代部分Spring XML配置

以上所有的討論都是基于dubbo在spring中的xml配置。但是,dubbo/spring本身也支持用annotation來作配置,所以我們也可以按dubbo官方文檔中的步驟,把相關(guān)annotation加到REST服務(wù)的實(shí)現(xiàn)中,取代一些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的配置更簡(jiǎn)單更精確,經(jīng)常也更便于維護(hù)(當(dāng)然現(xiàn)代IDE都可以在xml中支持比如類名重構(gòu),所以就這里的特定用例而言,xml的維護(hù)性也很好)。而xml對(duì)代碼的侵入性更小一些,尤其有利于動(dòng)態(tài)修改配置,特別是比如你要針對(duì)單個(gè)服務(wù)配置連接超時(shí)時(shí)間、每客戶端最大連接數(shù)、集群策略、權(quán)重等等。另外,特別對(duì)復(fù)雜應(yīng)用或者模塊來說,xml提供了一個(gè)中心點(diǎn)來涵蓋的所有組件和配置,更一目了然,一般更便于項(xiàng)目長(zhǎng)時(shí)期的維護(hù)。

當(dāng)然,選擇哪種配置方式?jīng)]有絕對(duì)的優(yōu)劣,和個(gè)人的偏好也不無關(guān)系。

添加自定義的Filter、Interceptor等

Dubbo的REST也支持JAX-RS標(biāo)準(zhǔn)的Filter和Interceptor,以方便對(duì)REST的請(qǐng)求與響應(yīng)過程做定制化的攔截處理。

其中,F(xiàn)ilter主要用于訪問和設(shè)置HTTP請(qǐng)求和響應(yīng)的參數(shù)、URI等等。例如,設(shè)置HTTP響應(yīng)的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主要用于訪問和修改輸入與輸出字節(jié)流,例如,手動(dòng)添加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();
    }
}

在標(biāo)準(zhǔn)JAX-RS應(yīng)用中,我們一般是為Filter和Interceptor添加@Provider annotation,然后JAX-RS runtime會(huì)自動(dòng)發(fā)現(xiàn)并啟用它們。而在dubbo中,我們是通過添加XML配置的方式來注冊(cè)Filter和Interceptor:

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

在此,我們可以將Filter、Interceptor和DynamicFeature這三種類型的對(duì)象都添加到extension屬性上,多個(gè)之間用逗號(hào)分隔。(DynamicFeature是另一個(gè)接口,可以方便我們更動(dòng)態(tài)的啟用Filter和Interceptor,感興趣請(qǐng)自行g(shù)oogle。)

當(dāng)然,dubbo自身也支持Filter的概念,但我們這里討論的Filter和Interceptor更加接近協(xié)議實(shí)現(xiàn)的底層,相比dubbo的filter,可以做更底層的定制化。

注:這里的XML屬性叫extension,而不是叫interceptor或者filter,是因?yàn)槌薎nterceptor和Filter,未來我們還會(huì)添加更多的擴(kuò)展類型。

如果REST的消費(fèi)端也是dubbo系統(tǒng)(參見下文的討論),則也可以用類似方式為消費(fèi)端配置Interceptor和Filter。但注意,JAX-RS中消費(fèi)端的Filter和提供端的Filter是兩種不同的接口。例如前面例子中服務(wù)端是ContainerResponseFilter接口,而消費(fèi)端對(duì)應(yīng)的是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標(biāo)準(zhǔn)的ExceptionMapper,可以用來定制特定exception發(fā)生后應(yīng)該返回的HTTP響應(yīng)。

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請(qǐng)求/響應(yīng)中的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級(jí)別日志輸出,例如,在log4j.xml中配置:

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

當(dāng)然,你也可以直接在ROOT logger打開INFO級(jí)別日志輸出:

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

然后在日志中會(huì)有類似如下的內(nèi)容輸出:

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日志輸出后,除了正常日志輸出的性能開銷外,也會(huì)在比如HTTP請(qǐng)求解析時(shí)產(chǎn)生額外的開銷,因?yàn)樾枰㈩~外的內(nèi)存緩沖區(qū)來為日志的輸出做數(shù)據(jù)準(zhǔn)備。

輸入?yún)?shù)的校驗(yàn)

dubbo的rest支持采用Java標(biāo)準(zhǔn)的bean validation annotation(JSR 303)來做輸入校驗(yàn)http://beanvalidation.org/

為了和其他dubbo遠(yuǎn)程調(diào)用協(xié)議保持一致,在rest中作校驗(yàn)的annotation必須放在服務(wù)的接口上,例如:

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

當(dāng)然,在很多其他的bean validation的應(yīng)用場(chǎng)景都是將annotation放到實(shí)現(xiàn)類而不是接口上。把a(bǔ)nnotation放在接口上至少有一個(gè)好處是,dubbo的客戶端可以共享這個(gè)接口的信息,dubbo甚至不需要做遠(yuǎn)程調(diào)用,在本地就可以完成輸入校驗(yàn)。

然后按照dubbo的標(biāo)準(zhǔn)方式在XML配置中打開驗(yàn)證:

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

在dubbo的其他很多遠(yuǎn)程調(diào)用協(xié)議中,如果輸入驗(yàn)證出錯(cuò),是直接將RpcException拋向客戶端,而在rest中由于客戶端經(jīng)常是非dubbo,甚至非java的系統(tǒng),所以不便直接拋出Java異常。因此,目前我們將校驗(yàn)錯(cuò)誤以XML的格式返回:

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

稍后也會(huì)支持其他數(shù)據(jù)格式的返回值。至于如何對(duì)驗(yàn)證錯(cuò)誤消息作國際化處理,直接參考bean validation的相關(guān)文檔即可。

如果你認(rèn)為默認(rèn)的校驗(yàn)錯(cuò)誤返回格式不符合你的要求,可以如上面章節(jié)所述,添加自定義的ExceptionMapper來自由的定制錯(cuò)誤返回格式。需要注意的是,這個(gè)ExceptionMapper必須用泛型聲明來捕獲dubbo的RpcException,才能成功覆蓋dubbo rest默認(rèn)的異常處理策略。為了簡(jiǎn)化操作,其實(shí)這里最簡(jiǎn)單的方式是直接繼承dubbo rest的RpcExceptionMapper,并覆蓋其中處理校驗(yàn)異常的方法即可:

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

然后將這個(gè)ExceptionMapper添加到XML配置中即可:

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

是否應(yīng)該透明發(fā)布REST服務(wù)

Dubbo的REST調(diào)用和dubbo中其它某些RPC不同的是,需要在服務(wù)代碼中添加JAX-RS的annotation(以及JAXB、Jackson的annotation),如果你覺得這些annotation一定程度“污染”了你的服務(wù)代碼,你可以考慮編寫額外的Facade和DTO類,在Facade和DTO上添加annotation,而Facade將調(diào)用轉(zhuǎn)發(fā)給真正的服務(wù)實(shí)現(xiàn)類。當(dāng)然事實(shí)上,直接在服務(wù)代碼中添加annotation基本沒有任何負(fù)面作用,而且這本身是Java EE的標(biāo)準(zhǔn)用法,另外JAX-RS和JAXB的annotation是屬于java標(biāo)準(zhǔn),比我們經(jīng)常使用的spring、dubbo等等annotation更沒有vendor lock-in的問題,所以一般沒有必要因此而引入額外對(duì)象。

另外,如果你想用前述的@Context annotation,通過方法參數(shù)注入HttpServletRequest(如public User getUser(@PathParam("id") Long id, @Context HttpServletRequest request)),這時(shí)候由于改變了服務(wù)的方法簽名,并且HttpServletRequest是REST特有的參數(shù),所以如果你的服務(wù)要支持多種RPC機(jī)制的話,則引入額外的Facade類是比較適當(dāng)?shù)摹?/p>

當(dāng)然,在沒有添加REST調(diào)用之前,你的服務(wù)代碼可能本身已經(jīng)就充當(dāng)了Facade和DTO的角色(至于為什么有些場(chǎng)景需要這些角色,有興趣可參考微觀SOA:服務(wù)設(shè)計(jì)原則及其實(shí)踐方式)。這種情況下,在添加REST之后,如果你再額外添加與REST相關(guān)的Facade和DTO,就相當(dāng)于對(duì)原有代碼對(duì)再一次包裝,即形成如下調(diào)用鏈:

RestFacade/RestDTO -> Facade/DTO -> Service

這種體系比較繁瑣,數(shù)據(jù)轉(zhuǎn)換之類的工作量也不小,所以一般應(yīng)盡量避免如此。

dubbo的提供端在調(diào)用REST服務(wù)時(shí)使用header

Dubbo通過RpcContextFilter將header取出分解之后設(shè)置到RpcContext的attachments,所以在提供端可以直接從RpcContext的attachments中獲取到消費(fèi)端設(shè)置的header信息:

    RpcContext.getContext().getAttachment(key1)
    RpcContext.getContext().getAttachment(key2)

REST服務(wù)消費(fèi)端詳解

這里我們用三種場(chǎng)景來分別討論:

  1. 非dubbo的消費(fèi)端調(diào)用dubbo的REST服務(wù)(non-dubbo –> dubbo)
  2. dubbo消費(fèi)端調(diào)用dubbo的REST服務(wù) (dubbo –> dubbo)
  3. dubbo的消費(fèi)端調(diào)用非dubbo的REST服務(wù) (dubbo –> non-dubbo)

場(chǎng)景1:非dubbo的消費(fèi)端調(diào)用dubbo的REST服務(wù)

這種場(chǎng)景的客戶端與dubbo本身無關(guān),直接選用相應(yīng)語言和框架中合適的方式即可。

如果是還是java的客戶端(但沒用dubbo),可以考慮直接使用標(biāo)準(zhǔn)的JAX-RS Client API或者特定REST實(shí)現(xiàn)的Client API來調(diào)用REST服務(wù)。下面是用JAX-RS Client API來訪問上述的UserService的registerUser():

User user = new User();
user.setName("Larry");

Client client = ClientBuilder.newClient();
WebTarget target = client.target("http://localhost:8080/services/users/register.json");
Response response = target.request().post(Entity.entity(user, MediaType.APPLICATION_JSON_TYPE));

try {
    if (response.getStatus() != 200) {
        throw new RuntimeException("Failed with HTTP error code : " + response.getStatus());
    }
    System.out.println("The generated id is " + response.readEntity(RegistrationResult.class).getId());
} finally {
    response.close();
    client.close(); // 在真正開發(fā)中不要每次關(guān)閉client,比如HTTP長(zhǎng)連接是由client持有的
}

上面代碼片段中的User和RegistrationResult類都是消費(fèi)端自己編寫的,JAX-RS Client API會(huì)自動(dòng)對(duì)它們做序列化/反序列化。

當(dāng)然,在java中也可以直接用自己熟悉的比如HttpClient,F(xiàn)astJson,XStream等等各種不同技術(shù)來實(shí)現(xiàn)REST客戶端,在此不再詳述。

場(chǎng)景2:dubbo消費(fèi)端調(diào)用dubbo的REST服務(wù)

這種場(chǎng)景下,和使用其他dubbo的遠(yuǎn)程調(diào)用方式一樣,直接在服務(wù)提供端和服務(wù)消費(fèi)端共享Java服務(wù)接口,并添加spring xml配置(當(dāng)然也可以用spring/dubbo的annotation配置),即可透明的調(diào)用遠(yuǎn)程REST服務(wù):

<dubbo:reference id="userService" interface="xxx.UserService"/>

如前所述,這種場(chǎng)景下必須把JAX-RS的annotation添加到服務(wù)接口上,這樣在dubbo在消費(fèi)端才能共享相應(yīng)的REST配置信息,并據(jù)之做遠(yuǎn)程調(diào)用:

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

如果服務(wù)接口的annotation中配置了多種數(shù)據(jù)格式,這里由于兩端都是dubbo系統(tǒng),REST的大量細(xì)節(jié)被屏蔽了,所以不存在用前述URL后綴之類選擇數(shù)據(jù)格式的可能。目前在這種情況下,排名最靠前的數(shù)據(jù)格式將直接被使用。

因此,我們建議你在定義annotation的時(shí)候最好把最合適的數(shù)據(jù)格式放到前面,比如以上我們是把json放在xml前面,因?yàn)閖son的傳輸性能優(yōu)于xml。

場(chǎng)景3:dubbo的消費(fèi)端調(diào)用非dubbo的REST服務(wù)

這種場(chǎng)景下,可以直接用場(chǎng)景1中描述的Java的方式來調(diào)用REST服務(wù)。但其實(shí)也可以采用場(chǎng)景2中描述的方式,即更透明的調(diào)用REST服務(wù),即使這個(gè)服務(wù)并不是dubbo提供的。

如果用場(chǎng)景2的方式,由于這里REST服務(wù)并非dubbo提供,一般也就沒有前述的共享的Java服務(wù)接口,所以在此我們需要根據(jù)外部REST服務(wù)的情況,自己來編寫Java接口以及相應(yīng)參數(shù)類,并添加JAX-RS、JAXB、Jackson等的annotation,dubbo的REST底層實(shí)現(xiàn)會(huì)據(jù)此去自動(dòng)生成請(qǐng)求消息,自動(dòng)解析響應(yīng)消息等等,從而透明的做遠(yuǎn)程調(diào)用?;蛘哌@種方式也可以理解為,我們嘗試用JAX-RS的方式去仿造實(shí)現(xiàn)一遍外部的REST服務(wù)提供端,然后把寫成服務(wù)接口放到客戶端來直接使用,dubbo的REST底層實(shí)現(xiàn)就能像調(diào)用dubbo的REST服務(wù)一樣調(diào)用其他REST服務(wù)。

例如,我們要調(diào)用如下的外部服務(wù)

http://api.foo.com/services/users/1001
http://api.foo.com/services/users/1002

獲取不同ID的用戶資料,返回格式是JSON

{
    "id": 1001,
    "name": "Larry"
}

我們可根據(jù)這些信息,編寫服務(wù)接口和參數(shù)類即可:

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

    private Long id;

    private String name;

    // …
}

對(duì)于spring中的配置,因?yàn)檫@里的REST服務(wù)不是dubbo提供的,所以無法使用dubbo的注冊(cè)中心,直接配置外部REST服務(wù)的url地址即可(如多個(gè)地址用逗號(hào)分隔):

<dubbo:reference id="userService" interface="xxx.UserService" url="rest://api.foo.com/services/"/>
注意:這里協(xié)議必須用rest://而不是http://之類。如果外部的REST服務(wù)有context path,則在url中也必須添加上(除非你在每個(gè)服務(wù)接口的@Path annotation中都帶上context path),例如上面的/services/。同時(shí)這里的services后面必須帶上/,這樣才能使dubbo正常工作。

另外,這里依然可以配置客戶端可啟動(dòng)的最大連接數(shù)和超時(shí)時(shí)間:

<dubbo:reference id="userService" interface="xxx.UserService" url="rest://api.foo.com/services/" timeout="2000" connections="10"/>

dubbo的消費(fèi)端在調(diào)用REST服務(wù)時(shí)配置自定義header

Dubbo進(jìn)行rest調(diào)用的時(shí)候,采用的是將RpcContext的attachment轉(zhuǎn)換為header的方式,所以,dubbo消費(fèi)端可以按以下方式進(jìn)行自定義header的設(shè)置:

    RpcContext.getContext().setAttachment("key1", "value1");
    RpcContext.getContext().setAttachment("key2", "value2");

即可設(shè)置如下格式的header:

    key1=value1
    key2=value2

Dubbo中JAX-RS的限制

Dubbo中的REST開發(fā)是完全兼容標(biāo)準(zhǔn)JAX-RS的,但其支持的功能目前是完整JAX-RS的一個(gè)子集,部分因?yàn)樗芟抻赿ubbo和spring的特定體系。

在dubbo中使用的JAX-RS的局限包括但不限于:

  1. 服務(wù)實(shí)現(xiàn)只能是singleton的,不能支持per-request scope和per-lookup scope
  2. 不支持用@Context annotation對(duì)服務(wù)的實(shí)例字段注入 ServletConfig、ServletContext、HttpServletRequest、HttpServletResponse等等,但可以支持對(duì)服務(wù)方法參數(shù)的注入。但對(duì)某些特定REST server實(shí)現(xiàn),(祥見前面的敘述),也不支持對(duì)服務(wù)方法參數(shù)的注入。

REST常見問題解答(REST FAQ)

Dubbo REST的服務(wù)能和Dubbo注冊(cè)中心、監(jiān)控中心集成嗎?

可以的,而且是自動(dòng)集成的,也就是你在dubbo中開發(fā)的所有REST服務(wù)都會(huì)自動(dòng)注冊(cè)到注冊(cè)中心和監(jiān)控中心,可以通過它們做管理。

但是,只有當(dāng)REST的消費(fèi)端也是基于dubbo的時(shí)候,注冊(cè)中心中的許多服務(wù)治理操作才能完全起作用。而如果消費(fèi)端是非dubbo的,自然不受注冊(cè)中心管理,所以其中很多操作是不會(huì)對(duì)消費(fèi)端起作用的。

Dubbo REST中如何實(shí)現(xiàn)負(fù)載均衡和容錯(cuò)(failover)?

如果dubbo REST的消費(fèi)端也是dubbo的,則Dubbo REST和其他dubbo遠(yuǎn)程調(diào)用協(xié)議基本完全一樣,由dubbo框架透明的在消費(fèi)端做load balance、failover等等。

如果dubbo REST的消費(fèi)端是非dubbo的,甚至是非java的,則最好配置服務(wù)提供端的軟負(fù)載均衡機(jī)制,目前可考慮用LVS、HAProxy、 Nginx等等對(duì)HTTP請(qǐng)求做負(fù)載均衡。

JAX-RS中重載的方法能夠映射到同一URL地址嗎?

http://stackoverflow.com/questions/17196766/can-resteasy-choose-method-based-on-query-params

JAX-RS中作POST的方法能夠接收多個(gè)參數(shù)嗎?

http://stackoverflow.com/questions/5553218/jax-rs-post-multiple-objects

Dubbo當(dāng)前體系的不足之處(與REST相關(guān)的)

我認(rèn)為dubbo當(dāng)前體系中顯然也有不少不足之處,這里列出幾個(gè)與REST有關(guān)的、并影響用戶使用的問題(不包括內(nèi)部實(shí)現(xiàn)的問題),供參考評(píng)論,為下一步重構(gòu)作準(zhǔn)備。

RpcContext的侵入性

在前文,前面我們已經(jīng)提到過RpcContext用法的侵入性,由于它是用單例的方式來訪問上下文信息,這完全不符合spring應(yīng)用的一般風(fēng)格,不利于應(yīng)用擴(kuò)展和單元測(cè)試。未來我們可能用依賴注入方式注入一個(gè)接口,再用它去訪問ThreadLocal中的上下文信息。

Protocol配置的局限性

dubbo支持多種遠(yuǎn)程調(diào)用方式,但所有調(diào)用方式都是用<dubbo:protocol/>來配置的,例如:

<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"/>

其實(shí),上面很多屬性實(shí)際上dubbo RPC遠(yuǎn)程調(diào)用方式特有的,很多dubbo中的其它遠(yuǎn)程調(diào)用方式根本就不支持例如server, client, codec, iothreads, accepts, payload等等(當(dāng)然,有的是條件所限不支持,有的是根本沒有必要支持)。這給用戶的使用徒增很多困惑,用戶也并不知道有些屬性(比如做性能調(diào)優(yōu))添加了實(shí)際上是不起作用的。

另一方面,各種遠(yuǎn)程調(diào)用方式往往有大量自己獨(dú)特的配置需要,特別是我們逐步為每種遠(yuǎn)程調(diào)用方式都添加更豐富、更高級(jí)的功能,這就不可避免的擴(kuò)展<protocol/>中的屬性(例如目前我們?cè)赗EST中已經(jīng)添加了keepalive和extension兩個(gè)屬性),到最后會(huì)導(dǎo)致<protocol/>臃腫不堪,用戶的使用也更加困惑。

當(dāng)然,dubbo中有一種擴(kuò)展<protocol/>的方式是用<dubbo:parameter/>,但這種方式顯然很有局限性,而且用法復(fù)雜,缺乏schema校驗(yàn)。

所以,最好的方式是為每種遠(yuǎn)程調(diào)用方式設(shè)置自己的protocol元素,比如<protocol-dubbo/>,<protocol-rest/>等等,每種元素用XML schema規(guī)定自己的屬性(當(dāng)然屬性在各種遠(yuǎn)程調(diào)用方式之間能通用是最好的)。

如此一來,例如前面提到過的extension配置也可以用更自由的方式,從而更清楚更可擴(kuò)展(以下只是舉例,當(dāng)然也許有更好的方式):

<dubbo:protocol-rest port="8080">
    <dubbo:extension>someInterceptor</dubbo:extension>
    <dubbo:extension>someFilter</dubbo:extension>
    <dubbo:extension>someDynamicFeature</dubbo:extension>
    <dubbo:extension>someEntityProvider</dubbo:extension>
</dubbo:protocol-rest>

XML命名不符合spring規(guī)范

dubbo的XML配置中大量命名都不符合spring規(guī)范,比如:

<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"/>

上面threadpool應(yīng)該改為thread-pool,iothreads應(yīng)該改為io-threads,單詞之間應(yīng)該用”-“分隔。這雖然看起來是個(gè)小問題,但也涉及到了可讀性,特別是可擴(kuò)展性,因?yàn)橛袝r(shí)候我們不可避免要用更多單詞來描述XML元素和屬性。

其實(shí)dubbo本身也是建議遵守spring到XML的命名規(guī)范。

REST最佳實(shí)踐

TODO

性能基準(zhǔn)測(cè)試

測(cè)試環(huán)境

粗略如下:

  • 兩臺(tái)獨(dú)立服務(wù)器
  • 4核Intel(R) Xeon(R) CPU E5-2603 0 @ 1.80GHz
  • 8G內(nèi)存
  • 服務(wù)器之間網(wǎng)絡(luò)通過百兆交換機(jī)
  • CentOS 5
  • JDK 7
  • Tomcat 7
  • JVM參數(shù)-server -Xms1g -Xmx1g -XX:PermSize=64M -XX:+UseConcMarkSweepGC

測(cè)試腳本

和dubbo自身的基準(zhǔn)測(cè)試保持接近:

10個(gè)并發(fā)客戶端持續(xù)不斷發(fā)出請(qǐng)求:

  • 傳入嵌套復(fù)雜對(duì)象(但單個(gè)數(shù)據(jù)量很?。蛔鋈魏翁幚?,原樣返回
  • 傳入50K字符串,不做任何處理,原樣返回(TODO:結(jié)果尚未列出)

進(jìn)行5分鐘性能測(cè)試。(引用dubbo自身測(cè)試的考慮:“主要考察序列化和網(wǎng)絡(luò)IO的性能,因此服務(wù)端無任何業(yè)務(wù)邏輯。取10并發(fā)是考慮到http協(xié)議在高并發(fā)下對(duì)CPU的使用率較高可能會(huì)先打到瓶頸。”)

測(cè)試結(jié)果

下面的結(jié)果主要對(duì)比的是REST和dubbo RPC兩種遠(yuǎn)程調(diào)用方式,并對(duì)它們作不同的配置,例如:

  • “REST: Jetty + XML + GZIP”的意思是:測(cè)試REST,并采用jetty server,XML數(shù)據(jù)格式,啟用GZIP壓縮。
  • “Dubbo: hessian2”的意思是:測(cè)試dubbo RPC,并采用hessian2序列化方式。

針對(duì)復(fù)雜對(duì)象的結(jié)果如下(響應(yīng)時(shí)間越小越好,TPS越大越好):

遠(yuǎn)程調(diào)用方式平均響應(yīng)時(shí)間平均TPS(每秒事務(wù)數(shù))
REST: Jetty + JSON7.8061280
REST: Jetty + JSON + GZIPTODOTODO
REST: Jetty + XMLTODOTODO
REST: Jetty + XML + GZIPTODOTODO
REST: Tomcat + JSON2.0824796
REST: Netty + JSON2.1824576
Dubbo: FST1.2118244
Dubbo: kyro1.1828444
Dubbo: dubbo serialization1.436982
Dubbo: hessian21.496701
Dubbo: fastjson1.5726352

rt

tps

僅就目前的結(jié)果,一點(diǎn)簡(jiǎn)單總結(jié):

  • dubbo RPC(特別是基于高效java序列化方式如kryo,fst)比REST的響應(yīng)時(shí)間和吞吐量都有較顯著優(yōu)勢(shì),內(nèi)網(wǎng)的dubbo系統(tǒng)之間優(yōu)先選擇dubbo RPC。
  • 在REST的實(shí)現(xiàn)選擇上,僅就性能而言,目前tomcat7和netty最優(yōu)(當(dāng)然目前使用的jetty和netty版本都較低)。tjws和sun http server在性能測(cè)試中表現(xiàn)極差,平均響應(yīng)時(shí)間超過200ms,平均tps只有50左右(為了避免影響圖片效果,沒在上面列出)。
  • 在REST中JSON數(shù)據(jù)格式性能優(yōu)于XML(數(shù)據(jù)暫未在以上列出)。
  • 在REST中啟用GZIP對(duì)企業(yè)內(nèi)網(wǎng)中的小數(shù)據(jù)量復(fù)雜對(duì)象幫助不大,性能反而有下降(數(shù)據(jù)暫未在以上列出)。

性能優(yōu)化建議

如果將dubbo REST部署到外部Tomcat上,并配置server=“servlet”,即啟用外部的tomcat來做為rest server的底層實(shí)現(xiàn),則最好在tomcat上添加如下配置:

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="20000"
               redirectPort="8443"
               minSpareThreads="20"
               enableLookups="false"
               maxThreads="100"
               maxKeepAliveRequests="-1"
               keepAliveTimeout="60000"/>

特別是maxKeepAliveRequests="-1”,這個(gè)配置主要是保證tomcat一直啟用http長(zhǎng)連接,以提高REST調(diào)用性能。但是請(qǐng)注意,如果REST消費(fèi)端不是持續(xù)的調(diào)用REST服務(wù),則一直啟用長(zhǎng)連接未必是最好的做法。另外,一直啟用長(zhǎng)連接的方式一般不適合針對(duì)普通webapp,更適合這種類似rpc的場(chǎng)景。所以為了高性能,在tomcat中,dubbo REST應(yīng)用和普通web應(yīng)用最好不要混合部署,而應(yīng)該用單獨(dú)的實(shí)例。

TODO more contents to add

擴(kuò)展討論

REST與Thrift、Protobuf等的對(duì)比

TODO

REST與傳統(tǒng)WebServices的對(duì)比

TODO

JAX-RS與Spring MVC的對(duì)比

初步看法,摘自http://www.infoq.com/cn/news/2014/10/dubbox-open-source?utm_source=infoq&utm_medium=popular_links_homepage#theCommentsSection

謝謝,對(duì)于jax-rs和spring mvc,其實(shí)我對(duì)spring mvc的rest支持還沒有太深入的看過,說點(diǎn)初步想法,請(qǐng)大家指正:spring mvc也支持annotation的配置,其實(shí)和jax-rs看起來是非常非常類似的。我個(gè)人認(rèn)為spring mvc相對(duì)更適合于面向web應(yīng)用的restful服務(wù),比如被AJAX調(diào)用,也可能輸出HTML之類的,應(yīng)用中還有頁面跳轉(zhuǎn)流程之類,spring mvc既可以做好正常的web頁面請(qǐng)求也可以同時(shí)處理rest請(qǐng)求。但總的來說這個(gè)restful服務(wù)是在展現(xiàn)層或者叫web層之類實(shí)現(xiàn)的而jax-rs相對(duì)更適合純粹的服務(wù)化應(yīng)用,也就是傳統(tǒng)Java EE中所說的中間層服務(wù),比如它可以把傳統(tǒng)的EJB發(fā)布成restful服務(wù)。在spring應(yīng)用中,也就把spring中充當(dāng)service之類的bean直接發(fā)布成restful服務(wù)??偟膩碚f這個(gè)restful服務(wù)是在業(yè)務(wù)、應(yīng)用層或者facade層。而MVC層次和概念在這種做比如(后臺(tái))服務(wù)化的應(yīng)用中通常是沒有多大價(jià)值的。當(dāng)然jax-rs的有些實(shí)現(xiàn)比如jersey,也試圖提供mvc支持,以更好的適應(yīng)上面所說的web應(yīng)用,但應(yīng)該是不如spring mvc。在dubbo應(yīng)用中,我想很多人都比較喜歡直接將一個(gè)本地的spring service bean(或者叫manager之類的)完全透明的發(fā)布成遠(yuǎn)程服務(wù),則這里用JAX-RS是更自然更直接的,不必額外的引入MVC概念。當(dāng)然,先不討論透明發(fā)布遠(yuǎn)程服務(wù)是不是最佳實(shí)踐,要不要添加facade之類。當(dāng)然,我知道在dubbo不支持rest的情況下,很多朋友采用的架構(gòu)是spring mvc restful調(diào)用dubbo (spring) service來發(fā)布restful服務(wù)的。這種方式我覺得也非常好,只是如果不修改spring mvc并將其與dubbo深度集成,restful服務(wù)不能像dubbo中的其他遠(yuǎn)程調(diào)用協(xié)議比如webservices、dubbo rpc、hessian等等那樣,享受諸多高級(jí)的服務(wù)治理的功能,比如:注冊(cè)到dubbo的服務(wù)注冊(cè)中心,通過dubbo監(jiān)控中心監(jiān)控其調(diào)用次數(shù)、TPS、響應(yīng)時(shí)間之類,通過dubbo的統(tǒng)一的配置方式控制其比如線程池大小、最大連接數(shù)等等,通過dubbo統(tǒng)一方式做服務(wù)流量控制、權(quán)限控制、頻次控制。另外spring mvc僅僅負(fù)責(zé)服務(wù)端,而在消費(fèi)端,通常是用spring restTemplate,如果restTemplate不和dubbo集成,有可能像dubbo服務(wù)客戶端那樣自動(dòng)或者人工干預(yù)做服務(wù)降級(jí)。如果服務(wù)端消費(fèi)端都是dubbo系統(tǒng),通過spring的rest交互,如果spring rest不深度整合dubbo,則不能用dubbo統(tǒng)一的路由分流等功能。當(dāng)然,其實(shí)我個(gè)人認(rèn)為這些東西不必要非此即彼的。我聽說spring創(chuàng)始人rod johnson總是愛說一句話,the customer is always right,其實(shí)與其非要探討哪種方式更好,不如同時(shí)支持兩種方式就是了,所以原來在文檔中也寫過計(jì)劃支持spring rest annoation,只是不知道具體可行性有多高。

未來

稍后可能要實(shí)現(xiàn)的功能:

  • spring mvc的rest annotation支持
  • 安全
  • OAuth
  • 異步調(diào)用
  • 完善gzip
  • 最大payload限制


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)