spring mvc异常处理

spring mvc异常处理

异常在软件开发过程中随处可见,是所有程序员都必须要思考并解决的问题。有些异常是不可避免必须要处理的,譬如通过ObjectMapperjson字符串转换成对象就必须显示地处理异常;有些则是开发过程中可能注意不到的异常,比如调用JdbcTemplatequeryForObject方法查询对象的时候,如果数据库中没有满足条件的数据或者查询到记录多于1条的时候,就会抛EmptyResultDataAccessException或者IncorrectResultSizeDataAccessException异常。

在何时何处处理异常是一门高深的学问,往往需要积累大量的经验才能够很好地把握处理异常的时机,在这里采用前辈们的经验共勉:

仅当清楚地明白当前需要处理捕获到的异常时才处理该异常,否则直接或者重新封装后向上抛

总会有异常会从底层传递到接口返回处,这时候必须对异常进行处理,否则将会极大地影响接口的友好度,一般都会将异常转换成用户友好的提示信息返回给接口调用方。

在spring mvc中,在接口层面有三种方式处理异常:

  1. 直接在Controller调用Service的时候捕获并处理异常
  2. Controller中统一处理指定类型的异常
  3. Controller外统一处理异常

第一种方式虽然很直接明了,但是需要在所有接口中都要try-catch异常,会导致代码上的重复以及不必要的异常处理逻辑。将异常提取到接口外处理有助于保持接口的简洁性。

下面开始介绍spring mvc中的异常处理方式。

异常处理

如果在请求映射(request mapping)或者请求处理(request handler)的过程中发生了异常,那么DispatcherServlet加会委托HandlerExceptionResolver链去处理这些异常。

HandlerExceptionResolver有如下几种:

  • SimpleMappingExceptionResolver:将异常类的名字试图的名字进行关联映射,往往用于将异常对应于指定的错误页面。
  • DefaultHandlerExceptionResolver:将异常HTTP状态码进行关联映射。
  • ResponseStatusExceptionResolver:将异常@ResponseStatus注解进行关联映射,并将@ResponseStatus中的值映射成HTTP状态码
  • ExceptionHandlerExceptionResolver:将异常@ExceptionHandler进行关联映射。当异常与@ExceptionHandler注解中的值一致的或者是其子类的时候,则调用@ExceptionHandler标注的方法处理该异常。

通过给以上四种HandlerExceptionResolver设定不同的order值,可以构造成不同的异常处理链处理异常,order值越大,处理的时机就越晚。

从spring的@EnableWebMvc-->DelegatingWebMvcConfiguration --> WebMvcConfigurationSupport.addDefaultHandlerExceptionResolvers的源码中可以发现:
spring mvc默认设置的处理链为DefaultHandlerExceptionResolverResponseStatusExceptionResolverExceptionHandlerExceptionResolver三种异常处理器

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
    ExceptionHandlerExceptionResolver exceptionHandlerResolver = this.createExceptionHandlerExceptionResolver();    exceptionHandlerResolver.setContentNegotiationManager(this.mvcContentNegotiationManager());        exceptionHandlerResolver.setMessageConverters(this.getMessageConverters());
exceptionHandlerResolver.setCustomArgumentResolvers(this.getArgumentResolvers());      exceptionHandlerResolver.setCustomReturnValueHandlers(this.getReturnValueHandlers());
    if (jackson2Present){            
        exceptionHandlerResolver.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
    }
    if (this.applicationContext != null) {   
        exceptionHandlerResolver.setApplicationContext(this.applicationContext);
    }
    exceptionHandlerResolver.afterPropertiesSet();
    exceptionResolvers.add(exceptionHandlerResolver);
    ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
    responseStatusResolver.setMessageSource(this.applicationContext);
    exceptionResolvers.add(responseStatusResolver);
    exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}

可以通过覆盖WebMvcConfigurer接口中的configureHandlerExceptionResolvers方法指定自定义的异常处理链。

@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver();
    exceptionHandlerExceptionResolver.setOrder(2);
    resolvers.add(exceptionHandlerExceptionResolver);
}

如果这些异常处理类还没有处理好抛出的异常或者处理后HTTP状态码是4xx或者5xx,那么Servlet容器将会渲染默认的错误页面,可以通过在web.xml中自定义错误页。

<error-page> 
    <location>/error</location> 
</error-page>

然后由DispatcherServlet发出error请求,可以通过如下方式捕获:

@RestController 
public class ErrorController {
@RequestMapping(path = "/error") 
public Map<String, Object> handle(HttpServletRequest request) { 
    Map<String, Object> map = new HashMap<String, Object>();    
    map.put("status", request.getAttribute("javax.servlet.error.status_code")); 
    map.put("reason", request.getAttribute("javax.servlet.error.message")); return map; }
}

异常捕获

@Controller@ControllerAdvice中可以通过@ExceptionHandler标注的方法捕获异常进行处理,只不过@Controller只能捕获本Controller中接口抛出的异常,而@ControllerAdvice可以捕获所有Controller中接口抛出的异常。
因此在这里介绍spring mvc中全局统一异常捕获机制。

统一的返回

首先对于项目中的接口,定义统一的接口返回格式。

public final class OutPut {
    private final static String STATUS = "status";
    private final static String CODE = "code";
    private final static String MSG = "msg";
    private final static String DATA = "data";
    private OutPut() {
        throw new AssertionError();
    }

    public static Map<String, Object> success(String msg, Object data) {
        Objects.requireNonNull(msg);
        Objects.requireNonNull(data);
        return ImmutableMap.of(STATUS, ResponseStatus.SUCCESS, CODE, ResponseCode.SUCCESS, MSG, msg, DATA, data);
    }

    public static Map<String, Object> failure(int code, String msg) {
        Objects.requireNonNull(msg);
        return ImmutableMap.of(STATUS, ResponseStatus.FAILURE, CODE, code, MSG, msg);
    }
}

定义项目中需要的错误编码

public interface ResponseCode {
    int SUCCESS = 200;
    int FAILURE = 400;
    int INNER_ERROR = 500;
}

全局异常处理器

通过@RestControllerAdvice注解指定全局异常处理器

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {IllegalArgumentException.class})
    public Map<String, Object> handleIllegalArgumentException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage());
    }
}

这里捕获了接口中常见的参数错误异常,读友们可以创建自定义的异常,通过类似的方式进行捕获处理。

当多个异常类型同事出现的时候,ExceptionDepthComparator会对所有的异常进行排序,会调用在异常继承链上该异常最近的父异常指定的处理方法。为了减少错误匹配的情况,建议方法参数给定指定的类型。

对于REST服务来说,可以让GlobalExceptionHandler继承ResponseEntityExceptionHandler,然后覆写父类的相关方法。

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = {RuntimeException.class})
    public Map<String, Object> handleRuntimeException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage() + "runtime");
    }

    @ExceptionHandler(value = {IllegalArgumentException.class})
    public Map<String, Object> handleIllegalArgumentException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage());
    }



    @ExceptionHandler(value = {Exception.class})
    public Map<String, Object> handleException(Exception e) {
        return OutPut.failure(ResponseCode.FAILURE, e.getMessage() + "exception");
    }

     @Override
    protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {

        return ResponseEntity.status(status).body(OutPut.failure(ResponseCode.TYPE_MIS_MATCH, ex.getValue() + "的类型不匹配,需要" + ex.getRequiredType()));
    }
}

在这里,覆写了父类的参数类型匹配错误异常(handleTypeMismatch),并且保持了与其他异常同样的错误格式,如下:

{
    "status": "failure",
    "code": 401,
    "msg": "abc的类型不匹配,需要int"
}

推荐阅读更多精彩内容