1. 简介

我们都知道前台的验证只是为了满足界面的友好性、客户体验性等等。但是如果仅靠前端进行数据合法性校验,是远远不够的。因为非法用户可能会直接从客户端获取到请求地址进行非法请求,所以后台的校验是必须的;特别是应用如果不允许输入空值,对数据的合法行有要求的情况下。

2. 开撸

2.1 项目结构

结构说明:

  1. ├── java
  2.    └── com
  3.    └── ldx
  4.    └── valid
  5.    ├── ValidApplication.java # 启动类
  6.    ├── annotation
  7.       └── Phone.java # 自定义验证注解
  8.    ├── config
  9.       └── ValidatorConfig.java # 表单验证配置类
  10.    ├── controller
  11.       └── SysUserController.java # 用户管理控制器
  12.    ├── exception
  13.       ├── BusinessException.java # 业务异常类
  14.       └── GlobalExceptionHandler.java # 统一异常处理类
  15.    ├── model
  16.       ├── SysUser.java # 用户信息实体
  17.       └── ValidationInterface.java # 表单验证的通用分组接口
  18.    ├── util
  19.       └── CommonResult.java # 接口返回封装类
  20.    └── validation
  21.    └── PhoneValidator.java #自定义验证实现类
  22. └── resources
  23. ├── application.yaml # 配置文件
  24. └── messages
  25. └── validation
  26. └── messages.properties # 自定义验证信息源

2.1 quick start

2.1.1 导入依赖

创建springboot项目导入以下依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>2.5.3</version>
  9. <relativePath/> <!-- lookup parent from repository -->
  10. </parent>
  11. <groupId>com.ldx</groupId>
  12. <artifactId>valid</artifactId>
  13. <version>0.0.1-SNAPSHOT</version>
  14. <name>valid</name>
  15. <description>表单验证demo</description>
  16. <properties>
  17. <java.version>1.8</java.version>
  18. </properties>
  19. <dependencies>
  20. <!-- web支持 -->
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-web</artifactId>
  24. </dependency>
  25. <!-- 表单验证 -->
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-validation</artifactId>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.projectlombok</groupId>
  32. <artifactId>lombok</artifactId>
  33. <optional>true</optional>
  34. </dependency>
  35. </dependencies>
  36. <build>
  37. <plugins>
  38. <plugin>
  39. <groupId>org.springframework.boot</groupId>
  40. <artifactId>spring-boot-maven-plugin</artifactId>
  41. <configuration>
  42. <excludes>
  43. <exclude>
  44. <groupId>org.projectlombok</groupId>
  45. <artifactId>lombok</artifactId>
  46. </exclude>
  47. </excludes>
  48. </configuration>
  49. </plugin>
  50. </plugins>
  51. </build>
  52. </project>

2.1.2 添加配置类

创建表单验证配置类,配置快速校验,不用等全部的参数校验完,只要有错,马上抛出。

  1. import org.hibernate.validator.HibernateValidator;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
  5. import javax.validation.Validation;
  6. import javax.validation.Validator;
  7. import javax.validation.ValidatorFactory;
  8. /**
  9. * 配置 Hibernate 参数校验
  10. * @author ludangxin
  11. * @date 2021/8/5
  12. */
  13. @Configuration
  14. public class ValidatorConfig {
  15. @Bean
  16. public MethodValidationPostProcessor methodValidationPostProcessor() {
  17. MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
  18. //快速校验,只要有错马上返回
  19. postProcessor.setValidator(validator());
  20. return postProcessor;
  21. }
  22. @Bean
  23. public Validator validator() {
  24. ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
  25. .configure()
  26. .addProperty("hibernate.validator.fail_fast", "true")
  27. .buildValidatorFactory();
  28. return validatorFactory.getValidator();
  29. }
  30. }

2.1.3 添加实体类

  1. import lombok.*;
  2. import javax.validation.constraints.*;
  3. import java.io.Serializable;
  4. /**
  5. * 用户信息管理
  6. * @author ludangxin
  7. * @date 2021/8/5
  8. */
  9. @Data
  10. public class SysUser implements Serializable {
  11. private static final long serialVersionUID = 1L;
  12. /**
  13. * 主键
  14. */
  15. private Long id;
  16. /**
  17. * 用户名
  18. */
  19. @NotEmpty(message = "用户名称不能为空")
  20. private String username;
  21. /**
  22. * 密码
  23. */
  24. @Size(min = 6, max = 16, message = "密码长度必须在{min}-{max}之间")
  25. private String password = "123456";
  26. /**
  27. * 邮箱地址
  28. */
  29. @Email(message = "邮箱地址不合法")
  30. @NotEmpty(message = "邮箱不能为空")
  31. private String email;
  32. /**
  33. * 电话
  34. */
  35. @Size(min = 11, max = 11, message = "手机号不合法")
  36. @NotEmpty(message = "手机号不能为空")
  37. private String phone;
  38. }

2.1.4 接口返回封装类

  1. import lombok.Data;
  2. import lombok.NoArgsConstructor;
  3. /**
  4. * 操作消息提醒
  5. * @author ludangxin
  6. * @date 2021/8/5
  7. */
  8. @Data
  9. @NoArgsConstructor
  10. public class CommonResult {
  11. /** 状态码 */
  12. private int code;
  13. /** 返回内容 */
  14. private String msg;
  15. /** 数据对象 */
  16. private Object data;
  17. /**
  18. * 初始化一个新创建的 CommonResult 对象
  19. * @param type 状态类型
  20. * @param msg 返回内容
  21. */
  22. public CommonResult(Type type, String msg) {
  23. this.code = type.value;
  24. this.msg = msg;
  25. }
  26. /**
  27. * 初始化一个新创建的 CommonResult 对象
  28. * @param type 状态类型
  29. * @param msg 返回内容
  30. * @param data 数据对象
  31. */
  32. public CommonResult(Type type, String msg, Object data) {
  33. this.code = type.value;
  34. this.msg = msg;
  35. if (data != null) {
  36. this.data = data;
  37. }
  38. }
  39. /**
  40. * 返回成功消息
  41. * @return 成功消息
  42. */
  43. public static CommonResult success() {
  44. return CommonResult.success("操作成功");
  45. }
  46. /**
  47. * 返回成功数据
  48. * @return 成功消息
  49. */
  50. public static CommonResult success(Object data) {
  51. return CommonResult.success("操作成功", data);
  52. }
  53. /**
  54. * 返回成功消息
  55. * @param msg 返回内容
  56. * @return 成功消息
  57. */
  58. public static CommonResult success(String msg) {
  59. return CommonResult.success(msg, null);
  60. }
  61. /**
  62. * 返回成功消息
  63. * @param msg 返回内容
  64. * @param data 数据对象
  65. * @return 成功消息
  66. */
  67. public static CommonResult success(String msg, Object data) {
  68. return new CommonResult(Type.SUCCESS, msg, data);
  69. }
  70. /**
  71. * 返回警告消息
  72. * @param msg 返回内容
  73. * @return 警告消息
  74. */
  75. public static CommonResult warn(String msg) {
  76. return CommonResult.warn(msg, null);
  77. }
  78. /**
  79. * 返回警告消息
  80. * @param msg 返回内容
  81. * @param data 数据对象
  82. * @return 警告消息
  83. */
  84. public static CommonResult warn(String msg, Object data) {
  85. return new CommonResult(Type.WARN, msg, data);
  86. }
  87. /**
  88. * 返回错误消息
  89. * @return 错误信息
  90. */
  91. public static CommonResult error() {
  92. return CommonResult.error("操作失败");
  93. }
  94. /**
  95. * 返回错误消息
  96. * @param msg 返回内容
  97. * @return 错误消息
  98. */
  99. public static CommonResult error(String msg) {
  100. return CommonResult.error(msg, null);
  101. }
  102. /**
  103. * 返回错误消息
  104. * @param msg 返回内容
  105. * @param data 数据对象
  106. * @return 错误消息
  107. */
  108. public static CommonResult error(String msg, Object data) {
  109. return new CommonResult(Type.ERROR, msg, data);
  110. }
  111. /**
  112. * 状态类型
  113. */
  114. public enum Type {
  115. /** 成功 */
  116. SUCCESS(200),
  117. /** 警告 */
  118. WARN(301),
  119. /** 错误 */
  120. ERROR(500);
  121. private final int value;
  122. Type(int value){
  123. this.value = value;
  124. }
  125. public int value() {
  126. return this.value;
  127. }
  128. }
  129. }

2.1.5 控制器

使用@Validated注解标识需要验证的类,使用BindingResult类接收错误信息

  1. import com.ldx.valid.exception.BusinessException;
  2. import com.ldx.valid.model.SysUser;
  3. import com.ldx.valid.model.ValidationInterface;
  4. import com.ldx.valid.util.CommonResult;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.validation.BindingResult;
  7. import org.springframework.validation.FieldError;
  8. import org.springframework.validation.annotation.Validated;
  9. import org.springframework.web.bind.annotation.*;
  10. import javax.validation.constraints.NotEmpty;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. import java.util.Objects;
  14. import java.util.stream.Collectors;
  15. /**
  16. * 用户管理控制器
  17. *
  18. * @author ludangxin
  19. * @date 2021/8/5
  20. */
  21. @Slf4j
  22. @RestController
  23. @RequestMapping("sys/user")
  24. public class SysUserController {
  25. private static final List<SysUser> USERS = new ArrayList<>();
  26. // 数据初始化
  27. static {
  28. SysUser user = new SysUser();
  29. user.setId(1L);
  30. user.setUsername("zhangsan");
  31. user.setPhone("13566666666");
  32. user.setEmail("example@qq.com");
  33. USERS.add(user);
  34. SysUser user1 = new SysUser();
  35. user1.setId(2L);
  36. user1.setUsername("lisi");
  37. user1.setPhone("13588888888");
  38. user1.setEmail("example1@qq.com");
  39. USERS.add(user1);
  40. }
  41. /**
  42. * 新增用户信息
  43. * @param sysUser 用户信息
  44. * @return 成功标识
  45. */
  46. @PostMapping
  47. public CommonResult add(@Validated @RequestBody SysUser sysUser, BindingResult result) {
  48. FieldError fieldError = result.getFieldError();
  49. if(Objects.nonNull(fieldError)) {
  50. String field = fieldError.getField();
  51. Object rejectedValue = fieldError.getRejectedValue();
  52. String msg = "[" + fieldError.getDefaultMessage() + "]";
  53. log.error("{}:字段=={}\t值=={}", msg, field, rejectedValue);
  54. return CommonResult.error(msg);
  55. }
  56. USERS.add(sysUser);
  57. return CommonResult.success("新增成功");
  58. }
  59. }

2.1.5 启动测试

新增时,故意将email信息填错,测试结果符合预期。

log日志:

  1. [nio-8080-exec-9] c.l.valid.controller.SysUserController : [邮箱地址不合法]:字段==email 值==123

3. 分组校验

groups是用来干什么的?

  1. 因为一个实体不可能只干一种操作,一个实体必然存在增删改查操作,那么问题就来了
  2. 如果我要根据id进行更新操作,那么id肯定不能为空
  3. 这时候我还要进行新增操作,因为id是新增数据库操作才产生的,接受数据的时候我肯定是没有id
  4. 所以就产生矛盾了
  5. 那么groups这个参数就起作用了,它可以表示我这个注解属于哪个组,这样就解决这个尴尬的问题了。

当在controller中校验表单数据时,如果使用了groups,那么没有在这个分组下的属性是不会校验的

3.1 添加分组接口

  1. /**
  2. * 用于表单验证的通用分组接口
  3. * @author ludangxin
  4. * @date 2021/8/5
  5. */
  6. public interface ValidationInterface {
  7. /**
  8. * 新增分组
  9. */
  10. interface add{}
  11. /**
  12. * 删除分组
  13. */
  14. interface delete{}
  15. /**
  16. * 查询分组
  17. */
  18. interface select{}
  19. /**
  20. * 更新分组
  21. */
  22. interface update{}
  23. }

如果还有其它特殊的分组要求 直接在DO中创建interface即可

例:如果还有个需要验证username 和 password(只有这两个参数) 的 select操作

直接在SysUser中创建UsernamePasswordValidView 的接口即可

3.2 修改实体类

将属性进行分组

  1. import lombok.Data;
  2. import javax.validation.constraints.Email;
  3. import javax.validation.constraints.NotEmpty;
  4. import javax.validation.constraints.NotNull;
  5. import javax.validation.constraints.Size;
  6. import java.io.Serializable;
  7. /**
  8. * 用户信息管理
  9. * @author ludangxin
  10. * @date 2021/8/5
  11. */
  12. @Data
  13. public class SysUser implements Serializable {
  14. private static final long serialVersionUID = 1L;
  15. /**
  16. * 主键
  17. */
  18. @NotNull(message = "id不能为空", groups = {ValidationInterface.update.class})
  19. private Long id;
  20. /**
  21. * 用户名
  22. */
  23. @NotEmpty(message = "用户名称不能为空", groups = {
  24. ValidationInterface.update.class,
  25. ValidationInterface.add.class})
  26. private String username;
  27. /**
  28. * 密码
  29. */
  30. @Size(min = 6, max = 16, message = "密码长度必须在{min}-{max}之间", groups = {
  31. ValidationInterface.update.class,
  32. ValidationInterface.add.class})
  33. private String password = "123456";
  34. /**
  35. * 邮箱地址
  36. */
  37. @Email(message = "邮箱地址不合法", groups = {
  38. ValidationInterface.update.class,
  39. ValidationInterface.add.class,
  40. ValidationInterface.select.class})
  41. @NotEmpty(message = "邮箱不能为空", groups = ValidationInterface.add.class)
  42. private String email;
  43. /**
  44. * 电话
  45. */
  46. @Size(min = 11, max = 11, message = "手机号不合法", groups = {
  47. ValidationInterface.update.class,
  48. ValidationInterface.add.class,
  49. ValidationInterface.select.class})
  50. @NotEmpty(message = "手机号不能为空",groups = {ValidationInterface.add.class})
  51. private String phone;
  52. }

3.3 修改控制器

添加操作方法,并且方法形参上指定验证的分组

  1. import com.ldx.valid.exception.BusinessException;
  2. import com.ldx.valid.model.SysUser;
  3. import com.ldx.valid.model.ValidationInterface;
  4. import com.ldx.valid.util.CommonResult;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.validation.BindingResult;
  7. import org.springframework.validation.FieldError;
  8. import org.springframework.validation.annotation.Validated;
  9. import org.springframework.web.bind.annotation.*;
  10. import javax.validation.constraints.NotEmpty;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. import java.util.Objects;
  14. import java.util.stream.Collectors;
  15. /**
  16. * 用户管理控制器
  17. * @author ludangxin
  18. * @date 2021/8/5
  19. */
  20. @Slf4j
  21. @RestController
  22. @RequestMapping("sys/user")
  23. public class SysUserController {
  24. private static final List<SysUser> USERS = new ArrayList<>();
  25. // 数据初始化
  26. static {
  27. SysUser user = new SysUser();
  28. user.setId(1L);
  29. user.setUsername("zhangsan");
  30. user.setPhone("13566666666");
  31. user.setEmail("example@qq.com");
  32. USERS.add(user);
  33. SysUser user1 = new SysUser();
  34. user1.setId(2L);
  35. user1.setUsername("lisi");
  36. user1.setPhone("13588888888");
  37. user1.setEmail("example1@qq.com");
  38. USERS.add(user1);
  39. }
  40. /**
  41. * 根据手机号或邮箱查询用户信息
  42. * @param sysUser 查询条件
  43. * @return 用户list
  44. */
  45. @GetMapping
  46. public CommonResult queryList(@Validated(value = ValidationInterface.select.class) SysUser sysUser,
  47. BindingResult result)
  48. {
  49. FieldError fieldError = result.getFieldError();
  50. if(Objects.nonNull(fieldError)) {
  51. return CommonResult.error(getErrorMsg(fieldError));
  52. }
  53. String phone = sysUser.getPhone();
  54. String email = sysUser.getEmail();
  55. if(phone == null && email == null) {
  56. return CommonResult.success(USERS);
  57. }
  58. List<SysUser> queryResult = USERS.stream()
  59. .filter(obj -> obj.getPhone().equals(phone) || obj.getEmail().equals(email))
  60. .collect(Collectors.toList());
  61. return CommonResult.success(queryResult);
  62. }
  63. /**
  64. * 新增用户信息
  65. * @param sysUser 用户信息
  66. * @return 成功标识
  67. */
  68. @PostMapping
  69. public CommonResult add(@Validated(value = ValidationInterface.add.class)
  70. @RequestBody SysUser sysUser,
  71. BindingResult result)
  72. {
  73. FieldError fieldError = result.getFieldError();
  74. if(Objects.nonNull(fieldError)) {
  75. return CommonResult.error(getErrorMsg(fieldError));
  76. }
  77. Long id = (long) (USERS.size() + 1);
  78. sysUser.setId(id);
  79. USERS.add(sysUser);
  80. return CommonResult.success("新增成功");
  81. }
  82. /**
  83. * 根据Id更新用户信息
  84. * @param sysUser 用户信息
  85. * @return 成功标识
  86. */
  87. @PutMapping("{id}")
  88. public CommonResult updateById(@PathVariable("id") Long id,
  89. @Validated(value = ValidationInterface.update.class)
  90. @RequestBody SysUser sysUser,
  91. BindingResult result)
  92. {
  93. FieldError fieldError = result.getFieldError();
  94. if(Objects.nonNull(fieldError)) {
  95. return CommonResult.error(getErrorMsg(fieldError));
  96. }
  97. for(int i = 0; i < USERS.size(); i++) {
  98. if(USERS.get(i).getId().equals(id)) {
  99. USERS.set(i,sysUser);
  100. }
  101. }
  102. return CommonResult.success("更新成功");
  103. }
  104. /**
  105. * 根据Id删除用户信息
  106. * @param id 主键
  107. * @return 成功标识
  108. */
  109. @DeleteMapping("{id}")
  110. public CommonResult deleteById(@PathVariable Long id) {
  111. USERS.removeIf(obj -> obj.getId().equals(id));
  112. return CommonResult.success("删除成功");
  113. }
  114. /**
  115. * 获取表单验证错误msg
  116. * @param fieldError 报错字段
  117. * @return msg
  118. */
  119. public String getErrorMsg(FieldError fieldError) {
  120. String field = fieldError.getField();
  121. Object rejectedValue = fieldError.getRejectedValue();
  122. String msg = "[" + fieldError.getDefaultMessage() + "]";
  123. log.error("{}:字段=={}\t值=={}", msg, field, rejectedValue);
  124. return msg;
  125. }
  126. }

3.4 启动测试

查询:

​ 输入不合法手机号

新增:

​ 正常情况

​ 去掉邮箱

修改:

​ 去掉id

删除:

4. 自定义验证

很多时候框架提供的功能并不能满足我们的业务场景,这时我们需要自定义一些验证规则来完成验证。

4.1 添加注解

  1. import com.ldx.valid.validation.PhoneValidator;
  2. import javax.validation.Constraint;
  3. import javax.validation.Payload;
  4. import java.lang.annotation.Documented;
  5. import java.lang.annotation.Retention;
  6. import java.lang.annotation.Target;
  7. import static java.lang.annotation.ElementType.*;
  8. import static java.lang.annotation.ElementType.TYPE_USE;
  9. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  10. /**
  11. * 验证手机号是否合法
  12. * @author ludangxin
  13. * @date 2021/8/7
  14. */
  15. @Documented
  16. @Constraint(validatedBy = {PhoneValidator.class})
  17. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
  18. @Retention(RUNTIME)
  19. public @interface Phone {
  20. //默认错误消息
  21. String message() default "不是一个合法的手机号";
  22. //分组
  23. Class<?>[] groups() default {};
  24. //载荷 将某些元数据信息与给定的注解声明相关联的方法
  25. Class<? extends Payload>[] payload() default {};
  26. //指定多个时使用
  27. @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
  28. @Retention(RUNTIME)
  29. @Documented
  30. @interface List {
  31. Phone[] value();
  32. }
  33. }

4.2 编写验证逻辑

  1. import javax.validation.ConstraintValidator;
  2. import com.ldx.valid.annotation.Phone;
  3. import javax.validation.ConstraintValidatorContext;
  4. import java.util.Objects;
  5. import java.util.regex.Pattern;
  6. /**
  7. * 手机号校验器
  8. * @author ludangxin
  9. * @date 2021/8/7
  10. */
  11. public class PhoneValidator implements ConstraintValidator<Phone, String> {
  12. /**
  13. * 手机号正则表达式
  14. */
  15. private static final String REGEXP_PHONE = "^1[3456789]\\d{9}$";
  16. @Override
  17. public boolean isValid(String value, ConstraintValidatorContext context) {
  18. if(Objects.isNull(value)) {
  19. return true;
  20. }
  21. return Pattern.matches(REGEXP_PHONE, value);
  22. }
  23. }

4.3 修改实体

SysUser.phone 属性添加注解@Phone

  1. @Phone(groups = {
  2. ValidationInterface.update.class,
  3. ValidationInterface.add.class,
  4. ValidationInterface.select.class})
  5. @NotEmpty(message = "手机号不能为空", groups = {ValidationInterface.add.class})
  6. private String phone;

4.4 启动测试

输入错误的手机号进行测试

4.5 @Pattern

当然validation也提供了基于正则匹配的注解@Pattern

  1. @Pattern(message = "手机号不合法", regexp = "^1[3456789]\\d{9}$", groups = {ValidationInterface.add.class})
  2. @NotEmpty(message = "手机号不能为空", groups = {ValidationInterface.add.class})
  3. private String phone;

注意是javax.validation.constraints包下的

测试

5. 调用过程验证

有的时候我们在参数传输过程中需要对传入的对象做参数验证,但是上面介绍的都是对参数绑定时的验证,那能不能使用validation进行验证呢?

答案肯定是可以的。

5.1 使用 spring bean

5.1.1 注入validator

bean validator 是我们在config文件中定义的bean,如果使用了springboot默认的配置ValidationAutoConfiguration::defaultValidator(),直接注入bean name defaultValidator即可

  1. @Resource(name = "validator")
  2. javax.validation.Validator validator;

5.1.2 定义验证方法

  1. public void validateParams(SysUser user) {
  2. validator.validate(user, ValidationInterface.add.class).stream().findFirst().ifPresent(obj -> {
  3. String objName = obj.getRootBean().getClass().getSimpleName();
  4. String fieldName = obj.getPropertyPath().toString();
  5. Object val = obj.getInvalidValue();
  6. String msg = obj.getMessage();
  7. String errMsg = MessageFormat.format(msg + ":对象:{0},字段:{1},值:{2}", objName, fieldName, val);
  8. throw new RuntimeException(errMsg);
  9. });

5.1.2 启动验证

调用新增方法,通过新增方法调用validateParams方法

报错日志如下

  1. java.lang.RuntimeException: 手机号不合法:对象:SysUser,字段:phone,值:135999

5.2 非spring环境验证

5.2.1 定义验证方法

直接获取默认的工厂类,然后获取验证对象进行验证

  1. public static void main(String[] args) {
  2. ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
  3. Validator validator = vf.getValidator();
  4. SysUser user = new SysUser();
  5. user.setId(1L);
  6. user.setUsername("zhangsan");
  7. user.setPhone("1356666");
  8. user.setEmail("example@qq.com");
  9. validator.validate(user, ValidationInterface.add.class).stream().findFirst().ifPresent(obj -> {
  10. String objName = obj.getRootBean().getClass().getSimpleName();
  11. String fieldName = obj.getPropertyPath().toString();
  12. Object val = obj.getInvalidValue();
  13. String msg = obj.getMessage();
  14. String errMsg = MessageFormat.format(msg + ":对象:{0},字段:{1},值:{2}", objName, fieldName, val);
  15. throw new RuntimeException(errMsg);
  16. });
  17. }

5.2.2 启动验证

报错信息如下,符合预期

  1. Exception in thread "main" java.lang.RuntimeException: 手机号不合法:对象:SysUser,字段:phone,值:1356666
  2. at com.ldx.valid.controller.SysUserController.lambda$main$4(SysUserController.java:215)
  3. at java.util.Optional.ifPresent(Optional.java:159)
  4. at com.ldx.valid.controller.SysUserController.main(SysUserController.java:209)

6. 方法参数验证

有的时候我们想在方法上直接进行参数验证,步骤如下

6.1 修改控制器

直接在类上添加注解@Validated,并在方法上直接进行验证

  1. @Slf4j
  2. @Validated
  3. @RestController
  4. @RequestMapping("sys/user")
  5. public class SysUserController {
  6. ... 省略代码
  7. /**
  8. * 根据手机号和邮箱查询用户信息
  9. * @param phone 手机号
  10. * @return 用户list
  11. */
  12. @GetMapping("selectByPhone")
  13. public CommonResult queryByPhone(@NotEmpty(message = "手机号不能为空") String phone) {
  14. List<SysUser> queryResult = USERS.stream()
  15. .filter(obj -> obj.getPhone().equals(phone))
  16. .collect(Collectors.toList());
  17. return CommonResult.success(queryResult);
  18. }
  19. }

6.2 启动验证

不给phone字段赋值,操作结果符合预期

错误日志:

  1. javax.validation.ConstraintViolationException: queryByPhone.phone: 手机号不能为空

7. 统一异常处理

在上面的参数验证中,验证的错误信息是通过BindingResult result参数进行接收的,在每个方法中异常处理如出一辙,特别麻烦。甚至在step 5,6都是直接将异常的堆栈信息返回给前端,这对于用来说是非常不友好的。而且有的情况下需要我们主动抛出业务异常,比方用户不能直接删除已绑定用户的角色。

所以,开撸。

7.1 创建业务异常类

  1. /**
  2. * 业务异常
  3. * @author ludangxin
  4. * @date 2021/8/5
  5. */
  6. public class BusinessException extends RuntimeException {
  7. private static final long serialVersionUID = 1L;
  8. protected final String message;
  9. public BusinessException(String message) {
  10. this.message = message;
  11. }
  12. public BusinessException(String message, Throwable e) {
  13. super(message, e);
  14. this.message = message;
  15. }
  16. @Override
  17. public String getMessage() {
  18. return message;
  19. }
  20. }

7.2 创建全局异常处理器

  1. import com.ldx.valid.util.CommonResult;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.validation.BindException;
  4. import org.springframework.validation.BindingResult;
  5. import org.springframework.validation.FieldError;
  6. import org.springframework.web.HttpRequestMethodNotSupportedException;
  7. import org.springframework.web.bind.annotation.ExceptionHandler;
  8. import org.springframework.web.bind.annotation.RestControllerAdvice;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.validation.ConstraintViolation;
  11. import javax.validation.ConstraintViolationException;
  12. import javax.validation.ValidationException;
  13. import java.util.Set;
  14. /**
  15. * 全局异常处理器
  16. *
  17. * @author ludangxin
  18. * @date 2021/8/5
  19. */
  20. @Slf4j
  21. @RestControllerAdvice
  22. public class GlobalExceptionHandler {
  23. /**
  24. * 参数绑定异常类 用于表单验证时抛出的异常处理
  25. */
  26. @ExceptionHandler(BindException.class)
  27. public CommonResult validatedBindException(BindException e){
  28. log.error(e.getMessage(), e);
  29. BindingResult bindingResult = e.getBindingResult();
  30. FieldError fieldError = e.getFieldError();
  31. String message = "[" + e.getAllErrors().get(0).getDefaultMessage() + "]";
  32. return CommonResult.error(message);
  33. }
  34. /**
  35. * 用于方法形参中参数校验时抛出的异常处理
  36. * @param e
  37. * @return
  38. */
  39. @ExceptionHandler(ConstraintViolationException.class)
  40. public CommonResult handle(ValidationException e) {
  41. log.error(e.getMessage(), e);
  42. String errorInfo = "";
  43. if(e instanceof ConstraintViolationException){
  44. ConstraintViolationException exs = (ConstraintViolationException) e;
  45. Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
  46. for (ConstraintViolation<?> item : violations) {
  47. errorInfo = errorInfo + "[" + item.getMessage() + "]";
  48. }
  49. }
  50. return CommonResult.error(errorInfo);
  51. }
  52. /**
  53. * 请求方式不支持
  54. */
  55. @ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
  56. public CommonResult handleException(HttpRequestMethodNotSupportedException e){
  57. log.error(e.getMessage(), e);
  58. return CommonResult.error("不支持' " + e.getMethod() + "'请求");
  59. }
  60. /**
  61. * 拦截未知的运行时异常
  62. */
  63. @ExceptionHandler(RuntimeException.class)
  64. public CommonResult notFount(RuntimeException e) {
  65. log.error("运行时异常:", e);
  66. return CommonResult.error("运行时异常:" + e.getMessage());
  67. }
  68. /**
  69. * 系统异常
  70. */
  71. @ExceptionHandler(Exception.class)
  72. public CommonResult handleException(Exception e) {
  73. log.error(e.getMessage(), e);
  74. return CommonResult.error("服务器错误,请联系管理员");
  75. }
  76. /**
  77. * 业务异常
  78. */
  79. @ExceptionHandler(BusinessException.class)
  80. public CommonResult businessException(HttpServletRequest request, BusinessException e) {
  81. log.error(e.getMessage());
  82. return CommonResult.error(e.getMessage());
  83. }
  84. }

7.3 修改控制器

删除方法中的BindingResult result参数,将错误直接抛给统一异常处理类去解决即可。

  1. import com.ldx.valid.exception.BusinessException;
  2. import com.ldx.valid.model.SysUser;
  3. import com.ldx.valid.model.ValidationInterface;
  4. import com.ldx.valid.service.UserService;
  5. import com.ldx.valid.util.CommonResult;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.validation.FieldError;
  9. import org.springframework.validation.annotation.Validated;
  10. import org.springframework.web.bind.annotation.*;
  11. import javax.annotation.Resource;
  12. import javax.validation.*;
  13. import javax.validation.constraints.NotEmpty;
  14. import java.text.MessageFormat;
  15. import java.util.ArrayList;
  16. import java.util.List;
  17. import java.util.stream.Collectors;
  18. /**
  19. * 用户管理控制器
  20. * @author ludangxin
  21. * @date 2021/8/5
  22. */
  23. @Slf4j
  24. @Validated
  25. @RestController
  26. @RequestMapping("sys/user")
  27. public class SysUserController {
  28. private static final List<SysUser> USERS = new ArrayList<>();
  29. // 数据初始化
  30. static {
  31. SysUser user = new SysUser();
  32. user.setId(1L);
  33. user.setUsername("zhangsan");
  34. user.setPhone("13566666666");
  35. user.setEmail("example@qq.com");
  36. USERS.add(user);
  37. SysUser user1 = new SysUser();
  38. user1.setId(2L);
  39. user1.setUsername("lisi");
  40. user1.setPhone("13588888888");
  41. user1.setEmail("example1@qq.com");
  42. USERS.add(user1);
  43. }
  44. /**
  45. * 根据手机号或邮箱查询用户信息
  46. * @param sysUser 查询条件
  47. * @return 用户list
  48. */
  49. @GetMapping
  50. public CommonResult queryList(@Validated(value = ValidationInterface.select.class) SysUser sysUser) {
  51. String phone = sysUser.getPhone();
  52. String email = sysUser.getEmail();
  53. if(phone == null && email == null) {
  54. return CommonResult.success(USERS);
  55. }
  56. List<SysUser> queryResult = USERS.stream()
  57. .filter(obj -> obj.getPhone().equals(phone) || obj.getEmail().equals(email))
  58. .collect(Collectors.toList());
  59. return CommonResult.success(queryResult);
  60. }
  61. /**
  62. * 根据手机号和邮箱查询用户信息
  63. * @param phone 手机号
  64. * @return 用户list
  65. */
  66. @GetMapping("selectByPhone")
  67. public CommonResult queryByPhone(@NotEmpty(message = "手机号不能为空") String phone) {
  68. List<SysUser> queryResult = USERS.stream()
  69. .filter(obj -> obj.getPhone().equals(phone))
  70. .collect(Collectors.toList());
  71. return CommonResult.success(queryResult);
  72. }
  73. /**
  74. * 新增用户信息
  75. * @param sysUser 用户信息
  76. * @return 成功标识
  77. */
  78. @PostMapping
  79. public CommonResult add(@Validated(value = ValidationInterface.add.class) @RequestBody SysUser sysUser) {
  80. Long id = (long) (USERS.size() + 1);
  81. sysUser.setId(id);
  82. USERS.add(sysUser);
  83. return CommonResult.success("新增成功");
  84. }
  85. /**
  86. * 根据Id更新用户信息
  87. * @param sysUser 用户信息
  88. * @return 成功标识
  89. */
  90. @PutMapping("{id}")
  91. public CommonResult updateById(@PathVariable("id") Long id,
  92. @Validated(value = ValidationInterface.update.class)
  93. @RequestBody SysUser sysUser)
  94. {
  95. for(int i = 0; i < USERS.size(); i++) {
  96. if(USERS.get(i).getId().equals(id)) {
  97. USERS.set(i,sysUser);
  98. }
  99. }
  100. return CommonResult.success("更新成功");
  101. }
  102. /**
  103. * 根据Id删除用户信息
  104. * @param id 主键
  105. * @return 成功标识
  106. */
  107. @DeleteMapping("{id}")
  108. public CommonResult deleteById(@PathVariable Long id) {
  109. USERS.removeIf(obj -> obj.getId().equals(id));
  110. return CommonResult.success("删除成功");
  111. }
  112. /**
  113. * 测试业务异常
  114. */
  115. @GetMapping("testException")
  116. public CommonResult testException(String name) {
  117. if(!"张三".equals(name)){
  118. throw new BusinessException("只有张三才可以访问");
  119. }
  120. return CommonResult.success();
  121. }
  122. }

7.4 启动测试

查询:

​ 输出错误的邮箱

根据手机号查询:

​ 输入空值手机号

新增:

​ 输入错误的手机号

测试主动抛出业务异常:

8. 自定义验证信息源

8.1 修改配置文件

  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.context.support.ReloadableResourceBundleMessageSource;
  4. import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
  5. import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
  6. import javax.validation.Validator;
  7. import java.util.Properties;
  8. /**
  9. * 配置 Hibernate 参数校验
  10. * @author ludangxin
  11. * @date 2021/8/5
  12. */
  13. @Configuration
  14. public class ValidatorConfig {
  15. @Bean
  16. public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
  17. MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
  18. postProcessor.setValidator(validator);
  19. return postProcessor;
  20. }
  21. /**
  22. * 实体类字段校验国际化引入
  23. */
  24. @Bean
  25. public Validator validator() {
  26. LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
  27. // 设置messages资源信息
  28. ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
  29. // 多个用逗号分割
  30. messageSource.setBasenames("classpath:/messages/validation/messages");
  31. // 设置字符集编码
  32. messageSource.setDefaultEncoding("UTF-8");
  33. validator.setValidationMessageSource(messageSource);
  34. // 设置验证相关参数
  35. Properties properties = new Properties();
  36. // 快速失败,只要有错马上返回
  37. properties.setProperty("hibernate.validator.fail_fast", "true");
  38. validator.setValidationProperties(properties);
  39. return validator;
  40. }
  41. }

8.2 添加信息源文件

  1. ├───resources
  2. └── messages
  3. └── validation
  4. └── messages.properties
  1. # messages.properties
  2. name.not.empty=用户名不能为空
  3. email.not.valid=${validatedValue}是邮箱地址?
  4. email.not.empty=邮箱不能为空
  5. phone.not.valid=${validatedValue}是手机号?
  6. phone.not.empty=手机号不能为空
  7. password.size.valid=密码长度必须在{min}-{max}之间
  8. id.not.empty=主键不能为空

8.3 修改实体类

  1. import com.ldx.valid.annotation.Phone;
  2. import lombok.Data;
  3. import org.hibernate.validator.constraints.Range;
  4. import javax.validation.constraints.*;
  5. import java.io.Serializable;
  6. /**
  7. * 用户信息管理
  8. * @author ludangxin
  9. * @date 2021/8/5
  10. */
  11. @Data
  12. public class SysUser implements Serializable {
  13. private static final long serialVersionUID = 1L;
  14. /**
  15. * 主键
  16. */
  17. @NotNull(message = "{id.not.empty}", groups = {ValidationInterface.update.class})
  18. private Long id;
  19. /**
  20. * 用户名
  21. */
  22. @NotEmpty(message = "{name.not.empty}", groups = {
  23. ValidationInterface.update.class,
  24. ValidationInterface.add.class})
  25. private String username;
  26. /**
  27. * 密码
  28. */
  29. @Size(min = 6, max = 16, message = "{password.size.valid}", groups = {
  30. ValidationInterface.update.class,
  31. ValidationInterface.add.class})
  32. private String password = "123456";
  33. /**
  34. * 邮箱地址
  35. */
  36. @Email(message = "{email.not.valid}",
  37. groups = {
  38. ValidationInterface.update.class,
  39. ValidationInterface.add.class,
  40. ValidationInterface.select.class})
  41. @NotEmpty(message = "{email.not.empty}", groups = ValidationInterface.add.class)
  42. private String email;
  43. /**
  44. * 电话
  45. */
  46. @Pattern(message = "{phone.not.valid}", regexp = "^1[3456789]\\d{9}$",
  47. groups = {ValidationInterface.add.class})
  48. @NotEmpty(message = "{phone.not.empty}", groups = {ValidationInterface.add.class})
  49. private String phone;
  50. }

8.4 启动测试

​ 输入错误的邮箱地址测试:

9. 预置注解清单

注解 说明
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

SpringBoot-表单验证-统一异常处理-自定义验证信息源的更多相关文章

  1. jQuery Validate 插件为表单提供了强大的验证功能

    之前项目开发中,表单校验用的jQuery Validate 插件,这个插件为表单提供了强大的验证功能,让客户端表单验证变得更简单,同时提供了大量的定制选项,满足应用程序各种需求.该插件捆绑了一套有用的 ...

  2. SpringBoot小技巧:统一异常处理

    SpringBoot小技巧:统一异常处理 情景描述 对于接口的定义,我们通常会有一个固定的格式,比如: 但是调用方在请求我们的API时把接口地址写错了,就会得到一个404错误,且不同于我们定义的数据格 ...

  3. amazeui的表单开关插件的自定义事件必须添加.bootstrapSwitch 命名空间,给了我们什么启示

    amazeui的表单开关插件的自定义事件必须添加.bootstrapSwitch 命名空间,给了我们什么启示 一.总结 一句话总结:详细看使用文档(说明文档说的真的是非常详细呢,不过循序渐进,不同阶段 ...

  4. easyui 表单验证validatetype——支持自定义验证

    easyui 的validatebox()提供了自定义验证的方法,为此我把一些常用的数据验证汇总了一下,代码如下: 代码 Code highlighting produced by Actipro C ...

  5. angularJS中的表单验证(包括自定义验证)

    表单验证是angularJS一项重要的功能,能保证我们的web应用不会被恶意或错误的输入破坏.Angular表单验证提供了很多表单验证指令,并且能将html5表单验证功能同他自己的验证指令结合起来使用 ...

  6. bootstrapValidator验证表单后清除当次验证的方法

    用bootstrapValidator的resetForm()方法: <!-- // create server begin --> <div class="modal f ...

  7. a标签指定的url,在表单提交前进行js验证的实现

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  8. JS:JS判断提交表单不能为空等验证

    这段代码在<form>中有οnsubmit="return on_submit()",如果 onsubmit ()返回 fasle,表单的元素就不会提交,即action ...

  9. 使用jquery.validate.js插件进行表单里控件的验证

    jsp中具体实现的代码: <%@ page language="java" contentType="text/html; charset=UTF-8" ...

随机推荐

  1. sql数据库新建作业,新建步骤时报错从 IClassFactory 为 CLSID 为 {AA40D1D6-CAEF-4A56-B9BB-D0D3DC976BA2} 的 COM 组件创建实例失败,原因是出现以下错误: c001f011。 (Microsoft.SqlServer.ManagedDTS)

    简单粗暴的重启sql数据库 其他网上找的方法 32位操作系统: 打开运行(命令提示符), 一.输入 cd c:\windows\system32 进入到c:\windows\system32路径中 二 ...

  2. 乘风破浪,.Net Core遇见MAUI(.NET Multi-platform App UI),进击现代化跨设备应用框架

    什么是MAUI https://github.com/dotnet/maui .NET Multi-platform App UI (MAUI) 的前身是Xamarin.Forms(适用于Androi ...

  3. 温故知新,DotNet Core SDK和.Net CLI十八般武艺

    简介 .NET命令行接口 (CLI) 工具是用于开发.生成.运行和发布.NET应用程序的跨平台工具链. https://docs.microsoft.com/zh-cn/dotnet/core/too ...

  4. Spring学习日记03_IOC_属性注入_集合类型属性

    Ioc操作Bean管理(xml注入集合属性) 注入数组类型属性 注入List集合类型属性 注入Map集合类型属性 Stu类 public class Stu { //1. 数组类型属性 private ...

  5. 第2章:Kubernetes核心概念

    Kubernetes是Google在2014年开源的一个容器集群管理系统,Kubernetes简称K8S. Kubernetes用于容器化应用程序的部署,扩展和管理,目标是让部署容器化应用简单高效. ...

  6. JS刷新窗口的几种方式

    浮层内嵌iframe及frame集合窗口,刷新父页面的多种方法   <script language=JavaScript>       parent.location.reload(); ...

  7. linux学习之路第四天

    用户和用户组的配置文件

  8. IDA Pro 6.0使用Qt 框架实现了跨平台的UI

    IDA Pro 6.0使用Qt 框架实现了跨平台的UI.它的好处是插件编写者还可以直接使用 Qt 开发跨平台 UI.但是编剧呢? 在这篇博文中,我们将说明如何使用PySide使用IDAPython为 ...

  9. muggle_ocr 下载安装

    pip install -i https://pypi.tuna.tsinghua.edu.cn/simple muggle_ocr

  10. 『心善渊』Selenium3.0基础 — 28、unittest中测试套件的使用

    目录 1.测试套件的作用 2.使用测试套件 (1)入门示例 (2)根据不同的条件加载测试用例(了解) (3)常用方式(推荐) 1.测试套件的作用 在我们实际工作,使用unittest框架会有两个问题: ...