程序出现异常会将堆栈信息也打印出来,但是我们在面对一些业务场景的时候需要自定义一些异常返回,并且暴露给用户的只能是业务提示,例如账号不能重复等,而不能将sql执行异常或者运行时错误的详细信息返回出去。
一. 实现一个简单的全局异常处理
新建一个GlobalExceptionHandler类,使用@RestControllerAdvice注解标记在类名上即可成功实现一个异常处理类,随后在内容中定义@ExceptionHandler(Exception.class)来捕获实际产生的异常,例如:
@RestControllerAdvice
public class GlobalExceptionHandler{
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public JSONObject handleException(Exception e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", g.getMessage());
return jsonObject;
}
}
当然,实际环境下我们不会使用JSONObject来作为异常返回,同时我们进行全局异常处理一般都是对一些特定的异常,或者是自定义异常。以下我们参考ruoyi的统一返回类AjaxResult进行改造,这里说明以下AjaxResult继承HashMap是希望使用Map的键值对返回属性,后续我们可以自定义类属性来替代HashMap:
/**
* 操作消息提醒
*
* @author ruoyi
*/
public class AjaxResult extends HashMap<String, Object>{
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data))
{
super.put(DATA_TAG, data);
}
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static AjaxResult error(String msg)
{
return AjaxResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static AjaxResult error(String msg, Object data)
{
return new AjaxResult(500, msg, data);
}
}
@RestControllerAdvice
public class GlobalExceptionHandler{
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e, HttpServletRequest request)
{
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return AjaxResult.error(e.getMessage());
}
/**
* 自定义验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
{
log.error(e.getMessage(), e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return AjaxResult.error(message);
}
}
至此,我们已经实现一个简单的全局异常处理,如上所示,总结以下几点:
- 使用@RestControllerAdvice标记类名
- 使用@ExceptionHandler(XxxException.class)标记方法名,表示拦截处理XxxException异常
- 方法体内具体描述拦截处理
二. 进阶改造自定义异常
通常,我们除了描述jdk默认的异常之外,还会自定义契合业务主体本身的业务异常。
自定义运行时异常比较简单,我们只需要继承RuntimeException即可,例如:
public class MyException extends RuntimeException{
public MyException(){
}
}
我们自定义了一个MyException异常,结合第一部分的内容,我们需要捕获该异常,即:
/**
* 自定义异常
*/
@ExceptionHandler(DemoModeException.class)
public AjaxResult handleMyException(DemoModeException e){
return AjaxResult.error("自定义异常被捕获");
}
实际场景中,我们为了方便管理异常情况,通常会定义一个异常结果枚举类,然后自定义异常只需要传入对应枚举类参数即可。
接下来提供一套完整的实战环境下的异常处理以供参考,首先定义一套参考的枚举类(参考自GitHub仓库-miaosha:https://github.com/qiurunze123/miaosha)
定义异常枚举类
public enum ResultStatus {
SUCCESS(0, "成功"),
FAILD(-1, "失败"),
EXCEPTION(-1, "系统异常"),
PARAM_ERROR(10000, "参数错误"),
SYSTEM_ERROR(10001, "系统错误"),
FILE_NOT_EXIST(10002, "文件不存在"),
FILE_NOT_DOWNLOAD(10003, "文件没有下载"),
FILE_NOT_GENERATE(10004, "文件没有生成"),
FILE_NOT_STORAGE(10005, "文件没有入库"),
SYSTEM_DB_ERROR(10006, "数据库系统错误"),
FILE_ALREADY_DOWNLOAD(10007, "文件已经下载"),
DATA_ALREADY_PEXISTS(10008, "数据已经存在"),
/**
* 注册登录
*/
RESIGETR_SUCCESS(20000, "注册成功!"),
RESIGETER_FAIL(200001, "注册失败!"),
CODE_FAIL(200002, "验证码不一致!"),
/**
* check
*/
BIND_ERROR(30001, "参数校验异常:%s"),
ACCESS_LIMIT_REACHED(30002, "请求非法!"),
REQUEST_ILLEGAL(30004, "访问太频繁!"),
SESSION_ERROR(30005, "Session不存在或者已经失效!"),
PASSWORD_EMPTY(30006, "登录密码不能为空!"),
MOBILE_EMPTY(30007, "手机号不能为空!"),
MOBILE_ERROR(30008, "手机号格式错误!"),
MOBILE_NOT_EXIST(30009, "手机号不存在!"),
PASSWORD_ERROR(30010, "密码错误!"),
USER_NOT_EXIST(30011, "用户不存在!"),
/**
* 订单模块
*/
ORDER_NOT_EXIST(60001, "订单不存在"),
/**
* 秒杀模块
*/
MIAO_SHA_OVER(40001, "商品已经秒杀完毕"),
REPEATE_MIAOSHA(40002, "不能重复秒杀"),
MIAOSHA_FAIL(40003, "秒杀失败");
/**
* 商品模块
*/
private int code;
private String message;
private ResultStatus(int code, String message) {
this.code = code;
this.message = message;
}
private ResultStatus(Object... args) {
this.message = String.format(this.message, args);
}
public int getCode() {
return this.code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public String getName() {
return this.name();
}
public String getOutputName() {
return this.name();
}
@Override
public String toString() {
return this.getName();
}
}
自定义全局异常类用来接收ResultStatus枚举类
public class GlobleException extends RuntimeException {
private ResultStatus status;
public GlobleException(ResultStatus status) {
super();
this.status = status;
}
public ResultStatus getStatus() {
return status;
}
public void setStatus(ResultStatus status) {
this.status = status;
}
}
改造统一返回类AjaxResult
/**
* 改造AjaxResult
*
*/
public class AjaxResult {
private ResultStatus status;
private int code;
private String message;
public AjaxResult(ResultStatus status) {
this.code = status.getCode();
this.message = status.getMessage();
this.status = status;
}
/**
* 返回错误消息
*
* @param 枚举信息 status
* @return 警告消息
*/
public static AjaxResult error(ResultStatus status)
{
return new AjaxResult(status);
}
}
定义全局异常处理类
@RestControllerAdvice
public class GlobalExceptionHandler{
/**
* 自定义验证异常
*/
@ExceptionHandler(GlobleException.class)
public AjaxResult handleGlobleException(GlobleException e)
{
log.error(e.getStatus().getMessage(), e);
return AjaxResult.error(e.getStatus());
}
}
使用
// ...
if (user == null) {
/**
* 对应枚举类MOBILE_NOT_EXIST(30009, "手机号不存在!")
*/
throw new GlobleException(MOBILE_NOT_EXIST);
}
这里触发的逻辑即可被我们的自定义的全局异常所捕获,捕获流程如下:
参考资料: