【问题标题】:Spring: How to choose response type on run time?Spring:如何在运行时选择响应类型?
【发布时间】:2015-07-25 13:10:19
【问题描述】:

我想在方法的运行时选择响应媒体类型。

例如下面的代码:

@RequestMapping(value = "/getRecord",
    produces = {"application/octet-stream", "application/json;charset=UTF-8" })
public byte[] getData(
    @RequestParam(value="id", required=true) Integer id)
    throws IOException
{
    if (id == 1)
        return createByteArray();
    throw new MyDataException();
}

在这段代码中,可能的响应类型实际上是 2。

  1. byte[](通过正常执行路径)
  2. MyDataException(通过异常执行路径)

MyDataException 稍后由异常处理程序处理,并转换为简单类。它可以转换为 json 响应。

首先,我认为如果我为@RequestMapping注解的produces选项提供2种响应类型,消息转换器将根据实际返回对象转换2种类型。但事实并非如此。

在spring类org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor中,如果produces选项存在,writeWithMessageConverters()方法在选择响应类型时只是忽略实际返回对象类型。

如何让Spring根据实际返回对象在运行时选择响应类型?

【问题讨论】:

    标签: spring spring-mvc spring-boot


    【解决方案1】:

    我建议您使用 @ControllerAdvice 注释来处理 Spring MVC 处理程序中的异常。这是一种非常优雅的方法(实际上有 3 种方法可以消除您的异常处理问题)分离错误处理问题,例如设置适当的 HTTP 响应代码(不是 2xx)和发回错误消息 /object。

    有一个很棒的博客here

    示例(借自 Spring 博客):-

    @ControllerAdvice
    class GlobalControllerExceptionHandler {
        @ResponseStatus(HttpStatus.CONFLICT)  // 409
        @ExceptionHandler(DataIntegrityViolationException.class)
        public void handleConflict() {
            // Nothing to do
        }
    }
    

    在你的情况下,我建议使用@ControllerAdvice 方法,例如:-

    @ControllerAdvice
    class GlobalControllerExceptionHandler {
        @ResponseStatus(HttpStatus.CONFLICT)  // 409
        @ResponseBody
        @ExceptionHandler(MyDataException.class)
        public AnyReturnType handleConflict(Exception exception) {
             return exception.getDetails();
        }
    }
    

    处理程序的返回类型也可以是 ModelAndView 对象,它将错误对象传递给您的视图层。

    有关详细信息,请参阅博客。

    【讨论】:

    • 实际上我使用@ControllerAdvice 处理MyDataException。问题是当响应转换完成时,Spring 不尊重异常的对象类型。它只是尝试将其转换为 application/octet-stream(这是我为produces 指定的第一种类型)并且失败了,而我希望将其转换为 application/json。
    • 不应该这样。我假设你的控制器是 RESTful 控制器(JSON 响应,没有视图层)。如果是这种情况,那么请使用 @RestController 而不是 @Controller,这样您就不必明确提及“生产”。将@ResponseBody 注释放在handleConflict 上应该可以使其工作。参考提到的博客有完整的图片。
    • 我的控制器是 RESTful 的。但它在正常情况下返回 application/octet-stream body。如果抛出异常,它应该通过 @ControllerAdvice 异常处理程序返回带有 HTTP 400 或 500 的 application/json 正文(包含错误信息)。
    • 我已经在使用@ResponseBody。事实上,我的 ExceptionHandler 类的代码几乎与您的 GlobalControllerExceptionHandler 相同。 (除了 ResponseStatus 是 HttpStatus.BAD_REQUEST) 无论如何,我会阅读您提供的博客文章。谢谢。
    • 我认为,该博客几乎没有您提供的更多信息。我现在会坚持我的答案。
    【解决方案2】:

    自我回答。

    • 删除produces
    • 将返回类型更改为ResponseEntity<byte[]>
    • 返回如下:

      HttpHeaders responseHeaders = new HttpHeaders();
      responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      return new ResponseEntity<byte[]>(createByteArray(), responseHeaders, HttpStatus.OK);
      

    因此,问题上的代码转换如下:

    @RequestMapping(value = "/getRecord")
    public ResponseEntity<byte[]> getData(@RequestParam(value="id", required=true) Integer id)
        throws IOException
    {
        if (id == 1)
        {
            HttpHeaders responseHeaders = new HttpHeaders();
            responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            return new ResponseEntity<byte[]>(createByteArray(), responseHeaders, HttpStatus.OK);
        }
        throw new MyDataException();
    }
    

    现在响应类型如下:

    • 在正常执行路径上,appliaction/octet-stream。
    • ON 异常执行路径,application/json。

    我引用了 StackOverflow 的答案 https://stackoverflow.com/a/4483387/3004042 为了这。另请参阅 Kumar Sambhav 关于设置异常处理程序的答案。

    如果几天后没有更好的答案,我会选择这个答案。

    【讨论】:

      【解决方案3】:

      另一种可能性是两种描述的解决方案之间的混合方法:

      @ControllerAdvice
      public class MyExceptionHandler extends ResponseEntityExceptionHandler {
      
      @ExceptionHandler({ MyDataException.class })
      protected ResponseEntity<Object> handleInvalidRequest(RuntimeException e, WebRequest request) {
          MyDataExceptionire = (MyDataException) e;
          List<FieldErrorResource> fieldErrorResources = new ArrayList<>();
      
          List<FieldError> fieldErrors = ire.getErrors().getFieldErrors();
          for (FieldError fieldError : fieldErrors) {
              FieldErrorResource fieldErrorResource = new FieldErrorResource();
              fieldErrorResource.setResource(fieldError.getObjectName());
              fieldErrorResource.setField(fieldError.getField());
              fieldErrorResource.setCode(fieldError.getCode());
              fieldErrorResource.setMessage(fieldError.getDefaultMessage());
              fieldErrorResources.add(fieldErrorResource);
          }
      
          ErrorResource error = new ErrorResource("MyDataException", ire.getMessage());
          error.setFieldErrors(fieldErrorResources);
      
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_JSON);
      
          return handleExceptionInternal(e, error, headers, HttpStatus.UNPROCESSABLE_ENTITY, request);
      }}
      

      this blog提出的解决方案

      编辑

      我还添加了博客中的 FieldError 和 ErrorResource 类,因为它将来可能会被删除:

      错误资源:

      @JsonIgnoreProperties(ignoreUnknown = true)
      public class ErrorResource {
      private String code;
      private String message;
      private List<FieldErrorResource> fieldErrors;
      
      public ErrorResource() { }
      
      public ErrorResource(String code, String message) {
          this.code = code;
          this.message = message;
      }
      
      public String getCode() { return code; }
      
      public void setCode(String code) { this.code = code; }
      
      public String getMessage() { return message; }
      
      public void setMessage(String message) { this.message = message; }
      
      public List<FieldErrorResource> getFieldErrors() { return fieldErrors; }
      
      public void setFieldErrors(List<FieldErrorResource> fieldErrors) {
          this.fieldErrors = fieldErrors;
      }
      }
      

      FieldErrorResource:

      @JsonIgnoreProperties(ignoreUnknown = true)
      public class FieldErrorResource {
      private String resource;
      private String field;
      private String code;
      private String message;
      
      public String getResource() { return resource; }
      
      public void setResource(String resource) { this.resource = resource; }
      
      public String getField() { return field; }
      
      public void setField(String field) { this.field = field; }
      
      public String getCode() { return code; }
      
      public void setCode(String code) { this.code = code; }
      
      public String getMessage() { return message; }
      
      public void setMessage(String message) { this.message = message; }}
      

      【讨论】:

      • 有趣。这与我的回答相反。虽然我将 ResponseEntity 用于正常流程,但这会将其用于异常流程。可以根据异常处理程序的数量和请求处理程序的数量来选择任一方法。顺便说一句,在这个解决方案的情况下,produces="application/octet-stream" 选项必须添加到返回 application/octet-stream 的请求处理程序中。
      • @zeodtr 是的,您是对的 :),此示例使用 json 作为返回类型,但您可以选择任何 MediaType。通常我喜欢在 ControllerAdvice 中有这样的逻辑,而不是在 Controller 中捕获它。
      • 我选择这个答案是因为将特殊代码(与 ResourceEntity 相关)放到异常处理程序而不是控制器中更合适。但也许这个例子有点太复杂了。在这种情况下,可以参考我的自我回答来处理 ResourceEntity。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-04-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多