SpringBoot参数检验、统一结果和全局异常处理

前言

今天给大家介绍一下SpringBoot(Spring也可以)项目中如何优雅的整合JSR-303进行参数校验 以及对全局的异常进行处理封装

什么是JSR-303

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回.Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint.

添加依赖

SpringBoot项目可以用starter组件

<dependency>
    <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

老点的spring项目可以用下面这个(同样适用于springBoot项目)

        <!-- 参数校验 -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.1.Final</version>
        </dependency>

SpringBoot项目设置全局异常处理

参数在校验失败的时候会抛出的MethodArgumentNotValidException异常,可以在全局的异常处理器中捕捉这个异常,将报错信息同一封装之后返回给客户端。

全局异常处理器代码如下

注意: 此控制器为接口形式 处理异常的方法均为接口的default方法 此举不同于其他博客的注解形式和继承形式 注解形式是直接控制全局灵活性欠佳(比如你有一个三方回调的控制器类不想被监控) 而继承的方式又绑定的太死 所以采用了接口和default方法

/**
 * <p>
 *     全局异常处理的基础类 必须实现此类
 * </p>
 * @author liang
 * @date 2021/5/28 17:46
 */
public interface HandleExceptionController {


    /**
     * 处理实现此类的所有程序抛出的业务代码异常
     * @param e 业务代码自定义异常
     * @return 封装好的统一返回结果
     */
    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    default ResponseResult handleException(ServiceException e) {
        ResponseResult result = new ResponseResult();
        result.setCode(e.getCode());
        result.setMessage(e.getMessage());
        result.setData(e.getData());
        return result;
    }


    /**
     * 处理实现此类的所有程序抛出的参数绑定异常
     * @param e 参数绑定异常
     * @return 封装好的统一返回结果
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    default ResponseResult methodArgumentNotValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        return new ResponseResult(ResultEnum.REQUIRED_MISSING.getCode(), message,null);
    }


    /**
     * 处理实现此类的所有程序抛出的未知异常
     * @param e 程序抛出来的异常
     * @return 封装好的统一返回结果
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    default ResponseResult handleException(Exception e) {
        return new ResponseResult(ResultEnum.ERROR, e.getMessage());
    }

}
示例Controller代码

@Validated注解是用来标识这个参数类需要进行参数校验的

@RestController
@RequestMapping("/api/test")
@Api(value = "TestController", tags = { "测试类" })
public class TestController implements HandleExceptionController {

    @ApiOperation(value = "测试查询", notes = "测试查询")
    @PostMapping(value = "/queryTest")
    public ResponseResult queryTest(@RequestBody @Validated QueryTestParam param){
        JSONObject result = new JSONObject();
        return new ResponseResult(ResultEnum.SUCCESS,result);
    }

}

参数类

省略get和set方法

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * @author liang
 * @date 2021年12月29日 14:23
 */
public class QueryTestParam {

    @NotNull(message = "integer不能为空")
    private Integer integer;

    @NotBlank(message = "string不能为空")
    private String string;
}

如果参数类中的integer为null或者string为空字符串则会抛出MethodArgumentNotValidException异常,紧接着此异常会被HandleExceptionController接口的methodArgumentNotValidException方法捕获到 注意这行代码: @ExceptionHandler(MethodArgumentNotValidException.class)这个注解表示你的方法要捕获处理的异常是什么 如果你要捕获的异常有继承关系 他会优先捕获子类的详细类型的异常 所以接口中有一个handleException方法来捕获最终的异常exception

javax参数校验

内嵌的注解列表
注释解释
@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@NotBlank被注释的元素必须为字符串而且不能为空
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@Pattern(value)被注释的元素必须符合指定的正则表达式
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内
如何使用

get和set方法省略

import javax.validation.constraints.Future;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * @author liang
 * @date 2021年12月29日 14:23
 */
public class QueryTestParam {

    @NotNull(message = "文章id不能为空")
    @Min(value = 1,message = "文章ID不能为负数")
    private Integer id;

    @NotBlank(message = "文章内容不能为空")
    private String content;

    @NotBlank(message = "作者Id不能为空")
    private String authorId;

    @Future(message = "提交时间不能为过去时间")
    private Date submitTime;
}

分组校验

举例说明 新增和修改一篇文章 修改只比新增接口多传一个主键id,这时候很明显不想要建立两个非常相似的实体类去分别接收,这时候该怎么办呢?
答案显而易见,就是进行分组校验 新增是一个分组 修改是一个分组

所有的校验注解都有一个groups属性用来指定分组,Class<?>[]类型,没有实际意义,因此只需要定义一个或者多个接口用来区分即可。

/**
 * @author liang
 * @date 2021年12月29日 14:23
 */
public class QueryTestParam {

    /**
     *添加的校验分组接口
     */
    public interface add{}

    /**
     * 修改的校验分组接口
     */
    public interface update{}

    @NotNull(message = "文章id不能为空",groups = update.class)
    @Min(value = 1,message = "文章ID不能为负数",groups = update.class)
    private Integer id;

    @NotBlank(message = "文章内容不能为空",groups = {add.class,update.class})
    private String content;

    @NotBlank(message = "作者Id不能为空",groups = {add.class,update.class})
    private String authorId;

    @Future(message = "提交时间不能为过去时间",groups = {add.class,update.class})
    private Date submitTime;

}

JSR303本身的@Valid并不支持分组校验,但是Spring在其基础提供了一个注解@Validated支持分组校验。@Validated这个注解value属性指定需要校验的分组。

@RestController
@RequestMapping("/api/test")
@Api(value = "TestController", tags = { "测试类" })
public class TestController implements HandleExceptionController {
    @ApiOperation(value = "测试查询", notes = "测试查询")
    @PostMapping(value = "/queryTest")
    public ResponseResult queryTest(@RequestBody @Validated(value =QueryTestParam.add.class ) QueryTestParam param){
        JSONObject result = new JSONObject();
        return new ResponseResult(ResultEnum.SUCCESS,result);
    }

}

嵌套校验

嵌套校验 就是一个实体中包含了另一个实体 而这两个实体都需要校验

嵌套校验很简单,只需要在嵌套的实体属性标注@Valid注解,则其中的属性也将会得到校验,否则不会校验。

/**
 * @author liang
 * @date 2021年12月29日 14:23
 */
public class QueryTestParam {

    @Valid
    private AddParam addParam;
}
最后附赠一个校验工具类 可以在代码里随时对对象进行javax的校验
import org.hibernate.validator.HibernateValidator;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/**
 * @author liang
 */
public class ValidatorUtils {

    private static final Validator VALIDATOR_ALL = Validation.byProvider(HibernateValidator.class).configure().failFast(false).buildValidatorFactory().getValidator();

    /**
     * 校验不通过则抛出去自定义异常 {@link ServiceException}
     * @param obj 需要校验的对象
     * @param groups 需要校验的分组(可变长参数列表)
     * @param <T> 对象类型
     */
    public static <T> void validatorByGroups(T obj, Class<?>... groups) {
        Set<ConstraintViolation<T>> constraintViolations = VALIDATOR_ALL.validate(obj, groups);
        // 抛出检验异常
        if (constraintViolations.size() > 0) {
            throw new ServiceException(ResultEnum.REQUIRED_MISSING.getCode(),constraintViolations.iterator().next().getMessage());
        }
    }

    /**
     * 校验不通过则抛出去自定义异常 {@link ServiceException}
     * @param obj 需要校验的对象
     * @param <T> 对象类型
     */
    public static <T> void validatorObject(T obj) {
        Set<ConstraintViolation<T>> constraintViolations = VALIDATOR_ALL.validate(obj);
        // 抛出检验异常
        if (constraintViolations.size() > 0) {
            throw new ServiceException(ResultEnum.REQUIRED_MISSING.getCode(),constraintViolations.iterator().next().getMessage());
        }
    }

}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

life or die

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值