异常的统一处理可以显著提升用户提升体验,也可以方便开发人员发现程序问题所在。
服务器端的异常如果不能返回给客户,会让客户端的使用感觉很糟糕,比如常见的 500 错误,客户端看到的是:
然后一头雾水,用户难免会想这系统这么烂?
如果我们把异常信息返回给前台,让前端进行处理,比如弹窗,用户了解到错误信息后,进而修改错误使用行为或者联系开发人员解决。
异常统一处理也不只有一种方法,本篇通过 Springboot 使用 @ControllerAdvice 和 @ExceptionHandler 两个注解来实现异常的统一处理。
1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
2. 创建异常接口和实现它的异常信息枚举类
通过接口可以很方便的获取枚举类中的信息。
BaseErrorInfoInterface 接口:
public interface BaseErrorInfoInterface {
/**
* 错误码
*/
String getResultCode();
/**
* 错误描述
*/
String getResultMsg();
}
异常信息枚举类 CommonEnum :
public enum CommonEnum implements BaseErrorInfoInterface {
// 数据操作错误定义
SUCCESS("200", "成功!"),
BODY_NOT_MATCH("400", "请求的数据格式不符!"),
SIGNATURE_NOT_MATCH("401", "请求的数字签名不匹配!"),
NOT_FOUND("404", "未找到该资源!"),
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
SERVER_BUSY("503", "服务器正忙,请稍后再试!");
/**
* 错误码
*/
private String resultCode;
/**
* 错误描述
*/
private String resultMsg;
CommonEnum(String resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
@Override
public String getResultCode() {
return resultCode;
}
@Override
public String getResultMsg() {
return resultMsg;
}
}
使用枚举类可以规范异常信息,防止不同的开发人员同一个异常,各有各的写法。
3. 创建返回结果统一处理包装类
无论是正常数据还是异常数据,统统用一个返回结果类包装起来,并通过 fastJSON 转为 json 对象。
创建返回结果类 BaseResult :
@Getter
@Setter
@AllArgsConstructor
public class BaseResult implements Serializable {
private static final long serialVersionUID = 1L;
public static final int RESULT_FAIL = 0;
public static final int RESULT_SUCCESS = 1;
//返回代码
private String code;
//返回消息
private String message;
//返回对象
private Object result;
/**
* 构造重载
* @param code
* @param message
*/
public BaseResult(String code, String message) {
this.code = code;
this.message = message;
}
/**
* 成功
*
* @return
*/
public static BaseResult success() {
return success(null);
}
/**
* 成功
*
* @param data
* @return
*/
public static BaseResult success(Object data) {
//枚举在这里就用上了
BaseResult br = new BaseResult(CommonEnum.SUCCESS.getResultCode(), CommonEnum.SUCCESS.getResultMsg(), data);
return br;
}
/**
* 失败
*
* @param errorInfoInterface
* @return
*/
public static BaseResult error(BaseErrorInfoInterface errorInfoInterface) {
BaseResult br = new BaseResult(errorInfoInterface.getResultCode(), errorInfoInterface.getResultMsg(), null);
return br;
}
/**
* 失败
*
* @param code
* @param message
* @return
*/
public static BaseResult error(String code, String message) {
BaseResult br = new BaseResult(code, message, null);
return br;
}
/**
* 失败
*
* @param message
* @return
*/
public static BaseResult error(String message) {
BaseResult br = new BaseResult("-1", message, null);
return br;
}
public String toString() {
return JSONObject.toJSONString(this);
}
}
4. 创建自定义业务异常类,继承 RuntimeException 类
BizException 类(biz=business):
@Getter
@Setter
public class BizException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
protected String errorCode;
/**
* 错误信息
*/
protected String errorMsg;
/**
* 无参构造
*/
public BizException() {
super();
}
/**
* BaseErrorInfoInterface 参数构造
* @param errorInfoInterface
*/
public BizException(BaseErrorInfoInterface errorInfoInterface) {
super(errorInfoInterface.getResultCode());
this.errorCode = errorInfoInterface.getResultCode();
this.errorMsg = errorInfoInterface.getResultMsg();
}
/**
* BaseErrorInfoInterface,Throwable 参数构造
* @param errorInfoInterface
* @param cause
*/
public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
super(errorInfoInterface.getResultCode(), cause);
this.errorCode = errorInfoInterface.getResultCode();
this.errorMsg = errorInfoInterface.getResultMsg();
}
/**
* 错误信息参数构造
* @param errorMsg
*/
public BizException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}
/**
* 错误信息、编码 参数构造
* @param errorCode
* @param errorMsg
*/
public BizException(String errorCode, String errorMsg) {
super(errorCode);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
/**
* 编码、信息、异常父类 参数构造
* @param errorCode
* @param errorMsg
* @param cause
*/
public BizException(String errorCode, String errorMsg, Throwable cause) {
super(errorCode, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
通过这个异常类,可以通过 throw new BizException() 来定制异常信息。
5. 创建统一异常处理器 GlobalExceptionHandler
前面四个步骤都是在为这一步做基础工作,没有前面的的步骤只有这一个类也可以完成统一异常处理,只是没那么规范。
重点是 @ControllerAdvice 注解
@ControllerAdvice //声明当前类为控制器增强类,除了统一异常处理可以用到,还可以用在全局数据绑定和数据初始化。
@ResponseBody //表示返回的对象,Spring会自动把该对象进行json转化,最后写入到Response中。
public class GlobalExceptionHandler {
Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理自定义的业务异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = BizException.class) // 匹配异常类,这里匹配了我们定制的异常类。
public BaseResult bizExceptionHandler(HttpServletRequest req, BizException e) {
logger.error("发生业务异常!原因是:{}", e.getErrorMsg());//用日志记录方便查阅
return BaseResult.error(e.getErrorCode(), e.getErrorMsg());
}
/**
* 处理空指针的异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = NullPointerException.class)
public BaseResult exceptionHandler(HttpServletRequest req, NullPointerException e) {
logger.error("发生空指针异常!原因是:", e);
return BaseResult.error(e.getMessage());
}
/**
* 处理其他异常
*
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
public BaseResult exceptionHandler(HttpServletRequest req, Exception e) {
logger.error("未知异常!原因是:", e);
return BaseResult.error(e.getMessage());
}
// 不嫌麻烦的话可以定义更多处理其他类异常的方法
}
做完上面的步骤就完成了统一异常的处理,接下来写个简单的接口做测试。
6. 写点简单的业务代码做测试
实体类 Dog:
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Dog {
private String name;
private int age;
private String id;
}
service 层 DogService:
@Service
public class DogService {
/**
* 测试自定义异常
*/
public void exceptionTest(){
throw new BizException(CommonEnum.INTERNAL_SERVER_ERROR);
}
/**
* 正常业务
* @param id
* @return
*/
public Dog findDog(String id){
if(id.equals("1001"))
return new Dog("kaka",2,id);
return null;
}
}
controller 层 DogController:
@RestController
public class DogController {
@Autowired
private DogService dogService;
/**
* 找狗
* @param id
* @return
*/
@GetMapping("/dog")
public String findDog(@RequestParam String id){
Dog dog = dogService.findDog(id);
if(dog == null)
return BaseResult.error("未找到这只狗").toString();
return BaseResult.success(dog).toString();//对返回结果做统一处理,有些情况下我们还需要在 success 方法中传入 HttpServletRequest 来用日志记录请求中的一些信息,有需要了再在 BaseResult 中创建对应的方法就行了。
}
/**
* 模拟业务类中抛出异常
* @return
*/
@PostMapping("/exception")
public String exception(){
dogService.exceptionTest();
return BaseResult.success().toString();// 由于考虑到 BaseResult 类在别处会用到,所以没有直接把类方法的返回值定为 JSON 如果嫌弃 .toString 这种写法(toString 已被重写为返回JSON),可以写个另外的类来继承这个类并重写里面的方法。
}
}
测试:
启动项目:
按照业务逻辑 当参数 id 不是 1001 时,controller 返回“未找到狗”的JSON 信息,否者返回正确狗的信息:
然后测试异常服务:
此时前台就可以通过解析 json 中的数据想办法提示给客户了。
通过异常统一处理和返回数据格式化,已经可以把服务器处理请求的结果清楚的返回给客户端了。