在 JAX-RS 2.0 客户端库中处理自定义错误响应

Posted

技术标签:

【中文标题】在 JAX-RS 2.0 客户端库中处理自定义错误响应【英文标题】:Handling custom error response in JAX-RS 2.0 client library 【发布时间】:2014-04-28 23:36:36 【问题描述】:

我开始在 JAX-RS 中使用新的客户端 API 库,到目前为止我真的很喜欢它。我发现了一件事我无法弄清楚。我使用的 API 有一个自定义错误消息格式,例如:


    "code": 400,
    "message": "This is a message which describes why there was a code 400."
 

它返回 400 作为状态码,但还包含一条描述性错误消息,告诉您您做错了什么。

但是,JAX-RS 2.0 客户端将 400 状态重新映射为通用状态,我丢失了良好的错误消息。它正确地将其映射到 BadRequestException,但带有通用的“HTTP 400 Bad Request”消息。

javax.ws.rs.BadRequestException: HTTP 400 Bad Request
    at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:908)
    at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:770)
    at org.glassfish.jersey.client.JerseyInvocation.access$500(JerseyInvocation.java:90)
    at org.glassfish.jersey.client.JerseyInvocation$2.call(JerseyInvocation.java:671)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:424)
    at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:667)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:396)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:296)

是否可以注入某种拦截器或自定义错误处理程序,以便我可以访问真正的错误消息。我一直在查看文档,但看不到任何方法。

我现在正在使用 Jersey,但我尝试使用 CXF 并得到了相同的结果。这是代码的样子。

Client client = ClientBuilder.newClient().register(JacksonFeature.class).register(GzipInterceptor.class);
WebTarget target = client.target("https://somesite.com").path("/api/test");
Invocation.Builder builder = target.request()
                                   .header("some_header", value)
                                   .accept(MediaType.APPLICATION_JSON_TYPE)
                                   .acceptEncoding("gzip");
MyEntity entity = builder.get(MyEntity.class);

更新:

我实施了下面评论中列出的解决方案。它略有不同,因为 JAX-RS 2.0 客户端 API 中的类发生了一些变化。我仍然认为默认行为是给出通用错误消息并丢弃真正的错误消息是错误的。我明白为什么它不会解析我的错误对象,但应该返回未解析的版本。我最终得到了库已经完成的复制异常映射。

感谢您的帮助。

这是我的过滤器类:

@Provider
public class ErrorResponseFilter implements ClientResponseFilter 

    private static ObjectMapper _MAPPER = new ObjectMapper();

    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException 
        // for non-200 response, deal with the custom error messages
        if (responseContext.getStatus() != Response.Status.OK.getStatusCode()) 
            if (responseContext.hasEntity()) 
                // get the "real" error message
                ErrorResponse error = _MAPPER.readValue(responseContext.getEntityStream(), ErrorResponse.class);
                String message = error.getMessage();

                Response.Status status = Response.Status.fromStatusCode(responseContext.getStatus());
                WebApplicationException webAppException;
                switch (status) 
                    case BAD_REQUEST:
                        webAppException = new BadRequestException(message);
                        break;
                    case UNAUTHORIZED:
                        webAppException = new NotAuthorizedException(message);
                        break;
                    case FORBIDDEN:
                        webAppException = new ForbiddenException(message);
                        break;
                    case NOT_FOUND:
                        webAppException = new NotFoundException(message);
                        break;
                    case METHOD_NOT_ALLOWED:
                        webAppException = new NotAllowedException(message);
                        break;
                    case NOT_ACCEPTABLE:
                        webAppException = new NotAcceptableException(message);
                        break;
                    case UNSUPPORTED_MEDIA_TYPE:
                        webAppException = new NotSupportedException(message);
                        break;
                    case INTERNAL_SERVER_ERROR:
                        webAppException = new InternalServerErrorException(message);
                        break;
                    case SERVICE_UNAVAILABLE:
                        webAppException = new ServiceUnavailableException(message);
                        break;
                    default:
                        webAppException = new WebApplicationException(message);
                

                throw webAppException;
            
        
    

【问题讨论】:

这让我对球衣感到沮丧。感谢您发布您的 ErrorResponseFilter 课程。 ErrorResponse 是什么类型的?我不知道它是从哪里来的。 您的过滤方法很好。您在 jax rs 客户端中发现了一个明显的设计错误。 Resteasy 也有同样的问题。 @ChuckM,在您的解决方案中,原始剩余异常被 ResponseProcessingException 包装,这不好。作为一种替代方法,您可以在不使用过滤器的情况下从异常中提取响应正文。 【参考方案1】:

我相信你想做这样的事情:

Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) 
    System.out.println( response.getStatusType() );
    return null;


return response.readEntity( MyEntity.class );

您可以尝试的另一件事(因为我不知道这个 API 将东西放在哪里——即在标题或实体中或什么中)是:

Response response = builder.get( Response.class );
if ( response.getStatusCode() != Response.Status.OK.getStatusCode() ) 
    // if they put the custom error stuff in the entity
    System.out.println( response.readEntity( String.class ) );
    return null;


return response.readEntity( MyEntity.class );

如果您希望一般将 REST 响应代码映射到 Java 异常,您可以添加一个客户端过滤器来执行此操作:

class ClientResponseLoggingFilter implements ClientResponseFilter 

    @Override
    public void filter(final ClientRequestContext reqCtx,
                       final ClientResponseContext resCtx) throws IOException 

        if ( resCtx.getStatus() == Response.Status.BAD_REQUEST.getStatusCode() ) 
            throw new MyClientException( resCtx.getStatusInfo() );
        

        ...

在上面的过滤器中,您可以为每个代码创建特定的异常,或者创建一个包含响应代码和实体的通用异常类型。

【讨论】:

如果我调用 builder.get(),那么它会返回一个 Response 对象并且不会自动抛出重新映射的异常。我想知道是否有更全球化的方式来处理这个问题?某种可以检查状态代码的拦截器,解组错误对象并映射到异常类型,如 BadRequestException。我有 30 个不同的 API 调用,所以我想在全球范围内处理这个问题。 您的意思是要将响应代码映射到异常吗?在这个问题中,我认为您只是在询问供应商提供的响应字符串。我会修改我的答案来处理这个问题。 ClientFilter 是我正在寻找的解决方案类型。我在 Jersey 中使用新的 JAX-RS 2.0 客户端 API,但没有看到 ClientFilter 类。有一个 ClientResponseFilter 代替。我假设这是现在要实现的类/接口? jax-rs-spec.java.net/nonav/2.0/apidocs/javax/ws/rs/client/… 在我的例子中(使用带有内置 JAX-RS 客户端的 Apache CXF)我不得不使用 ResponseExceptionMapper 来将 Response 映射到适当的(自定义)Exception类型,因为ClientResponseFilter 抛出的自定义异常没有传播到调用线程(CXF 的PhaseInterceptorChain 自己捕获并丢弃自定义异常,最终导致相同的旧 JAX-RS 异常(InternalServerErrorException in我的情况)被抛出到调用线程)【参考方案2】:

除了编写自定义过滤器之外,还有其他方法可以向 Jersey 客户端获取自定义错误消息。 (虽然过滤器是一个很好的解决方案)

1) 在 HTTP 标头字段中传递错误消息。 详细的错误消息可以在 JSON 响应和附加的标头字段中,例如“x-error-message”。

服务器添加 HTTP 错误标头。

ResponseBuilder rb = Response.status(respCode.getCode()).entity(resp);
if (!StringUtils.isEmpty(errMsg))
    rb.header("x-error-message", errMsg);

return rb.build();

Client 捕获异常,在我的例子中是 NotFoundException,并读取响应标头。

try 
    Integer accountId = 2222;
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("http://localhost:8080/rest-jersey/rest");
    webTarget = webTarget.path("/accounts/"+ accountId);
    Invocation.Builder ib = webTarget.request(MediaType.APPLICATION_JSON);
    Account resp = ib.get(new GenericType<Account>() 
    );
 catch (NotFoundException e) 
    String errorMsg = e.getResponse().getHeaderString("x-error-message");
    // do whatever ...
    return;

2) 另一种解决方案是捕获异常并读取响应内容。

try 
    // same as above ...
 catch (NotFoundException e) 
    String respString = e.getResponse().readEntity(String.class);
    // you can convert to JSON or search for error message in String ...
    return;
 

【讨论】:

解决方案 2) 这里的干扰比创建过滤器要小得多。服务器返回的实体在抛出的异常中间接可用(通过 Response 对象)。【参考方案3】:

WebApplicationException 类是为此而设计的,但由于某种原因,它会忽略并覆盖您指定为消息参数的内容。

出于这个原因,我创建了自己的扩展 WebAppException,它尊重参数。它是一个单一的类,不需要任何响应过滤器或映射器。

我更喜欢异常而不是创建 Response,因为它可以在处理时从任何地方抛出。

简单用法:

throw new WebAppException(Status.BAD_REQUEST, "Field 'name' is missing.");

班级:

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.core.Response.StatusType;

public class WebAppException extends WebApplicationException 
    private static final long serialVersionUID = -9079411854450419091L;

    public static class MyStatus implements StatusType 
        final int statusCode;
        final String reasonPhrase;

        public MyStatus(int statusCode, String reasonPhrase) 
            this.statusCode = statusCode;
            this.reasonPhrase = reasonPhrase;
        

        @Override
        public int getStatusCode() 
            return statusCode;
        
        @Override
        public Family getFamily() 
            return Family.familyOf(statusCode);
        
        @Override
        public String getReasonPhrase() 
            return reasonPhrase;
        
    

    public WebAppException() 
    

    public WebAppException(int status) 
        super(status);
    

    public WebAppException(Response response) 
        super(response);
    

    public WebAppException(Status status) 
        super(status);
    

    public WebAppException(String message, Response response) 
        super(message, response);
    

    public WebAppException(int status, String message) 
        super(message, Response.status(new MyStatus(status, message)). build());
    

    public WebAppException(Status status, String message) 
        this(status.getStatusCode(), message);
    

    public WebAppException(String message) 
        this(500, message);
    


【讨论】:

【参考方案4】:

为遇到此问题的人提供更简洁的解决方案:

调用.get(Class&lt;T&gt; responseType) 或任何其他将结果类型作为参数Invocation.Builder 的方法将返回所需类型的值,而不是Response。作为副作用,这些方法将检查接收到的状态码是否在 2xx 范围内,否则抛出适当的WebApplicationException

来自documentation:

Throws: WebApplicationException in case 的响应状态码 服务器返回的响应不成功,并且 指定的响应类型不是响应。

这允许捕获WebApplicationException,检索实际的Response,将包含的实体处理为异常详细信息(ApiExceptionInfo)并引发适当的异常(ApiException)。

public <Result> Result get(String path, Class<Result> resultType) 
    return perform("GET", path, null, resultType);


public <Result> Result post(String path, Object content, Class<Result> resultType) 
    return perform("POST", path, content, resultType);


private <Result> Result perform(String method, String path, Object content, Class<Result> resultType) 
    try 
        Entity<Object> entity = null == content ? null : Entity.entity(content, MediaType.APPLICATION_JSON);
        return client.target(uri).path(path).request(MediaType.APPLICATION_JSON).method(method, entity, resultType);
     catch (WebApplicationException webApplicationException) 
        Response response = webApplicationException.getResponse();
        if (response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) 
            throw new ApiException(response.readEntity(ApiExceptionInfo.class), webApplicationException);
         else 
            throw webApplicationException;
        
    

ApiExceptionInfo 是我的应用程序中的自定义数据类型:

import lombok.Data;

@Data
public class ApiExceptionInfo 

    private int code;

    private String message;


ApiException 是我的应用程序中的自定义异常类型:

import lombok.Getter;

public class ApiException extends RuntimeException 

    @Getter
    private final ApiExceptionInfo info;

    public ApiException(ApiExceptionInfo info, Exception cause) 
        super(info.toString(), cause);
        this.info = info;
    


【讨论】:

【参考方案5】:

[至少使用 Resteasy]@Chuck M 提供的基于ClientResponseFilter 的解决方案有一个很大的缺点。

当您基于 ClientResponseFilter 使用它时,您的 BadRequestException, NotAuthorizedException, ... 异常被 javax.ws.rs.ProcessingException 包裹。

不得强迫您的代理的客户端捕获此javax.ws.rs.ResponseProcessingException 异常。

没有过滤器,我们得到一个原始的休息异常。如果我们默认捕获和处理,它不会给我们太多:

catch (WebApplicationException e) 
 //does not return response body:
 e.toString();
 // returns null:
 e.getCause();

问题可以在另一个层面上解决,当您从错误中提取描述时。 WebApplicationException 异常,它是所有其余异常的父级,包含 javax.ws.rs.core 。回复。只需编写一个辅助方法,如果异常是WebApplicationException 类型,它也会检查响应正文。这是Scala中的代码,但想法应该很清楚。该方法返回对其余异常的清晰描述:

  private def descriptiveWebException2String(t: WebApplicationException): String = 
    if (t.getResponse.hasEntity)
      s"$t.toString. Response: $t.getResponse.readEntity(classOf[String])"
    else t.toString
  

现在我们将责任转移到客户端上,以显示确切的错误。只需使用共享异常处理程序即可最大程度地减少客户端的工作量。

【讨论】:

【参考方案6】:

以下对我有用

Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();

【讨论】:

以上是关于在 JAX-RS 2.0 客户端库中处理自定义错误响应的主要内容,如果未能解决你的问题,请参考以下文章

JAX-RS自定义ExceptionMapper不拦截RuntimeException

如何使用符合 JAX-RS 2.0 的 RESTEasy 客户端 API 启用 NTLM 身份验证?

在 Boost Property 树库中,我如何以自定义方式处理文件未找到错误(C++)

java - 如何在spring boot java中编写一个函数来处理JPA存储库中的自定义查询?

REST 请求处理

JAX-RS 和自定义授权