SpringMVC 控制器统一异常处理
摘要
介绍spring mvc控制器中统一处理异常的两种方式:HandlerExceptionResolver以及@ExceptionHandler;以及使用@ControllerAdvice将@ExceptionHandler方法的影响扩大。
一、问题的提出
Spring MVC 项目的开发中,不管是底层的数据库操作过程,业务层的业务逻辑的处理,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常。
这些异常可以在每个单独的环节捕获,处理;但是大多数情况下,异常情况都会反馈到控制器(无论是通过抛出异常的方式,还是自定义特殊返回值,如null等的方式),然后由控制器结合具体异常情况,返回特定信息(通常是不同的返回码,错误信息)给http请求的调用方。
然而,每个环节都单独捕获处理异常,业务代码可读性不强,工作量大且不好统一,维护的工作量也很大。那么,能不能将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护?答案是肯定的。
二、统一异常处理
对于spring mvc来说,一次http请求在服务端处理涉及到的环节一般如下:
每个环节都有可能发生异常;问题的解决思路,恰恰是对于异常处理的自然过程: 能够处理异常就捕获处理,不能处理异常就将异常抛出(或者转换抛出)。
一般来说,服务层和持久层发生的异常,这两层都无能为力,因为这些异常情况会转换为相关的信息返回到http调用方。既然不能处理,何不直接抛出(转换抛出)到控制层?然后由http请求的入口处——控制层统一处理。
那么,可能的处理方法是这样的:
controller:
@RequestMapping(...)
public Object doController(){
try {
invokeService();
} catch(CustomizedEx1 e) {
// 返回码1
} catch(CustomizedEx2 e) {
// 返回吗2
} ...
catch(Exception e) {
// 系统异常 ?
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这样可以做到在一次请求中,统一在入口控制器方法处处理异常。但是这样的话,对于每个请求,在控制器中处理将请求处理委托给服务层的代码外,不得不书写捕获各种异常的catch块,对于懒惰的程序员来说,无疑是灾难性的操作。
封装
考虑一下异常的种类,事实上业务异常的种类是有限的,不同的请求出现的异常情况无非就那么几种。这时可将catch处理封装起来,作为一个统一的方法,共各个controller方法调用。
superController:
class SuperController {
public Object uniformExHandle(Exception e) {
if (e instanceof CustomizedEx1) {
// 返回码1
} else if (e instanceof CustomizedEx2) {
// 返回码2
}...
else {
// 系统异常 ?
}
}
}
specificContoller:
@Controller
class HelloController extends SuperController {
@RequestMapping(...)
public Object doController(){
try {
invokeService();
}
catch(Exception e) {
uniformExHandle(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
封装异常处理,为了各个控制器能够方便调用,抽象一个控制器的父类,供各个具体控制器继承。
松耦合
上面的封装+控制器统一异常处理,似乎解决了开始提出的问题,事实上也解决了问题。但是也引入了新的问题:所有控制器不得不继承 SuperController 以获得统一处理异常的能力。
这是一种紧耦合的体现,彷佛回到了EJB时代,为了获取框架的功能,一个类必须实现一堆类,继承一堆接口。这也是良好的设计提倡 少用继承,多用组合 的原因。
诚然,使用组合的方式,将异常统一处理暴露出去供控制器方法调用,是一种松耦合的方法。但是既然在spring mvc的生态中,spring mvc也考虑到了这个问题,提供了两种方式实现控制器的异常处理:
使用实现HandlerExceptionResolver接口的类处理异常
使用@Exception注解的方法处理异常
这两种方法原理一样,区别只在使用方式而已。
三、HandlerExceptionResolver
参考HandlerExceptionResolver的jdk文档,就能轻松了解如何使用。
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
1
2
3
4
5
6
实现HandlerExceptionResolver接口的类能够解决在处理器映射或处理器方法执行过程中产生的异常,通常导向错误的view,实现类通常需要注册到应用spring上下文中才能生效;其resolveException方法试图解决在处理器执行期间抛出的异常,并在适当的情况下的返回代表特定错误页面的ModelAndView。返回的ModelAndView为空时标明异常已经被成功地解决,但是没有错误页面返回,例如,设置了错误码。
简单的使用方式:
@Component // 必须注册到spring容器中才有效
public class GlobalExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
String exMsg = "";
if(null != ex) {
exMsg = ex.getMessage();
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("exception");
Map<String, String> map = new HashMap<String, String>();
map.put( "key", "exception occured: " + exMsg);
modelAndView.addAllObjects(map);
return modelAndView;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
原理
DispatcherServlet是SpringMVC的核心,当然他也负责了这个“全局异常的处理”。
1)分发请求中捕获异常:
doDispatch()是DispatcherServlet分发请求的入口,方法中捕获请求执行可能的异常,并交给processDispatchResult()处理
DispatcherServlet#doDispatch():
try {
...
// Actually invoke the handler.
v = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
} catch (Exception ex) {
dispatchException = ex;
}
// 处理结果以及异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
1
2
3
4
5
6
7
8
9
10
11
2)processDispatchResult核心
DispatcherServlet#processDispatchResult():
if (exception != null) { // 处理结果,存在异常
if (exception instanceof ModelAndViewDefiningException) {
...
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 调用异常处理获得 ModelAndView
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
DispatcherServlet#processHandlerException():
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可见最终遍历了DispatcherServlet的handlerExceptionResolvers,依次调用配置的exception resolver来处理异常,直到异常处理器返回的ModelAndView不为空。
3)handlerExceptionResolvers初始化
在初始化阶段,会初始化异常处理器,将spring容器中注册的HandlerExceptionResolver加入到DispatcherServlet的handlerExceptionResolvers列表中:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
...
initHandlerExceptionResolvers(context);
...
}
private void initHandlerExceptionResolvers(ApplicationContext context) {
if (this.detectAllHandlerExceptionResolvers) {
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
四、ExceptionHandler
上面的HandlerExceptionResolver方式也需要实现这个接口;另一种注解方式是使用@ExceptionHandler,只需在指定的控制器中简单使用即可:
@Controller
class ExampleController {
@ExceptionHandler(Exception.class)
@ReponseBody
public Object exceptionHandler(Exception e) {
...
return new Object();
}
@RequestMapping(...)
public Object doController() {
}
1
2
3
4
5
6
7
8
9
10
11
12
**!!!**需要注意的是,注解@ExceptionHandler修饰的方法,只能处理所在控制器的@RequestMapping方法的未捕获异常,超出该控制器,或者没有使用@RequestMapping修饰的方法调用,发生的未捕获异常都不会被处理。
@ExceptionHandler
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* Exceptions handled by the annotated method. If empty, will default to any
* exceptions listed in the method argument list.
*/
Class<? extends Throwable>[] value() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
简要说来,使用这个注解标注的方法能处理方法所在Controller的处理器中未捕获的异常,处理异常的方法可以有多种方式的签名,参数可以有
异常类型的参数;Exception或者特定类型的异常类,要与value中指定的异常匹配
request和response对象;javax.servlet.ServletRequest/javax.servlet.ServletResponse,javax.servlet.http.HttpServletRequest/javax.servlet.http.HttpServletRequest
Session对象
等等
返回值可以是:
ModelAndView, model object, Map, View
表示视图名的String
@Response修饰,设置响应内容;使用配置的message converts将返回值转换为响应流
HttpEntity / ResponseEntity,同样使用message converts转换
void,如果方法自己处理http response输出
可以看出@ExceptionHandler方式灵活得多,而且其原理与HandlerExceptionResolver是一样的。
全局配置
由于@ExceptionHandler方法只能处理同一个控制器内的方法,这样每一个控制器都要声明@ExceptionHandler方法?
很自然的可以想到在所有控制器的一个父类中声明一个@ExceptionHandler方法,即可全局处理。更优雅的方式是使用@ControllerAdvice;
正如其名字一样,注解修饰的类是“协助”其他控制器,是@Component的具化注解,通过类路径扫描(component scan)修饰的类可以被自动检测(注册到spring容器)。
典型的用法是用来定义 @ExceptionHandler, @InitBinder, 和 @ModelAttribute方法,这些方法可运用于所有的@RequestMapping方法。默认情况下@ControllerAdvice的修饰类会“协助”所有已知的控制器。
以下为使用demo:
@ControllerAdvice
public class UniformControllerExHandler {
@ExceptionHandler(Throwable.class)
@ResponseBody
public Object exHandler(Throwable e) {
AgentBaseResponse resp = new AgentBaseResponse();
resp.setRetMsg(e.getMessage());
log.error("控制器异常(Throwable), 返回: " + JSON.toJSONString(resp), e);
return resp;
}
}
1
2
3
4
5
6
7
8
9
10
11
五、总结
spring mvc中业务方法的异常,可以在控制层统一处理。
通过实现spring提供的HandlerExceptionResolver接口,并把实现类注入到spring容器,可统一处理控制器方法未捕获的异常。
另一种方法是使用@ExceptionHandler,借助@ControllerAdvice可将影响扩大到每一个控制器。
SpringMVC 控制器统一异常处理的更多相关文章
- 【SpringMVC】统一异常处理
一.需求 二.统一异常处理解决方案 2.1 定义异常 2.2 异常处理 2.3 配置统一异常处理器 2.4 异常处理逻辑 一.需求 一般项目中都需要作异常处理,基于系统架构的设计考虑,使用统一的异常处 ...
- 【SpringMVC学习07】SpringMVC中的统一异常处理
我们知道,系统中异常包括:编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发.测试通过手段减少运行时异常的发生.在开发中,不管是dao层 ...
- SpringMVC框架08——统一异常处理
前言 在Spring MVC 应用的开发中,不管是对底层数据库操作,还是业务层或控制层操作,都会不可避免地遇到各种可预知的.不可预知的异常需要处理.如果每个过程都单独处理异常,那么系统的代码耦合度高, ...
- Java springmvc 统一异常处理的方案
前言:为什么要统一异常处理?经常在项目中需要统一处理异常,将异常封装转给前端.也有时需要在项目中统一处理异常后,记录异常日志,做一下统一处理. Springmvc 异常统一处理的方式有三种. 一.使用 ...
- springMVC统一异常处理
Spring MVC处理异常有3种方式: 使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver: 实现Spring的异常处理接口HandlerExc ...
- Java生鲜电商平台-统一异常处理及架构实战
Java生鲜电商平台-统一异常处理及架构实战 补充说明:本文讲得比较细,所以篇幅较长. 请认真读完,希望读完后能对统一异常处理有一个清晰的认识. 背景 软件开发过程中,不可避免的是需要处理各种异常,就 ...
- 使用Spring MVC统一异常处理实战
1 描述 在J2EE项目的开发中,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的.不可预知的异常需要处理.每个过程都单独处理异常,系统的代码耦合 ...
- SpringCloud学习之Zuul统一异常处理及回退
一.Filter中统一异常处理 其实在SpringCloud的Edgware SR2版本中对于ZuulFilter中的错误有统一的处理,但是在实际开发当中对于错误的响应方式,我想每个团队都有自己的处理 ...
- SpringBoot 统一异常处理
统一异常处理: @ControllerAdvice public class GlobalExceptionHandler { private Logger logger = LoggerFactor ...
随机推荐
- YTU 2439: C++习题 复数类--重载运算符+
2439: C++习题 复数类--重载运算符+ 时间限制: 1 Sec 内存限制: 128 MB 提交: 1022 解决: 669 题目描述 定义一个复数类Complex,重载运算符"+ ...
- bzoj1798 1
1798: [Ahoi2009]Seq 维护序列seq Time Limit: 30 Sec Memory Limit: 64 MBSubmit: 5866 Solved: 2079[Submit ...
- 476. Number Complement(补数)
Given a positive integer, output its complement number. The complement strategy is to flip the bits ...
- Linux 软链接 硬链接 ln命令(简约说明版)
注意:路径使用绝对路径!! 解决方法: 当我们需要在不同的目录下用到同一个文件时,会用到以下命令. 命令:ln 作用:为某一个文件在另外一个位置建立一个同步的链接 语法:ln [option] 源文件 ...
- P1971 [NOI2011]兔兔与蛋蛋游戏
传送门 思路比较迷--题解在这里 //minamoto #include<bits/stdc++.h> #define R register #define fp(i,a,b) for(R ...
- 【数据结构】27、红黑树,节点插入,修复平衡操作总结(针对jdk8中hashmap冲突过多链表转红黑树)
二叉树节点插入 0.如果只有一个节点,那么就直接作为根,涂黑,如果父为黑,或者祖父为空,那么不做操作 1.叔叔节点不为空且为红 那么就修改父,叔叔,祖父节点颜色,最后把当前节点设置为祖父节点,在进行平 ...
- Step 4: Install and Configure Databases
https://www.cloudera.com/documentation/enterprise/latest/topics/cm_ig_installing_configuring_dbs.htm ...
- MYSQL 配置远程连接
例如,你想myuser使用mypassword从任何主机连接到mysql服务器的话. GRANT ALL PRIVILEGES ON *.* TO 'myuser'@'%'IDENTIFIED BY ...
- poj 3253 Fence Repair (水哈夫曼树)
题目链接: http://poj.org/problem?id=3253 题目大意: 有一根木棍,需要截成n节,每节都有固定的长度,一根长度为x的木棒结成两段,需要花费为x,问截成需要的状态需要最小的 ...
- _bzoj1008 [HNOI2008]越狱【计数】
传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=1008 水题... #include <cstdio> const int mod ...