【问题标题】:Spring MVC - content-type aware response from filterSpring MVC - 来自过滤器的内容类型感知响应
【发布时间】:2019-12-27 02:46:24
【问题描述】:

当您编写 Spring REST 端点时,Spring 提供了非常方便的方法来处理基于内容类型的多种响应格式。您只需要注册MessageConverters,基本上就是这样。其余的由 Spring 负责。

但是,您如何访问这些转换器,或者如何让它们将任何Object 转换为过滤器中的ServletResponse

我需要的是,我有一个过滤器,它是链中的第一个。当通往控制器的任何过滤器失败时,我想发送用户响应。但是,我想以通用响应结构和尊重用户Accept 标头(json/xml/...)的格式发送它。例如:

public class GenericExceptionHandlerFilter implements Filter {    
  @Override
  public void doFilter(ServletRequest servletRequest, 
      ServletResponse servletResponse, FilterChain filterChain) {
    try {
      filterChain.doFilter(servletRequest, servletResponse);
    } catch (Exception e) {
      log.error("Unhandled type of exception.", e);
      String response = messageConverter.convert(new MyResponse(e), String.class);
      HttpServletResponse httpServletResponse = (...)servletResponse;
      httpServletResponse.setStatus(500);
      httpServletResponse.getWriter().print(response);
      httpServletResponse.getWriter().flush();
      httpServletResponse.getWriter().close();
    }
  }
}

作为我想要的回应:

{
  "requestId": "UUID"
  "code": "err-1"
  "message": "Filter has failed."
}

<response>
  <requestId>UUID</requestId>
  <code>err-1</code>
  <message>Filter has failed.</message>
</response>

我怎样才能得到正确的messageConverter

【问题讨论】:

    标签: java spring rest spring-mvc filter


    【解决方案1】:

    基于@PraveenKumarLalasangi 的响应,我已通过以下方式实现:

    服务器配置:

    public class ServerConfig extends WebMvcConfigurationSupport {
      @Bean
      public FilterRegistrationBean<GenericExceptionHandlerFilter> genericExceptionFilter() {
        FilterRegistrationBean<GenericExceptionHandlerFilter> registration = new FilterRegistrationBean<>();
        GenericExceptionHandlerFilter genericExceptionHandlerFilter = new GenericExceptionHandlerFilter(
            httpResponseMapper(), getMessageConverters());
        registration.setFilter(genericExceptionHandlerFilter);
        registration.setOrder(FiltersOrder.GENERIC_EXCEPTION_HANDLER);
        return registration;
      }
    }
    

    以及处理我的异常的过滤器:

    public class GenericExceptionHandlerFilter implements Filter {
      private final List<HttpMessageConverter<?>> converters;
    
      public GenericExceptionHandlerFilter(<HttpMessageConverter<?>> converters) {
        this.converters = converters;
      }
    
      @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
          FilterChain filterChain) throws IOException {
        try {
          filterChain.doFilter(servletRequest, servletResponse);
        } catch (Exception e) {
          MyCommonResponse commonResponse = new MyCommonResponse(e);
          write(servletRequest, servletResponse, comonResponse);
          log.error("Unhandled exception.", e);
        }
      }
    
      private void write(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Object object) throws IOException {
        String accept = servletRequest.getHeader("accept");
        for (HttpMessageConverter messageConverter : converters) {
          if (messageConverter.canWrite(object.getClass(), MediaType.valueOf(accept))) {
            HttpOutputMessage outputMessage = new ServletServerHttpResponse(servletResponse);
            messageConverter.write(object, MediaType.valueOf(accept), outputMessage);
          }
        }
      }
    }
    

    【讨论】:

      【解决方案2】:

      您可以通过自定义配置的消息转换器以两种方式发送常见错误响应(来自过滤器)

      方法一:

      在过滤器中,捕获到您的 api 的异常转发请求,以便它根据接受标头确定内容类型并以预期格式发送错误响应。

      try
      {
          filterChain.doFilter(request, response);
      }
      catch (Exception e)
      {
          req.getRequestDispatcher("/sendError/"+e.getLocalizedMessage()).forward(req, resp);
      }
      

      您的 API 将是

      @RequestMapping(value = "/sendError/{errorMessage}", method = { RequestMethod.GET, RequestMethod.POST })
      public @ResponseBody MyResponse sendError(@PathVariable String errorMessage)
      {
          return new MyResponse("UUID-1","500",errorMessage);
      }
      

      MyResponse POJO 在哪里

      public class MyResponse
      {
          private String requestId;
          private String code;
          private String message;
          
          public MyResponse(){}
          
          public MyResponse(String requestId, String code, String message)
          {
              this.requestId = requestId;
              this.code = code;
              this.message = message;
          }
      ...
      }
      

      Controller 方法上的@ResponseBody 向 Spring 表明该方法的返回值直接序列化为由客户端接收到的 Accept 头决定的 HTTP 响应体。

      方法二:

      如下配置自定义消息转换器,并将所有转换器添加到静态列表中,以便可以从过滤器中访问

      @Configuration
      @EnableWebMvc
      @ComponentScan(basePackages = "com.pvn.mvctiles")
      public class ApplicationConfiguration implements WebMvcConfigurer
      {
          public static List<HttpMessageConverter<?>> convertersRef = new ArrayList<>();
          
          ...
      
          @Override
          public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
          {
              converters.add(createXmlHttpMessageConverter());
              converters.add(new MappingJackson2HttpMessageConverter());
              convertersRef.addAll(converters);
          }
          
          
          private HttpMessageConverter<Object> createXmlHttpMessageConverter()
          {
              MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();
      
              XStreamMarshaller xstreamMarshaller = new XStreamMarshaller();
              xmlConverter.setMarshaller(xstreamMarshaller);
              xmlConverter.setUnmarshaller(xstreamMarshaller);
      
              return xmlConverter;
          }
      }
      

      在你的过滤器中

      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException
      {
          HttpServletRequest req = (HttpServletRequest) request;
          HttpServletResponse resp = (HttpServletResponse) response;
          
          try
          {
              filterChain.doFilter(request, response);
          }
          catch (Exception e)
          {
              MyResponse myResp = new MyResponse("UUID", "500", e.getLocalizedMessage());
              write(req, resp, myResp);
          }
      }
      
      
      @SuppressWarnings({ "rawtypes", "unchecked" })
      private void write(HttpServletRequest req, HttpServletResponse resp, MyResponse myResp) throws IOException
      {
          String accept= req.getHeader("accept");
      
          for (HttpMessageConverter messageConverter : ApplicationConfiguration.convertersRef)
          {
              if (messageConverter.canWrite(UserDetails.class, MediaType.valueOf(accept)))
              {
                  HttpOutputMessage outputMessage = new ServletServerHttpResponse(resp);
                  messageConverter.write(myResp, MediaType.valueOf(accept), outputMessage);
              }
          }
      }
      

      两者都可以正常工作,但方法 1 似乎是最简单的。

      【讨论】:

      • 如果您想在 servlet 过滤器中将消息转换器作为 bean 获取,请参考我的回答 how-can-i-get-a-spring-bean-in-a-servlet-filter
      • 非常感谢您的帮助和指导。抱歉我的回复晚了,我没有时间早点看。关于转换器,当您扩展WebMvcConfigurationSupport 类而不是实现WebMvcConfigurer 时,也很容易获得它们。然后你可以得到它们,例如Spring Boot 初始化它们。为了将它们添加到过滤器中,我在构造函数中传递了它们。
      【解决方案3】:

      您似乎正在使用过滤器滚动您自己的异常处理逻辑。实际上,Spring MVC 已经提供了一个等效的特性 (@RestControllerAdvice),它允许您从控制器捕获所有异常并将异常处理逻辑整合到一个地方:

      @RestControllerAdvice 是一个组合注解,使用 @ControllerAdvice 和 @ResponseBody,这基本上意味着 @ExceptionHandler 方法通过 消息转换(相对于视图分辨率或模板渲染)。

      您可以认为,每当控制器抛出异常时,都会由@RestControllerAdvice 方法处理。 @RestControllerAdvice 方法返回的值会和控制器方法成功返回一样,也就是会遍历所有注册的MessageConverters,根据接受标头。

      入门示例如下所示:

      @RestControllerAdvice
      public class GlobalExceptionHandler {
      
          @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
          @ExceptionHandler(Exception.class)
          public ErrorResponse handleException(Exception ex) {
              //Create ErrorResponse based on the Exception
              if(blabla...){
                  return new ErrorResponse("fooCode1", "foo Message1");
              }else if(barbar....){
                  return new ErrorResponse("fooCode2", "foo Message2");
              }
          }
      }
      

      假设您使用 Jackson 将对象序列化为 JSON/XML:

      @JacksonXmlRootElement(localName = "response")
      public class ErrorResponse {
      
          @JsonProperty("requestId")
          private String requestId = UUID.randomUUID().toString();
      
          @JsonProperty("code")
          private String code;
      
          @JsonProperty("message")
          private String message;
      
          public ErrorResponse(String code, String message) {
              this.code = code;
              this.message = message;
          }
      }
      

      【讨论】:

      • 嗨@KenChan,感谢您的回复。是的,我知道@ControllerAdvice,但是,不幸的是,当您编写它时,它只处理来自控制器的异常。请求在到达任何控制器之前通过过滤器。因此,当过滤器中抛出一些异常时,@ControllerAdvice 不会处理它,用户只会收到默认的 HTML 响应。
      • 是的,明白你的意思。所以基本上,你想在普通的 Servlet 过滤器中使用 Spring MVC 特性吗?您是否考虑将现有的 Filter 逻辑移至 spring HandlerInterceptor ?其实Filter能做的每一件事,HandlerInterceptor 也能做,功能更丰富,更优雅地解决你的问题
      猜你喜欢
      • 1970-01-01
      • 2011-04-06
      • 1970-01-01
      • 1970-01-01
      • 2016-11-11
      • 2018-01-18
      • 2017-03-16
      • 2017-06-09
      • 1970-01-01
      相关资源
      最近更新 更多