本文首发于稀土掘金:全局异常 @ControllerAdvice 该怎么写,该账号即为本人账号,非搬运。
问题由来
很多小伙伴刚进公司做项目的时候,会看到项目里面有一个@ControllerAdvice标记的类,整个类的编码结构大概是这样子:
@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {@ExceptionHandler(MyException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public MyExceptionResponse myException(MyException e) {//记录日志log.error("自定义的异常:" + e.getMessage());//其他处理逻辑。。。。//返回结果MyExceptionResponse response = new MyExceptionResponse(1001, "自定义的异常:" + e.getMessage());return response;}@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public MyExceptionResponse exception(Exception e) {//记录日志log.error("exception异常:" + e.getMessage());//其他处理逻辑。。。。//返回结果MyExceptionResponse response = new MyExceptionResponse(1002, "exception异常:" + e.getMessage());return response;}
}
其实你随便在网上搜一下@ControllerAdvice或者@ExceptionHandler,就知道这个是统一的全局异常处理,不过你有没有想过,我们为什么需要统一的全局异常处理,以及他的编码结构为啥发展成了上面的样子。
为什么要处理异常
为什么需要处理异常,因为异常是不可避免的,程序总是可能发生异常,所以我们需要处理异常,如果你就是不编写处理异常的代码,程序里面发生的异常最终会抛给spring,而spring其实是提供了异常处理的,也就是你不处理异常,异常就由spring来处理,那么spring处理异常的结果就是,你会在浏览器看到如下界面:
如果你点开F12去看,会看到返回的响应是这样的:
其中Content-Type是text/html,说明spring确实给你返回了一个html界面。
如果你是使用postman、idea的http client这种测试工具,会得到类似这样的结果:
这两种返回结果都是存在的,spring会根据客户端类型来判断是返回html界面还是返回一个json对象。
然而,这两种结果给到前端都是不合适的,一方面是用户体验不好,用户看不懂。另一方面即使用户给你反馈这样的结果,作为程序员的你也看不出来哪里出错了。这里说明一下,系统抛出的异常(包括RuntimeException甚至Exception),其实是带有具体信息的,只是这些异常抛给spring后,spring没有返回具体内容,而是返回上面这些东西,也就是spring把具体信息给隐藏了,只返回了一些静态的默认内容。
因为spring默认返回的结果不够好,所以我们要自定义处理异常的逻辑, 而代码的调用顺序是@Controller->@Service->@Mapper,如果想要捕获所有的异常,就得在最外层进行捕获,也就是在@Controller进行捕获,于是你会写出类似这样的代码:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/{id}")public ResponseEntity queryById(@PathVariable("id") Long id) {try {//执行业务逻辑User user = userService.queryById(id);//返回结果给前端HttpStatus httpStatus = HttpStatus.OK;ResponseEntity<User> responseEntity = new ResponseEntity<>(user, httpStatus);return responseEntity;} catch (Exception e) {String msg = "查询用户信息异常,id:" + id;//记录日志log.error(msg);//返回异常信息给前端HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;ResponseEntity<String> responseEntity = new ResponseEntity<>(msg, httpStatus);return responseEntity;}}@PostMapping("/add")public ResponseEntity addUser(@RequestBody User user) {try {//执行业务逻辑User user = userService.addUser(user);//返回结果给前端HttpStatus httpStatus = HttpStatus.OK;ResponseEntity<User> responseEntity = new ResponseEntity<>(user, httpStatus);return responseEntity;} catch (Exception e) {String msg = "新增用户信息异常,用户信息:" + JSON.toJSONString(user);//记录日志log.error(msg);//返回异常信息给前端HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;ResponseEntity<String> responseEntity = new ResponseEntity<>(msg, httpStatus);return responseEntity;}}
}
仔细琢磨一下你会发现这种写法非常难受,首先你需要在每一个@Controller类的每一个方法里面都写上try{}catch(){}块,这种重复性就不是我们能接受的。其次执行正常和异常的返回数据结构是不一样的,执行正常的时候一般返回当前业务对应的JavaBean,比如上面例子的User,执行异常的时候不可能返回User,取而代之的是要想办法把异常信息返回给前端,这就导致方法的返回值得定义成ResponseEntity,然后你每次都要去装填ResponseEntity对象。
为了避免如此麻烦的写法,自spring3.2开始,给我们提供了@ControllerAdvice注解来进行统一的异常处理,他的执行效果是这样的:
如果你使用@ControllerAdvice标记了一个类,那么在程序里发生的所有异常,spring都会将他传给@ControllerAdvice标记的那个类里面,让你自己处理异常。该注解基本用法如下:
@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public String runtimeException(RuntimeException e) {//记录日志log.error("runtime异常:" + e.getMessage());//返回结果return "runtime异常:" + e.getMessage();}@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public String exception(Exception e) {//记录日志log.error("exception异常:" + e.getMessage());//返回结果return "exception异常:" + e.getMessage();}
}
说明:
- 首先你不需要在每一个@Controller的每一个@RequestMapping方法里都写try{}catch(){}代码块了,spring会将发生的异常传递到@ControllerAdvice类里面的@ExceptionHandler方法,具体传递到哪个方法,是由异常的类型来进行划分的,也就是同一类型的异常执行相同的处理逻辑,像上面的例子就是将Exception下面的RutimeException做单独处理,其余的都归到Exception来处理。
- 其次你的@Controller里的@RequestMapping方法的返回值类型不再需要写ResponseEntity了,可以写具体业务对应的JavaBean,因为发生异常时的返回值类型由@ExceptionHandler所在方法的返回值定义了。
- @ControllerAdvice的用法和@Controller有点像,比如都可以在类上面添加@ResponseBody,让类里面的方法可以返回字符串或直接返回JavaBean。
- 最后说下@ResponseStatus,这个注解是用来设置http响应码的,这里我设置的“HttpStatus.INTERNAL_SERVER_ERROR”会返回500给前端,如果你不设置的话,会返回默认值200。
以上是spring提供的统一异常处理@ControllerAdvice的基本用法,不过他和文章开头的代码还是有些区别,我们来看看这段代码还有什么可以优化一下。
自定义业务异常类
就异常的来源来说,除了系统抛出的异常,其实我们自己也可以抛出异常,比如在业务执行过程中发现不对的时候,会通过抛异常的方式让程序终止:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {User user = userService.queryById(id);if(user == null){throw new RuntimeException("该用户不存在");}//可能还有其他的业务逻辑。。。。//最终返回结果return user;
}
其实这样子抛出的异常,也可以被上面“基本用法”当中的@ControllerAdvice接收和处理,但是这种异常情况,其实是在我们的预期范围内的,是在我们的预期下主动抛出的,是一种“业务异常”,他和系统抛出的RuntimeException不一样,像空指针、数组越界之类的(他们都是RuntimeException的子类)他们表示你代码写的有问题,代码不够完善,而我们自己抛出的异常,是一种在预期范围内的业务逻辑上的错误,我们希望把他们给区分开来,于是会为这种业务异常单独定义一个类:
public class MyException extends RuntimeException {private String errorMsg;public MyException() {}//省略getter、setter等方法
}
然后在抛异常的时候抛出自定义类的异常:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {User user = userService.queryById(id);if(user == null){throw new MyException("该用户不存在");}//可能还有其他的业务逻辑。。。。//最终返回结果return user;
}
于是@ControllerAdvice里面的写法会变成这样:
@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {//主要改动点在这里,把RuntimeException.class改成了MyException.class@ExceptionHandler(MyException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public String myException(MyException e) {//记录日志log.error("自定义的异常:" + e.getMessage());//返回结果return "自定义的异常:" + e.getMessage();}@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public String exception(Exception e) {//记录日志log.error("exception异常:" + e.getMessage());//返回结果return "exception异常:" + e.getMessage();}
}
其实就是把原来的RuntimeException改成了自定义的异常,另外仍然由Exception处理自定义异常之外的所有异常。
统一返回给前端的数据结构
接下来的一个优化就是,统一返回给前端的数据结构,上面代码当中返回给前端的都是一个字符串,用来描述异常信息,但其实更好的做法是像@Controller里的接口那样,返回一个JavaBean,甚至是所有类型的异常都返回一个格式统一的JavaBean,于是我们会定义一个用于表示返回值数据结构的类:
public class MyExceptionResponse implements Serializable {//其实这个code不是必须的,//不过下面的errorMsg就比较有必要,你总得有一个字符串来描述你的异常信息吧private int code;private String errorMsg;public MyExceptionResponse() {}//其他的什么构造方法、getter、setter方法就不写了,大家能看明白就行
}
最终得到的@ControllerAdvice就变成下面这样:
@Slf4j
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {@ExceptionHandler(MyException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public MyExceptionResponse runtimeException(MyException e) {//记录日志log.error("自定义的异常:" + e.getMessage());//其他处理逻辑。。。。//返回结果MyExceptionResponse response = new MyExceptionResponse(1001, "自定义的异常:" + e.getMessage());return response;}@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public MyExceptionResponse exception(Exception e) {//记录日志log.error("exception异常:" + e.getMessage());//其他处理逻辑。。。。//返回结果MyExceptionResponse response = new MyExceptionResponse(1002, "exception异常:" + e.getMessage());return response;}
}
其他补充
注意一下,@ControllerAdvice是对@Controller进行功能增强,搭配@ExceptionHandler进行全局异常处理只是他其中的一个用法(可以认为异常处理也是一种功能增强),他还可以搭配@InitBinder或者@ModelAttribute实现其他功能,基本都是对@Controller进行增强。