@ControllerAdvice和@ExceptionHandler注解处理全局异常

目录
  1. 1. 一、@ControllerAdvice和@ExceptionHandler简介
    1. 1.1. 1.1、@ControllerAdvice
    2. 1.2. 1.2、全局异常处理
  2. 2. 二、为什么要做Controller层的异常统一处理以及统一结果返回
  3. 3. 三、使用@ExceptionHandler和@ControllerAdvice做到统一处理
    1. 3.1. 3.1、@ExceptionHandler和@ControllerAdvice基本使用
    2. 3.2. 3.2、@ExceptionHandler具体异常的处理
  4. 4. 四、自定义异常处理类CustomUserException
    1. 4.1. 4.1、自定义异常处理类
    2. 4.2. 4.2、通过@ControllerAdvice和@ExceptionHandler注解,实现统一异常捕获
    3. 4.3. 4.3、在业务代码中在需要抛出异常的地方抛出对应的异常即可

前言:开发过程中,难免有的程序会因为某些原因抛出异常,而这些异常一般都是利用try ,catch的方式处理异常或者throw,throws的方式抛出异常不管。这种方法对于程序员来说处理也比较麻烦,对客户来说也不太友好,所以我们希望既能方便程序员编写代码,不用过多的自己去处理各种异常编写重复的代码又能提升用户的体验,这时候全局异常处理就显得很重要也很便捷了。

一、@ControllerAdvice和@ExceptionHandler简介

在构建RestFul接口的今天,我们一般会限定好返回数据的格式,有利于前端调用解析,比如:

1
2
3
4
5
{
"code": 0,
"data": {},
"msg": "操作成功"
}

但有时却往往会产生一些bug,这时候就破坏了返回数据的一致性,导致调用者无法解析。所以我们常常会定义一个全局的异常拦截器。

1.1、@ControllerAdvice

@ControllerAdvice 是Spring 3.2提供的新注解,可以对Controller中使用到@RequestMapping注解的方法做逻辑处理。

@ControllerAdvice,很多初学者可能都没有听说过这个注解,实际上这是一个非常有用的注解。顾名思义,这是一个增强的 Controller,一般配合@ExceptionHandler使用来处理全局异常。注意不能自己try和catch异常,否则就不会被全局异常处理捕获到。

使用这个注解 ,可以实现三个方面的功能:

  • 全局异常处理
  • 全局数据绑定
  • 全局数据预处理

灵活使用这三个功能,可以帮助我们简化很多工作,需要注意的是,这是 SpringMVC 提供的功能,在 Spring Boot 中可以直接使用,这里只介绍全局异常处理,需要其他功能可以访问参考链接。

1.2、全局异常处理

Springboot对于全局异常的处理做了不错的支持,它提供了两个可用的注解。

@ControllerAdvice:用来开启全局的异常捕获

@ExceptionHandler:说明捕获哪些异常,对哪些异常进行处理。

使用 @ControllerAdvice结合@ExceptionHandler 实现全局异常处理,只需要定义类,添加该注解即可定义方式如下:

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
//可以使Spring自动把要返回的对象转化成json文本写入到响应体中,比如自定义的ResultBean
@ResponseBody
@ControllerAdvice
public class MyGlobalExceptionHandler
{
// 专门用来捕获和处理Controller层的异常
@ExceptionHandler(Exception.class)
public ModelAndView customException(Exception e)
{
ModelAndView mv = new ModelAndView();
mv.addObject("message", e.getMessage());
mv.setViewName("myerror");
return mv;
}

// 专门用来捕获和处理Controller层的空指针异常
@ExceptionHandler(NullPointerException.class)
public ModelAndView nullPointerExceptionHandler(NullPointerException e)
{
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("success",false);
mv.addObject("mesg","请求发生了空指针异常,请稍后再试");
return mv;
}
}

在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法…,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。

@ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。

二、为什么要做Controller层的异常统一处理以及统一结果返回

不知道你平时在写Controller层接口的时候,有没有注意过抛出异常该怎么处理,是否第一反应是想着用个try-catch来捕获异常?但是这样地处理只适合那种编译器主动提示的检查时异常,因为你不用try-catch就过不了编译检查,所以你能主动地抓获异常并进行处理。但是,如果存在运行时异常且你没有来得及想到去处理它的时候会发生什么呢?我们可以来先看看下面的这个没有处理运行时异常的例子:

1
2
3
4
5
6
7
@RestController
public class ExceptionRest {
@GetMapping("getNullPointerException")
public Map<String,Object> getNullPointerException(){
throw new NullPointerException("出现了空指针异常");
}
}

以上代码在基于maven的SpringMVC项目中,使用tomcat启动后,浏览器端发起如下请求:

http://localhost:8080/zxtest/getNullPointerException

访问后得到的结果是这样的,浏览器收到的报错信息:

 可以看到,我们在Controller接口层抛出了一个空指针异常,然后没有捕获,结果异常堆栈就会返回给前端浏览器,给用户造成了非常不好的体验。

除此之外,前端从报错信息中能看到后台系统使用的服务器及中间件类型、所采用的框架信息及类信息,甚至如果后端抛出的是SQL异常,那么还可以看到SQL异常的具体查询的参数信息,这是一个中危安全漏洞,是必须要修复的。

三、使用@ExceptionHandler和@ControllerAdvice做到统一处理

当出现这种运行时异常的时候,我们想到的最简单的方法也许就是给可能会抛出异常的代码加上异常处理,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class ExceptionRest {
private Logger log = LoggerFactory.getLogger(ExceptionRest.class);
@GetMapping("getNullPointerException")
public Map<String,Object> getNullPointerException(){
Map<String,Object> returnMap = new HashMap<String,Object>();
try{
throw new NullPointerException("出现了空指针异常");
}catch(NullPointerException e){
log.error("出现了空指针异常",e);
returnMap.put("success",false);
returnMap.put("mesg","请求发生异常,请稍后再试");
}
return returnMap;
}
}

因为我们手动地在抛出异常的地方加上了处理,并妥善地返回发生异常时该返回给前端的内容,因此,当我们再次在浏览器发起相同的请求时得到就是以下内容:

1
2
3
4
{
success: false,
mesg: "请求发生异常,请稍后再试"
}

貌似问题得到了解决,但是你能确保你可以在所有可能会发生异常的地方都正好捕获了异常并处理吗?你能确保团队的其他人也这么做?

很明显,你需要一个统一的异常捕获与处理方案。

Spring3.2以后,SpringMVC引入了ExceptionHandler的处理方法,使得对异常的处理变得更加简单和精确,你唯一需要做的就是新建一个Controller,然后再里面加上两个注解即可完成Controller层所有异常的捕获与处理。

3.1、@ExceptionHandler和@ControllerAdvice基本使用

新建一个Controller如下:

1
2
3
4
5
6
7
8
9
10
@ControllerAdvice
public class ExceptionConfigController {
@ExceptionHandler
public ModelAndView exceptionHandler(Exception e){
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("success",false);
mv.addObject("mesg","请求发生了异常,请稍后再试");
return mv;
}
}

我们在如上的代码中,类上加了@ControllerAdvice注解,表示它是一个增强版的controller,然后在里面创建了一个返回ModelAndView对象的exceptionHandler方法,其上加上@ExceptionHandler注解,表示这是一个异常处理方法,然后在方法里面写上具体的异常处理及返回参数逻辑即可,如此就完成了所有的工作,真的是太方便了。

我们在浏览器发起调用后就返回了如下的结果:

1
{success: false,mesg: "请求发生了异常,请稍后再试"}

3.2、@ExceptionHandler具体异常的处理

相比与HandlerExceptionResolver而言,使用@ExceptionHandler更能灵活地对不同的异常进行分别的处理。并且,当抛出的异常是指定异常的子类,那么照样能够被捕获和处理。

我们改变下controller层的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class ExceptionController {

@GetMapping("getNullPointerException")
public Map<String, Object> getNullPointerException() {
throw new NullPointerException("出现了空指针异常");
}

@GetMapping("getClassCastException")
public Map<String, Object> getClassCastException() {
throw new ClassCastException("出现了类型转换异常");
}

@GetMapping("getIOException")
public Map<String, Object> getIOException() throws IOException {
throw new IOException("出现了IO异常");
}
}

已知NullPointerExceptionClassCastException都继承RuntimeException,而RuntimeExceptionIOException都继承Exception

我们在ExceptionConfigController做这样的处理:

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
@ControllerAdvice
public class ExceptionConfigController
{
// 专门用来捕获和处理Controller层的空指针异常
@ExceptionHandler(NullPointerException.class)
public ModelAndView nullPointerExceptionHandler(NullPointerException e){
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("success",false);
mv.addObject("mesg","请求发生了空指针异常,请稍后再试");
return mv;
}

// 专门用来捕获和处理Controller层的运行时异常
@ExceptionHandler(RuntimeException.class)
public ModelAndView runtimeExceptionHandler(RuntimeException e){
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("success",false);
mv.addObject("mesg","请求发生了运行时异常,请稍后再试");
return mv;
}

// 专门用来捕获和处理Controller层的异常
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(Exception e){
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("success",false);
mv.addObject("mesg","请求发生了异常,请稍后再试");
return mv;
}
}
  • 当我们在Controller层抛出NullPointerException时,就会被nullPointerExceptionHandler进行处理,然后拦截。

    1
    {success: false,mesg: "请求发生了空指针异常,请稍后再试"}
  • 当我们在Controller层抛出ClassCastException时,就会被runtimeExceptionHandler进行处理,然后拦截。

    1
    {success: false,mesg: "请求发生了运行时异常,请稍后再试"}
  • 当我们在Controller层抛出IOException时,就会被exceptionHandler进行处理,然后拦截。

    1
    {success: false,mesg: "请求发生了异常,请稍后再试"}

SpringMVC为我们提供的Controller层异常处理真的是太方便了,尤其是@ExceptionHandler,推荐大家使用。

四、自定义异常处理类CustomUserException

4.1、自定义异常处理类

在程序中,可能会遇到JDK提供的任何标准异常类都无法充分描述清楚我们想要表达的问题,又或者捕捉数据库异常时候不知道具体异常名字,这种情况下可以创建自己的异常类,即自定义异常类,在需要的时候手动throw出,然后再全局异常统一处理。

自定义异常类只需从Exception类或者它的子类派生一个子类即可。自定义异常类如果继承Exception类,则为受检查异常,必须对其进行处理;如果不想处理,可以让自定义异常类继承运行时异常RuntimeException类。习惯上,自定义异常类应该包含2个构造器:一个是默认的构造器,另一个是带有详细信息的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 自定义异常的步骤:
* (1)继承 Exception 或 RuntimeException
* (2)定义构造方法
*/
public class CustomUserException extends RuntimeException {

private Integer code;

public CustomUserException(UserResponseEnum userResponseEnum){
super(userResponseEnum.getDescription());
this.code = userResponseEnum.getCode();
}

public Integer getCode() {
return code;
}
}

在构造自定义业务异常对象时使用了枚举的方式,将常见的业务错误提示语对应的错误代码进行映射,枚举类如下所示:

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
public enum UserResponseEnum {

USER_NOT_FOUND(50001,"用户不存在"),

USER_AUTHENTICATION_ERROR(50002,"用户密码不正确");

private Integer code;

private String description;

UserResponseEnum(Integer code, String description) {
this.code = code;
this.description = description;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

4.2、通过@ControllerAdvice@ExceptionHandler注解,实现统一异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@ControllerAdvice(basePackages = {"com.hs.controller"})
public class CustomExceptionAdvice
{

private static final Logger logger = LoggerFactory.getLogger(CustomExceptionAdvice.class);

/**
* 处理与用户相关的业务异常
* @return
*/
@ExceptionHandler(CustomUserException.class)
public BaseResult UserExceptionHandler(HttpServletRequest request,CustomUserException e){
logger.error("用户信息异常:Host:{} invoke URL:{},错误信息:{}",request.getRemoteHost(),request.getRequestURL(),e.getMessage());
return new BaseResult(e.getCode(),false,e.getMessage());
}
}

4.3、在业务代码中在需要抛出异常的地方抛出对应的异常即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 根据主键获取用户实体
* @param id
* @return
*/
public User selectById(String id)
{
User user = userMapper.selectByPrimaryId(id);
if (user == null) {
throw new CustomUserException(UserResponseEnum.USER_NOT_FOUND);
}
return user;
}
--------------------------------------
返回信息如下:
{
"code": 50001,
"data": false,
"message": "用户不存在"
}
前端即可根据返回信息对用户进行友好的提示。

参考链接:
SpringBoot异常捕获与封装处理,看完你学会了吗?​​​​​​
Java自定义异常详解
如何让业务代码写的好看(六):异常处理规范
全局异常捕获,异常流处理业务逻辑