http://dangdangdotcom.github.io/dubbox/rest.htmljavascript
dubbo支持多種遠程調用方式,例如dubbo RPC(二進制序列化 + tcp協議)、http invoker(二進制序列化 + http協議,至少在開源版本沒發現對文本序列化的支持)、hessian(二進制序列化 + http協議)、WebServices (文本序列化 + http協議)等等,但缺少對當今特別流行的REST風格遠程調用(文本序列化 + http協議)的支持。html
有鑑於此,咱們基於標準的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡寫),爲dubbo提供了接近透明的REST調用支持。因爲徹底兼容Java標準API,因此爲dubbo開發的全部REST服務,將來脫離dubbo或者任何特定的REST底層實現通常也能夠正常運行。java
特別值得指出的是,咱們並不須要徹底嚴格遵照REST的原始定義和架構風格。即便著名的Twitter REST API也會根據狀況作適度調整,而不是機械的遵照原始的REST風格。git
附註:咱們將這個功能稱之爲REST風格的遠程調用,即RESTful Remoting(抽象的遠程處理或者調用),而不是叫RESTful RPC(具體的遠程「過程」調用),是由於REST和RPC自己能夠被認爲是兩種不一樣的風格。在dubbo的REST實現中,能夠說有兩個面向,其一是提供或消費正常的REST服務,其二是將REST做爲dubbo RPC體系中一種協議實現,而RESTful Remoting同時涵蓋了這個面向。github
如下摘自維基百科:web
這裏我還想特別補充REST的顯著優勢:基於簡單的文本格式消息和通用的HTTP協議,使它具有極廣的適用性,幾乎全部語言和平臺都對它提供支持,同時其學習和使用的門檻也較低。正則表達式
正是因爲REST在適用性方面的優勢,因此在dubbo中支持REST,能夠爲當今多數主流的遠程調用場景都帶來(顯著)好處:spring
顯著簡化企業內部的異構系統之間的(跨語言)調用。此處主要針對這種場景:dubbo的系統作服務提供端,其餘語言的系統(也包括某些不基於dubbo的java系統)作服務消費端,二者經過HTTP和文本消息進行通訊。即便相比Thrift、ProtoBuf等二進制跨語言調用方案,REST也有本身獨特的優點(詳見後面討論)apache
顯著簡化對外Open API(開放平臺)的開發。既能夠用dubbo來開發專門的Open API應用,也能夠將原內部使用的dubbo service直接「透明」發佈爲對外的Open REST API(固然dubbo自己將來最好能夠較透明的提供諸如權限控制、頻次控制、計費等諸多功能)json
顯著簡化手機(平板)APP或者PC桌面客戶端開發。相似於2,既能夠用dubbo來開發專門針對無線或者桌面的服務器端,也能夠將原內部使用的dubbo service直接」透明「的暴露給手機APP或桌面程序。固然在有些項目中,手機或桌面程序也能夠直接訪問以上場景2中所述的Open API。
顯著簡化瀏覽器AJAX應用的開發。相似於2,既能夠用dubbo來開發專門的AJAX服務器端,也能夠將原內部使用的dubbo service直接」透明「的暴露給瀏覽器中JavaScript。固然,不少AJAX應用更適合與web框架協同工做,因此直接訪問dubbo service在不少web項目中未必是一種很是優雅的架構。
爲企業內部的dubbo系統之間(即服務提供端和消費端都是基於dubbo的系統)提供一種基於文本的、易讀的遠程調用方式。
必定程度簡化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
而任何客戶端均可以將包含用戶信息的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服務是要被髮布到特定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" />
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實現所涉及的版本。
下面咱們擴充「快速入門」中的UserService,進一步展現在dubbo中REST服務提供端的開發要點。
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數據。
在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被直接忽略。
在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);
因爲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。
目前在dubbo中,咱們支持5種嵌入式rest server的實現,並同時支持採用外部應用服務器來作rest server的實現。rest server的實現是經過以下server這個XML屬性來選擇的:
<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>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>
即必須將dubbo的BootstrapListener和DispatherServlet添加到web.xml,以完成dubbo的REST功能與外部servlet容器的集成。
注意:若是你是用spring的ContextLoaderListener來加載spring,則必須保證BootstrapListener配置在ContextLoaderListener以前,不然dubbo初始化會出錯。
其實,這種場景下你依然能夠堅持用嵌入式server,但外部應用服務器的servlet容器每每比嵌入式server更增強大(特別是若是你是部署到更健壯更可伸縮的WebLogic,WebSphere等),另外有時也便於在應用服務器作統一管理、監控等等。
在遠程調用中,值得獲取的上下文信息可能有不少種,這裏特別以獲取客戶端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"或者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()); }
注意:這種方式只能在設置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容器。
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的contextpath保持一致。例如,對於部署爲tomcat ROOT路徑的應用,這裏的contextpath必須與web.xml中DispacherServlet的<url-pattern/>
徹底一致:
<servlet-mapping>
<servlet-name>dispatcher</servlet-name> <url-pattern>/services/*</url-pattern> </servlet-mapping>
能夠爲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"才能生效。
能夠配置服務器提供端所能同時接收的最大HTTP鏈接數,防止REST server被過多鏈接撐爆,以做爲一種最基本的自我保護機制:
<dubbo:protocol name="rest" accepts="500" server="tomcat/>
注意:這個配置目前只對server="tomcat"才能生效。
若是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控制。
Dubbo的REST支持用GZIP壓縮請求和響應的數據,以減小網絡傳輸時間和帶寬佔用,但這種方式會也增長CPU開銷。
TODO more contents to add
以上全部的討論都是基於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提供了一箇中心點來涵蓋的全部組件和配置,更一目瞭然,通常更便於項目長時期的維護。
固然,選擇哪一種配置方式沒有絕對的優劣,和我的的偏好也不無關係。
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和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()); } }
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"/>
Dubbo rest支持輸出全部HTTP請求/響應中的header字段和body消息體。
在XML配置中添加以下自帶的REST filter:
<dubbo:protocol name="rest" port="8888" extension="com.alibaba.dubbo.rpc.protocol.rest.support.LoggingFilter"/>
而後配置在logging配置中至少爲com.alibaba.dubbo.rpc.protocol.rest.support打開INFO級別日誌輸出,例如,在log4j.xml中配置:
<logger name="com.alibaba.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"/>
Dubbo的REST調用和dubbo中其它某些RPC不一樣的是,須要在服務代碼中添加JAX-RS的annotation(以及JAXB、Jackson的annotation),若是你以爲這些annotation必定程度「污染」了你的服務代碼,你能夠考慮編寫額外的Facade和DTO類,在Facade和DTO上添加annotation,而Facade將調用轉發給真正的服務實現類。固然事實上,直接在服務代碼中添加annotation基本沒有任何負面做用,並且這自己是Java EE的標準用法,另外JAX-RS和JAXB的annotation是屬於java標準,比咱們常用的spring、dubbo等等annotation更沒有vendor lock-in的問題,因此通常沒有必要所以而引入額外對象。
另外,若是你想用前述的@Context annotation,經過方法參數注入HttpServletRequest(如public User getUser(@PathParam("id") Long id, @Context HttpServletRequest request)
),這時候因爲改變了服務的方法簽名,而且HttpServletRequest是REST特有的參數,若是你的服務要支持多種RPC機制的話,則引入額外的Facade類是比較適當的。
固然,在沒有添加REST調用以前,你的服務代碼可能自己已經就充當了Facade和DTO的角色(至於爲何有些場景須要這些角色,有興趣可參考微觀SOA:服務設計原則及其實踐方式)。這種狀況下,在添加REST以後,若是你再額外添加與REST相關的Facade和DTO,就至關於對原有代碼對再一次包裝,即造成以下調用鏈:
RestFacade/RestDTO -> Facade/DTO -> Service
這種體系比較繁瑣,數據轉換之類的工做量也不小,因此通常應儘可能避免如此。
這裏咱們用三種場景來分別討論:
這種場景的客戶端與dubbo自己無關,直接選用相應語言和框架中合適的方式便可。
若是是仍是java的客戶端(但沒用dubbo),能夠考慮直接使用標準的JAX-RS Client API或者特定REST實現的Client API來調用REST服務。下面是用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(); // 在真正開發中不要每次關閉client,好比HTTP長鏈接是由client持有的 }
上面代碼片斷中的User和RegistrationResult類都是消費端本身編寫的,JAX-RS Client API會自動對它們作序列化/反序列化。
固然,在java中也能夠直接用本身熟悉的好比HttpClient,FastJson,XStream等等各類不一樣技術來實現REST客戶端,在此再也不詳述。
這種場景下,和使用其餘dubbo的遠程調用方式同樣,直接在服務提供端和服務消費端共享Java服務接口,並添加spring xml配置(固然也能夠用spring/dubbo的annotation配置),便可透明的調用遠程REST服務:
<dubbo:reference id="userService" interface="xxx.UserService"/>
如前所述,這種場景下必須把JAX-RS的annotation添加到服務接口上,這樣在dubbo在消費端才能共享相應的REST配置信息,並據之作遠程調用:
@Path("users") public interface UserService { @GET @Path("{id : \\d+}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) User getUser(@PathParam("id") Long id); }
若是服務接口的annotation中配置了多種數據格式,這裏因爲兩端都是dubbo系統,REST的大量細節被屏蔽了,因此不存在用前述URL後綴之類選擇數據格式的可能。目前在這種狀況下,排名最靠前的數據格式將直接被使用。
所以,咱們建議你在定義annotation的時候最好把最合適的數據格式放到前面,好比以上咱們是把json放在xml前面,由於json的傳輸性能優於xml。
這種場景下,能夠直接用場景1中描述的Java的方式來調用REST服務。但其實也能夠採用場景2中描述的方式,即更透明的調用REST服務,即便這個服務並非dubbo提供的。
若是用場景2的方式,因爲這裏REST服務並不是dubbo提供,通常也就沒有前述的共享的Java服務接口,因此在此咱們須要根據外部REST服務的狀況,本身來編寫Java接口以及相應參數類,並添加JAX-RS、JAXB、Jackson等的annotation,dubbo的REST底層實現會據此去自動生成請求消息,自動解析響應消息等等,從而透明的作遠程調用。或者這種方式也能夠理解爲,咱們嘗試用JAX-RS的方式去仿造實現一遍外部的REST服務提供端,而後把寫成服務接口放到客戶端來直接使用,dubbo的REST底層實現就能像調用dubbo的REST服務同樣調用其餘REST服務。
例如,咱們要調用以下的外部服務
http://api.foo.com/services/users/1001 http://api.foo.com/services/users/1002
獲取不一樣ID的用戶資料,返回格式是JSON
{
"id": 1001, "name": "Larry" }
咱們可根據這些信息,編寫服務接口和參數類便可:
@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; // … }
對於spring中的配置,由於這裏的REST服務不是dubbo提供的,因此沒法使用dubbo的註冊中心,直接配置外部REST服務的url地址便可(如多個地址用逗號分隔):
<dubbo:reference id="userService" interface="xxx.UserService" url="rest://api.foo.com/services/"/>
注意:這裏協議必須用rest://而不是http://之類。若是外部的REST服務有context path,則在url中也必須添加上(除非你在每一個服務接口的@Pathannotation中都帶上context path),例如上面的/services/。同時這裏的services後面必須帶上/,這樣才能使dubbo正常工做。
另外,這裏依然能夠配置客戶端可啓動的最大鏈接數和超時時間:
<dubbo:reference id="userService" interface="xxx.UserService" url="rest://api.foo.com/services/" timeout="2000" connections="10"/>
Dubbo中的REST開發是徹底兼容標準JAX-RS的,但其支持的功能目前是完整JAX-RS的一個子集,部分由於它要受限於dubbo和spring的特定體系。
在dubbo中使用的JAX-RS的侷限包括但不限於:
能夠的,並且是自動集成的,也就是你在dubbo中開發的全部REST服務都會自動註冊到服務冊中心和監控中心,能夠經過它們作管理。
可是,只有當REST的消費端也是基於dubbo的時候,註冊中心中的許多服務治理操做才能徹底起做用。而若是消費端是非dubbo的,天然不受註冊中心管理,因此其中不少操做是不會對消費端起做用的。
若是dubbo REST的消費端也是dubbo的,則Dubbo REST和其餘dubbo遠程調用協議基本徹底同樣,由dubbo框架透明的在消費端作load balance、failover等等。
若是dubbo REST的消費端是非dubbo的,甚至是非java的,則最好配置服務提供端的軟負載均衡機制,目前可考慮用LVS、HAProxy、 Nginx等等對HTTP請求作負載均衡。
http://stackoverflow.com/questions/17196766/can-resteasy-choose-method-based-on-query-params
http://stackoverflow.com/questions/5553218/jax-rs-post-multiple-objects
我認爲dubbo當前體系中顯然也有很多不足之處,這裏列出幾個與REST有關的、並影響用戶使用的問題(不包括內部實現的問題),供參考評論,爲下一步重構做準備。
在前文,前面咱們已經提到過RpcContext用法的侵入性,因爲它是用單例的方式來訪問上下文信息,這徹底不符合spring應用的通常風格,不利於應用擴展和單元測試。將來咱們可能用依賴注入方式注入一個接口,再用它去訪問ThreadLocal中的上下文信息。
dubbo支持多種遠程調用方式,但全部調用方式都是用<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"/>
其實,上面不少屬性實際上dubbo RPC遠程調用方式特有的,不少dubbo中的其它遠程調用方式根本就不支持例如server, client, codec, iothreads, accepts, payload等等(固然,有的是條件所限不支持,有的是根本沒有必要支持)。這給用戶的使用徒增不少困惑,用戶也並不知道有些屬性(好比作性能調優)添加了其實是不起做用的。
另外一方面,各類遠程調用方式每每有大量本身獨特的配置須要,特別是咱們逐步爲每種遠程調用方式都添加更豐富、更高級的功能,這就不可避免的擴展<protocol/>
中的屬性(例如目前咱們在REST中已經添加了keepalive和extension兩個屬性),到最後會致使<protocol/>
臃腫不堪,用戶的使用也更加困惑。
固然,dubbo中有一種擴展<protocol/>
的方式是用<dubbo:parameter/>
,但這種方式顯然頗有侷限性,並且用法複雜,缺少schema校驗。
因此,最好的方式是爲每種遠程調用方式設置本身的protocol元素,好比<protocol-dubbo/>
,<protocol-rest/>
等等,每種元素用XML schema規定本身的屬性(固然屬性在各類遠程調用方式之間能通用是最好的)。
如此一來,例如前面提到過的extension配置也能夠用更自由的方式,從而更清楚更可擴展(如下只是舉例,固然也許有更好的方式):
<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>
dubbo的XML配置中大量命名都不符合spring規範,好比:
<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應該改成thread-pool,iothreads應該改成io-threads,單詞之間應該用"-"分隔。這雖然看起來是個小問題,但也涉及到了可讀性,特別是可擴展性,由於有時候咱們不可避免要用更多單詞來描述XML元素和屬性。
其實dubbo自己也是建議遵照spring到XML的命名規範。
TODO
粗略以下:
和dubbo自身的基準測試保持接近:
10個併發客戶端持續不斷髮出請求:
進行5分鐘性能測試。(引用dubbo自身測試的考慮:「主要考察序列化和網絡IO的性能,所以服務端無任何業務邏輯。取10併發是考慮到http協議在高併發下對CPU的使用率較高可能會先打到瓶頸。」)
下面的結果主要對比的是REST和dubbo RPC兩種遠程調用方式,並對它們做不一樣的配置,例如:
針對複雜對象的結果以下(響應時間越小越好,TPS越大越好):
遠程調用方式 | 平均響應時間 | 平均TPS(每秒事務數) |
---|---|---|
REST: Jetty + JSON | 7.806 | 1280 |
REST: Jetty + JSON + GZIP | TODO | TODO |
REST: Jetty + XML | TODO | TODO |
REST: Jetty + XML + GZIP | TODO | TODO |
REST: Tomcat + JSON | 2.082 | 4796 |
REST: Netty + JSON | 2.182 | 4576 |
Dubbo: FST | 1.211 | 8244 |
Dubbo: kyro | 1.182 | 8444 |
Dubbo: dubbo serialization | 1.43 | 6982 |
Dubbo: hessian2 | 1.49 | 6701 |
Dubbo: fastjson | 1.572 | 6352 |
僅就目前的結果,一點簡單總結:
若是將dubbo REST部署到外部Tomcat上,並配置server="servlet",即啓用外部的tomcat來作爲rest server的底層實現,則最好在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",這個配置主要是保證tomcat一直啓用http長鏈接,以提升REST調用性能。可是請注意,若是REST消費端不是持續的調用REST服務,則一直啓用長鏈接未必是最好的作法。另外,一直啓用長鏈接的方式通常不適合針對普通webapp,更適合這種相似rpc的場景。因此爲了高性能,在tomcat中,dubbo REST應用和普通web應用最好不要混合部署,而應該用單獨的實例。
TODO more contents to add
TODO
TODO
謝謝,對於jax-rs和spring mvc,其實我對spring mvc的rest支持尚未太深刻的看過,說點初步想法,請你們指正:
spring mvc也支持annotation的配置,其實和jax-rs看起來是很是很是相似的。
我我的認爲spring mvc相對更適合於面向web應用的restful服務,好比被AJAX調用,也可能輸出HTML之類的,應用中還有頁面跳轉流程之類,spring mvc既能夠作好正常的web頁面請求也能夠同時處理rest請求。但總的來講這個restful服務是在展示層或者叫web層之類實現的
而jax-rs相對更適合純粹的服務化應用,也就是傳統Java EE中所說的中間層服務,好比它能夠把傳統的EJB發佈成restful服務。在spring應用中,也就把spring中充當service之類的bean直接發佈成restful服務。總的來講這個restful服務是在業務、應用層或者facade層。而MVC層次和概念在這種作好比(後臺)服務化的應用中一般是沒有多大價值的。
固然jax-rs的有些實現好比jersey,也試圖提供mvc支持,以更好的適應上面所說的web應用,但應該是不如spring mvc。
在dubbo應用中,我想不少人都比較喜歡直接將一個本地的spring service bean(或者叫manager之類的)徹底透明的發佈成遠程服務,則這裏用JAX-RS是更天然更直接的,沒必要額外的引入MVC概念。固然,先不討論透明發布遠程服務是否是最佳實踐,要不要添加facade之類。
固然,我知道在dubbo不支持rest的狀況下,不少朋友採用的架構是spring mvc restful調用dubbo (spring) service來發布restful服務的。這種方式我以爲也很是好,只是若是不修改spring mvc並將其與dubbo深度集成,restful服務不能像dubbo中的其餘遠程調用協議好比webservices、dubbo rpc、hessian等等那樣,享受諸多高級的服務治理的功能,好比:註冊到dubbo的服務註冊中心,經過dubbo監控中心監控其調用次數、TPS、響應時間之類,經過dubbo的統一的配置方式控制其好比線程池大小、最大鏈接數等等,經過dubbo統一方式作服務流量控制、權限控制、頻次控制。另外spring mvc僅僅負責服務端,而在消費端,一般是用spring restTemplate,若是restTemplate不和dubbo集成,有可能像dubbo服務客戶端那樣自動或者人工干預作服務降級。若是服務端消費端都是dubbo系統,經過spring的rest交互,若是spring rest不深度整合dubbo,則不能用dubbo統一的路由分流等功能。
固然,其實我我的認爲這些東西沒必要要非此即彼的。我據說spring創始人rod johnson老是愛說一句話,the customer is always right,其實與其非要探討哪一種方式更好,不如同時支持兩種方式就是了,因此原來在文檔中也寫過計劃支持spring rest annoation,只是不知道具體可行性有多高。
稍後可能要實現的功能: