boot-admin开源项目中有关后端参数校验的最佳实践

打印 上一主题 下一主题

主题 904|帖子 904|积分 2712

我们在项目开发中,经常会对一些参数进行校验,比如非空校验、长度校验,以及定制的业务校验规则等,如果使用if/else语句来对请求的每一个参数一一校验,就会出现大量与业务逻辑无关的代码,繁重不堪且繁琐的校验,会大大降低我们的工作效率,而且准确性也无法保证。为保证数据的正确性、完整性,前后端都需要进行数据检验。本文对开源 boot-admin 项目的后端校验实践进行总结,以飨码友。
boot-admin 是一款采用前后端分离模式、基于 SpringCloud 微服务架构的SaaS后台管理框架。系统内置基础管理、权限管理、运行管理、定义管理、代码生成器和办公管理6个功能模块,集成分布式事务 Seata、工作流引擎 Flowable、业务规则引擎 Drools、后台作业调度框架 Quartz 等,技术栈包括 Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。
项目源码仓库github
项目源码仓库gitee
引入Maven依赖
  1. <dependency>
  2.     <groupId>javax.validation</groupId>
  3.     <artifactId>validation-api</artifactId>
  4.     <version>2.0.1.Final</version>
  5. </dependency>
复制代码
参数校验实践

定义校验对象
  1. @Data
  2. /** 组合校验注解(方式1) **/
  3. @OverallValid(value = "check1" ,message="女士不得小于16岁。")
  4. @OverallValid(value = "check2" ,message="男士不得小于18岁。")
  5. public class User {
  6.     //字符个数检测(内置注解)
  7.     @Size(min = 1,max = 10,message = "姓名长度必须为1到10")
  8.     //占用空间长度检测(自定义注解)
  9.     @StringLength(min = 1,max = 12,message = "姓名的保存长度不允许超过12个字节。")
  10.     private String name;
  11.     //利用枚举类检测(自定义注解)
  12.     @EnumValid(target = SexEnum.class, message = "性别的取值范围是【1】和【2】")
  13.     private String sex;
  14.     //注意 @NotNull @NotEmpty @NotBlank 的区别
  15.     @NotBlank(message = "姓氏是必填项。")
  16.     private String firstName;
  17.     @Min(value = 10,message = "年龄最小为10")
  18.     @Max(value = 100,message = "年龄最大为100")
  19.     private Integer age;
  20.     @Past(message = "出生时间必须为过去时间")
  21.     private Date birth;
  22.     @NotEmpty(message = "兴趣不能为空")
  23.     private List<String> interest;
  24.     //嵌套检测
  25.     @Valid
  26.     private List<User> children;
  27.     @Valid
  28.     private User father;
  29.     @Valid
  30.     private User mother;
  31.     /** 组合校验(方式2) **/
  32.     @BooleanValid(message = "男性年龄需在60岁以下")
  33.     public boolean getValid1(){
  34.         if(sex.equalsIgnoreCase("1") && age >= 60 ){
  35.             return false;
  36.         }
  37.         return true;
  38.     }
  39.     /** 组合校验(方式2) **/
  40.     @BooleanValid(message = "女性年龄需在55岁以下")
  41.     public boolean getValid2(){
  42.         if(sex.equalsIgnoreCase("2") && age >= 55 ){
  43.             return false;
  44.         }
  45.         return true;
  46.     }
  47.     /** 组合校验(方式1)方法 **/
  48.     public boolean check1(){
  49.         if(sex.equalsIgnoreCase("2") && age < 16 ){
  50.             return false;
  51.         }
  52.         return true;
  53.     }
  54.     /** 组合校验(方式1)方法 **/
  55.     public boolean check2(){
  56.         if(sex.equalsIgnoreCase("1") && age < 18 ){
  57.             return false;
  58.         }
  59.         return true;
  60.     }
  61. }
复制代码
相关枚举类:
  1. public enum SexEnum {
  2.     男("1"),女("2");
  3.     private final String value;
  4.     SexEnum(String value) {
  5.         this.value = value;
  6.     }
  7.     public String getValue() {
  8.         return value;
  9.     }
  10. }
复制代码
参数校验(在 Controller 中使用)
  1. @RestController
  2. @RequestMapping("/api/system")
  3. @Slf4j
  4. public class DemoController {
  5.     //注入校验信息采集器
  6.     @Resource
  7.     private FormValidator formValidator;
  8.     @PostMapping("/free/user/check")
  9.     public ResultDTO check(@Valid @RequestBody User user, BindingResult bindingResult, HttpServletRequest request) throws Exception{
  10.         /** 参数校验 **/
  11.         if (bindingResult.hasErrors()) {
  12.             return formValidator.generateMessage(bindingResult);
  13.         }
  14.         /** 继续执行业务逻辑 **/
  15.         return ResultDTO.success();
  16.     }
  17. }
复制代码
在Controller中使用的校验结果信息采集器实现

接口定义:
  1. public interface FormValidator {
  2.     ResultDTO generateMessage(BindingResult bindingResult) throws Exception;
  3. }
复制代码
类实现:
  1. @Service
  2. @Slf4j
  3. public class FormValidatorImpl implements FormValidator {
  4.     @Override
  5.     public ResultDTO generateMessage(BindingResult bindingResult) throws Exception {
  6.         String msg = this.getMessage(bindingResult);
  7.         return ResultDTO.failureCustom(msg);
  8.     }
  9.     /**
  10.      * 生成校验结果
  11.      * @param bindingResult
  12.      * @return
  13.      */
  14.     private String getMessage(BindingResult bindingResult){
  15.         log.info(bindingResult.toString());
  16.         List<ObjectError> objectErrorList=bindingResult.getAllErrors();
  17.         String msg= this.getFormValidErrsMsgNoBr(objectErrorList);
  18.         log.info(msg);
  19.         return msg;
  20.     }
  21.     private String getFormValidErrsMsgNoBr(List<ObjectError> objectErrorList) {
  22.         if (objectErrorList==null) {
  23.             return "";
  24.         }
  25.         StringBuffer csv = new StringBuffer();
  26.         csv.append("数据验证未通过:[");
  27.         for (int i = 0; i < objectErrorList.size(); i++){
  28.             if (i > 0){
  29.                 csv.append("],[");
  30.             }
  31.             csv.append(objectErrorList.get(i).getDefaultMessage());
  32.         }
  33.         csv.append("]");
  34.         return csv.toString();
  35.     }
  36. }
复制代码
相关注解介绍

JSR-303 规范常用注解

以下列举常用内置注解,可直接使用。
注解描述@Valid对po实体尽心校验@AssertFalse所注解的元素必须是Boolean类型,且值为false@AssertTrue所注解的元素必须是Boolean类型,且值为true@DecimalMax所注解的元素必须是数字,且值小于等于给定的值@DecimalMin所注解的元素必须是数字,且值大于等于给定的值@Digits所注解的元素必须是数字,且值必须是指定的位数@Future所注解的元素必须是将来某个日期@Max所注解的元素必须是数字,且值小于等于给定的值@Min所注解的元素必须是数字,且值大于等于给定的值@Range所注解的元素需在指定范围区间内@NotNull所注解的元素值不能为null@NotBlank所注解的元素值有内容@Null所注解的元素值为null@Past所注解的元素必须是某个过去的日期@PastOrPresent所注解的元素必须是过去某个或现在日期@Pattern所注解的元素必须满足给定的正则表达式@Size所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内@Email所注解的元素需满足Email格式自定义注解

仅仅使用内置的注解,无法满足复杂的业务需求,故扩展下面几个自定义注解。
UTF-8 字符串长度校验

对字符串长度的校验目的,一般是用于保证数据表字段可以容纳,当字符串内容是中文时,内置的 @Size 是不适用的,此时就需要自行扩展 UTF-8 字符串长度校验。
注解类:
  1. @Target( {
  2.         METHOD,
  3.         FIELD,
  4.         ANNOTATION_TYPE,
  5.         CONSTRUCTOR,
  6.         PARAMETER
  7. })
  8. @Retention(RUNTIME)
  9. @Documented
  10. @Constraint(validatedBy = {StringLengthValidator.class})
  11. public @interface StringLength {
  12.     int max() default 4000;
  13.     int min() default 0;
  14.     String message() default "字符串长度不符合要求。";
  15.     Class<?>[] groups() default {};
  16.     Class<? extends Payload>[] payload() default {};
  17. }
复制代码
注解类实现:
  1. @Slf4j
  2. public class StringLengthValidator implements ConstraintValidator<StringLength, String> {
  3.     private int max;
  4.     private int min;
  5.     @Override
  6.     public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
  7.         try {
  8.             if(StringUtils.isBlank(value)){
  9.                 if(min > 0){
  10.                     return false;
  11.                 }else {
  12.                     return true;
  13.                 }
  14.             }
  15.             byte[] tmpbyte = value.getBytes("UTF-8");
  16.             int length = tmpbyte.length;
  17.             if(length < min || length > max){
  18.                 return false;
  19.             }
  20.             return true;
  21.         }catch (Exception ex){
  22.             log.error("注解校验StringLength发生异常。");
  23.             log.error(ex.getMessage(),ex);
  24.             return false;
  25.         }
  26.     }
  27.     @Override
  28.     public void initialize(StringLength constraintAnnotation) {
  29.         max = constraintAnnotation.max();
  30.         min = constraintAnnotation.min();
  31.     }
  32. }
复制代码
枚举类字符串校验

有时需要校验参数值必须是系统定义的枚举值(字符串),此时需要扩展以下注解。
注解类:
  1. @Target( {
  2.         METHOD,
  3.         FIELD,
  4.         ANNOTATION_TYPE,
  5.         CONSTRUCTOR,
  6.         PARAMETER
  7. })
  8. @Retention(RUNTIME)
  9. @Documented
  10. @Constraint(validatedBy = {MobileValidator.class})
  11. public @interface Mobile {
  12.     String regexp() default "";
  13.     String message() default "手机号码格式不正确";
  14.     Class<?>[] groups() default {};
  15.     Class<? extends Payload>[] payload() default {};
  16. }
复制代码
注解类实现:
  1. public class MobileValidator implements ConstraintValidator<Mobile, String> {
  2.     /**
  3.      * 手机号的正则表达式.
  4.      */
  5.     private static Pattern pattern = Pattern.compile(
  6.             "^0?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$");
  7.     @Override
  8.     public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
  9.         Matcher m = pattern.matcher(value);
  10.         return m.matches();
  11.     }
  12.     @Override
  13.     public void initialize(Mobile constraintAnnotation) {}
  14. }
复制代码
Bean 内多属性组合校验(组合校验)

此类校验一般属于业务逻辑校验,常常要求多个属性符合一定的逻辑设定。此时需要在Bean中编写校验方法,并在类定义前面添加自定义注解 @OverallValid 或者在方法前面加上自定义注解 @BooleanValid
方式1:

注解在类定义前面,类方法要求:

  • 方法的可访问属性:public
  • 方法的返回类型: boolean
    @OverallValid注解类:
[code]@Target({METHOD, FIELD,TYPE})@Retention(RUNTIME)@Repeatable(OverallValids.class)@Documented@Constraint(validatedBy = {OverallValidImpl.class})public @interface OverallValid {    String value() default "overallValid";    String message() default "组合校验未通过。";    Class[] groups() default {};    Class[] groups() default {};    Class
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

美丽的神话

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表