Spring Boot demo系列(四):Spring Web+Validation
2021.2.24 更新
1 概述
本文主要讲述了如何使用Hibernate Validator
以及@Valid/@Validate
注解。
2 校验
对于一个普通的Spring Boot
应用,经常可以在业务层看到以下类似的操作:
if(id == null)
{...}
if(username == null)
{...}
if(password == null)
{...}
这是很正常的,但是会显得代码很繁琐,一个更好的做法就是使用Hibernate Validator
。
3 Hibernate Validator
JSR
是Java Specification Requests
的缩写,意思是Java规范提案
,JSR-303
是Java EE 6
的一项子规范,叫作Bean Validation
,Hibernate Validator
是Bean Validator
的参考实现。其中JSR-303
内置constraint
如下:
@Null
:被注解元素必须为null
@NotNull
:必须不为null
@AssertTrue
/@AssertFalse
:必须为true
/false
@Min(value)
/@Max(value)
:指定最小值/最大值(可以相等)@DecimalMin(value)
/DecimalMax(value)
:指定最小值/最大值(不能相等)@Size(min,max)
:大小在给定范围@Digits(integer,fraction)
:将字符串转为浮点数,并且规定整数位数最大integer
位,小数位数最大fraction
位@Past
:必须是一个过去日期@Future
:必须是将来日期@Pattern
:必须符合正则表达式
其中Hibernate Validator
添加的constraint
如下:
@Email
:必须符合邮箱格式@Length(min,max)
:字符串长度范围@Range
:数字在指定范围
而在Spring
中,对Hibernate Validator
进行了二次封装,添加了自动校验并且可以把校验信息封装进特定的BindingResult
中。
4 基本使用
注解直接在实体类的对应字段加上即可:
@Setter
@Getter
public class User {
@NotBlack(message = "邮箱不能为空")
@Email(message = "邮箱非法")
private String email;
@NotBlack(message = "电话不能为空")
private String phone;
}
控制层:
@CrossOrigin(value = "http://localhost:3000")
@RestController
public class TestController {
@PostMapping("/test")
public boolean test(@RequestBody @Valid User user)
{
return true;
}
}
测试:
可以看到把phone
字段留空或者使用非法邮箱格式时直接抛出异常。
5 异常处理
前面说过校验出错会把异常放进BindingResult
中,具体的处理方法就是加上对应参数即可,控制层修改如下:
@PostMapping("/test")
public boolean test(@RequestBody @Valid User user, BindingResult result)
{
if(result.hasErrors())
result.getAllErrors().forEach(System.out::println);
return true;
}
可以通过getAllErrors
获取所有的错误,这样就可以对具体错误进行处理了。
6 快速失败模式
Hibernate Validator
有两种校验模式:
- 普通模式:默认,检验所有属性,然后返回所有验证失败信息
- 快速失败模式:只要有一个验证失败便返回
使用快速失败模式需要通过HiberateValidateConfiguration
以及ValidateFactory
创建Validator
,并且使用Validator.validate
手动校验,首先可以添加一个生成Validator
的类:
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Configuration;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
@Configuration
public class FailFastValidator {
private final Validator validator;
public FailFastValidator()
{
validator = Validation
.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
public Set<ConstraintViolation<User>> validate(User user)
{
return validator.validate(user);
}
}
接着修改控制层,去掉User
上的@Valid
,同时注入validator
进行手动校验:
import com.example.demo.entity.User;
import com.example.demo.failfast.FailFastValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.ConstraintViolation;
import java.util.Set;
@CrossOrigin(value = "http://localhost:3000")
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {
private final FailFastValidator validator;
@PostMapping("/test")
public boolean test(@RequestBody User user)
{
Set<ConstraintViolation<User>> message = validator.validate(user);
message.forEach(System.out::println);
return true;
}
}
这样一旦校验失败便会返回,而不是校验完所有的字段记录所有错误信息再返回。
7 @Valid
与@Validated
@Valid
位于javax.validation
下,而@Validated
位于org.springframework.validation.annotation
下,是@Valid
的一次封装,在@Valid
的基础上,增加了分组以及组序列的功能,下面分别进行介绍。
7.1 分组
当不同的情况下需要不同的校验方式时,可以使用分组功能,比如在某种情况下需要注册时不需要校验邮箱,而修改信息的时候需要校验邮箱,则实体类可以如下设计:
@Setter
@Getter
public class User {
@NotBlank(message = "邮箱不能为空",groups = GroupB.class)
@Email(message = "邮箱非法",groups = GroupB.class)
private String email;
@NotBlank(message = "电话不能为空",groups = {GroupA.class,GroupB.class})
private String phone;
public interface GroupA{}
public interface GroupB{}
}
接着修改控制层,并使用@Validate
代替原来的@Valid
:
public class TestController {
@PostMapping("/test")
public boolean test(@RequestBody @Validated(User.GroupA.class) User user)
{
return true;
}
}
在GroupA
的情况下,只校验电话,测试如下:
而如果修改为GroupB
:
public boolean test(@RequestBody @Validated(User.GroupB.class) User user)
这样就邮箱与电话都校验:
7.2 组序列
默认情况下,校验是无序的,也就是说,对于下面的实体类:
public class User {
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱非法")
private String email;
@NotBlank(message = "电话不能为空")
private String phone;
}
先校验哪一个并没有固定顺序,修改控制层如下,返回错误信息:
@PostMapping("/test")
public String test(@RequestBody @Validated User user, BindingResult result)
{
for (ObjectError allError : result.getAllErrors()) {
return allError.getDefaultMessage();
}
return "true";
}
可以看到两次测试的结果不同:
因为顺序不固定,而如果指定了顺序:
public class User {
@NotBlank(message = "邮箱不能为空",groups = First.class)
@Email(message = "邮箱非法",groups = First.class)
private String email;
@NotBlank(message = "电话不能为空",groups = Third.class)
private String phone;
public interface First{}
public interface Second{}
public interface Third{}
@GroupSequence({First.class,Second.class,Third.class})
public interface Group{}
}
同时控制层指定顺序:
public String test(@RequestBody @Validated(User.Group.class) User user, BindingResult result)
这样就一定会先校验First
,也就是先校验邮箱是否为空。
8 自定义注解
尽管使用上面的各种注解已经能解决很多情况了,但是对于一些特定的情况,需要一些特别的校验,而自带的注解不能满足,这时就需要自定义注解了,比如上面的电话字段,国内的是11位的,而且需要符合某些条件(比如默认区号+86
等),下面就自定义一个专门用于手机号码的注解:
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
String message() default "请使用合法的手机号码";
Class<?> [] groups() default {};
Class<? extends Payload> [] payload() default {};
}
同时定义一个验证类:
public class PhoneValidator implements ConstraintValidator<Phone,String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if(s.length() != 11)
return false;
return Pattern.matches("^((17[0-9])|(14[0-9])|(13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$",s);
}
}
接着修改实体类,加上注解即可:
@Phone
@NotBlank(message = "电话不能为空")
private String phone;
测试如下,可以看到虽然是11位了,但是格式非法,因此返回相应信息:
9 来点AOP
默认情况下Hibernate Validator
不是快速失败模式的,但是如果配成快速失败模式就不能用@Validate
了,需要手动实例化一个Validator
,这是一种很麻烦的操作,虽然说可以利用组序列“伪装”成一个快速失败模式,但是有没有更好的解决办法呢?
有!
就是。。。
自己动手使用AOP
实现校验。
9.1 依赖
AOP
这种高级的东西当然是用别人的轮子啊:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
9.2 验证注解
首先自定义一个验证注解,这个注解的作用类似@Validate
:
public @interface UserValidate {}
9.3 字段验证
自定义一些类似@NotEmpty
等的注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyEmail {
String message() default "邮箱不能为空,且需要一个合法的邮箱";
int order();
}
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPhone {
String message() default "电话不能为空,且需要一个合法的电话";
int order();
}
9.4 定义验证器
@Aspect
@Component
public class UserValidator {
@Pointcut("@annotation(com.example.demo.aop.UserValidate)")
public void userValidate(){}
@Before("userValidate()")
public void validate(JoinPoint point) throws EmailException, PhoneException, IllegalAccessException {
User user = (User)point.getArgs()[0];
TreeMap<Integer,Annotation> treeMap = new TreeMap<>();
HashMap<Integer,Object> allFields = new HashMap<>();
for (Field field : user.getClass().getDeclaredFields()) {
field.setAccessible(true);
for (Annotation annotation : field.getAnnotations()) {
if(annotation.annotationType() == MyEmail.class)
{
treeMap.put(((MyEmail)annotation).order(),annotation);
allFields.put(((MyEmail)annotation).order(),field.get(user));
}
else if(annotation.annotationType() == MyPhone.class)
{
treeMap.put(((MyPhone)annotation).order(),annotation);
allFields.put(((MyPhone)annotation).order(),field.get(user));
}
}
}
for (Map.Entry<Integer, Annotation> entry : treeMap.entrySet()) {
Class<? extends Annotation> type = entry.getValue().annotationType();
if(type == MyEmail.class)
{
validateEmail((String)allFields.get(entry.getKey()));
}
else if(type == MyPhone.class)
{
validatePhone((String)allFields.get(entry.getKey()));
}
}
}
private static void validateEmail(String s) throws EmailException
{
throw new EmailException();
}
private static void validatePhone(String s) throws PhoneException
{
throw new PhoneException();
}
}
这个是实现校验的核心,首先定义一个切点:
@Pointcut("@annotation(com.example.demo.aop.UserValidate)")
public void userValidate(){}
该切点应用在注解@UserValidate
上,接着定义验证方法validate
,首先通过切点获取其中的参数以及参数中的注解,并且模拟了组序列,先使用TreeMap
进行排序,最后针对遍历该TreeMap
,对不同的注解分别调用不同的方法校验。
实体类简单定义顺序即可:
public class User {
@MyEmail(order = 2)
private String email;
@MyPhone(order = 1)
private String phone;
}
控制类中的注解定义在方法上:
@PostMapping("/test")
@UserValidate
public String test(@RequestBody User user)
{
return "true";
}
这样就自定义实现了一个简单的JSR-303
了。
当然该方法还有很多的不足,比如需要配合全局异常处理,不然的话会直接抛出异常:
前端也是直接返回异常:
一般情况下还是推荐使用Hibernate Validator
,应对常规情况足够了。
10 参考源码
Java
版:
Kotlin
版:
Spring Boot demo系列(四):Spring Web+Validation的更多相关文章
- Spring Boot 项目学习 (四) Spring Boot整合Swagger2自动生成API文档
0 引言 在做服务端开发的时候,难免会涉及到API 接口文档的编写,可以经历过手写API 文档的过程,就会发现,一个自动生成API文档可以提高多少的效率. 以下列举几个手写API 文档的痛点: 文档需 ...
- Spring Boot demo系列(二):简单三层架构Web应用
2021.2.24 更新 1 概述 这是Spring Boot的第二个Demo,一个只有三层架构的极简Web应用,持久层使用的是MyBatis. 2 架构 一个最简单的Spring Boot Web应 ...
- Spring Boot进阶系列四
这边文章主要实战如何使用Mybatis以及整合Redis缓存,数据第一次读取从数据库,后续的访问则从缓存中读取数据. 1.0 Mybatis MyBatis 是支持定制化 SQL.存储过程以及高级映射 ...
- Spring Boot demo系列(五):Docker部署
2021.2.24 更新 1 概述 本文讲述了如何使用Docker部署Spring Boot应用,首先介绍了Docker的安装过程,接着介绍了Docker的一些基础知识,最后讲述了Dockerfile ...
- Spring Boot demo系列(一):Hello World
2021.2.24 更新 1 新建工程 打开IDEA选择新建工程并选择Spring Initializer: 可以在Project JDK处选择JDK版本,下一步是选择包名,语言,构建工具以及打包工具 ...
- Spring Boot demo系列(十):Redis缓存
1 概述 本文演示了如何在Spring Boot中将Redis作为缓存使用,具体的内容包括: 环境搭建 项目搭建 测试 2 环境 Redis MySQL MyBatis Plus 3 Redis安装 ...
- Spring Boot demo系列(九):Jasypt
2021.2.24 更新 1 概述 Jasypt是一个加密库,Github上有一个集成了Jasypt的Spring Boot库,叫jasypt-spring-boot,本文演示了如何使用该库对配置文件 ...
- Spring Boot demo系列(六):HTTPS
2021.2.24 更新 1 概述 本文演示了如何给Spring Boot应用加上HTTPS的过程. 2 证书 虽然证书能自己生成,使用JDK自带的keytool即可,但是生产环境是不可能使用自己生成 ...
- Spring Boot 应用系列 2 -- Spring Boot 2 整合MyBatis和Druid
本系列将分别演示单数据源和多数据源的配置和应用,本文先演示单数据源(MySQL)的配置. 1. pom.xml文件配置 需要在dependencies节点添加: <!-- MySQL --> ...
随机推荐
- JVM系列(四):java方法的查找过程实现
经过前面几章的简单介绍,我们已经大致了解了jvm的启动框架和执行流程了.不过,这些都是些无关痛痒的问题,几行文字描述一下即可. 所以,今天我们从另一个角度来讲解jvm的一些东西,以便可以更多一点认知. ...
- 【SpringMVC】 4.3 拦截器
SpringMVC学习记录 注意:以下内容是学习 北京动力节点 的SpringMVC视频后所记录的笔记.源码以及个人的理解等,记录下来仅供学习 第4章 SpringMVC 核心技术 4.3 拦截器 ...
- Basic认证时添加请求头
http Basic认证 http协议定义的一种认证方式,将客户端id和客户端密码按照"客户端ID:客户端密码"的格式拼接,并用base64编 码,放在header中请求服务端, ...
- SpringBoot整合MyBatis-Plus框架(代码生成器)
MyBatis-Plus的简介 Mybatis-Plus(简称MP)是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发.提高效率而生. 代码生成器 通用的CU ...
- WPF -- DataTemplate与ControlTemplate结合使用
如深入浅出WPF中的描述,DataTemplate为数据的外衣,ControlTemplate为控件的外衣.ControlTemplate控制控件的样式,DataTemplate控制数据显示的样式,D ...
- [源码解析] 消息队列 Kombu 之 基本架构
[源码解析] 消息队列 Kombu 之 基本架构 目录 [源码解析] 消息队列 Kombu 之 基本架构 0x00 摘要 0x01 AMQP 1.1 基本概念 1.2 工作过程 0x02 Poll系列 ...
- 快速电路仿真器(FastSPICE)中的高性能矩阵向量运算实现
今年10-11月份参加了EDA2020(第二届)集成电路EDA设计精英挑战赛,通过了初赛,并参加了总决赛,最后拿了一个三等奖,虽然成绩不是很好,但是想把自己做的分享一下,我所做的题目是概伦电子出的F题 ...
- POJ-3468(线段树+区间更新+区间查询)
A Simple Problem With Integers POJ-3468 这题是区间更新的模板题,也只是区间更新和区间查询和的简单使用. 代码中需要注意的点我都已经标注出来了,容易搞混的就是up ...
- pytorch(01)环境配置及安装
pytorch pytorch定位:深度学习框架 人工智能:多领域交叉科学技术 机器学习:计算机智能决策算法 深度学习:高效的机器学习算法 pytorch实现模型训练需要5个模块 数据 将数据从硬盘读 ...
- Excel_不打开文件进行跨工作簿查询
在使用Excel时,我们经常会遇到这种问题,我的数据源在表1里面,但是我要在表2里面做报表,用lookup和offset等公式都需要打开表1操作,否则就会报错.那么有没有办法在不打开表1的情况下在表2 ...