spring mvc异常处理
异常在软件开发过程中随处可见,是所有程序员都必须要思考并解决的问题。有些异常是不可避免必须要处理的,譬如通过ObjectMapper
将json
字符串转换成对象就必须显示地处理异常;有些则是开发过程中可能注意不到的异常,比如调用JdbcTemplate
的queryForObject
方法查询对象的时候,如果数据库中没有满足条件的数据或者查询到记录多于1条的时候,就会抛EmptyResultDataAccessException
或者IncorrectResultSizeDataAccessException
异常。
在何时何处处理异常是一门高深的学问,往往需要积累大量的经验才能够很好地把握处理异常的时机,在这里采用前辈们的经验共勉:
仅当清楚地明白当前需要处理捕获到的异常时才处理该异常,否则直接或者重新封装后向上抛
总会有异常会从底层传递到接口返回处,这时候必须对异常进行处理,否则将会极大地影响接口的友好度,一般都会将异常转换成用户友好的提示信息返回给接口调用方。
在spring mvc中,在接口层面有三种方式处理异常:
- 直接在
Controller
调用Service
的时候捕获并处理异常 - 在
Controller
中统一处理指定类型的异常 - 在
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默认设置的处理链为DefaultHandlerExceptionResolver
、 ResponseStatusExceptionResolver
和ExceptionHandlerExceptionResolver
三种异常处理器
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"
}