如何在Spring中包装JSON响应

use*_*956 5 spring json spring-mvc

假设我在Spring中有两组控制器:

  • /jsonapi1/*
  • /jsonapi2/*

两者都返回要解释为JSON文本的对象.

我想要某种过滤器来包装来自这些控制器的响应,以便:

  1. 原始响应包含在另一个对象中.

    例如,如果/ jsonapi1/count返回:

    {"num_humans":123, "num_androids":456}
    
    Run Code Online (Sandbox Code Playgroud)

    那么响应应该被包装并返回如下:

    { "status":0,
      "content":{"num_humans":123, "num_androids":456}
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 如果控制器中发生异常,则过滤器应捕获异常并按如下方式报告

    { "status":5,
      "content":"Something terrible happened"
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 其他控制器的响应将保持不变.

我们目前正在自定义MappingJackson2HttpMessageConverter传递给以WebMvcConfigurerAdapter.configureMessageConverters执行上述任务.工作得很好,除了这种方法似乎不可能选择它适用的URL(或控制器类).

是否可以将这些类型的包装器应用于单个控制器类或URL?


更新:Servlet过滤器看起来像一个解决方案.是否可以选择将哪个过滤器应用于哪些控制器方法或哪些URL?

Mis*_*sha 7

我理解你的问题的方式,你有三个选择.

选项1

手工包裹在简单的对象SuccessResponse,ErrorResponse,SomethingSortOfWrongResponse有你需要的字段等对象.此时,您具有每个请求的灵活性,更改其中一个响应包装器上的字段是微不足道的,唯一真正的缺点是代码重复,如果许多控制器的请求方法可以并且应该组合在一起.

选项#2

正如您所提到的,过滤器可以设计用于执行脏工作,但要小心Spring过滤器不会授予您访问请求或响应数据的权限.以下是它的外观示例:

@Component
public class ResponseWrappingFilter extends GenericFilterBean {

    @Override
    public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain chain) {

        // Perform the rest of the chain, populating the response.
        chain.doFilter(request, response);

        // No way to read the body from the response here. getBody() doesn't exist.
        response.setBody(new ResponseWrapper(response.getStatus(), response.getBody());
    }
} 
Run Code Online (Sandbox Code Playgroud)

如果你找到一种方法在该过滤器中设置主体,那么是的,你可以很容易地将它包起来.否则,这个选项是死路一条.

选项#3

A-HA.所以你到目前为止.代码重复不是一个选项,但您坚持要从控制器方法中包装响应.我想介绍真正的解决方案 - 面向方面的编程(AOP),Spring非常支持.

如果您不熟悉AOP,则前提如下:您在代码中定义匹配(如正则表达式匹配)点的表达式.这些点称为连接点,而与它们匹配的表达式称为切入点.然后,当切入任何切入点或切入点组合时,您可以选择执行其他任意代码,称为建议.定义切入点和建议的对象称为方面.

它非常适合在Java中表达自己更流利.唯一的缺点是较弱的静态类型检查.不用多说,这是你在面向方面的编程中的响应包装:

@Aspect
@Component
public class ResponseWrappingAspect {

    @Pointcut("within(@org.springframework.stereotype.Controller *)")
    public void anyControllerPointcut() {}

    @Pointcut("execution(* *(..))")
    public void anyMethodPointcut() {}

    @AfterReturning(
        value = "anyControllerPointcut() && anyMethodPointcut()",
        returning = "response")
    public Object wrapResponse(Object response) {

        // Do whatever logic needs to be done to wrap it correctly.
        return new ResponseWrapper(response);
    }

    @AfterThrowing(
        value = "anyControllerPointcut() && anyMethodPointcut()",
        throwing = "cause")
    public Object wrapException(Exception cause) {

        // Do whatever logic needs to be done to wrap it correctly.
        return new ErrorResponseWrapper(cause);
    }
}
Run Code Online (Sandbox Code Playgroud)

最终结果将是您寻求的非重复响应包装.如果您只希望某个或一个控制器收到此效果,则更新切入点以仅匹配该控制器实例内的方法(而不是任何持有@Controller注释的类).

您需要包含一些AOP依赖项,在配置类中添加启用AOP的注释,并确保组件扫描此类所在的包.


Adi*_*Adi 5

我为此苦苦挣扎了好几天。@Misha的解决方案对我不起作用。我终于能够使用ControllerAdviceResponseBodyAdvice使其工作。

ResponseBodyAdvice允许在控制器返回的响应上(但将其转换为HttpResponse并提交之前)注入自定义转换逻辑。

这是我的控制器方法的外观:

@RequestMapping("/global/hallOfFame")
    public List<HallOfFame> getAllHallOfFame() {
        return hallOfFameService.getAllHallOfFame();
}
Run Code Online (Sandbox Code Playgroud)

现在,我想在响应周围添加一些标准字段,例如devmessageusermessage。该逻辑进入ResponseAdvice:

@ControllerAdvice
public class TLResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
        ServerHttpResponse response) {
        // TODO Auto-generated method stub
        final RestResponse<Object> output = new RestResponse<>();
        output.setData(body);
        output.setDevMessage("ResponseAdviceDevMessage");
        output.setHttpcode(200);
        output.setStatus("Success");
        output.setUserMessage("ResponseAdviceUserMessage");
        return output;
    }
}
Run Code Online (Sandbox Code Playgroud)

实体类如下所示:

@Setter // All lombok annotations
@Getter
@ToString
public class RestResponse<T> {

    private String status;
    private int httpcode;
    private String devMessage;
    private String userMessage;

    private T data;
}

@Entity
@Data // Lombok
public class HallOfFame {

    @Id
    private String id;
    private String name;
}
Run Code Online (Sandbox Code Playgroud)

处理异常,只需创建另一个ControllerAdviceExceptionHandler在此链接中使用示例。

该解决方案的优点:

  1. 它可以使您的控制器保持清洁。您可以从控制器方法中支持任何返回类型。
  2. 控制器返回类型类不需要按照AOP方法的要求扩展某些基类。
  3. 您无需使用HttpServletResponseWrappers遍历Spring过滤器。他们提出了性能惩罚。

编辑-2019年9月17日

要处理异常,请使用@ExceptionHandler。请参考下面的代码。

@ExceptionHandler(Exception.class)
@ResponseBody
public MyResponseEntity<Object> handleControllerException(HttpServletRequest request, Throwable ex) {
    // default value
    int httpCode = HttpStatus.INTERNAL_SERVER_ERROR.value();

    if(ex instanceof ResourceNotFoundException) {
        httpCode = HttpStatus.NOT_FOUND.value();
    }
    ...
 }
Run Code Online (Sandbox Code Playgroud)