摘要
介绍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 控制器统一异常处理的更多相关文章

  1. 【SpringMVC】统一异常处理

    一.需求 二.统一异常处理解决方案 2.1 定义异常 2.2 异常处理 2.3 配置统一异常处理器 2.4 异常处理逻辑 一.需求 一般项目中都需要作异常处理,基于系统架构的设计考虑,使用统一的异常处 ...

  2. 【SpringMVC学习07】SpringMVC中的统一异常处理

    我们知道,系统中异常包括:编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发.测试通过手段减少运行时异常的发生.在开发中,不管是dao层 ...

  3. SpringMVC框架08——统一异常处理

    前言 在Spring MVC 应用的开发中,不管是对底层数据库操作,还是业务层或控制层操作,都会不可避免地遇到各种可预知的.不可预知的异常需要处理.如果每个过程都单独处理异常,那么系统的代码耦合度高, ...

  4. Java springmvc 统一异常处理的方案

    前言:为什么要统一异常处理?经常在项目中需要统一处理异常,将异常封装转给前端.也有时需要在项目中统一处理异常后,记录异常日志,做一下统一处理. Springmvc 异常统一处理的方式有三种. 一.使用 ...

  5. springMVC统一异常处理

    Spring MVC处理异常有3种方式: 使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver: 实现Spring的异常处理接口HandlerExc ...

  6. Java生鲜电商平台-统一异常处理及架构实战

    Java生鲜电商平台-统一异常处理及架构实战 补充说明:本文讲得比较细,所以篇幅较长. 请认真读完,希望读完后能对统一异常处理有一个清晰的认识. 背景 软件开发过程中,不可避免的是需要处理各种异常,就 ...

  7. 使用Spring MVC统一异常处理实战

    1 描述 在J2EE项目的开发中,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的.不可预知的异常需要处理.每个过程都单独处理异常,系统的代码耦合 ...

  8. SpringCloud学习之Zuul统一异常处理及回退

    一.Filter中统一异常处理 其实在SpringCloud的Edgware SR2版本中对于ZuulFilter中的错误有统一的处理,但是在实际开发当中对于错误的响应方式,我想每个团队都有自己的处理 ...

  9. SpringBoot 统一异常处理

    统一异常处理: @ControllerAdvice public class GlobalExceptionHandler { private Logger logger = LoggerFactor ...

随机推荐

  1. 识别jar的编译JDK版本

    解压jar,获取xxx.calss文件 dos命令行javap -verbose classname import java.io.InputStream; import java.io.PrintW ...

  2. Ubuntu系统下实现Android工程调用独立编译的C++程序和GMP、PBC库

    目的: 实现使用C++编写代码供Android工程调用.C++代码中可以使用STL库,也可以使用常用的由源码编译生成的库,如PBC.因为PBC是基于GMP库的,所以这里只记录了GMP和PBC库的编译安 ...

  3. 软件开发-MSF方法(《构建之法》读书笔记2)

    MSF-微软解决方案框架,是一套大型系统开发指南,它描述了如何用组队模型.过程模型和应用模型来开发Client/Server结构的应用程序,是在微软的工具和技术的基础上建立并开发分布式企业系统应用的参 ...

  4. mybaits错误解决:There is no getter for property named 'id' in class 'java.lang.String'

    在使用mybaitis传参数的时候,如果仅传入一个类型为String的参数,那么在 xml文件中应该使用_parameter 来代替参数名. 正确的写法: <span style="f ...

  5. yaffs2根文件系统的构建过程

    基于BusyBox-1.19.2  (以其它作者的作为参考) 1. 下载BusyBox的源码 http://busybox.net/ 2. 解压#tar xvzf busybox-1.19.2.tgz ...

  6. vue 使用过程中自己遇到的bug

    需要安装npm git(windows系统需要安装) npm 是node的包管理工具 npm 国内的网站比较慢,推荐使用cnpm(淘宝的镜像) cnpm(npm) install 创建依赖-----因 ...

  7. 去除inline-block的间隙

    产生间隙的原因就是标签之间的空格,去除的方法: 1 设置父元素的font-size:0;空格字符的宽高都为0, <div class="demo1 demo2"> &l ...

  8. JSP+MySQL实例

    转自:https://www.yiibai.com/jsp/jsp_mysql.html 在本章中,我们将讨论如何使用JSP访问数据库(这里以MySQL数据库为例).并假设您对JDBC应用程序的工作方 ...

  9. Ceph之PG数调整

    1. PG介绍 PG, Placement Groups.CRUSH先将数据分解成一组对象,然后根据对象名称.复制级别和系统中的PG数等信息执行散列操作,再将结果生成PG ID.可以将PG看做一个逻辑 ...

  10. $Edmonds-Karp$[网络流]

    \(原题戳这里\) >最大流最小割定理$(Maximum Flow, Minimum Cut Theorem): $ 网络的最大流等于最小割 具体的证明分三部分 1.任意一个流都小于等于任意一个 ...