【问题标题】:How can I wrap a JSON response in Spring如何在 Spring 中包装 JSON 响应
【发布时间】:2014-07-27 04:05:44
【问题描述】:

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

  • /jsonapi1/*
  • /jsonapi2/*

两者都返回将被解释为 JSON 文本的对象。

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

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

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

    {"num_humans":123, "num_androids":456}
    

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

    { "status":0,
      "content":{"num_humans":123, "num_androids":456}
    }
    
  2. 如果控制器发生异常,则过滤器应捕获异常并报告如下

    { "status":5,
      "content":"Something terrible happened"
    }
    
  3. 其他控制器的响应保持不变。

我们目前正在自定义传递给WebMvcConfigurerAdapter.configureMessageConvertersMappingJackson2HttpMessageConverter 以执行上述任务。效果很好,只是这种方法似乎无法选择性地选择它适用的 URL(或控制器类)。

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


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

【问题讨论】:

    标签: json spring spring-mvc


    【解决方案1】:

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

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

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

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

    现在我想在响应周围添加一些标准字段,例如 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;
        }
    }
    

    实体类如下所示:

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

    要处理异常,只需使用ExceptionHandler 创建另一个ControllerAdvice。使用示例in this link

    此解决方案的优点:

    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();
        }
        ...
     }
    

    【讨论】:

    • 这个例子对我不起作用。问题是异常也流经“beforeBodyWrite”,因此您不能设置http状态200。
    • 更新了处理异常的答案。
    【解决方案2】:

    我也在使用 AOP 和 @Around。开发了一个自定义注释并将其用于切入点。我正在使用全局响应。它具有类型为List的状态、消息和数据

    List <? extends parent> dataList
    

    (可以解决你的类转换异常)。所有实体都扩展了这个 Parent 类。这样我可以将所有数据设置到我的列表中。 此外,我将消息键用作带有自定义注释的参数并将其设置为实际操作。 希望这会有所帮助。

    【讨论】:

      【解决方案3】:

      按照我理解你的问题的方式,你有三个选择。

      选项#1

      手动将您的对象包装在简单的SuccessResponseErrorResponseSomethingSortOfWrongResponse 等具有您需要的字段的对象中。在这一点上,您拥有每个请求的灵活性,更改其中一个响应包装器上的字段是微不足道的,唯一真正的缺点是代码重复,如果控制器的许多请求方法可以而且应该组合在一起。

      选项 #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());
          }
      } 
      

      如果您找到一种方法在该过滤器中设置正文,那么是的,您可以轻松地将其包裹起来。否则,这个选项是死路一条。

      选项#3

      啊哈。所以你走到了这一步。代码重复不是一种选择,但您坚持要包装来自控制器方法的响应。我想介绍一下真正的解决方案——面向方面的编程(AOP),Spring 非常支持它。

      如果你不熟悉AOP,前提是:你在代码中定义一个匹配(就像正则表达式匹配)点的表达式。这些点称为连接点,而匹配它们的表达式称为切入点。然后,当任何切入点或切入点组合匹配时,您可以选择执行额外的任意代码,称为 advice。定义切入点和建议的对象称为 aspect

      非常适合用 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);
          }
      }
      

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

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

      【讨论】:

      • 谢谢@Misha。我遇到了两个问题。 i) AfterThrowing 丢弃它返回的值。 @ Around 工作得很好。 ii) 如果 wrapResponse() 返回的类型不是包装控制器方法的返回类型的子类,那么我们在 com.example.controller.TestController$$EnhancerByCGLIB$$be70c6b.myControllerMethod() 这是一个很大的限制——我的大多数控制器都返回各种支持 JSON 的类型,我不想修改每个控制器,以便它们共享一个共同的返回类型。另一个解决方案/解决方法?
      • @user48956 你说的很对,我自己也不是很精通。至于更改控制器返回类型,我不确定您有多少选择!出于这个原因,我倾向于将它们保留为Object,尽管在内部,类型边界保持尽可能严格。静态返回类型不会影响其 JSON 序列化,因为大多数库(包括 Jackson)使用反射来检索适当的 getter、setter 和字段。
      • 遗憾的是,在我的项目中为我这样做的目的是允许使用 Swagger 记录 REST API。返回对象删除结果类型的文档.. :-(
      • 我想知道是否可以将 servlet 过滤器应用于选定的端点集?
      • 正如我之前所说,在过滤器中更新您的响应是不可能的。你最好的办法是弄清楚如何在方法的返回值被分配之后注入返回类型,因为这是任何代码最后一次关心你的自定义类。我确信这可以通过 AOP 实现。
      【解决方案4】:

      我管理来自控制器的自定义响应的最简单方法是利用 Map 变量。

      所以你的代码最终看起来像:

      public @ResponseBody Map controllerName(...) {
      
      Map mapA = new HashMap();
      
      mapA.put("status", "5");
      mapA.put("content", "something went south");
      
      return mapA;
      
      }
      

      的美妙之处在于您可以通过任何一千种方式对其进行配置。 目前我用于对象传输、自定义异常处理和数据报告,太容易了。

      希望对你有帮助

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-04-13
        • 2017-06-12
        • 1970-01-01
        • 2022-01-14
        • 2014-10-12
        • 1970-01-01
        • 1970-01-01
        • 2016-05-01
        相关资源
        最近更新 更多