在Dubbo中开发REST风格的远程调用(RESTful Remoting)

概述

dubbo支持多种远程调用方式,例如dubbo RPC(二进制序列化 + tcp协议)、http invoker(二进制序列化 + http协议,至少在开源版本没发现对文本序列化的支持)、hessian(二进制序列化 + http协议)、WebServices (文本序列化 + http协议)等等,但缺少对当今特别流行的REST风格远程调用(文本序列化 + http协议)的支持。java

有鉴于此,咱们基于标准的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的简写),为dubbo提供了接近透明的REST调用支持。因为彻底兼容Java标准API,因此为dubbo开发的全部REST服务,将来脱离dubbo或者任何特定的REST底层实现通常也能够正常运行。git

特别值得指出的是,咱们并不须要彻底严格遵照REST的原始定义和架构风格。即便著名的Twitter REST API也会根据状况作适度调整,而不是机械的遵照原始的REST风格。github

附注:咱们将这个功能称之为REST风格的远程调用,即RESTful Remoting(抽象的远程处理或者调用),而不是叫RESTful RPC(具体的远程“过程”调用),是由于REST和RPC自己能够被认为是两种不一样的风格。在dubbo的REST实现中,能够说有两个面向,其一是提供或消费正常的REST服务,其二是将REST做为dubbo RPC体系中一种协议实现,而RESTful Remoting同时涵盖了这个面向。web

REST的优势

如下摘自维基百科:正则表达式

  • 可更高效利用缓存来提升响应速度spring

  • 通信自己的无状态性可让不一样的服务器的处理一系列请求中的不一样请求,提升服务器的扩展性编程

  • 浏览器便可做为客户端,简化软件需求json

  • 相对于其余叠加在HTTP协议之上的机制,REST的软件依赖性更小api

  • 不须要额外的资源发现机制浏览器

  • 在软件技术演进中的长期的兼容性更好

这里我还想特别补充REST的显著优势:基于简单的文本格式消息和通用的HTTP协议,使它具有极广的适用性,几乎全部语言和平台都对它提供支持,同时其学习和使用的门槛也较低。

应用场景

正是因为REST在适用性方面的优势,因此在dubbo中支持REST,能够为当今多数主流的远程调用场景都带来(显著)好处:

  1. 显著简化企业内部的异构系统之间的(跨语言)调用。此处主要针对这种场景:dubbo的系统作服务提供端,其余语言的系统(也包括某些不基于dubbo的java系统)作服务消费端,二者经过HTTP和文本消息进行通讯。即便相比Thrift、ProtoBuf等二进制跨语言调用方案,REST也有本身独特的优点(详见后面讨论)

  2. 显著简化对外Open API(开放平台)的开发。既能够用dubbo来开发专门的Open API应用,也能够将原内部使用的dubbo service直接“透明”发布为对外的Open REST API(固然dubbo自己将来最好能够较透明的提供诸如权限控制、频次控制、计费等诸多功能)

  3. 显著简化手机(平板)APP或者PC桌面客户端开发。相似于2,既能够用dubbo来开发专门针对无线或者桌面的服务器端,也能够将原内部使用的dubbo service直接”透明“的暴露给手机APP或桌面程序。固然在有些项目中,手机或桌面程序也能够直接访问以上场景2中所述的Open API。

  4. 显著简化浏览器AJAX应用的开发。相似于2,既能够用dubbo来开发专门的AJAX服务器端,也能够将原内部使用的dubbo service直接”透明“的暴露给浏览器中JavaScript。固然,不少AJAX应用更适合与web框架协同工做,因此直接访问dubbo service在不少web项目中未必是一种很是优雅的架构。

  5. 为企业内部的dubbo系统之间(即服务提供端和消费端都是基于dubbo的系统)提供一种基于文本的、易读的远程调用方式。

  6. 必定程度简化dubbo系统对其它异构系统的调用。能够用相似dubbo的简便方式“透明”的调用非dubbo系统提供的REST服务(无论服务提供端是在企业内部仍是外部)

须要指出的是,我认为1~3是dubbo的REST调用最有价值的三种应用场景,而且咱们为dubbo添加REST调用,其最主要到目的也是面向服务的提供端,即开发REST服务来提供给非dubbo的(异构)消费端。

概括起来,全部应用场景以下图所示:
no image found

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

标准Java REST API:JAX-RS简介

JAX-RS是标准的Java REST API,获得了业界的普遍支持和应用,其著名的开源实现就有不少,包括Oracle的Jersey,RedHat的RestEasy,Apache的CXF和Wink,以及restlet等等。另外,全部支持JavaEE 6.0以上规范的商用JavaEE应用服务器都对JAX-RS提供了支持。所以,JAX-RS是一种已经很是成熟的解决方案,而且采用它没有任何所谓vendor lock-in的问题。

JAX-RS在网上的资料很是丰富,例以下面的入门教程:

更多的资料请自行google或者百度一下。就学习JAX-RS来讲,通常主要掌握其各类annotation的用法便可。

注意:dubbo是基于JAX-RS 2.0版本的,有时候须要注意一下资料或REST实现所涉及的版本。

REST服务提供端详解

下面咱们扩充“快速入门”中的UserService,进一步展现在dubbo中REST服务提供端的开发要点。

HTTP POST/GET的实现

REST服务中虽然建议使用HTTP协议中四种标准方法POST、DELETE、PUT、GET来分别实现常见的“增删改查”,但实际中,咱们通常状况直接用POST来实现“增改”,GET来实现“删查”便可(DELETE和PUT甚至会被一些防火墙阻挡)。

前面已经简单演示了POST的实现,在此,咱们为UserService添加一个获取注册用户资料的功能,来演示GET的实现。

这个功能就是要实现客户端经过访问以下不一样URL来获取不一样ID的用户资料:

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

固然,也能够经过其余形式的URL来访问不一样ID的用户资料,例如:

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

JAX-RS自己能够支持全部这些形式。可是上面那种在URL路径中包含查询参数的形式(http://localhost:8080/users/1001) 更符合REST的通常习惯,因此更推荐你们来使用。下面咱们就为UserService添加一个getUser()方法来实现这种形式的URL访问:

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

@GET:指定用HTTP GET方法访问

@Path("{id : \d+}"):根据上面的功能需求,访问getUser()的URL应当是“http://localhost:8080/users/ + 任意数字",而且这个数字要被作为参数传入getUser()方法。 这里的annotation配置中,@Path中间的{id: xxx}指定URL相对路径中包含了名为id参数,而它的值也将被自动传递给下面用@PathParam("id")修饰的方法参数id。{id:后面紧跟的\d+是一个正则表达式,指定了id参数必须是数字。

@Produces({MediaType.APPLICATION_JSON}):指定getUser()输出JSON格式的数据。框架会自动将User对象序列化为JSON数据。

Annotation放在接口类仍是实现类

在Dubbo中开发REST服务主要都是经过JAX-RS的annotation来完成配置的,在上面的示例中,咱们都是将annotation放在服务的实现类中。但其实,咱们彻底也能够将annotation放到服务的接口上,这两种方式是彻底等价的,例如:

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

在通常应用中,咱们建议将annotation放到服务实现类,这样annotation和java实现代码位置更接近,更便于开发和维护。另外更重要的是,咱们通常倾向于避免对接口的污染,保持接口的纯净性和普遍适用性。

可是,如后文所述,若是咱们要用dubbo直接开发的消费端来访问此服务,则annotation必须放到接口上。

若是接口和实现类都同时添加了annotation,则实现类的annotation配置会生效,接口上的annotation被直接忽略。

JSON、XML等多数据格式的支持

在dubbo中开发的REST服务能够同时支持传输多种格式的数据,以给客户端提供最大的灵活性。其中咱们目前对最经常使用的JSON和XML格式特别添加了额外的功能。

好比,咱们要让上例中的getUser()方法支持分别返回JSON和XML格式的数据,只须要在annotation中同时包含两种格式便可:

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

或者也能够直接用字符串(还支持通配符)表示MediaType:

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

若是全部方法都支持一样类型的输入输出数据格式,则咱们无需在每一个方法上作配置,只须要在服务类上添加annotation便可:

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

在一个REST服务同时对多种数据格式支持的状况下,根据JAX-RS标准,通常是经过HTTP中的MIME header(content-type和accept)来指定当前想用的是哪一种格式的数据。

可是在dubbo中,咱们还自动支持目前业界广泛使用的方式,即用一个URL后缀(.json和.xml)来指定想用的数据格式。例如,在添加上述annotation后,直接访问http://localhost:8888/users/1001.json则表示用json格式,直接访问http://localhost:8888/users/1002.xml则表示用xml格式,比用HTTP Header更简单直观。Twitter、微博等的REST API都是采用这种方式。

若是你既不加HTTP header,也不加后缀,则dubbo的REST会优先启用在以上annotation定义中排位最靠前的那种数据格式。

注意:这里要支持XML格式数据,在annotation中既能够用MediaType.TEXT_XML,也能够用MediaType.APPLICATION_XML,可是TEXT_XML是更经常使用的,而且若是要利用上述的URL后缀方式来指定数据格式,只能配置为TEXT_XML才能生效。

中文字符支持

为了在dubbo REST中正常输出中文字符,和一般的Java web应用同样,咱们须要将HTTP响应的contentType设置为UTF-8编码。

基于JAX-RS的标准用法,咱们只须要作以下annotation配置便可:

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

为了方便用户,咱们在dubbo REST中直接添加了一个支持类,来定义以上的常量,能够直接使用,减小出错的可能性。

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

XML数据格式的额外要求

因为JAX-RS的实现通常都用标准的JAXB(Java API for XML Binding)来序列化和反序列化XML格式数据,因此咱们须要为每个要用XML传输的对象添加一个类级别的JAXB annotation,不然序列化将报错。例如为getUser()中返回的User添加以下:

@XmlRootElementpublic class User implements Serializable {    // ...}

此外,若是service方法中的返回值是Java的 primitive类型(如int,long,float,double等),最好为它们添加一层wrapper对象,由于JAXB不能直接序列化primitive类型。

例如,咱们想让前述的registerUser()方法返回服务器端为用户生成的ID号:

long registerUser(User user);

因为primitive类型不被JAXB序列化支持,因此添加一个wrapper对象:

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

并修改service方法:

RegistrationResult registerUser(User user);

这样不但可以解决XML序列化的问题,并且使得返回的数据都符合XML和JSON的规范。例如,在JSON中,返回的将是以下形式:

{"id": 1001}

若是不加wrapper,JSON返回值将直接是

1001

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

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

这种wrapper对象其实利用所谓Data Transfer Object(DTO)模式,采用DTO还能对传输数据作更多有用的定制。

定制序列化

如上所述,REST的底层实现会在service的对象和JSON/XML数据格式之间自动作序列化/反序列化。但有些场景下,若是以为这种自动转换不知足要求,能够对其作定制。

Dubbo中的REST实现是用JAXB作XML序列化,用Jackson作JSON序列化,因此在对象上添加JAXB或Jackson的annotation便可以定制映射。

例如,定制对象属性映射到XML元素的名字:

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

定制对象属性映射到JSON字段的名字:

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

更多资料请参考JAXB和Jackson的官方文档,或自行google。

配置REST Server的实现

目前在dubbo中,咱们支持5种嵌入式rest server的实现,并同时支持采用外部应用服务器来作rest server的实现。rest server的实现是经过以下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等),另外有时也便于在应用服务器作统一管理、监控等等。

获取上下文(Context)信息

在远程调用中,值得获取的上下文信息可能有不少种,这里特别以获取客户端IP为例。

在dubbo的REST中,咱们有两种方式获取客户端IP。

第一种方式,用JAX-RS标准的@Context annotation:

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

用Context修饰getUser()的一个方法参数后,就能够将当前的HttpServletRequest注入进来,而后直接调用servlet api获取IP。

注意:这种方式只能在设置server="tjws"或者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。

配置端口号和Context Path

dubbo中的rest协议默认将采用80端口,若是想修改端口,直接配置:

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

另外,如前所述,咱们能够用@Path来配置单个rest服务的URL相对路径。但其实,咱们还能够设置一个全部rest服务都适用的基础相对路径,即java web应用中常说的context path。

只须要添加以下contextpath属性便可:

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

之前面代码为例:

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

如今registerUser()的完整访问路径为:

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

注意:若是你是选用外部应用服务器作rest server,即配置:

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

则必须保证这里设置的port、contextpath,与外部应用服务器的端口、DispatcherServlet的上下文路径(即webapp path加上servlet url pattern)保持一致。例如,对于部署为tomcat ROOT路径的应用,这里的contextpath必须与web.xml中DispacherServlet的<url-pattern/>彻底一致:

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

配置线程数和IO线程数

能够为rest服务配置线程池大小:

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

注意:目前线程池的设置只有当server="netty"或者server="jetty"或者server="tomcat"的时候才能生效。另外,若是server="servlet",因为这时候启用的是外部应用服务器作rest server,不受dubbo控制,因此这里的线程池设置也无效。

若是是选用netty server,还能够配置Netty的IO worker线程数:

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

配置长链接

Dubbo中的rest服务默认都是采用http长链接来访问,若是想切换为短链接,直接配置:

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

注意:这个配置目前只对server="netty"和server="tomcat"才能生效。

配置最大的HTTP链接数

能够配置服务器提供端所能同时接收的最大HTTP链接数,防止REST server被过多链接撑爆,以做为一种最基本的自我保护机制:

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

注意:这个配置目前只对server="tomcat"才能生效。

配置每一个消费端的超时时间和HTTP链接数

若是rest服务的消费端也是dubbo系统,能够像其余dubbo RPC机制同样,配置消费端调用此rest服务的最大超时时间以及每一个消费端所能启动的最大HTTP链接数。

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

固然,因为这个配置针对消费端生效的,因此也能够在消费端配置:

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

可是,一般咱们建议配置在服务提供端提供此类配置。按照dubbo官方文档的说法:“Provider上尽可能多配置Consumer端的属性,让Provider实现者一开始就思考Provider服务特色、服务质量的问题。”

注意:若是dubbo的REST服务是发布给非dubbo的客户端使用,则这里<dubbo:service/>上的配置彻底无效,由于这种客户端不受dubbo控制。

GZIP数据压缩

Dubbo的REST支持用GZIP压缩请求和响应的数据,以减小网络传输时间和带宽占用,但这种方式会也增长CPU开销。

TODO more contents to add

用Annotation取代部分Spring XML配置

以上全部的讨论都是基于dubbo在spring中的xml配置。可是,dubbo/spring自己也支持用annotation来做配置,因此咱们也能够按dubbo官方文档中的步骤,把相关annotation加到REST服务的实现中,取代一些xml配置,例如:

@Service(protocol = "rest")
@Path("users")public class UserServiceImpl implements UserService {    @Autowired
    private UserRepository userRepository;    @POST
    @Path("register")    @Consumes({MediaType.APPLICATION_JSON})    public void registerUser(User user) {        // save the user
        userRepository.save(user);
    }   
}

annotation的配置更简单更精确,常常也更便于维护(固然现代IDE均可以在xml中支持好比类名重构,因此就这里的特定用例而言,xml的维护性也很好)。而xml对代码对侵入性更小一些,尤为有利于动态修改配置,特别是好比你要针对单个服务配置链接超时时间、每客户端最大链接数、集群策略、权重等等。另外,特别对复杂应用或者模块来讲,xml提供了一个中心点来涵盖的全部组件和配置,更一目了然,通常更便于项目长时期的维护。

固然,选择哪一种配置方式没有绝对的优劣,和我的的偏好也不无关系。

添加自定义的Filter、Interceptor等

Dubbo的REST也支持JAX-RS标准的Filter和Interceptor,以方便对REST的请求与响应过程作定制化的拦截处理。

其中,Filter主要用于访问和设置HTTP请求和响应的参数、URI等等。例如,设置HTTP响应的cache header:

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

Interceptor主要用于访问和修改输入与输出字节流,例如,手动添加GZIP压缩:

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

在标准JAX-RS应用中,咱们通常是为Filter和Interceptor添加@Provider annotation,而后JAX-RS runtime会自动发现并启用它们。而在dubbo中,咱们是经过添加XML配置的方式来注册Filter和Interceptor:

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

在此,咱们能够将Filter、Interceptor和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());
    } 
}

添加自定义的Exception处理

Dubbo的REST也支持JAX-RS标准的ExceptionMapper,能够用来定制特定exception发生后应该返回的HTTP响应。

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

和Interceptor、Filter相似,将其添加到XML配置文件中便可启用:

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

配置HTTP日志输出

Dubbo rest支持输出全部HTTP请求/响应中的header字段和body消息体。

在XML配置中添加以下自带的REST filter:

<dubbo:protocol name="rest" port="8888" extension="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"/>

是否应该透明发布REST服务

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

这种体系比较繁琐,数据转换之类的工做量也不小,因此通常应尽可能避免如此。

相关文章
相关标签/搜索