REST 协议

基于标准 Java REST API - JAX-RS 2.0(Java API for RESTful Web Services 的简称)的 REST 调用支持

快速开始

在 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 服务要发布到指定的 URL 供任何语言的客户端甚至浏览器访问,这里添加了 JAX-RS 的几个标准注解进行相关配置。

@Path(“users”): 指定访问 UserService 的 URL 的相对路径为 /users,即 http://localhost:8080/users

@Path(“register”): 指定访问 registerUser() 方法的 URL 的相对路径为 /register,结合前面 @Path 为 UserService 指定的路径,调用 UserService.register() 的完整路径为 http://localhost:8080/users/register

@POST: 指定使用 HTTP POST 方法访问 registerUser()

@Consumes({MediaType.APPLICATION_JSON}): 指定 registerUser() 接收 JSON 格式的数据。REST 框架会自动将 JSON 数据反序列化为 User 对象

最后,在 spring 配置文件中添加此服务以完成所有服务开发工作

<!-- Expose the service on port 8080 using the rest protocol -->
<dubbo:protocol name="rest" port="8080"/>

<!-- Declare the service interface that needs to be exposed -->
<dubbo:service interface="xxx.UserService" ref="userService"/>

<!-- Implement services like local beans -->
<bean id="userService" class="xxx.UserServiceImpl" />

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() 方法。在这里的注解配置中,@Path 中间的 {id: xxx} 指定 URL 相对路径包含一个名为 id 的参数,它的值会自动传递给下面用 @PathParam(“id”) 修饰的方法参数 id。{id: 后面的 \d+ 是一个正则表达式,指定 id 参数必须是一个数字。

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

注解放在接口类还是实现类

Dubbo 中 REST 服务的开发主要通过 JAX-RS 注解进行配置。在上面的例子中,我们将注解放在了服务实现类中。但实际上,我们也可以将注解放在服务的接口上。两种方式完全等效,例如

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

在一般的应用中,我们建议将注解放在服务实现类中,这样注解的位置和 java 实现代码更接近,便于开发和维护。另外,更重要的是,我们一般倾向于避免污染接口,保持接口的纯洁性和广泛适用性。

但是,正如后面提到的,如果我们要使用 dubbo 直接开发的消费者来访问这个服务,那么注解就必须放在接口上。

如果接口和实现类都添加了注解,则实现类的注解配置会生效,接口上的注解会被直接忽略。

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

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

例如,如果我们希望上面例子中的 getUser() 方法支持返回 JSON 和 XML 格式的数据,我们只需要在注解中包含这两种格式

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

如果所有方法都支持相同类型的输入和输出数据格式,我们不需要对每个方法都进行配置,只需要在服务类上添加注解即可

@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 头部(content-type 和 accept)来指定当前想要哪种格式的数据。

但在 dubbo 中,我们也自动支持目前业界常用的方法,即使用 URL 后缀(.json 和 .xml)来指定想要的数据格式。例如,添加上述注解后,直接访问 http://localhost:8888/users/1001.json 表示使用 json 格式,直接访问 http://localhost:8888/users/1002.xml 表示使用 xml 格式,比使用 HTTP Header 更简单直观。Twitter、微博等的 REST API 都使用了这种方式。如果既不添加 HTTP 头部也不添加后缀,dubbo 的 REST 会优先使用上述注解定义中排名第一的数据格式。

注意:要在 REST 中支持 XML 格式数据,你可以在注解中使用 MediaType.TEXT_XML 或 MediaType.APPLICATION_XML,但 TEXT_XML 更常用,并且如果你想使用上述 URL 后缀的方式来指定数据格式,则只能配置成 TEXT_XML 时才能生效。

中文字符支持

为了能在 dubbo REST 中正常输出中文,和通常的 Java Web 应用一样,我们需要将 HTTP 响应的 contentType 设置为 UTF-8 编码。

基于 JAX-RS 的标准用法,我们只需要进行如下注解配置即可

@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 注解,否则序列化时会报错。例如,为 getUser() 中返回的 User 添加如下

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

另外,如果服务方法中返回的值是 Java 基本类型(如 int、long、float、double 等),最好也为其添加一层包装对象,因为 JAXB 不能直接序列化基本类型。

例如,我们希望前面提到的 registerUser() 方法返回服务器为用户生成的用户 ID 号

long registerUser(User user);

由于 JAXB 序列化不支持基本类型,所以添加一个包装对象

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

并修改服务方法

RegistrationResult registerUser(User user);

这样做不仅解决了 XML 序列化的问,而且让返回的数据更加符合 XML 和 JSON 的规范。例如,在 JSON 中,返回将是如下形式

{"id": 1001}

如果不添加包装,则 JSON 返回值将直接是

1001

而在 XML 中,添加包装后的返回值将是

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

这种包装对象实际上就是应用了所谓的数据传输对象(Data Transfer Object,DTO)模式,使用 DTO 还可以对传输的数据做更多有用的定制。

自定义序列化

前面提到,REST 的底层实现会自动进行服务对象和 JSON/XML 数据格式之间的序列化/反序列化。但在某些场景下,如果你觉得这种自动转换并不能满足需求,你也可以进行自定义。

Dubbo 中 REST 的实现分别使用了 JAXB 和 Jackson 来进行 XML 和 JSON 的序列化,因此,你可以通过在对象上添加 JAXB 或 Jackson 注解来自定义映射关系。

例如,自定义对象属性映射到 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

<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 更多内容待补充)

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

上述配置使用嵌入式 tjws 或 Sun HTTP server 作为 rest server。这两种 server 实现都非常轻量级,在集成测试中使用非常方便快速启动,当然,在负载不高的生产环境也能胜任。注意:tjws 目前已被弃用,因为它不能很好地与 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>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 将无法正常初始化。

其实,在这种场景下,你仍然可以使用嵌入式 server,但外部应用服务器的 servlet 容器往往比嵌入式 server 功能更强大(特别是如果你部署到 WebLogic、WebSphere 等更加健壮、可扩展的应用服务器中),而且有时候也便于在应用服务器上进行统一管理、监控等。

获取上下文信息

在远程调用中,可能有很多种类的上下文信息值得获取,这里以获取客户端 IP 为例进行说明。

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

第一种方式,使用 JAX-RS 标准的 @Context 注解

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、tomcat、jetty 或 servlet 时才能生效,因为只有这几种 server 实现提供了 servlet 容器。另外,标准的 JAX-RS 还支持使用 @Context 来修饰服务类的实例字段来获取 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 的 context path(即 webapp 路径加上 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 工作线程数

<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 被过多的连接 overwhelmed,作为一种基本的自我保护机制

<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 官方文档的说明:“服务提供者在 Consumer 端尽可能多配置 Consumer 端属性,让 Provider 端配置者一开始就思考 Provider 服务特点,服务质量的问题。”

注意:如果 dubbo 的 REST 服务是发布给非 dubbo 客户端的,则这里在 <dubbo:service/> 上的配置是完全无效的,因为此类客户端不受 dubbo 控制。

使用注解替换部分 Spring XML 配置

以上所有讨论都是基于 dubbo 在 spring 中的 xml 配置。但是,dubbo/spring 本身也支持使用注解进行配置,所以我们也可以按照 dubbo 官方文档中的步骤,在 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);
    }
}

注解的配置方式更加简洁、精确,并且通常也更容易维护(当然,现代 IDE 都可以支持在 xml 中进行例如类名重构等操作,所以就这里的具体使用场景来说,xml 也是非常易于维护的)。而 xml 对代码的侵入性更低,尤其有利于配置的动态修改,例如你希望针对单个服务配置连接超时时间、每个客户端的最大连接数、集群策略、权重等等。此外,尤其是针对复杂的应用或模块来说,xml 提供了一个中心点来涵盖所有的组件和配置,一目了然,通常也更有利于项目的长期维护。

当然,选择哪种配置方式并没有绝对的优劣之分,也无关个人喜好。

添加自定义 Filter、Interceptor 等

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

其中,Filter 主要用于访问和设置 HTTP 请求和响应参数、URI 等,例如设置 HTTP 响应的缓存头

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 应用中,我们一般通过添加 @Provider 注解的方式来标注 Filter 和 Interceptor,然后 JAX-RS 运行时会自动发现并启用它们。而在 dubbo 中,我们通过添加 XML 配置的方式来注册 Filter 和 Interceptor

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

这里,我们可以将 Filter、Interceptor 和 DynamicFeature 三种类型的对象都添加到 extension 属性中,并以逗号分隔。(DynamicFeature 是另外一个接口,可以方便我们更加动态地启用 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,可以用来定制在特定异常发生后应该返回的 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 请求/响应中的头字段和消息体。

在 XML 配置中添加以下内置的 REST 过滤器

<dubbo:protocol name="rest" port="8888" extension="org.apache.dubbo.rpc.protocol.rest.support.LoggingFilter"/>

然后在日志配置中配置为至少 org.apache.dubbo.rpc.protocol.rest.support 开启 INFO 级别的日志输出,例如在 log4j.xml 中配置

<logger name="org.apache.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 are:
{"id":1,"name":"dang"}

开启 HTTP 日志输出后,除了正常的日志输出的性能开销之外,在解析 HTTP 请求时还会产生额外的开销,例如因为需要建立额外的内存缓冲区来准备日志输出的数据。

输入参数校验

Dubbo 的 rest 支持 Java 标准的 bean validation 注解(JSR 303)进行输入校验 http://beanvalidation.org/

为了和其他 dubbo 远程调用协议保持一致,rest 中进行校验的注解必须放在服务接口上,例如

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

当然,在很多其他的 bean validation 应用场景中,注解是放在实现类而不是接口上的。将注解放在接口上至少有一个好处,就是 dubbo 客户端可以共享这个接口的信息,dubbo 甚至可以在不进行远程调用的情况下,就在本地完成输入校验。

然后按照 dubbo 的标准方式在 XML 配置中开启 validation

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

在 dubbo 的很多其他远程调用协议中,如果输入校验失败,会直接向客户端抛出 RpcException,但在 rest 中,由于客户端往往是非 dubbo 体系甚至是非 Java 体系,直接抛出一个 Java 异常是不太方便的。因此,目前我们是以 XML 的形式返回校验错误

<violationReport>
    <constraint Violations>
        <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()));
        }
        // Use json output instead of xml output
        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"/>

上次修改时间:2023 年 1 月 2 日: 增强英文文档 (#1798) (95a9f4f6c1c)