完善中……javascript
本文篇幅较长,由于REST自己涉及面较多。另外,本文参照Spring等的文档风格,不只仅局限于框架用法的阐述,同时也努力呈现框架的设计理念和优良应用的架构思想。java
对于想粗略了解dubbo和REST的人,只需浏览
概述
至标准Java REST API:JAX-RS简介
几节便可。git
TODO 生成可点击的目录github
dubbo支持多种远程调用方式,例如dubbo RPC(二进制序列化 + tcp协议)、http invoker(二进制序列化 + http协议,至少在开源版本没发现对文本序列化的支持)、hessian(二进制序列化 + http协议)、WebServices (文本序列化 + http协议)等等,但缺少对当今特别流行的REST风格远程调用(文本序列化 + http协议)的支持。web
有鉴于此,咱们基于标准的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风格。spring
附注:咱们将这个功能称之为REST风格的远程调用,即RESTful Remoting(抽象的远程处理或者调用),而不是叫RESTful RPC(具体的远程“过程”调用),是由于REST和RPC自己能够被认为是两种不一样的风格。在dubbo的REST实现中,能够说有两个面向,其一是提供或消费正常的REST服务,其二是将REST做为dubbo RPC体系中一种协议实现,而RESTful Remoting同时涵盖了这个面向。apache
如下摘自维基百科:编程
这里我还想特别补充REST的显著优势:基于简单的文本格式消息和通用的HTTP协议,使它具有极广的适用性,几乎全部语言和平台都对它提供支持,同时其学习和使用的门槛也较低。json
正是因为REST在适用性方面的优势,因此在dubbo中支持REST,能够为当今多数主流的远程调用场景都带来(显著)好处:
显著简化企业内部的异构系统之间的(跨语言)调用。此处主要针对这种场景:dubbo的系统作服务提供端,其余语言的系统(也包括某些不基于dubbo的java系统)作服务消费端,二者经过HTTP和文本消息进行通讯。即便相比Thrift、ProtoBuf等二进制跨语言调用方案,REST也有本身独特的优点(详见后面讨论)
显著简化对外Open API(开放平台)的开发。既能够用dubbo来开发专门的Open API应用,也能够将原内部使用的dubbo service直接“透明”发布为对外的Open REST API(固然dubbo自己将来最好能够较透明的提供诸如权限控制、频次控制、计费等诸多功能)
显著简化手机(平板)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容器。
为了简化编程,在此你也能够用泛型的方式来直接获取特定类型的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。
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>
能够为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链接数,防止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,只是不知道具体可行性有多高。
稍后可能要实现的功能: