SpringMVC 异常处理
我们知道进行异常处理是 Controller 层的职责之一,但在每一个处理方法里使用try...catch
块感觉又不太聪明的亚子...在 SpringMVC 中有 4 种常用方式,可以对异常进行统一处理:
- 使用 SpringMVC 预定义的
SimpleMappingExceptionResolver
,发生指定异常后跳转页面; - 实现
HandlerExceptionResolver
,自定义异常处理器; - 使用注解
@ExceptionHandler
; - 使用注解
@ControllerAdvice
配合@ExceptionHandler
。
下面来说说这 4 种方法的具体使用。
1. 配置 SimpleMappingExceptionResolver
只需要在 SpringMVC 配置文件中注册该异常处理器 Bean 即可,没有 id 属性,无需显式调用或被注入给其它 Bean。它对异常的处理方式仅限于跳转到指定的响应页面,在页面中进行处理。
示例:
// 一个自定义的异常类
public class MemberException extends Exception {
public MemberException() {
super();
}
public MemberException(String msg) {
super(msg);
}
}
配置指定异常和需要跳转的页面:
<!-- SpirngMVC配置文件 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="cn.cna.exception.MemberException">/errors/memberErrors.jsp</prop>
<!-- 当请求参数的值与接收该参数的处理器方法形参的类型不匹配时,会抛出类型匹配异常TypeMismatchException -->
<prop key="org.springframework.beans.TypeMismatchException">/errors/typeException.jsp</prop>
</props>
</property>
<property name="defaultErrorView" value="/errors/defaultErrors.jsp"/>
<property name="exceptionAttribute" value="ex"/>
</bean>
属性解释:
- exceptionMappings:Properties 类型属性,用于指定不同类型的异常所对应的响应页面,Key 为异常类的全限定类名,value 为响应页面路径;
- defaultErrorView:指定默认的异常响应页面,若发生的异常没有在 exceptionMappings 中指定,则使用该页面;
- exceptionAttribute:捕获到的异常对象,可以在异常响应页面中使用。
这样,Controller 方法可以不用显式进行异常处理,而是通过添加throws
异常声明,将异常处理的职责交给上层调用者,进而转交给注册好的 SimpleMappingExceptionResolver,根据不同的异常实例进行不同的页面响应。
2. 实现 HandlerExceptionResolver
使用第 1 种方法可以实现发生指定异常后的跳转,但如果想要在捕获异常后执行其他的操作,就需要自己实现HandlerExceptionResolver
接口,并同样在 SpringMVC 配置文件中注册。
HandlerExceptionResolver
只有一个resolveException
方法需要我们去实现,来看看代码:
public class MemberExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, response httpServletResponse, Object handler, Exception ex) {
ModelAndView mv = new ModelAndView();
// 将异常对象加入数据模型中
mv.addObject("ex",ex);
// 设置默认错误响应页面
mv.setViewName("/errors/defaultErrors.jsp");
// 设置MemberException响应页面
if (ex instanceof MemberException) {
mv.setViewName("/errors/memberErrors.jsp");
}
// 其他...
return mv;
}
}
在 SpringMVC 配置文件中注册:
<bean class="cn.cna.resolver.MyExceptionResolver"/>
你可能有个疑问,如果项目采用的是前后端分离,需要返回 Json 数据又该如何做?有两个方法:
- 用 response 的 writer 直接输出 Json 数据
@Override
public ModelAndView resolveException(HttpServletRequest request, response httpServletResponse, Object handler, Exception ex) {
// 避免中文乱码
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
try {
// 为了简化演示,这里省略了对象转Json字符串的操作
String jsonStr = "";
if (ex instanceof MemberException) {
response.setStatus(HttpStatus.OK.value());
jsonStr = "{'code':1,'message':'业务异常'}";
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
jsonStr = "{'code':500,'message':'服务器未知异常'}";
}
response.getWriter().print(jsonStr);
response.getWriter().flush();
response.getWriter().close(); // 关闭了输出,确保后面不会再使用该response,比如拦截器
} catch (IOException e) {
e.printStackTrace();
}
return null; // 后续不会有渲染步骤,但若还有处理器将继续执行
}
- 借助 MappingJackson2JsonView
MappingJackson2JsonView
是一个 Json 视图,用法如下:
@Override
public ModelAndView resolveException(HttpServletRequest request, response httpServletResponse, Object handler, Exception ex) {
ModelAndView mv = new ModelAndView();
MappingJackson2JsonView view = new MappingJackson2JsonView();
view.setJsonPrefix("fsxJson"); // 设置 Json 前缀
// view.setModelKey(); 只序列化指定的key
mv.setView(view);
// 添加key-value非常方便
mv.addObject("code", "1");
mv.addObject("message", "业务异常");
return mv;
}
详细可见:@ExceptionHandler or HandlerExceptionResolver?如何优雅处理全局异常?【享学Spring MVC】,YourBatman,CSDN,写得非常深入的一篇文章,值得好好研究。
3. 使用 @ExceptionHandler
@ExceptionHandler
是 Spring 3.0 后提供的处理异常的注解,目的在于对 REST 应用提供更多的支持(是的,ModelAndView
不再是必须的)。被@ExceptionHandler
标注的方法将成为一个异常处理器,通过value
属性(类型是一个Class<?>
数组)指定该方法所要匹配的异常。
// org.springframework.web.bind.annotation.ExceptionHandler
@Target(ElementType.METHOD) // 只能标注在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
// 指定异常类型,可以多个
Class<? extends Throwable>[] value() default {};
}
使用:
//MemberException异常处理
@ExceptionHandler(MemberException.class)
public ApiResult handleMemberException(MemberException ex){
return ApiResult.error().setMessage(ex.getMessage());
}
但是请注意,被@ExceptionHandler
注解的方法只能处理同一个 Controller 中的其他方法产生的异常。一般会将异常处理方法专门定义在一个 Controller 中,让其它 Controller 继承它。但是 Java 是单继承的呀,如果基类 Controller 中还需要封装其他逻辑,就会显得臃肿,职责也不够明确。
@ControllerAdvice + @ExceptionHandler
@ControllerAdvice
是 Spring3.2 提供的新注解,基于 AOP 思想,可以指定一个 Controller 增强器,配合@ExceptionHandler
可用于全局的异常处理,现在不必纠结于继承了。
// 全局异常处理
// 如果是REST应用,就使用@RestControllerAdvice,不用给每个方法写@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(MemberException.class)
public ApiResult handleMemberException(MemberException ex) {
return ApiResult.error().setMessage(ex.getMessage());
}
// 添加其他异常处理方法...
}
5. 关于优先级
从高到低:
- @Controller + @ExceptionHandler
- @ControllerAdvice + @ExceptionHandler
- HandlerExceptionResolver
三种方式并存的情况下,优先级越高的先选择,被捕获处理了的异常与后面无关,没有被捕获的就会继续传递。
为了避免在处理异常的过程中又产生异常的情况,一个好的办法是设计一个 HandlerExceptionResolver 放在末位,用最不会出 bug 的代码来处理一切前面不能处理的异常。
END
参考:
springmvc异常处理,JS_HCX,简书
@ControllerAdvice实现优雅地处理异常,KEN DO EVERTHING,CSDN
Spring 异常处理三种方式,喜欢日向雏田一样的女子啊,CSDN