关于java:如何在项目中优雅的校验参数

49次阅读

共计 10572 个字符,预计需要花费 27 分钟才能阅读完成。

本文看点

前言

验证数据是贯通所有应用程序层 (从表示层到长久层) 的常见工作。通常在每一层实现雷同的验证逻辑,这既费时又容易出错。为了防止反复这些验证,开发人员常常将验证逻辑间接捆绑到域模型中,将域类与验证代码混在一起,这些验证代码实际上是对于类自身的元数据,与业务逻辑不相干。

JSR 380——Bean Validation2.0——定义了用于实体和办法验证的元数据模型和 API,将数据校验逻辑通过注解的模式封装在实体对象中。

1. 对于 JSR

JSR 是 Java Specification Requests 的缩写,意思是 Java 标准提案。是指向 JCP(Java Community Process)提出新增一个标准化技术规范的正式申请。任何人都能够提交 JSR,以向 Java 平台削减新的 API 和服务。JSR 已成为 Java 界的一个重要规范。

JSR-303 是 JAVA EE 6 中的一项子标准,起初的版本是 Bean Validation 1.1(JSR-349),目前最新版本是 Bean Validation 2.0(JSR-380),Hibernate Validator 是 Bean Validation 的参考实现,除了 Jakarta Bean 验证 API 定义的束缚之外,Hibernate Validator 还有一些附加的 constraint;并且 spring-boot-starter-web 默认集成了 Hibernate Validator。(springboot2.3 版本曾经移除 hibernate-validator 的依赖,须要手动引入

2. 为什么应用 Hibernate Validator

  • 进步代码整洁度;
  • 验证逻辑与业务逻辑之间进行了拆散,升高了程序耦合度;
  • 对立且标准的验证形式,无需你再次编写反复的验证代码;
  • 你将更专一于你的业务,将这些繁琐的事件通通丢在一边。

3. 注解介绍

JSR 380 内置罕用注解

注解 详细信息
@Null 被正文的元素必须为 null
@NotNull 被正文的元素必须不为 null
@AssertTrue 被正文的元素必须为 true
@AssertFalse 被正文的元素必须为 false
@Min(value) 被正文的元素能够是字符串、数值类型,如果元素是字符串类型,将值转为 BigDecimal 类型,并与 value 属性进行比对,值必须大于等于指定的 value 值
@Max(value) 被正文的元素能够是字符串、数值类型,如果元素是字符串类型,将值转为 BigDecimal 类型,并与 value 属性进行比对,值必须小于等于指定的 value 值
@DecimalMin(value) 被正文的元素能够是字符串、数值(能够带小数点),将注解内 value 的值转为 BigDecimal 类型,必须大于等于指定的最小值(能够配置是否等于 value,默认是蕴含的)
@DecimalMax(value) 被正文的元素能够是字符串、数值(能够带小数点),将注解内 value 的值转为 BigDecimal 类型,其值必须小于等于指定的最大值(能够配置是否等于 value,默认是蕴含的)
@Size(max, min) 被正文的元素的大小必须在指定的范畴内,可用于字符串、Collection、Map、数组等类型
@Digits (integer, fraction) 被正文的元素必须是一个数字,其值必须在可承受的范畴内
@Past 被正文的元素必须是一个过来的日期
@Future 被正文的元素必须是一个未来的日期
@Pattern(value) 被正文的元素必须合乎指定的正则表达式
@Email 被正文的元素必须是电子邮箱地址
@NotBlank 验证字符串非 null,且 trim 后长度必须大于 0
@NotEmpty 实用于 String、Collection、Map 或者数组不能为 Null 且长度或元素个数必须大于 0
@Valid 具体作用上面会列举

Hibernate Validator 附加的 constraint

注解 详细信息
@Length 被正文的字符串的大小必须在指定的范畴内
@URL 依据 RFC2396 规范校验正文的字符串必须是一个的无效的 url
@Range 被正文的元素必须在适合的范畴内,利用于数值或字符串
@UniqueElements 查看带正文的汇合是否只蕴含惟一的元素。相等性是应用 equals()办法确定的。
@SafeHtml 查看带正文的值是否蕴含潜在的歹意片段,如<script/>。如用这个注解须要引入 jsoup 的依赖,用来解析 html 代码

留神

  • @NotNull:实用于任何类型被注解的元素,必须不能为 NULL
  • @NotEmpty:实用于 String、Collection、Map 或者数组,不能为 Null 且长度或元素个数必须大于 0
  • @NotBlank:验证字符串非 null,且 trim 后长度必须大于 0

@Validated 与 @Valid 的区别:

  • @Validated 注解是 spring 提供的,提供了一个分组性能,能够在入参验证时,依据不同的分组采纳不同的验证机制。没有增加分组属性时,默认验证没有分组的验证属性(Default 分组);
  • @Validated:能够用在类型、办法和办法参数上,然而不能用在成员属性(字段)上
  • @Validated:用在办法入参上无奈独自提供嵌套验证性能,也无奈提醒框架进行嵌套验证。能配合嵌套验证注解 @Valid 进行嵌套验证。
  • @Valid:作为规范 JSR-303 标准,还没有排汇分组的性能;
  • @Valid:能够用在办法、办法参数、构造函数、办法参数和成员属性(字段)上
  • @Valid 加在办法参数时并不可能主动进行嵌套验证,而是用在须要嵌套验证类的相应字段上,来配合办法参数上 @Validated 或 @Valid 来进行嵌套验证。

4. 应用

因为 spring-boot-starter-web(springboot 2.3 以下版本)依赖默认集成了 Hibernate Validator,所以无需增加任何依赖和相干配置,只须要在我的项目中引入 spring-boot-starter-web 依赖即可(演示 springboot 版本为 2.1.2.RELEASE),因为要用到 @SafeHtml 注解,这里须要加上 jsoup 的依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
 <!-- 解析 html 片段 -->
<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.8.3</version>
</dependency>

Hibernate Validator 有两种校验模式:

  • 一般模式(会校验完所有的属性,而后返回所有的验证失败信息,默认是这个模式)
  • 疾速失败返回模式 ( 只有有一个字段验证失败,就返回后果)

    在 @Configuration Class 中配置以下代码,将 Validator 设置为疾速失败返回模式

    @Bean
        public Validator validator(){ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class)
                    .configure()
                    .addProperty("hibernate.validator.fail_fast", "true")
                    .buildValidatorFactory();
            Validator validator = validatorFactory.getValidator();
            return validator;
        }

a. 对象校验

1. 在对象中增加注解

@Data
public class User {
    // 注解对动态变量不失效
    @NotBlank(message = "性别不能为空")
    private static String sex;
    
    @NotBlank(message = "姓名不能为空")
    @Size(min = 2,max = 5,message = "姓名长度不标准")
    private String name;
    
    @NotNull(message = "年龄不能为空")
    @Max(value = 30,message = "年龄超过最大值 30")
    @Range(min=30,max=60)
    private Integer age;
    
    @DecimalMax(value = "108.88",message = "超过最大 108.88",inclusive = false)
    private Double price;
    
    @Past(message = "生日不能大于以后日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;
    
    @Email(message = "电子邮箱格局不正确")
    private String email;
    
    @SafeHtml(message = "非法申请参数")
    private String content;
}

2. 进入 Controller 对应办法,在须要校验的对象前增加 @Valid 注解即可(校验对动态变量不失效),在应用 @Valid 注解的参数后能够紧跟着一个 BindingResult 类型的参数,用于获取校验后果(将校验后果封装在 BingdingResult 对象中,不会抛出异样)

留神:@Valid 和 BindingResult 是一一对应的,如果有多个 @Valid,那么每个 @Valid 前面跟着的 BindingResult 就是这个 @Valid 的验证后果,程序不能乱

    // 单个对象校验
    @PostMapping("user")
    // 校验参数后边跟 BindingResult,spring 不会抛出异样, 将校验后果封装在这个对象中
    public String person(@Valid User user,BindingResult bindingResult){System.out.println(user);
        StringBuilder sb = new StringBuilder();
        if(bindingResult.hasErrors()){List<ObjectError> allErrors = bindingResult.getAllErrors();
            for(ObjectError error:allErrors){sb.append(error.getDefaultMessage()+",");
            }
        }
        return sb.toString();}

3. 如果此时去掉实体对象前面的 BindingResult,如校验未通过会抛出 BindException 异样,须要在全局异样处理器中捕捉并对立解决

4. 全局异样处理器配置

@RestControllerAdvice
@Slfj
@AutoConfigurationPackage
public class GlobalExceptionHandler {
    //spring-context 包外面的异样
    // 实体对象前不加 @RequestBody 注解, 单个对象内属性校验未通过抛出的异样类型
    @ExceptionHandler(BindingException.class)
    public ResponseEntity<ExceptionResponseVO> methodArguments(BindingException e){log.warn("throw BindingException,{}",e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionResponseVO.error(NEError.INVALID_PARAMETER, e.getBindingResult().getFieldError().getDefaultMessage()));
    }
    
       // 实体对象前不加 @RequestBody 注解, 校验办法参数或办法返回值时, 未校验通过时抛出的异样
    //Validation-api 包外面的异样
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ExceptionResponseVO> methodArguments(ValidationException e){log.warn("throw ValidationException,{}",e);
       return ResponseEntity.status(HttpStatus.BAD_REQUEST)            .body(ExceptionResponseVO.error(NEError.INVALID_PARAMETER,e.getCause().getMessage()));
    }
    
    //spring-context 包外面的异样, 实体对象前加 @RequestBody 注解, 抛出的异样为该类异样
    // 办法参数如果带有 @RequestBody 注解,那么 spring mvc 会应用 RequestResponseBodyMethodProcessor      // 对参数进行序列化, 并对参数做校验
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ExceptionResponseVO> methodArguments(MethodArgumentNotValidException e){log.warn("throw MethodArgumentNotValidException,{}",e);
       return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(ExceptionResponseVO.error(NEError.INVALID_PARAMETER,                e.getBindingResult().getFieldError().getDefaultMessage()));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity methodArguments(Exception e){log.warn("throw exception,{}",e);
        return ResponseEntity.badRequest().body(e.getMessage());
    }
}

b. 级联校验

如果一个对象外部蕴含另一个对象作为属性,属性上加 @Valid,能够验证作为属性的对象外部的验证

@Data
public class User2 {@NotBlank(message = "姓名不能为空")
    private String name;
    @Max(value = 50,message = "年龄不能为空")
    private Integer age;
    @Valid
    @NotNull(message = "商品不能为空")
    private Goods goods;
}
@Data
public class Goods{@NotBlank(message = "商品名称不能为空")
    private String goodsName;
    @NotNull(message = "商品价格不能为空")
    private Double goodsPrice;
}

如果级联校验内元素的属性校验未通过,抛出 MethodArgumentNotValidException 异样,留神在全局异样处理器捕捉该异样并解决

// 级联校验
 @PostMapping("cascade")
public String cascade(@Valid @RequestBody User2 user2){return "OK";}

c. 容器元素校验

用来校验实体对象内汇合中的元素,在容器泛型前加注解,可实现对容器单个元素的校验;如下:

@Data
public class User3 {@NotBlank(message = "姓名不能为空")
    private String name;
    @Max(value = 50,message = "年龄不能为空")
    private Integer age;
    
    @Valid
    @NotEmpty(message = "商品列表不能为空")
    private List<@NotNull(message = "商品不能为空") Goods> goodsList;
}

如果容器元素校验未通过,抛出异样MethodArgumentNotValidException(与级联校验抛出的一样)

// 容器元素校验
@PostMapping("container")
public String container(@Valid @RequestBody User3 user3){return "OK";}

d. 办法的校验

JSR 303 规范定义接口 ExecutableValidator,用来校验办法参数,Hibernate Validator 实现了该接口(ValidatorImpl.class),不仅对 Object 的属性进行校验,还能够对办法参数、返回值、结构函数参数等进行校验;Spring 在此基础上进行了扩大,增加了 MethodValidationPostProcessor 拦截器,通过 AOP 实现对办法的校验;此时抛出的异样是javax.validation.ConstraintViolationException

留神:必须在 Controller 下面加上注解 @Validated,否则校验规定有效

@RestController
@RequestMapping("validator")
@Validated
public class ValidatorController {@GetMapping("demo1")
    public String test1(@Range(min = 1,max = 999,message = "起始笔数超过区间范畴")@RequestParam int pageIndex, @Range(min = 1,max = 999,message = "查问笔数超过区间范畴")@RequestParam int pageSize){return "ok";}
}

除了校验 Controller 办法外,也可校验 Service(必须是单例的 bean,否则不失效,因为办法参数校验逻辑底层用 AOP 来实现)等办法,用法如下:

@Service
@Validated
public class UserService {
    // 校验办法参数
    public String queryUserName(@NotNull(message = "用户参数不能为空") User user){return user.getName();
    }
    
    // 校验办法返回值
    @NotNull(message = "用户信息不存在")
    public User queryUser(User user){return null;}
}

e. 分组校验的实现

分组

同一个校验规定,不可能实用于所有的业务场景,对每一个业务场景去编写一个校验规定,又显得特地冗余。实际上咱们能够用到 Hibernate-Validator 的分组性能,达到对不同场景做出不同的校验逻辑,缩小 DTO 对象的创立。

比方一个 User 对象,新增的时候不须要测验 id(系统生成),批改的时候须要测验 id 属性,要想复用 Class,就能够应用 Hibernate Validator 的分组。

实例代码:

@Data
public class UserGroup {@NotNull(message = "id 不能为空",groups = UpdateUser.class)
    private Integer id;
    @NotBlank(message = "姓名不能为空",groups = AddUser.class)
    private String name;
    @NotNull(message = "年龄不能为空",groups = AddUser.class)
    private Integer age;

    public interface AddUser{}
    public interface UpdateUser{}}

增加用户:在须要校验的对象后面加 @Validated 注解(不能应用 @Valid 注解),并配置分组 class,此时 AddUser 的分组校验规定失效。

    // 分组校验: 增加用户
    @PostMapping("addUser")
    public String addUser(@Validated(UserGroup.AddUser.class) UserGroup userGroup){return "OK";}

批改用户:配置 UpdateUser 分组

    // 分组校验: 批改用户
    @PostMapping("updateUser")
    public String updateUser(@Validated(UserGroup.UpdateUser.class) UserGroup userGroup){return "OK";}

应用分组能极大的复用须要验证的 Class 信息,而不是按业务反复编写冗余的类。

留神:如果指定了校验组,则该属性将不再属于默认的校验组 Default.class,则在省略校验组参数的状况下,将不会校验自定义校验组的属性。

组序列

除了按组指定是否验证之外,还能够指定组的验证程序,后面组验证不通过的,前面组不进行验证;其中 @GroupSequence 提供组序列的模式进行程序式校验,即先校验 @Save 分组的,如果校验不通过就不进行后续的校验分组了。我认为程序化的校验,场景更多的是在业务解决类,例如联动的属性验证,值的有效性很大水平上不能从代码的枚举或常量类中来校验。

实例代码:

@Data
public class UserDTO {@NotNull(message = "id 不能为空",groups = {UpdateUser.class})
    private Integer id;
    @NotBlank(message = "姓名不能为空",groups = {AddUser.class})
    private String name;
    @NotNull(message = "年龄不能为空",groups = {AddUser.class})
    private Integer age;
    @NotNull(message = "版本不能为空")// 不配置 goups,默认就是 Default 分组
    private Integer version;

    @GroupSequence({AddUser.class, UpdateUser.class, Default.class})
    public interface AddUpdateGroup{}
    public interface AddUser{}
    public interface UpdateUser{}}

首先校验 AddUser 分组的注解,如果 AddUser 校验不通过,就不会去校验 UpdateUser 和 Default 的分组

@PostMapping("user")
public String saveUser(@Validated(UserDTO.AddUpdateGroup.class) UserDTO userDTO){userMapper.addUser(userDTO);
    return "ok";
}

5. 自定义 constraint

个别状况,自定义验证能够解决很多问题;某些业务场景下又须要做一些特地的参数校验,此时,咱们能够实现 validator 的接口,自定义验证器。

创立自定义注解 @Sex,该注解是放在字段上的,也能够依据业务场景放在办法或者 Class 下面)用于判断性别是否合乎束缚

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SexConstraintValidator.class)
public @interface Sex {String message() default "性别参数有误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};}

创立自定义验证器

public class SexConstraintValidator
        implements ConstraintValidator<Sex,String> {
    /**
     * 性别束缚逻辑
     * @param value
     * @param constraintValidatorContext
     * @return
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        // 如果 value 为 null, 那么该校验规定不失效; 可搭配 @NotNull 注解应用, 更加灵便
        if(value == null){return true;}
        return "男".equals(value) || "女".equals(value);
    }
}

要验证的 DTO 对象

@Data
public class UserDTO {@NotNull(message = "id 不能为空")
    private Integer id;
    @NotBlank(message = "姓名不能为空")
    private String name;
    @NotNull(message = "年龄不能为空")
    private Integer age;
    @NotNull(message = "版本不能为空")
    private Integer version;
    @Sex
    private String sex;
}

在 UserDTO 对象前加 @Valid 注解,可实现对性别字段的合法性校验,sex 只能传入“男“或“女”。

这只是一个小例子,大家能够依据业务场景自定义参数校验器,例如敏感词校验、预防 sql 注入、js 脚本攻打等等,都能够用自定义校验器来实现。

关注我理解更多

正文完
 0