在本系列前三节中,讲解了Spring MVC的处理流程和怎么处理请求,以及请求参数的校验,这已经足以应付大多数 Web 开发的工作了,但是一个优秀的 MVC 框架,应该有优雅的异常处理机制以及灵活的拦截器机制。本节中,将会讲解 Spring MVC 中的异常处理以及拦截器。
异常处理
在开发过程中,我们经常会跟异常打交道,不管是业务异常还是系统异常,我们都应该处理它并记录到日志,返回良好的错误信息或者页面。Spring MVC 提供了优化的异常处理,可以基于 Controller 级别,或者全局异常处理
Controller 级别的异常处理
定义 BusinessException 异常类,表示业务异常
1
2public class BusinessException extends RuntimeException{
}
在 Controller 捕获异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@RestController
public class UserController{
@PostMapping("/user/register")
public Map register(User user){
// 假设这里注册失败,用户名已存在,则会抛出 BusinessException 异常
userServices.register(user);
Map result = new HashMap<>(4);
result.put("status","SUCCESS");
return result;
}
// 捕获 BusinessException
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Map handleException(BusinessException ex){
Map result = new HashMap<>(4);
result.put("status","ERROR");
result.put("Message",ex.getMessage());
return result;
}
}
在 Controller 内部如果有被@ExceptionHandler标记的方法,则代表该方法只捕获该 Controller 内部处理方法抛出的指定异常,并处理。
全局异常处理
在 Controller 内部处理异常,好是好,有个问题,那就是如果我要在多个 Controller 中都要捕获这个异常呢?那么我们不应该每个 Controller 内都编写@ExceptionHandler方法,应该在某一个地方同一处理。
@ControllerAdvice
@ControllerAdvice是一个@Component,用于定义被@ExceptionHandler,@InitBinder和@ModelAttribute标记的方法,这些方法会被应用于所有 Controller 中,等同于在这里面编写一次,就等于在所有 Controller 都已经编写了。
定义一个被@ControllerAdvice标注的Controller增强类
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52@ControllerAdvice
public class GlobalExceptionHandler{
/**
* 全局处理业务异常
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Map handleBusinessException(BusinessException ex){
Map result = new HashMap<>(4);
result.put("status", "ERROR");
result.put("Message", ex.getMessage());
return result;
}
/**
* 全局处理系统异常
*/
@ExceptionHandler(Exception.class)
public ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Exception ex){
// 记录异常信息到日志...
//判断客户端是否是ajax请求
boolean isAjax = "XMLHttpRequest".equalsIgnoreCase(request.getHeader("x-requested-with"));
if (isAjax) {
//如果是ajax请求,返回json
response.setContentType("application/json;charset=UTF-8");
Map result = new HashMap<>(4);
result.put("status", "ERROR");
result.put("errors", ex.getMessage());
PrintWriter writer = null;
try {
writer = response.getWriter();
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
writer.write(json);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
writer.close();
}
return null;
} else {
//如果不是ajax请求,返回视图
ModelAndView result = new ModelAndView();
result.addObject("errors", ex.getMessage());
result.setViewName("error");
return result;
}
}
}
HandlerExceptionResolver
全局处理异常除了在控制器增强类中处理,还能自定义异常解析器处理
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
28
29
30
31
32
33
34
35
36
37
38public class GlobalHandlerExceptionResolver implements HandlerExceptionResolver{
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception ex){
// 记录异常信息到日志...
//判断客户端是否是ajax请求
boolean isAjax = "XMLHttpRequest".equalsIgnoreCase(request.getHeader("x-requested-with"));
//只有业务异常才将详细信息返回给客户端
String errorMessage = ex instanceof BusinessException ? ex.getMessage() : "系统错误!";
if (isAjax) {
//如果是ajax请求,返回json
response.setContentType("application/json;charset=UTF-8");
Map result = new HashMap<>(4);
result.put("status", "ERROR");
result.put("errors", errorMessage);
PrintWriter writer = null;
try {
writer = response.getWriter();
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
writer.write(json);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
writer.close();
}
return null;
} else {
//如果不是ajax请求,返回视图
ModelAndView result = new ModelAndView();
result.addObject("errors", errorMessage);
result.setViewName("error");
return result;
}
}
}
将自定义的异常解析器注册到 WebApplicationContext
1
拦截器
在 Spring MVC 中,我们可以自定义拦截器来在进行处理请求之前或之后来切入我们的代码,例如:审计日志,记录性能,授权认证校验等
定义一个类实现HandlerInterceptor接口,HandlerInterceptor类包含以下三个方法
preHandle:在处理请求之前被调用,返回 false 则表示不继续往下处理了
postHandle:在处理完请求之后、渲染视图之前被调用
afterCompletion:preHandle返回true时,在渲染页面之后被调用,一般用于释放资源
这里我们定义两个拦截器,一个是记录请求的日志,一个是记录请求耗时(注意:这里下面代码不作具体实现,只是展示拦截过程),来展示拦截器的处理流程和顺序
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50/**
* 记录请求日志
*/
public class AuditLogInterceptor implements HandlerInterceptor{
/**
* 处理请求之前被调用,如果返回false则表示不继续往下处理
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
System.out.println("LogInterceptor.preHandle");
return true;
}
/**
* 处理请求之后被调用,但是在渲染页面之前
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{
System.out.println("LogInterceptor.postHandle");
}
/**
* 渲染页面之后被调用,preHandle返回true时才会被调用,一般用于释放资源
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{
System.out.println("LogInterceptor.afterCompletion");
}
}
/**
* 记录处理请求耗时
*/
public class TimingInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
System.out.println("TimingInterceptor.preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{
System.out.println("TimingInterceptor.postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception{
System.out.println("TimingInterceptor.afterCompletion");
}
}
在 WebApplicationContext 中配置拦截器,拦截器是有配置顺序的,会按照顺序执行拦截代码
1
2
3
4
5
6
7
8
9
10
11
12
启动项目并访问一个请求,查看拦截过程和每个方法的执行顺序
1
2
3
4
5
6
7LogInterceptor.preHandle
TimingInterceptor.preHandle
--------处理请求中-----------
TimingInterceptor.postHandle
LogInterceptor.postHandle
TimingInterceptor.afterCompletion
LogInterceptor.afterCompletion