likes
comments
collection
share

SpringBoot整合【全局异常处理+错误码枚举+JSR303校验】

作者站长头像
站长
· 阅读数 16

1.不做全局异常的问题

我们写的代码如果不做全局异常有什么问题? 我们可以在controller中做个快速的演示,加一个测试接口:

@RequestMapping("/test/{id}")
public JSONResult test(@PathVariable Integer id){
    employeeService.test(id);
    return JSONResult.success();
}

在业务层中做如下的处理,模拟异常的产生:

@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {

    public void test(Integer id){
        if(id == 0){
            throw new NullPointerException("id不能为空");
        }else if(id == -1){
            throw new RuntimeException("id不存在!");
        }else if(id > 0 ){
            int i = id / 0;
        }
    }
}

当我们通过浏览器去访问的时候得到的结果就是这样的: SpringBoot整合【全局异常处理+错误码枚举+JSR303校验】 这样是肯定不适合的,我们应该返回统一的错误处理,并且交代清楚错误的类型,交给前端进行统一错误页面展示。并且在我们的controller中要捕获对应的异常:

@RequestMapping("/test/{id}")
public JSONResult test(@PathVariable Integer id){
    try {
        employeeService.test(id);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return JSONResult.success();
}

为了不在controller中写大量的try catch因为我们需要全局统一异常处理。

2.全局统一异常处理

Spring  Boot  默认会处理所有未被处理的异常,将其转换为  HTTP  响应并返回客户端。但是,我们希望能够自定义响应格式、统一异常处理以便做到更好的异常管理。

要实现全局异常处理,我们需要写一个类并使用  @ControllerAdvice  注解:

/**
 * @Description 全局统一异常处理器
 * @Author Raymon
 * @Version 1.0
 */
@Slf4j
@RestControllerAdvice //开启全局注解并且返回json格式数据
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class) //用于标记需要处理的异常类型
    public ResponseResult exceptionHandler(Exception e) {
        log.error("全局异常处理:{}", e.getMessage(), e);
        return JSONResult.error("系统异常,请稍后再试!");
    }
}

细节说明:

  • @ControllerAdvice  注解用于标记全局异常处理类。
  • @ExceptionHandler  注解用于标记需要处理的异常类型。
  • @ResponseBody  注解用于指定响应体格式为  json。
  • ResponseResult  为我们自定义的响应格式。

如果返回的都是JSON格式,可以直接在类上使用@RestControllerAdvice

最后在我们的异常类中,可以通过  throw new RuntimeException("错误消息")  抛出异常,并在全局异常中处理。例如:

public class MyService {
	public void doSomething() {
    	// 设置异常信息
    	throw new RuntimeException("异常消息");
    }
}

到这里,我们已经实现了全局异常处理。当发生异常时,会进入  GlobalExceptionHandler  类中的相应方法进行异常处理,并返回指定格式的响应。

3.自定义异常+枚举异常错误码

在Java中,通常使用枚举类型来定义统一的错误码。这样可以方便地管理、扩展和维护各种错误码,同时也可以避免使用魔术数的风险。同时我们可以约束好统一的错误码描述,更好的展示给前端开发人员,但是前端人员主要通过错误码来进行判断,描述只是起到一个解释作用。

下面是一个简单的示例:

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @Description 统一错误码以及描述定义
 * @Author Raymon
 * @Version 1.0
 */
@Getter
@AllArgsConstructor
public enum ErrorCode {
    SUCCESS(0, "操作成功"),
    UNKNOWN_ERROR(-1, "未知错误"),
    INVALID_PARAM(1, "参数错误"),
    DB_ERROR(2, "数据库错误");
    
    //错误码
    private final Integer code;
    //错误码描述
    private final String description;

}

Java 枚举中没有 setter 方法。枚举是一种特殊的类,它的实例在定义时就已经被确定,所以不需要 setter 方法来修改其属性值。枚举中的实例属性应该被定义为 final 类型的字段,这样就保证其值不会被修改。

那么接下来想要给前端统一返回错误码以及错误描述,就需要优化我们的统一异常处理器了,增加一个自定义的异常:

/**
 * @Description 全局自定义异常
 * @Author Raymon
 * @Version 1.0
 */
public class GlobalCustomerException extends RuntimeException{
    //定义错误码
    private Integer code;

    //自定义错误码以及错误描述
    public GlobalCustomerException(String message, Integer code) {
        super(message);
        this.code = code;
    }

    //默认异常,未知错误
    public GlobalCustomerException() {
        super(ErrorCode.UNKNOWN_ERROR.getDescription());
        this.code = ErrorCode.UNKNOWN_ERROR.getCode();
    }
}

在可能出问题的地方抛出自定义异常:

throw new GlobalCustomerException(ErrorCode.INVALID_PARAM.getDescription(),ErrorCode.INVALID_PARAM.getCode());

但是我们的全局异常处理器并没有去捕获该自定义异常,因此我们还需要优化:

/**
 * @Description 全局统一异常处理器
 * @Author Raymon
 * @Version 1.0
 */
@Slf4j
@RestControllerAdvice //开启全局注解并且返回json格式数据
public class GlobalExceptionHandler {
    /**
     * 全局默认异常处理
     * @param e Exception
     * @return  默认错误提示
     */
    @ExceptionHandler(Exception.class) //用于标记需要处理的异常类型
    public JSONResult exceptionHandler(Exception e) {
        log.error("全局异常处理:{}", e.getMessage(), e);
        return JSONResult.error("系统异常,请稍后再试!");
    }

    /**
     * 全局自定义异常处理
     * @param e 自定义异常
     * @return 自定义错误码+描述
     */
    @ExceptionHandler(GlobalCustomerException.class)
    public JSONResult exceptionHandler(GlobalCustomerException e) {
        log.error("全局自定义异常处理:{},code:{}", e.getMessage(),e.getCode());
        return JSONResult.error(e.getMessage(),e.getCode());
    }
}

最后查看日志: SpringBoot整合【全局异常处理+错误码枚举+JSR303校验】 至此全局统一异常处理搭建完毕,后续根据自己项目中的需求进行扩展和优化即可。

4.统一结果集封装

/**
 * @Description 统一结果集封装
 * @Author Raymon
 * @Version 1.0
 */
@Data
public class JSONResult<T> {

    private boolean success = true;
    private String message = "成功";
    //错误码,用来描述错误类型 ,0 表示没有错误
    private Integer code = 0;

    //返回的数据
    private T data;

    public static JSONResult success(){
        return new JSONResult();
    }
    public static <T> JSONResult<T> success(T data){
        JSONResult instance = new JSONResult();
        instance.setData(data);
        return instance;
    }

    public static <T> JSONResult<T> success(T data,Integer code){
        JSONResult instance = new JSONResult();
        instance.setCode(code);
        instance.setData(data);
        return instance;
    }

    public static JSONResult error(String message,Integer code){
        JSONResult instance = new JSONResult();
        instance.setMessage(message);
        instance.setSuccess(false);
        instance.setCode(code);
        return instance;
    }

    public static JSONResult error(){
        JSONResult jsonResult = new JSONResult();
        jsonResult.setSuccess(false);
        return jsonResult;
    }

    public static JSONResult error(String message){
        return error(message,null);
    }

}

5.优化

如果每次抛异常都要这样写很长的方式,其实并不优雅:

throw new GlobalCustomerException(ErrorCode.INVALID_PARAM.getDescription(),ErrorCode.INVALID_PARAM.getCode());

其实我们的自定义异常该构造方法中也无法就是将接收到的两个值继续赋值给异常类中的code和message,如下:

public GlobalCustomerException(String message, Integer code) {
    super(message);
    this.code = code;
}

因此,我们完全可以直接传入一个枚举即可,在构造方法中取出枚举的值赋值到code和message即可 改造自定义异常类:

/**
 * @Description 全局自定义异常
 * @Author Raymon
 * @Version 1.0
 */
@Getter
public class GlobalCustomerException extends RuntimeException{
    //定义错误码
    private Integer code;

    //自定义错误码以及错误描述
    public GlobalCustomerException(String message, Integer code) {
        super(message);
        this.code = code;
    }
    //接收错误码枚举构建自定义异常-----增加
    public GlobalCustomerException(ErrorCode errorCode) {
        super(errorCode.getDescription());
        this.code = errorCode.getCode();
    }

    //默认异常,未知错误
    public GlobalCustomerException() {
        super(ErrorCode.UNKNOWN_ERROR.getDescription());
        this.code = ErrorCode.UNKNOWN_ERROR.getCode();
    }
}

那我们后续抛异常,直接如下就很优雅简单了:

throw new GlobalCustomerException(ErrorCode.INVALID_PARAM);

6.JSR303校验

  • 参数校验是程序开发中必不可少的步骤,用户在前端页面上填写表单时,前端js程序会校验参数的合法性,当数据到了后端,为了防止恶意操作,保持程序的健壮性,后端同样需要对数据进行校验,后端参数校验最简单的做法是直接在业务方法里面进行判断,当判断成功之后再继续往下执行,但这样带给我们的是代码的耦合,冗余。当我们多个地方需要校验时,我们就需要在每一个地方调用校验程序,导致代码很冗余,且不美观
  • JSR303就是为了优美的参数校验出现的

1.相关注解

  • Bean Validation中内置的constraint

    Constraint详细信息
    @Null被注释的元素必须为 null
    @NotNull被注释的元素必须不为 null
    @AssertTrue被注释的元素必须为 true
    @AssertFalse被注释的元素必须为 false
    @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Size(max, min)被注释的元素的大小必须在指定的范围内
    @Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
    @Past被注释的元素必须是一个过去的日期
    @Future被注释的元素必须是一个将来的日期
    @Pattern(value)被注释的元素必须符合指定的正则表达式
  • Hibernate Validator 附加的 constraint

    Constraint详细信息
    @Email被注释的元素必须是电子邮箱地址
    @Length被注释的字符串的大小必须在指定的范围内
    @NotEmpty被注释的字符串的必须非空
    @Range被注释的元素必须在合适的范围内

2.使用

2.1.导入Jar包

  • JSR303所需要的Jar包
<!--spring-boot-validation依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 在spring-boot-starter-web中有上述Jar包
<!--spring-boot-web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2.在参数实体类的字段上加注解

  • 字段可以加在实体类字段上,也可以加在方法参数前
  • 要求如下:
    • 姓名、电话、邮箱不能为空
    • 电话、邮箱格式需要正则校验
  • 代码实现:
/**
 * @author raymon
 */
@TableName("t_employee")
public class Employee extends Model<Employee> {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 姓名
     */
    @TableField("real_name")
    @NotEmpty(message = "用户名不能为空")
    private String realName;
    /**
     * 电话
     */
    @Pattern(regexp = "^((13[0-9])|(14[0,1,4-9])|(15[0-3,5-9])|(16[2,5,6,7])|(17[0-8])|(18[0-9])|(19[0-3,5-9]))\\d{8}$",message = "手机号格式不正确")
    @NotEmpty(message = "手机号不能为空")
    private String tel;
    /**
     * 邮箱
     */
    @Email(message = "邮箱格式不正确")
    @NotEmpty(message = "邮箱不能为空")
    private String email;
    /**
     * 创建时间
     */
    @TableField("input_time")
    private Date inputTime;
    /**
     * 状态:0正常,1锁定,2注销
     */
    private Integer state;
    /**
     * 部门id
     */
    @TableField("dept_id")
    private Long deptId;
    /**
     * 员工类型 , 1平台普通员工 ,2平台客服人员,3平台管理员,4机构员工,5,机构管理员或其他
     */
    private Integer type;
    @TableField("login_id")
    private Long loginId;

}
  • 在接口接收参数上添加@Valid注解进行校验:
/**
* 保存和修改公用的
*/
@PostMapping(value="/save")
public JSONResult saveOrUpdate(@RequestBody @Valid Employee employee){
    if(employee.getId()!=null){
        employeeService.updateById(employee);
    }else{
        employeeService.insert(employee);
    }
    return JSONResult.success();
}

测试后结果:

{
  "success": false,
  "message": "参数错误:手机号格式不正确,邮箱格式不正确",
  "code": 1,
  "data": null
}

OK,以上就为搭建全局异常处理+错误码枚举+JSR303校验整合的完整内容。大家写好后在公司的各个项目中都可以直接引入快速使用。