在进入正题之前,先给大家看一段代码,如下所示。
package com.panda.handle_try_catch_gracefully.controller;
import com.panda.handle_try_catch_gracefully.common.Result;
import com.panda.handle_try_catch_gracefully.domain.po.User;
import com.panda.handle_try_catch_gracefully.domain.vo.UserVO;
import com.panda.handle_try_catch_gracefully.enums.ExceptionEnum;
import com.panda.handle_try_catch_gracefully.exceptions.BusinessException;
import com.panda.handle_try_catch_gracefully.service.IUserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("ori/user")
public class OriginUserController {
@Resource
private IUserService userService;
@GetMapping("getUserInfo")
public Result getUserInfo(String userId) {
try {
return userService.getUserInfo(userId);
} catch (Exception e) {
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("listUserInfo")
public Result> listUserInfo(UserVO query) {
try {
return userService.listUserInfo(query);
} catch (Exception e) {
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("saveUser")
public Result saveUser(User user) {
try {
return userService.saveUser(user);
} catch (Exception e) {
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("updateUser")
public Result updateUser(User user) {
try {
return userService.updateUser(user);
} catch (Exception e) {
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("deleteUser")
public Result deleteUser(@RequestParam("userId") String userId) {
try {
return userService.deleteUser(userId);
} catch (Exception e) {
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
}
}
不知道大家在项目中是否遇到过上面这种情况——controller里满屏的try-catch代码块,真的是闪瞎了我的钛合金狗眼呀。
虽然丑陋,但是这些try-catch代码块还是起到了一定的作用的——给前端响应一些通俗易懂的提示信息,如“用户编码不能为空”、“保存用户信息异常”、“更新用户信息失败”等。
如果没有这些丑陋的try-catch代码块,一旦抛出异常,用户看到的可能就是类似
Exception in thread "main" java.lang.ArithmeticException: / by zero
Exception in thread "main" java.lang.NullPointerException
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 2 out of bounds for length 0
之类的错误信息,对用户来说,无异于天书,极大地降低了用户体验度。
try-catch:虽然我长得丑,但是我有用呀!
这当然是我们不能容忍的!
作为一个有着强迫症的程序员,怎么能容忍这样的代码出现在我的项目中呢!
外行看热闹,可能看不出上面这段代码有什么丑陋的,但是,内行看门道,一眼就可以看出上面代码的丑陋之处。说一千道一万,上面这段代码究竟丑陋在哪里呢?
上面也提到了,try-catch代码块满屏飞,且处理逻辑一致,都是捕获异常,然后抛出异常或者返回错误信息,但这并不符合代码的可重用原则。
上面的代码只是一个controller,一个项目中可能有几十上百,甚至更多个controller呢!因此,对这块代码的主要优化思路,就是找出一种方法代替多次出现的try-catch代码块,并且原有的功能不能缺失。
这种方法就是本文要讲的内容——全局异常处理!
所谓全局异常处理,也叫统一异常处理,是一种统一处理异常的思路。
这种方法的好处在于,只需要在一个地方处理异常逻辑,就可以将controller的异常给捕获掉,而不用我们在每个controller类中写重复且丑陋的try-catch代码块,来捕获异常。
Spring Boot 的全局异常处理有两个很重要的注解,一个是ControllerAdvice注解或者RestControllerAdvice注解,另一个是ExceptionHandler注解。
ControllerAdvice注解或者RestControllerAdvice注解在类上使用,表示开启全局异常的捕获。
ExceptionHandler注解在方法上使用,可以通过value属性指定一个或多个异常,并捕获指定的异常,一般在方法体内对捕获到的异常进行解析,然后进行输出或返回等操作。
ControllerAdvice注解是Spring 3.2中新增的一个注解,是Controller的增强器,它的作用是给Controller(控制器)添加统一的操作或处理。
ControllerAdvice注解最常见的使用场景就是,结合ExceptionHandler注解用于全局异常处理。
ControllerAdvice注解是在类上声明的注解,用法主要有以下三点:
全局异常处理
结合ExceptionHandler注解,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的。
全局数据预处理
结合InitBinder注解,用于对请求参数预处理,将表单中的参数绑定到实体上,或者对日期、金额类参数进行格式转换等。
全局数据绑定
结合ModelAttribute注解,将方法参数或方法返回值绑定到命名模型属性,该属性向web视图公开。
basePackages
该属性可以指定一个或多个包路径,这些包及其子包下的所有Controller都被ControllerAdvice注解管理。
@RestControllerAdvice(basePackages={"com.panda","com.cat"})
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
return "error";
}
}
basePackageClasses
该属性的作用和 basePackages 差不多。
该属性可以指定一个或多个Controller类,这些类所属的包及其子包下的所有 Controller 都被该ControllerAdvice注解管理。
@RestControllerAdvice(basePackageClasses={UserController.class, OrderController.class})
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
return "error";
}
}
assignableTypes
该属性指定一个或多个 Controller 类,这些类被该ControllerAdvice注解管理。
@RestControllerAdvice(assignableTypes={UserController.class, OrderController.class})
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
return "error";
}
}
annotations
该属性指定一个或多个注解,被这些注解所标记的Controller会被该ControllerAdvice注解管理。
@RestControllerAdvice(annotations = {UserAnnotation.class, OrderAnnotation.class})
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
return "error";
}
}
RestControllerAdvice注解是一个组合注解,由ControllerAdvice注解和ResponseBody注解组成。可见其作用和ControllerAdvice注解差不多。
RestControllerAdvice注解是Spring 4.3中新增的一个注解,也是Controller的增强器,它的作用同样是给Controller(控制器)添加统一的操作或处理。
RestControllerAdvice注解和ControllerAdvice注解的区别
1、当我们自定义的全局异常处理类加上ControllerAdvice注解时,如果异常处理方法需要返回json数据,则需要给每个异常处理方法添加ResponseBody注解。
2、当我们自定义的全局异常处理类加上RestControllerAdvice注解时,异常处理方法自动返回JSON格式的数据,我们不需要在该方法上再添加ResponseBody注解。
该注解的属性和ControllerAdvice注解的属性相同,不再赘述。
ExceptionHandler注解用来统一处理方法抛出的异常。
value
指定需要处理的一个或者多个异常类。
如果为空,则默认处理异常处理方法参数列表中列出的所有异常。
@ExceptionHandler(value = BusinessException.class)
public Result businessExceptionHandler(BusinessException businessException) {
log.error(businessException.getErrorMsg(), businessException);
return Result.fail(businessException.getCode(), businessException.getErrorMsg());
}
@ExceptionHandler
public Result exceptionHandler1(Exception exception) {
log.error(exception.getMessage());
ExceptionEnum exceptionEnum = ExceptionEnum.UNKNOWN;
if (exception instanceof BusinessException) {
exceptionEnum = ExceptionEnum.INTERNAL_SERVER_ERROR;
}
if (exception instanceof IndexOutOfBoundsException) {
exceptionEnum = ExceptionEnum.ILLEGAL_ARGUMENT_ERROR;
}
return Result.fail(exceptionEnum);
}
搭建Spring Boot项目的过程就不在赘述了,项目结构如下所示。
mykits
com.panda
0.0.1-SNAPSHOT
4.0.0
handle-try-catch-gracefully
11
11
com.google.guava
guava
org.apache.commons
commons-lang3
commons-io
commons-io
commons-collections
commons-collections
commons-codec
commons-codec
controller类,和上面的OriginUserController相比,代码量少了很多——try-catch代码块消失了。
放眼望去,整体代码清爽不少,核心代码尽收眼底。
package com.panda.handle_try_catch_gracefully.controller;
import com.panda.handle_try_catch_gracefully.common.Result;
import com.panda.handle_try_catch_gracefully.domain.po.User;
import com.panda.handle_try_catch_gracefully.domain.vo.UserVO;
import com.panda.handle_try_catch_gracefully.service.IUserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("user")
public class UserController {
@Resource
private IUserService userService;
@GetMapping("getUserInfo")
public Result getUserInfo(String userId) {
return userService.getUserInfo(userId);
}
@PostMapping("listUserInfo")
public Result> listUserInfo(UserVO query) {
return userService.listUserInfo(query);
}
@PostMapping("saveUser")
public Result saveUser(User user) {
return userService.saveUser(user);
}
@PostMapping("updateUser")
public Result updateUser(User user) {
return userService.updateUser(user);
}
@PostMapping("deleteUser")
public Result deleteUser(@RequestParam("userId") String userId) {
return userService.deleteUser(userId);
}
}
封装的统一返回结果。
code:响应结果代码,在本文中,可以是自定义的代码,也可以是ExceptionEnum枚举的code。
msg:错误信息。主要在方法调用失败时,返回给前端的提示信息。当然在方法调用成功时,也可以返回给前端一个类似“请求成功”之类的信息。
successFlag:响应成功与否的标志。true表示成功,false表示失败。
data:返回给前端的数据。一般只有在响应成功时才会向前端返回数据,以查询类方法居多。
package com.panda.handle_try_catch_gracefully.common;
import com.panda.handle_try_catch_gracefully.enums.ExceptionEnum;
public class Result {
private String code;
private String msg;
private Boolean successFlag;
private T data;
public static Result success() {
Result result = new Result<>();
result.successFlag(true);
return result;
}
public static Result success(T data) {
Result result = new Result<>();
result.successFlag(true).data(data);
return result;
}
public static Result fail(String code, String errorMsg) {
Result result = new Result<>();
result.successFlag(false).code(code).msg(errorMsg);
return result;
}
public static Result fail(ExceptionEnum exceptionEnum) {
Result result = new Result<>();
result.successFlag(false).code(exceptionEnum.getCode()).msg(exceptionEnum.getErrorMsg());
return result;
}
public Result code(String code) {
this.code = code;
return this;
}
public Result msg(String msg) {
this.msg = msg;
return this;
}
public Result successFlag(Boolean successFlag) {
this.successFlag = successFlag;
return this;
}
public Result data(T data) {
this.data = data;
return this;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
public Boolean getSuccessFlag() {
return successFlag;
}
}
用户信息实体类。
在实际项目中,User的各个属性一般对应用户表的各个字段。
package com.panda.handle_try_catch_gracefully.domain.po;
import lombok.Data;
import java.util.Date;
@Data
public class User {
private String id;
private String name;
private String mobilePhone;
private Date createTime;
private String createBy;
private Date updateTime;
private String updateBy;
private Integer validFlag;
private Integer deleteFlag;
}
用户信息。
返回给前端的实体类。
在本文中,为了省事,也作为查询条件。在实际项目中,应该和查询条件区分开,用类似UserQuery之类的实体表示用户查询条件。
package com.panda.handle_try_catch_gracefully.domain.vo;
import com.panda.handle_try_catch_gracefully.domain.po.User;
import lombok.Data;
@Data
public class UserVO extends User {
}
异常枚举类。
code:异常代码。
errorMsg:异常提示信息。
package com.panda.handle_try_catch_gracefully.enums;
public enum ExceptionEnum {
/**
* 请求错误!
*/
BAD_REQUEST("400", "请求错误!"),
/**
* 未经授权的请求!
*/
UNAUTHORIZED("401", "未经授权的请求!"),
/**
* 没有访问权限!
*/
FORBIDDEN("403", "没有访问权限!"),
/**
* 请求的资源未不到!
*/
NOT_FOUND("404", "请求的资源未不到!"),
/**
* 服务器内部错误!
*/
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
/**
* 服务器正忙,请稍后再试!
*/
BAD_GATEWAY("502", "服务器正忙,请稍后再试!"),
/**
* 服务器正忙,请稍后再试!
*/
SERVICE_UNAVAILABLE("503", "服务器正忙,请稍后再试!"),
/**
* 网关超时!
*/
GATEWAY_TIMEOUT("504", "网关超时!"),
/**
* 非法参数异常!
*/
ILLEGAL_ARGUMENT_ERROR("10000", "非法参数异常!"),
/**
* 用户ID不能为空!
*/
USER_ID_NOT_BLANK("10001", "用户ID不能为空!"),
/**
*
*/
UNKNOWN("9999", "未知异常!");
/**
* 错误码
*/
private final String code;
/**
* 错误描述
*/
private final String errorMsg;
ExceptionEnum(String code, String errorMsg) {
this.code = code;
this.errorMsg = errorMsg;
}
public String getCode() {
return code;
}
public String getErrorMsg() {
return errorMsg;
}
}
在实际项目中,推荐使用自定义的业务异常类,而不用RuntimeException。
如果代码中抛出了RuntimeException,IDEA会提示上图之类的信息(可能要安装插件)。
package com.panda.handle_try_catch_gracefully.exceptions;
import com.panda.handle_try_catch_gracefully.enums.ExceptionEnum;
public class BusinessException extends RuntimeException {
/**
* 异常枚举
*/
private ExceptionEnum exceptionEnum;
/**
* 错误码
*/
private final String code;
/**
* 错误信息
*/
private final String errorMsg;
public BusinessException(ExceptionEnum exceptionEnum) {
super(String.format("code = %s, errorMsg = %s", exceptionEnum.getCode(), exceptionEnum.getErrorMsg()));
this.exceptionEnum = exceptionEnum;
this.code = exceptionEnum.getCode();
this.errorMsg = exceptionEnum.getErrorMsg();
}
public BusinessException(String code, String errorMsg) {
super(String.format("code = %s, errorMsg = %s", code, errorMsg));
this.code = code;
this.errorMsg = errorMsg;
}
public BusinessException(String code, String errorMsg, Object... args) {
super("code = " + code + ", errorMsg = " + String.format(errorMsg, args));
this.code = code;
this.errorMsg = String.format(errorMsg, args);
}
public ExceptionEnum getExceptionEnum() {
return exceptionEnum;
}
public String getErrorMsg() {
return errorMsg;
}
public String getCode() {
return code;
}
}
统一异常处理类。
注意@RestControllerAdvice注解。
在本类中,我们创建三个方法,分别处理不同场景的异常。
businessExceptionHandler方法处理抛出BusinessException异常的场景,即一旦抛出BusinessException异常,就会被businessExceptionHandler方法处理。
illegalArgumentExceptionHandler方法处理抛出IllegalArgumentException异常的场景,即一旦抛出IllegalArgumentException异常,就会被illegalArgumentExceptionHandler方法处理。
exceptionHandler方法是兜底的,捕获抛出Exception异常的场景,即一旦抛出Exception异常,就会被exceptionHandler方法处理。
当然,你可以根据实际项目需要,创建更多的异常处理方法,如空指针异常处理方法、数组越界异常处理方法、类找不到异常处理方法等。
package com.panda.handle_try_catch_gracefully.handler;
import com.panda.handle_try_catch_gracefully.common.Result;
import com.panda.handle_try_catch_gracefully.enums.ExceptionEnum;
import com.panda.handle_try_catch_gracefully.exceptions.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class UnifiedExceptionHandler {
/**
* 业务异常处理
*
* @param businessException 业务异常信息
*/
@ExceptionHandler(value = BusinessException.class)
public Result businessExceptionHandler(BusinessException businessException) {
log.error(businessException.getErrorMsg(), businessException);
return Result.fail(businessException.getCode(), businessException.getErrorMsg());
}
/**
* 未知异常处理
*
* @param exception 异常信息
*/
@ExceptionHandler(value = Exception.class)
public Result exceptionHandler(Exception exception) {
log.error(exception.getMessage(), exception);
return Result.fail(ExceptionEnum.UNKNOWN.getCode(), ExceptionEnum.UNKNOWN.getErrorMsg());
}
/**
* 参数异常处理
*
* @param exception 异常信息
*/
@ExceptionHandler(value = IllegalArgumentException.class)
public Result illegalArgumentExceptionHandler(IllegalArgumentException exception) {
log.error(exception.getMessage(), exception);
return Result.fail(ExceptionEnum.ILLEGAL_ARGUMENT_ERROR.getCode(), ExceptionEnum.ILLEGAL_ARGUMENT_ERROR.getErrorMsg());
}
}
用户接口类。
在本例中定义了5个方法:
getUserInfo:根据用户ID查询用户信息。
listUserInfo:根据查询条件查询符合条件用户列表。
saveUser:保存用户信息。
updateUser:更新用户信息。
deleteUser:删除用户信息。
package com.panda.handle_try_catch_gracefully.service;
import com.panda.handle_try_catch_gracefully.common.Result;
import com.panda.handle_try_catch_gracefully.domain.po.User;
import com.panda.handle_try_catch_gracefully.domain.vo.UserVO;
import java.util.List;
public interface IUserService {
Result getUserInfo(String userId);
Result> listUserInfo(UserVO query);
Result saveUser(User user);
Result updateUser(User user);
Result deleteUser(String userId);
}
用户接口实现类。
由于本文的重点不是实现业务逻辑,因此各个实现方法并没有详细的业务逻辑,而是抛出不同的异常,验证我们上面给出统一异常处理类是否可以真的处理各种异常。
package com.panda.handle_try_catch_gracefully.service.impl;
import com.panda.handle_try_catch_gracefully.common.Result;
import com.panda.handle_try_catch_gracefully.domain.po.User;
import com.panda.handle_try_catch_gracefully.domain.vo.UserVO;
import com.panda.handle_try_catch_gracefully.service.IUserService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements IUserService {
@Override
public Result getUserInfo(String userId) {
// 默认抛出异常
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
@Override
public Result> listUserInfo(UserVO query) {
// 默认抛出异常
throw new IllegalArgumentException();
}
@Override
public Result saveUser(User user) {
// 默认抛出异常
throw new IndexOutOfBoundsException();
}
@Override
public Result updateUser(User user) {
// 默认抛出异常
throw new RuntimeException();
}
@Override
public Result deleteUser(String userId) {
// 默认抛出异常
throw new ClassCastException();
}
}
测试方法
http://localhost:8080/user/getUserInfo
测试结果
{
"code": "500",
"msg": "服务器内部错误!",
"successFlag": false,
"data": null
}
从上面的代码可知,getUserInfo方法抛出的异常如下:
throw new BusinessException(ExceptionEnum.INTERNAL_SERVER_ERROR);
恰好可以被UnifiedExceptionHandler的businessExceptionHandler方法监听。
测试结论
测试成功。
统一异常处理类可以正常捕获BusinessException异常。
测试方法
http://localhost:8080/user/listUserInfo
测试结果
{
"code": "10000",
"msg": "非法参数异常!",
"successFlag": false,
"data": null
}
从上面的代码可知,listUserInfo方法抛出的异常如下:
throw new IllegalArgumentException();
恰好可以被UnifiedExceptionHandler的illegalArgumentExceptionHandler方法监听。
测试结论
测试成功。
统一异常处理类可以正常捕获IllegalArgumentException异常。
测试方法
http://localhost:8080/user/saveUser
测试结果
{
"code": "500",
"msg": "服务器内部错误!",
"successFlag": false,
"data": null
}
从上面的代码可知,saveUser方法抛出的异常如下:
throw new IndexOutOfBoundsException();
虽然UnifiedExceptionHandler没有专门处理IndexOutOfBoundsException异常的方法,但是有一个兜底的exceptionHandler方法,该方法可以监听Exception及其子类的异常类型。
而IndexOutOfBoundsException异常恰好是Exception的子类,因此可以被正常监听到。
测试结论
测试成功。
统一异常处理类可以正常捕获IndexOutOfBoundsException异常。
测试方法
http://localhost:8080/user/updateUser
测试结果
{
"code": "9999",
"msg": "未知异常!",
"successFlag": false,
"data": null
}
从上面的代码可知,updateUser方法抛出的异常如下:
throw new RuntimeException();
同上。
虽然UnifiedExceptionHandler没有专门处理RuntimeException异常的方法,但是有一个兜底的exceptionHandler方法,该方法可以监听Exception及其子类的异常类型。
而RuntimeException异常恰好是Exception的子类,因此可以被正常监听到。
测试结论
测试成功。
统一异常处理类可以正常捕获RuntimeException异常。
测试方法
http://localhost:8080/user/deleteUser
测试结果
{
"code": "9999",
"msg": "未知异常!",
"successFlag": false,
"data": null
}
从上面的代码可知,deleteUser方法抛出的异常如下:
throw new ClassCastException();
同上。
虽然UnifiedExceptionHandler没有专门处理ClassCastException异常的方法,但是有一个兜底的exceptionHandler方法,该方法可以监听Exception及其子类的异常类型。
而ClassCastException异常恰好是Exception的子类,因此可以被正常监听到。
测试结论
测试成功。
统一异常处理类可以正常捕获ClassCastException异常。