SpringBoot学习小结之参数校验
前言在日常的开发中,后端经常会对前端传来的参数进行校验,这会导致大量的重复代码出现在后台代码中,会让我们的代码显得很臃肿这种对数据验证很常见,在JSR(Java Specification Requests)中就有一项JSR-303专门用来处理这种情况,它是针对在Java EE and Java SE中对Java Bean validation的提案,目标是为Java应用程序开发人员提供类级约
文章目录
前言
在日常的开发中,后端经常会对前端传来的参数进行校验,这会导致大量的重复代码出现在后台代码中,会让我们的代码显得很臃肿
这种对数据验证很常见,在JSR(Java Specification Requests)中就有一项JSR-303专门用来处理这种情况,它是针对在Java EE and Java SE中对Java Bean validation的提案,目标是为Java应用程序开发人员提供类级约束声明和验证工具,以及约束元数据 repository和 query API
JSR-303只提供了接口声明,并没有具体实现,而Hibernate Validator实现了这套规范,并增加了一些属于自己的特性
Springboot2.3之前引入web依赖默认会引入Hibernate Validator,但是在之后就不会引入了,详细信息可以查看这个ISSUE #19550,现在引入需要添加pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
一、校验注解
1.1 jsr-303 标准注解
jsr标准注解,位于javax.validation.constraints下
注解名 | 作用 | 支持的类型 |
---|---|---|
@AssertFalse | 必须为false | Boolean boolean null |
@AssertTrue | 必须为true | Boolean boolean null |
@DecimalMax | 所标记的属性必须小于等于给定的最大值 | BigDecimal BigInteger CharSequence byte short int long 还有它们的包装类 null |
@DecimalMin | 所标记的属性必须大于等于给定的最大值 | BigDecimal BigInteger CharSequence byte short int long 还有它们的包装类 null |
@Digits | 必须是数字,在给定的范围内 | BigDecimal BigInteger CharSequence byte short int long 还有它们的包装类 null |
必须满足邮箱格式 | CharSequence, null | |
@Future | 必须是未来的时间 | java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate null |
@FutureOrPresent | 必须是现在或未来的时间 | java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate null |
@Max | 必须小于等于给定的最大值 | BigDecimal,BigInteger,byte,short,int,long,还有它们的包装类,null(和DecimalMax相比,没有字符串) |
@Min | 必须大于等于给定的最大值 | BigDecimal,BigInteger,byte,short,int,long,还有它们的包装类,null(和DecimalMax相比,没有字符串) |
@Negative | 必须是负数 | BigDecimal BigInteger byte short int long float double 还有它们的包装类 null |
@NegativeOrZero | 必须是负数或0 | BigDecimal BigInteger byte short int long float double 还有它们的包装类 null |
@NotBlank | 必须不是null并且包含至少一个非空白字符 | CharSequence |
@NotEmpty | 必须不为null并且不为空 | CharSequence Collection Map Array |
@NotNull | 必须不为空 | any type |
@Null | 必须为空 | any type |
@Past | 必须是过去的时间 | java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate null |
@PastPresent | 必须是过去或现在的时间 | java.util.Date java.util.Calendar java.time.Instant java.time.LocalDate java.time.LocalDateTime java.time.LocalTime java.time.MonthDay java.time.OffsetDateTime java.time.OffsetTime java.time.Year java.time.YearMonth java.time.ZonedDateTime java.time.chrono.HijrahDate java.time.chrono.JapaneseDate java.time.chrono.MinguoDate java.time.chrono.ThaiBuddhistDate null |
@Pattern | 必须匹配正则表达式 | CharSequence null |
@Positive | 必须是正数 | BigDecimal BigInteger byte short int long float double 还有它们的包装类 null |
@PositiveOrZero | 必须是正数或0 | BigDecimal BigInteger byte short int long float double 还有它们的包装类 null |
@Size | size必须在给定范围内 | CharSequence Collection Map Array |
@Valid | 递归校验对象的属性 | any type |
注:允许为null的属性为null时就不会校验,除了NotNull
1.2 Hibernate注解
位于org.hibernate.validator.constraints下
注解名 | 作用 | 支持的类型 |
---|---|---|
@Length | 检测字符串长度 | String null |
@Range | 检测数字范围 | 必须是数字或能转为数字的字符串 null |
@ScriptAssert | 类级别,支持JSR-223脚本表达式验证 | |
@UniqueElements | 检测每个元素都是唯一的 | Collection |
@URL | 检测字符串是否URL | String |
1.3 @Valid和@Validated区别
@Valid | @Validated | |
---|---|---|
提供者 | JSR-303规范 | Spring |
所在包 | javax.validation | org.springframework.validation.annotation |
是否支持分组 | 否 | 是 |
标记位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE,METHOD,PARAMETER |
是否支持嵌套校验 | 是 | 否 |
注:使用@Validated时,不会校验所标记对象的方法
二、组校验使用
下面以一个User类为例子,演示group validate使用
2.1 pom依赖
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.2 实体类
@Data
public class User {
@NotNull(groups = {UserGroup.Query.class, UserGroup.Update.class, UserGroup.Delete.class}, message = "id不能为空")
@Null(groups = {UserGroup.Insert.class, UserGroup.Login.class}, message = "不能传入id")
private Integer id;
@NotBlank(groups = {UserGroup.Insert.class, UserGroup.Login.class}, message = "账号不能为空")
@Size(min = 6, max = 16, groups = {UserGroup.Insert.class, UserGroup.Login.class, UserGroup.Update.class}, message = "账号必须在6-16位")
private String username;
@NotBlank(groups = {UserGroup.Insert.class, UserGroup.Login.class}, message = "密码不能为空")
@Size(min = 6, max = 16, groups = {UserGroup.Insert.class, UserGroup.Login.class, UserGroup.Update.class}, message = "密码必须在6-16位")
private String password;
@Range(min = 0, max = 150, groups = {UserGroup.Insert.class, UserGroup.Update.class}, message = "年龄不能超过0-150")
@NotNull(groups = UserGroup.Insert.class, message = "年龄不能为空")
private Integer age;
@Email(groups = {UserGroup.Insert.class, UserGroup.Update.class}, message = "邮箱格式不对")
@NotNull(groups = {UserGroup.Insert.class}, message = "邮箱不能为空")
private String email;
// 手机号正则表达式,见 https://github.com/VincentSit/ChinaMobilePhoneNumberRegex
@Pattern(regexp = "^(?:\\+?86)?1(?:3\\d{3}|5[^4\\D]\\d{2}|8\\d{3}|7(?:[0-35-9]\\d{2}|4(?:0\\d|1[0-2]|9\\d))|9[0-35-9]\\d{2}|6[2567]\\d{2}|4(?:(?:10|4[01])\\d{3}|[68]\\d{4}|[579]\\d{2}))\\d{6}$",
groups = {UserGroup.Insert.class,UserGroup.Update.class}, message = "手机号格式错误")
@NotNull(groups = {UserGroup.Insert.class}, message = "手机号不能为空")
private String mobile;
}
2.3 全局异常处理类
@RestControllerAdvice
public class GloadExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
List<ObjectError> objectErrors = bindingResult.getGlobalErrors();
String returnResult = Stream.concat(objectErrors.stream(), fieldErrors.stream()).map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(System.lineSeparator()));
return ResponseEntity.badRequest().body(returnResult);
}
}
2.4 配置类
@Configuration
public class MyConfiguration {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
//返回时间数据序列化
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
//接收时间数据反序列化
builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
};
}
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
2.5 组定义
public interface CommonGroup {
interface Query{
}
interface Insert {
}
interface Update {
}
interface Delete {
}
}
public interface UserGroup extends CommonGroup{
interface Login{};
}
2.6 Controller
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@PostMapping("/login")
ResponseEntity login(@RequestBody @Validated(UserGroup.Login.class) User user) {
return ResponseEntity.ok(user);
}
@PostMapping("/register")
ResponseEntity register(@RequestBody @Validated(UserGroup.Insert.class) User user) {
return ResponseEntity.ok(user);
}
@PostMapping("/update")
ResponseEntity update(@RequestBody @Validated(UserGroup.Update.class) User user) {
return ResponseEntity.ok(user);
}
@PostMapping("/query")
ResponseEntity query(@RequestBody @Validated(UserGroup.Query.class) User user) {
return ResponseEntity.ok(user);
}
@PostMapping("/delete")
ResponseEntity delete(@RequestBody @Validated(UserGroup.Delete.class) User user) {
return ResponseEntity.ok(user);
}
}
2.7 测试类
@SpringBootTest
class DemovalidApplicationTests {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
void testUserRegisterValid() throws Exception {
String url = "/user/register";
String requestJsonMissingUsername = "{\n" +
" \"password\": \"avebwew32f\",\n" +
" \"age\": 13,\n" +
" \"email\": \"163@163.com\",\n" +
" \"mobile\": \"13638888888\"\n" +
"}";
assertThat(this.getFromUrl(url, requestJsonMissingUsername)).contains("账号不能为空");
String requestJsonMissingPassword = "{\n" +
" \"username\": \"avebwew32f\",\n" +
" \"age\": 13,\n" +
" \"email\": \"163@163.com\",\n" +
" \"mobile\": \"13638888888\"\n" +
"}";
assertThat(this.getFromUrl(url, requestJsonMissingPassword)).contains("密码不能为空");
String requestJsonShortUsername = "{\n" +
" \"username\": \"ave\",\n" +
" \"password\": \"163@163.com\",\n" +
" \"age\": 13,\n" +
" \"email\": \"163@163.com\",\n" +
" \"mobile\": \"13638888888\"\n" +
"}";
assertThat(this.getFromUrl(url, requestJsonShortUsername)).contains("账号必须在6-16位");
}
private String getFromUrl(String url, String requestJson) throws Exception {
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post(url).contentType(MediaType.APPLICATION_JSON)
.content(requestJson)
.accept(MediaType.APPLICATION_JSON)
.characterEncoding("UTF-8"))
.andReturn();
return result.getResponse().getContentAsString(Charset.defaultCharset());
}
}
三、拓展使用
3.1 业务场景1:传入两个时间,startTime必须在endTime前面
有以下三个方案可供选择,各有优缺点
-
使用@AssertTrue
@Data public class Time { @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; @AssertTrue(message = "结束时间必须在开始时间后面") public boolean isValid() { return endTime.compareTo(startTime) >= 0; } }
@PostMapping("/time") ResponseEntity time(@RequestBody @Valid Time time) { return ResponseEntity.ok(time); }
使用这种方法就是简单,只需要加个方法就可以,但是它只能使用@Valid做校验,而使用组校验时则必须用到@Validated,所以当用到组校验时就不能用这种方法了
-
使用@ScriptAssert
@Data @ScriptAssert(lang = "javascript", alias = "_", script = "_.isValid()", message = "结束时间必须在开始时间后面") public class Time { @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; public boolean isValid() { return endTime.compareTo(startTime) >= 0; } }
@PostMapping("/time") ResponseEntity time(@RequestBody @Validated Time time) { return ResponseEntity.ok(time); }
@ScriptAssert在官方文档中给出的例子就是两个时间比对,但由于官方例子中时间类型是Date,而我用的是LocalDateTime,在js脚本中没有找到具体如何使用,后来发现可以调用内部的方法
-
自定义注解
@Target({ ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = TimeValidator.class) public @interface TimeValid { String message() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
public class TimeValidator implements ConstraintValidator<TimeValid, Time> { @Override public void initialize(TimeValid constraintAnnotation) { // 可以在这获取注解中的值 } @Override public boolean isValid(Time value, ConstraintValidatorContext context) { return value.getEndTime().isAfter(value.getStartTime()); } }
@Data @TimeValid(message = "开始时间必须在结束时间后面") public class Time { @NotNull(message = "开始时间不能为空") private LocalDateTime startTime; @NotNull(message = "结束时间不能为空") private LocalDateTime endTime; }
@PostMapping("/time") ResponseEntity time(@RequestBody @Valid Time time) { return ResponseEntity.ok(time); }
3.2 业务场景2:注册账号不能和已存在用户相同
这个可以使用自定义注解,只需要在isValid方法里访问数据库就能比对用户账号是否一致了
具体代码实现略
3.3 业务场景3:根据用户级别,校验角色个数
@Data
@GroupSequenceProvider(VipUserSequenceProvider.class)
public class VipUser {
@Max(1)
@Min(0)
@NotNull
private Integer level;
@NotNull(groups = {VipUserGroup.level1.class, VipUserGroup.level2.class})
@Size(min = 0, max = 2, groups = VipUserGroup.level1.class, message = "角色个数必须在0和2之间")
@Size(min = 0, max = 5, groups = VipUserGroup.level2.class, message = "角色个数必须在0和5之间")
private List<String> roles;
}
public interface VipUserGroup {
interface level1{}
interface level2{}
}
public class VipUserSequenceProvider implements DefaultGroupSequenceProvider<VipUser> {
@Override
public List<Class<?>> getValidationGroups(VipUser object) {
List<Class<?>> list = new ArrayList<>();
list.add(VipUser.class);
Optional.ofNullable(object).map(VipUser::getLevel)
.ifPresent(level -> list.add(level == 0 ? VipUserGroup.level1.class : VipUserGroup.level2.class) );
return list;
}
}
@PostMapping("/vip")
ResponseEntity vip(@RequestBody @Validated VipUser user) {
return ResponseEntity.ok(user);
}
参考
更多推荐
所有评论(0)