如何在 Spring MVC 中针对 HTML 和 JSON 请求以不同方式处理异常

Posted

技术标签:

【中文标题】如何在 Spring MVC 中针对 HTML 和 JSON 请求以不同方式处理异常【英文标题】:How to handle exceptions in Spring MVC differently for HTML and JSON requests 【发布时间】:2014-06-28 06:02:11 【问题描述】:

我在 Spring 4.0.3 中使用以下异常处理程序来拦截异常并向用户显示自定义错误页面:

@ControllerAdvice
public class ExceptionHandlerController

    @ExceptionHandler(value = Exception.class)
    public ModelAndView handleError(HttpServletRequest request, Exception e)
    
        ModelAndView mav = new ModelAndView("/errors/500"));
        mav.addObject("exception", e);
        return mav;
    

但现在我想要对 JSON 请求进行不同的处理,因此当发生异常时,我会收到此类请求的 JSON 错误响应。目前上述代码也是由 JSON 请求触发的(使用 Accept: application/json 标头),并且 javascript 客户端不喜欢 html 响应。

如何以不同方式处理 HTML 和 JSON 请求的异常?

【问题讨论】:

【参考方案1】:

诀窍是让 REST 控制器具有两个映射,其中一个指定 "text/html" 并返回一个有效的 HTML 源。下面的示例在 Spring Boot 2.0 中进行了测试,假设存在一个名为 "error.html" 的单独模板。

@RestController
public class CustomErrorController implements ErrorController 

    @Autowired
    private ErrorAttributes errorAttributes;

    private Map<String,Object> getErrorAttributes( HttpServletRequest request ) 
        WebRequest webRequest = new ServletWebRequest(request);
        boolean includeStacktrace = false;
        return errorAttributes.getErrorAttributes(webRequest,includeStacktrace);
    

    @GetMapping(value="/error", produces="text/html")
    ModelAndView errorHtml(HttpServletRequest request) 
        return new ModelAndView("error.html",getErrorAttributes(request));
    

    @GetMapping(value="/error")
    Map<String,Object> error(HttpServletRequest request) 
        return getErrorAttributes(request);
    

    @Override public String getErrorPath()  return "/error"; 



参考

ModelAndView -- HTML 的返回类型 DefaultErrorAttributes -- 用于呈现 HTML 模板(和 JSON 响应)的数据 BasicErrorController.java -- Spring Boot 源代码,此示例源自该源代码

【讨论】:

您可以选择也可以不选择使用@ExceptionHandler 进一步注释错误处理程序。【参考方案2】:

由于我没有找到任何解决方案,我编写了一些代码来手动检查请求的accept 标头以确定格式。然后,我检查用户是否已登录,如果已登录,则发送完整的堆栈跟踪或简短的错误消息。

我使用 ResponseEntity 能够返回 JSON 或 HTML,例如 here。 代码:

@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleExceptions(Exception ex, HttpServletRequest request) throws Exception 

    final HttpHeaders headers = new HttpHeaders();
    Object answer; // String if HTML, any object if JSON
    if(jsonHasPriority(request.getHeader("accept"))) 
        logger.info("Returning exception to client as json object");
        headers.setContentType(MediaType.APPLICATION_JSON);
        answer = errorJson(ex, isUserLoggedIn());
     else 
        logger.info("Returning exception to client as html page");
        headers.setContentType(MediaType.TEXT_HTML);
        answer = errorHtml(ex, isUserLoggedIn());
    
    final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    return new ResponseEntity<>(answer, headers, status);


private String errorHtml(Exception e, boolean isUserLoggedIn) 
    String error = // html code with exception information here
    return error;


private Object errorJson(Exception e, boolean isUserLoggedIn) 
    // return error wrapper object which will be converted to json
    return null;


/**
 * @param acceptString - HTTP accept header field, format according to HTTP spec:
 *      "mime1;quality1,mime2;quality2,mime3,mime4,..." (quality is optional)
 * @return true only if json is the MIME type with highest quality of all specified MIME types.
 */
private boolean jsonHasPriority(String acceptString) 
    if (acceptString != null) 
        final String[] mimes = acceptString.split(",");
        Arrays.sort(mimes, new MimeQualityComparator());
        final String firstMime = mimes[0].split(";")[0];
        return firstMime.equals("application/json");
    
    return false;


private static class MimeQualityComparator implements Comparator<String> 
    @Override
    public int compare(String mime1, String mime2) 
        final double m1Quality = getQualityofMime(mime1);
        final double m2Quality = getQualityofMime(mime2);
        return Double.compare(m1Quality, m2Quality) * -1;
    


/**
 * @param mimeAndQuality - "mime;quality" pair from the accept header of a HTTP request,
 *      according to HTTP spec (missing mimeQuality means quality = 1).
 * @return quality of this pair according to HTTP spec.
 */
private static Double getQualityofMime(String mimeAndQuality) 
    //split off quality factor
    final String[] mime = mimeAndQuality.split(";");
    if (mime.length <= 1) 
        return 1.0;
     else 
        final String quality = mime[1].split("=")[1];
        return Double.parseDouble(quality);
    

【讨论】:

【参考方案3】:

ControllerAdvice 注释有一个名为 basePackage 的元素/属性,可以设置它来确定它应该扫描哪些包以查找控制器并应用建议。因此,您可以做的是将处理正常请求的控制器和处理 AJAX 请求的控制器分离到不同的包中,然后编写 2 个带有适当 ControllerAdvice 注释的异常处理控制器。例如:

@ControllerAdvice("com.acme.webapp.ajaxcontrollers")
public class AjaxExceptionHandlingController 
...
@ControllerAdvice("com.acme.webapp.controllers")
public class ExceptionHandlingController 

【讨论】:

【参考方案4】:

由于您拥有 HttpServletRequest,您应该能够获得请求“Accept”标头。然后你可以根据它处理异常。

类似:

String header = request.getHeader("Accept");
if(header != null && header.equals("application/json")) 
    // Process JSON exception
 else 
    ModelAndView mav = new ModelAndView("/errors/500"));
    mav.addObject("exception", e);
    return mav;

【讨论】:

【参考方案5】:

使用@ControllerAdvice 让异常处理程序发送一个包含字段错误的 DTO。

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ValidationErrorDTO processValidationError(MethodArgumentNotValidException ex) 
    BindingResult result = ex.getBindingResult();
    List<FieldError> fieldErrors = result.getFieldErrors();

    return processFieldErrors(fieldErrors);

这个代码是这个网站的:http://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-adding-validation-to-a-rest-api/ 在那里查看更多信息。

【讨论】:

【参考方案6】:

最好的方法(尤其是在 servlet 3 中)是向容器注册一个错误页面,并使用它来调用 Spring @Controller。这样,您就可以以标准 Spring MVC 方式处理不同的响应类型(例如,为您的机器客户端使用 @RequestMapping 和 producer=...)。

我从您的其他问题中看到您正在使用 Spring Boot。如果您升级到快照(换句话说,1.1 或更高版本),您会立即获得此行为(请参阅BasicErrorController)。如果你想覆盖它,你只需要将 /error 路径映射到你自己的@Controller

【讨论】:

我不太清楚您的解决方案。 register an error page with container 是什么意思并用它来调用控制器?您能否详细说明一些示例或提供更多解释的链接? 这有更多细节。 ***.com/questions/25356781/… @Dave Syer 当您说“向容器注册错误页面”时,我想您会想到 web.xml 中的 和可能的 。据我所知,在使用这种技术时,您会丢失一些上下文(例如,处理此类错误的控制器将无法访问主体或用户发出的原始请求)。知道如何规避这个问题吗?您如何掌握原始请求,而不是容器提出的新请求? (或防止吞咽……不确定是否真的提出了新请求) 确实,容器错误页面相当有限(但请求属性可用,因此主体和错误通常可用)。 Spring Boot 解决方案仍然有效,并为您提供了更高级别的 API 来使用。 @Dave Syer 如果出现 403:forbidden 由于 Spring-Security 触发的无效 CSRF 检查,控制器中的“一切”(主体、请求参数等)都会丢失。您是否认为这可能是由于我的配置中的错误,或者在这种情况下,这种行为是正常的?如有必要,我会在堆栈上创建一个完整的帖子,具体取决于您的回答:)【参考方案7】:

自 spring 4 以来,controlleradvice 注释具有多个可以设置的属性。您可以定义多个应用不同规则的控制器建议。

一个属性是“注解。也许您可以在 json 请求映射上使用特定的注解,或者您可能会发现另一个属性更有用?

【讨论】:

我不相信功能是创作者的想法,但这绝对是实现他的目标的有趣方式......如果它有效(我没有需要自己测试)。只是为了澄清起见,您建议根据发生错误时的响应方式,使用不同的自定义注释标记每个控制器。 IE。 @HtmlErrorResponse@JsonErrorResponse,然后让您的单独建议控制器设置注释属性,即。 @ControllerAdvice(annotations = HtmlErrorResponse.class) 获取 html 错误响应。

以上是关于如何在 Spring MVC 中针对 HTML 和 JSON 请求以不同方式处理异常的主要内容,如果未能解决你的问题,请参考以下文章

Spring MVC源码分析--视图解析过程

spring mvc 是啥

请问,java高手,spring mvc拦截器如何拦截所有的请求啊,包括html和jsp页面?

spring mvc+spring + hibernate 整合

Spring MVC 和表单绑定:如何从列表中删除项目?

Spring MVC中如何传递对象参数