【问题标题】:Spring Error Controller response with Not Acceptable不可接受的 Spring 错误控制器响应
【发布时间】:2017-05-27 18:18:49
【问题描述】:

我已经构建了一个错误控制器,它应该是在我的 Spring REST 服务中捕获异常的“最后一行”。但是,我似乎无法将 POJO 作为响应类型返回。为什么杰克逊不为这个案子工作?

我的班级看起来像:

@RestController
public class CustomErrorController implements ErrorController
{
  private static final String PATH = "/error";

  @Override
  public String getErrorPath()
  {
     return PATH;
  }


  @RequestMapping (value = PATH)
  public ResponseEntity<WebErrorResponse> handleError(HttpStatus status, HttpServletRequest request)
  {
     WebErrorResponse response = new WebErrorResponse();

    // original requested URI
    String uri = String.valueOf(request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI));
    // status code
    String code = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
    // status message
    String msg = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_MESSAGE));

    response.title = "Internal Server Error";
    response.type = request.getMethod() + ": " + uri;
    response.code = Integer.valueOf(code);
    response.message = msg;

    // build headers
    HttpHeaders headers = new HttpHeaders();

    headers.setContentType(MediaType.APPLICATION_JSON_UTF8);

    // build the response
    return new ResponseEntity<>(response, headers, status);
}

public class WebErrorResponse
{
/**
 * The error message.
 */
public String message;

/**
 * The status code.
 */
public int code;

/**
 * The error title.
 */
public String title;

/**
 * The error type.
 */
public String type;
}

这应该可以,但唯一的响应是 Jetty 错误消息 406 - 不可接受。

将响应实体主体类型更改为 String 效果很好。 怎么了?也许这是一个错误?

P.S:使用 Spring 4.2.8、Spring Boot 1.3.8。

【问题讨论】:

    标签: java spring rest spring-boot


    【解决方案1】:

    最终解决方案

    在 Google 中进行了多次试错循环和往返之后,我终于找到了一个可以满足我要求的解决方案。 Spring 中错误处理的主要问题是由默认行为和小文档引起的。

    只使用Spring而不使用Spring Boot是没有问题的。但同时使用两者来构建 Web (REST) 服务就像地狱一样。

    所以我想分享我的解决方案来帮助遇到同样问题的每个人......

    你需要的是:

    • 一个 Spring Java 配置类
    • spring 的异常处理程序(使用 @ControllerAdvice 并扩展 ResponseEntityExceptionHandler
    • 错误控制器(使用 @Controller 并扩展 AbstractErrorController
    • 通过 Jackson 生成错误响应的简单 POJO(可选)

    配置(剪掉重要部分)

    @Configuration
    public class SpringConfig extends WebMvcConfigurerAdapter
    {
       // ... init stuff if needed
    
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer)
    {
        // setup content negotiation (automatic detection of content types)
        configurer
                // use format parameter and extension to detect mimetype
                .favorPathExtension(true).favorParameter(true)
                // set default mimetype
                .defaultContentType(MediaType.APPLICATION_XML)
                .mediaType(...)
                // and so on ....
     }
    
     /**
     * Configuration of the {@link DispatcherServlet} bean.
     *
     * <p>This is needed because Spring and Spring Boot auto-configuration override each other.</p>
     *
     * @see <a href="http://stackoverflow.com/questions/28902374/spring-boot-rest-service-exception-handling">
     *      Stackoverflow - Spring Boot REST service exception handling</a>
     *
     * @param dispatcher dispatcher servlet instance
     */
    @Autowired
    @SuppressWarnings ("SpringJavaAutowiringInspection")
    public void setupDispatcherServlet(DispatcherServlet dispatcher)
    {
        // FIX: for global REST error handling
        // enable exceptions if endpoint not found (instead of static error page)
        dispatcher.setThrowExceptionIfNoHandlerFound(true);
    }
    
    /**
     * Creates the error properties used to setup the global REST error controller.
     *
     * <p>Using {@link ErrorProperties} is compliant to base implementation if Spring Boot's
     * {@link org.springframework.boot.autoconfigure.web.BasicErrorController}.</p>
     *
     *
     * @return error properties
     */
    @Bean
    public ErrorProperties errorProperties()
    {
        ErrorProperties properties = new ErrorProperties();
    
        properties.setIncludeStacktrace(ErrorProperties.IncludeStacktrace.NEVER);
        properties.setPath("/error");
    
        return properties;
    }
    // ...
    }
    

    Spring 异常处理程序:

    @ControllerAdvice(annotations = RestController.class)
    public class WebExceptionHandler extends ResponseEntityExceptionHandler
    {
    /**
     * This function handles the exceptions.
     *
     * @param e the thrown exception
     *
     * @return error message as XML-document
     */
    @ExceptionHandler (Exception.class)
    public ResponseEntity<Object> handleErrorResponse(Exception e)
    {
        logger.trace("Catching Exception in REST API.", e);
    
        return handleExceptionInternal(e, null, null, null, null);
    }
    
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex,
                                                             Object body,
                                                             HttpHeaders headers,
                                                             HttpStatus status,
                                                             WebRequest request)
    {
        logger.trace("Catching Spring Exception in REST API.");
        logger.debug("Using " + getClass().getSimpleName() + " for exception handling.");
    
        // fatal, should not happen
        if(ex == null) throw new NullPointerException("empty exception");
    
        // set defaults
        String title = "API Error";
        String msg   = ex.getMessage();
    
        if(status == null) status = HttpStatus.BAD_REQUEST;
    
        // build response body
        WebErrorResponse response = new WebErrorResponse();
    
        response.type = ex.getClass().getSimpleName();
        response.title = title;
        response.message = msg;
        response.code = status.value();
    
        // build response headers
        if(headers == null) headers = new HttpHeaders();
    
        try {
            headers.setContentType(getContentType(request));
        }
        catch(NullPointerException e)
        {
            // ignore (empty headers will result in default)
        }
        catch(IllegalArgumentException e)
        {
            // return only status code
            return new ResponseEntity<>(status);
        }
    
        return new ResponseEntity<>(response, headers, status);
    }
    
    /**
     * Checks the given request and returns the matching response content type
     * or throws an exceptions if the requested content type could not be delivered.
     *
     * @param request current request
     *
     * @return response content type matching the request
     *
     * @throws NullPointerException     if the request does not an accept header field
     * @throws IllegalArgumentException if the requested content type is not supported
     */
    private static MediaType getContentType(WebRequest request) throws NullPointerException, IllegalArgumentException
    {
        String accepts = request.getHeader(HttpHeaders.ACCEPT);
    
        if(accepts==null) throw new NullPointerException();
    
        // XML
        if(accepts.contains(MediaType.APPLICATION_XML_VALUE) ||
           accepts.contains(MediaType.TEXT_XML_VALUE) ||
           accepts.contains(MediaType.APPLICATION_XHTML_XML_VALUE))
            return MediaType.APPLICATION_XML;
        // JSON
        else if(accepts.contains(MediaType.APPLICATION_JSON_VALUE))
            return MediaType.APPLICATION_JSON_UTF8;
        // other
        else throw new IllegalArgumentException();
    }
    }
    

    还有 Spring Boot 的错误控制器:

    @Controller
    @RequestMapping("/error")
    public class CustomErrorController extends AbstractErrorController
    {
        protected final Logger logger = LoggerFactory.getLogger(getClass());
    
        /**
         * The global settings for this error controller.
         */
        private final ErrorProperties properties;
    
        /**
         * Bean constructor.
         *
         * @param properties global properties
         * @param attributes default error attributes
         */
        @Autowired
        public CustomErrorController(ErrorProperties properties, ErrorAttributes attributes)
        {
            super(attributes);
    
            this.properties = new ErrorProperties();
        }
    
        @Override
        public String getErrorPath()
        {
            return this.properties.getPath();
        }
    
        /**
         * Returns the configuration properties of this controller.
         *
         * @return error properties
         */
        public ErrorProperties getErrorProperties()
        {
            return this.properties;
        }
    
        /**
         * This function handles runtime and application errors.
         *
         * @param request the incorrect request instance
         *
         * @return error message as XML-document
         */
        @RequestMapping (produces = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE})
        @ResponseBody
        public ResponseEntity<Object> handleError(HttpServletRequest request)
        {
            logger.trace("Catching Exception in REST API.");
            logger.debug("Using {} for exception handling." , getClass().getSimpleName());
    
            // original requested REST endpoint
            String endpoint = String.valueOf(request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI));
            // status code
            String code = String.valueOf(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
            // thrown exception
            Exception ex = ((Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION));
    
            if(ex == null) {
                ex = new RuntimeException(String.valueOf(request.getAttribute(RequestDispatcher.ERROR_MESSAGE)));
            }
    
            // release nested exceptions (we want source exception only)
            if(ex instanceof NestedServletException && ex.getCause() instanceof Exception) {
                ex = (Exception) ex.getCause();
            }
    
            // build response body
            WebErrorResponse response = new WebErrorResponse();
    
            response.title   = "Internal Server Error";
            response.type    = ex.getClass().getSimpleName();
            response.code    = Integer.valueOf(code);
            response.message = request.getMethod() + ": " + endpoint+"; "+ex.getMessage();
    
            // build response headers
            HttpHeaders headers = new HttpHeaders();
    
            headers.setContentType(getResponseType(request));
    
            // build the response
            return new ResponseEntity<>(response, headers, getStatus(request));
        }
    
        /*@RequestMapping (produces = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_XML_VALUE})
        public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request)
        {
            Boolean stacktrace = properties.getIncludeStacktrace().equals(ErrorProperties.IncludeStacktrace.ALWAYS);
    
            Map<String, Object> r = getErrorAttributes(request, stacktrace);
    
            return new ResponseEntity<Map<String, Object>>(r, getStatus(request));
        }*/
    
        /**
         * Extracts the response content type from the "Accept" HTTP header field.
         *
         * @param request request instance
         *
         * @return response content type
         */
        private MediaType getResponseType(HttpServletRequest request)
        {
            String accepts = request.getHeader(HttpHeaders.ACCEPT);
    
            // only XML or JSON allowed
            if(accepts.contains(MediaType.APPLICATION_JSON_VALUE))
                return MediaType.APPLICATION_JSON_UTF8;
            else return MediaType.APPLICATION_XML;
        }
    }
    

    就是这样,POJO WebErrorResponse 是一个仅使用公共字符串和 int 字段的普通类。

    上述类适用于支持 XML 和 JSON 的 REST API。 它是如何工作的:

    • 来自控制器(自定义和应用程序逻辑)的异常将由 Spring 异常处理程序处理
    • 来自 Spring 的异常将由 Spring 异常处理程序处理(例如缺少参数)
    • 404(缺少端点)将由 Spring Boot 错误控制器处理
    • mimetype 问题(例如,请求图像/png 但抛出异常)将首先移至 Spring 异常处理程序,然后重定向到 Spring Boot 错误控制器(由于 mimetype 异常)

    我希望这将为像我一样困惑的其他人澄清一些事情。

    最好的问候,

    压缩包

    【讨论】:

    • 感谢您的解决方案!我有一个RestController 生产text/csv,但无法让错误控制器做同样的事情。如果客户端不包含 Accept 标头,我会不断收到 406 错误。设置默认内容类型就可以了。
    【解决方案2】:

    这是一个内容协商问题。基本上,请求要求以特定格式的响应,而服务器表示它无法以该格式传递响应。

    这里有几件事可能是问题所在。

    1. 您的请求没有为 Accept 标头指定 application/json
    2. 您的请求确实指定了一个值为 application/jsonAccept 标头,但 Spring Web MVC 配置未设置为处理 JSON 内容类型。

    在第一种情况下,最好指定请求者可以处理的类型。无论这是否是您的确切问题,我都会这样做。

    在第二种情况下,Spring 默认通过 XML 进行内容协商。您可以通过将 WebMvcConfigurer 添加到 ApplicationContext 来修改此默认行为:

    public class WebConfig extends WebMvcConfigurerAdapter {
      @Override
      public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_JSON);
      }
    }
    

    此外,明智的做法是在您的 @RequestMapping 注释上更加明确。确保使用consumesproduces 的“提示”参数(通过AcceptsContent-type 请求标头协助映射请求)。

    【讨论】:

    • 您好,感谢您的建议。这就是我首先想到的。我的 REST 服务有多个端点,可提供 JSON、XML 和二进制内容(如 PNG)。是否可以设置一个在没有内容协商问题的情况下返回 XML 的错误处理程序?我的意思是,如果客户端要求二进制图像,如何通过 Spring 处理“类似 REST”的请求错误?有什么提示吗?我不想使用 ModelView 或 HTML 错误页面...
    • 此外,您能否阐明@ControllerAdvice 类和ErrorController 实现之间的联系。控制器建议似乎被重定向到错误控制器?如何处理正确的输出?
    • 我同意这是一个内容协商问题,但它是 Spring 的内部问题。如果客户端不发送Accept 标头,则使用rest 控制器的produces 字段。但是,默认的BasicErrorController.error() 方法不会设置内容类型。所以,在Spring内部某处,当返回错误响应时,检测到不符合rest控制器的produces,返回406错误。像@Zipunrar 在他的解决方案中所做的那样强制使用默认内容类型可以解决此问题。
    猜你喜欢
    • 1970-01-01
    • 2014-06-07
    • 1970-01-01
    • 2019-02-08
    • 1970-01-01
    • 1970-01-01
    • 2015-05-17
    • 2016-07-15
    • 1970-01-01
    相关资源
    最近更新 更多