• SpringBoot-表单验证-统一异常处理-自定义验证信息源


    1. 简介

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

    2. 开撸

    2.1 项目结构

    结构说明:

    ├── java
    │   └── com
    │       └── ldx
    │           └── valid
    │               ├── ValidApplication.java # 启动类
    │               ├── annotation
    │               │   └── Phone.java # 自定义验证注解
    │               ├── config
    │               │   └── ValidatorConfig.java # 表单验证配置类
    │               ├── controller
    │               │   └── SysUserController.java # 用户管理控制器
    │               ├── exception
    │               │   ├── BusinessException.java # 业务异常类
    │               │   └── GlobalExceptionHandler.java # 统一异常处理类
    │               ├── model
    │               │   ├── SysUser.java # 用户信息实体
    │               │   └── ValidationInterface.java # 表单验证的通用分组接口
    │               ├── util
    │               │   └── CommonResult.java # 接口返回封装类
    │               └── validation
    │                   └── PhoneValidator.java #自定义验证实现类
    └── resources
        ├── application.yaml # 配置文件
        └── messages
            └── validation
                └── messages.properties # 自定义验证信息源
    

    2.1 quick start

    2.1.1 导入依赖

    创建springboot项目导入以下依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.5.3</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.ldx</groupId>
        <artifactId>valid</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>valid</name>
        <description>表单验证demo</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <!-- web支持 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!-- 表单验证 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                            </exclude>
                        </excludes>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    2.1.2 添加配置类

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

    import org.hibernate.validator.HibernateValidator;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import javax.validation.ValidatorFactory;
    
    /**
     * 配置 Hibernate 参数校验
     * @author ludangxin
     * @date 2021/8/5
     */
    @Configuration
    public class ValidatorConfig {
        @Bean
        public MethodValidationPostProcessor methodValidationPostProcessor() {
            MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
            //快速校验,只要有错马上返回
            postProcessor.setValidator(validator());
            return postProcessor;
        }
    
        @Bean
        public Validator validator() {
            ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .addProperty("hibernate.validator.fail_fast", "true")
                .buildValidatorFactory();
            return validatorFactory.getValidator();
        }
    }
    

    2.1.3 添加实体类

    import lombok.*;
    import javax.validation.constraints.*;
    import java.io.Serializable;
    
    /**
     * 用户信息管理
     * @author ludangxin
     * @date 2021/8/5
     */
    @Data
    public class SysUser  implements Serializable {
        private static final long serialVersionUID = 1L;
        /**
         * 主键
         */
        private Long id;
    
        /**
         * 用户名
         */
        @NotEmpty(message = "用户名称不能为空")
        private String username;
    
        /**
         * 密码
         */
        @Size(min = 6, max = 16, message = "密码长度必须在{min}-{max}之间")
        private String password = "123456";
    
        /**
         * 邮箱地址
         */
        @Email(message = "邮箱地址不合法")
        @NotEmpty(message = "邮箱不能为空")
        private String email;
    
        /**
         * 电话
         */
        @Size(min = 11, max = 11, message = "手机号不合法")
        @NotEmpty(message = "手机号不能为空")
        private String phone;
    }
    

    2.1.4 接口返回封装类

    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * 操作消息提醒
     * @author ludangxin
     * @date 2021/8/5
     */
    @Data
    @NoArgsConstructor
    public class CommonResult {
        /** 状态码 */
        private int code;
    
        /** 返回内容 */
        private String msg;
    
        /** 数据对象 */
        private Object data;
    
        /**
         * 初始化一个新创建的 CommonResult 对象
         * @param type 状态类型
         * @param msg 返回内容
         */
        public CommonResult(Type type, String msg) {
            this.code = type.value;
            this.msg = msg;
        }
    
        /**
         * 初始化一个新创建的 CommonResult 对象
         * @param type 状态类型
         * @param msg 返回内容
         * @param data 数据对象
         */
        public CommonResult(Type type, String msg, Object data) {
            this.code = type.value;
            this.msg = msg;
            if (data != null) {
                this.data = data;
            }
        }
    
        /**
         * 返回成功消息
         * @return 成功消息
         */
        public static CommonResult success() {
            return CommonResult.success("操作成功");
        }
    
        /**
         * 返回成功数据
         * @return 成功消息
         */
        public static CommonResult success(Object data) {
            return CommonResult.success("操作成功", data);
        }
    
        /**
         * 返回成功消息
         * @param msg 返回内容
         * @return 成功消息
         */
        public static CommonResult success(String msg) {
            return CommonResult.success(msg, null);
        }
    
        /**
         * 返回成功消息
         * @param msg 返回内容
         * @param data 数据对象
         * @return 成功消息
         */
        public static CommonResult success(String msg, Object data) {
            return new CommonResult(Type.SUCCESS, msg, data);
        }
    
        /**
         * 返回警告消息
         * @param msg 返回内容
         * @return 警告消息
         */
        public static CommonResult warn(String msg) {
            return CommonResult.warn(msg, null);
        }
    
        /**
         * 返回警告消息
         * @param msg 返回内容
         * @param data 数据对象
         * @return 警告消息
         */
        public static CommonResult warn(String msg, Object data) {
            return new CommonResult(Type.WARN, msg, data);
        }
    
        /**
         * 返回错误消息
         * @return 错误信息
         */
        public static CommonResult error() {
            return CommonResult.error("操作失败");
        }
    
        /**
         * 返回错误消息
         * @param msg 返回内容
         * @return 错误消息
         */
        public static CommonResult error(String msg) {
            return CommonResult.error(msg, null);
        }
    
        /**
         * 返回错误消息
         * @param msg 返回内容
         * @param data 数据对象
         * @return 错误消息
         */
        public static CommonResult error(String msg, Object data) {
            return new CommonResult(Type.ERROR, msg, data);
        }
    
        /**
         * 状态类型
         */
        public enum Type {
            /** 成功 */
            SUCCESS(200),
            /** 警告 */
            WARN(301),
            /** 错误 */
            ERROR(500);
            private final int value;
    
            Type(int value){
                this.value = value;
            }
    
            public int value() {
                return this.value;
            }
        }
    }
    

    2.1.5 控制器

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

    import com.ldx.valid.exception.BusinessException;
    import com.ldx.valid.model.SysUser;
    import com.ldx.valid.model.ValidationInterface;
    import com.ldx.valid.util.CommonResult;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.FieldError;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    import javax.validation.constraints.NotEmpty;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Objects;
    import java.util.stream.Collectors;
    
    /**
     * 用户管理控制器
     *
     * @author ludangxin
     * @date 2021/8/5
     */
    @Slf4j
    @RestController
    @RequestMapping("sys/user")
    public class SysUserController {
       private static final List<SysUser> USERS = new ArrayList<>();
    
       // 数据初始化
       static {
          SysUser user = new SysUser();
          user.setId(1L);
          user.setUsername("zhangsan");
          user.setPhone("13566666666");
          user.setEmail("example@qq.com");
          USERS.add(user);
          SysUser user1 = new SysUser();
          user1.setId(2L);
          user1.setUsername("lisi");
          user1.setPhone("13588888888");
          user1.setEmail("example1@qq.com");
          USERS.add(user1);
       }
    
       /**
        * 新增用户信息
        * @param sysUser 用户信息
        * @return 成功标识
        */
       @PostMapping
       public CommonResult add(@Validated @RequestBody SysUser sysUser, BindingResult result) {
          FieldError fieldError = result.getFieldError();
    
          if(Objects.nonNull(fieldError)) {
             String field = fieldError.getField();
             Object rejectedValue = fieldError.getRejectedValue();
             String msg = "[" + fieldError.getDefaultMessage() + "]";
             log.error("{}:字段=={}	值=={}", msg, field, rejectedValue);
             return CommonResult.error(msg);
          }
    
          USERS.add(sysUser);
          return CommonResult.success("新增成功");
       }
    }
    

    2.1.5 启动测试

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

    log日志:

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

    3. 分组校验

    groups是用来干什么的?

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

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

    3.1 添加分组接口

    /**
     * 用于表单验证的通用分组接口
     * @author ludangxin
     * @date 2021/8/5
     */
    public interface ValidationInterface {
        /**
         * 新增分组
         */
        interface add{}
    
        /**
         * 删除分组
         */
        interface delete{}
    
        /**
         * 查询分组
         */
        interface select{}
    
        /**
         * 更新分组
         */
        interface update{}
    }
    

    如果还有其它特殊的分组要求 直接在DO中创建interface即可
    例:如果还有个需要验证username 和 password(只有这两个参数) 的 select操作
    直接在SysUser中创建UsernamePasswordValidView 的接口即可

    3.2 修改实体类

    将属性进行分组

    import lombok.Data;
    import javax.validation.constraints.Email;
    import javax.validation.constraints.NotEmpty;
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
    import java.io.Serializable;
    
    /**
     * 用户信息管理
     * @author ludangxin
     * @date 2021/8/5
     */
    @Data
    public class SysUser implements Serializable {
        private static final long serialVersionUID = 1L;
        /**
         * 主键
         */
        @NotNull(message = "id不能为空", groups = {ValidationInterface.update.class})
        private Long id;
    
        /**
         * 用户名
         */
        @NotEmpty(message = "用户名称不能为空", groups = {
                  ValidationInterface.update.class, 
                  ValidationInterface.add.class})
        private String username;
    
        /**
         * 密码
         */
        @Size(min = 6, max = 16, message = "密码长度必须在{min}-{max}之间", groups = {
              ValidationInterface.update.class, 
              ValidationInterface.add.class})
        private String password = "123456";
    
        /**
         * 邮箱地址
         */
        @Email(message = "邮箱地址不合法", groups = {
               ValidationInterface.update.class, 
               ValidationInterface.add.class, 
               ValidationInterface.select.class})
        @NotEmpty(message = "邮箱不能为空", groups = ValidationInterface.add.class)
        private String email;
    
        /**
         * 电话
         */
        @Size(min = 11, max = 11, message = "手机号不合法", groups = {
              ValidationInterface.update.class,
              ValidationInterface.add.class,
              ValidationInterface.select.class})
        @NotEmpty(message = "手机号不能为空",groups = {ValidationInterface.add.class})
        private String phone;
    }
    
    

    3.3 修改控制器

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

    import com.ldx.valid.exception.BusinessException;
    import com.ldx.valid.model.SysUser;
    import com.ldx.valid.model.ValidationInterface;
    import com.ldx.valid.util.CommonResult;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.FieldError;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    import javax.validation.constraints.NotEmpty;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Objects;
    import java.util.stream.Collectors;
    
    /**
     * 用户管理控制器
     * @author ludangxin
     * @date 2021/8/5
     */
    @Slf4j
    @RestController
    @RequestMapping("sys/user")
    public class SysUserController {
       private static final List<SysUser> USERS = new ArrayList<>();
    
       // 数据初始化
       static {
          SysUser user = new SysUser();
          user.setId(1L);
          user.setUsername("zhangsan");
          user.setPhone("13566666666");
          user.setEmail("example@qq.com");
          USERS.add(user);
          SysUser user1 = new SysUser();
          user1.setId(2L);
          user1.setUsername("lisi");
          user1.setPhone("13588888888");
          user1.setEmail("example1@qq.com");
          USERS.add(user1);
       }
    
       /**
        * 根据手机号或邮箱查询用户信息
        * @param sysUser 查询条件
        * @return 用户list
        */
       @GetMapping
       public CommonResult queryList(@Validated(value = ValidationInterface.select.class) SysUser sysUser,
                                     BindingResult result)
       {
          FieldError fieldError = result.getFieldError();
    
          if(Objects.nonNull(fieldError)) {
             return CommonResult.error(getErrorMsg(fieldError));
          }
    
          String phone = sysUser.getPhone();
          String email = sysUser.getEmail();
    
          if(phone == null && email == null) {
             return CommonResult.success(USERS);
          }
    
          List<SysUser> queryResult = USERS.stream()
                .filter(obj -> obj.getPhone().equals(phone) || obj.getEmail().equals(email))
                .collect(Collectors.toList());
          return CommonResult.success(queryResult);
       }
    
       /**
        * 新增用户信息
        * @param sysUser 用户信息
        * @return 成功标识
        */
       @PostMapping
       public CommonResult add(@Validated(value = ValidationInterface.add.class)
                               @RequestBody SysUser sysUser,
                               BindingResult result)
       {
          FieldError fieldError = result.getFieldError();
    
          if(Objects.nonNull(fieldError)) {
             return CommonResult.error(getErrorMsg(fieldError));
          }
         
          Long id = (long) (USERS.size() + 1);
          sysUser.setId(id);
          USERS.add(sysUser);
          return CommonResult.success("新增成功");
       }
    
       /**
        * 根据Id更新用户信息
        * @param sysUser 用户信息
        * @return 成功标识
        */
       @PutMapping("{id}")
       public CommonResult updateById(@PathVariable("id") Long id,
                                      @Validated(value = ValidationInterface.update.class)
                                      @RequestBody SysUser sysUser,
                                      BindingResult result)
       {
          FieldError fieldError = result.getFieldError();
    
          if(Objects.nonNull(fieldError)) {
             return CommonResult.error(getErrorMsg(fieldError));
          }
    
          for(int i = 0; i < USERS.size(); i++) {
             if(USERS.get(i).getId().equals(id)) {
                USERS.set(i,sysUser);
             }
          }
          return CommonResult.success("更新成功");
       }
    
       /**
        * 根据Id删除用户信息
        * @param id 主键
        * @return 成功标识
        */
       @DeleteMapping("{id}")
       public CommonResult deleteById(@PathVariable Long id) {
          USERS.removeIf(obj -> obj.getId().equals(id));
          return CommonResult.success("删除成功");
       }
    
       /**
        * 获取表单验证错误msg
        * @param fieldError 报错字段
        * @return msg
        */
       public String getErrorMsg(FieldError fieldError) {
          String field = fieldError.getField();
          Object rejectedValue = fieldError.getRejectedValue();
          String msg = "[" + fieldError.getDefaultMessage() + "]";
          log.error("{}:字段=={}	值=={}", msg, field, rejectedValue);
          return msg;
       }
    }
    

    3.4 启动测试

    查询:

    ​ 输入不合法手机号

    新增:

    ​ 正常情况

    ​ 去掉邮箱

    修改:

    ​ 去掉id

    删除:

    4. 自定义验证

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

    4.1 添加注解

    import com.ldx.valid.validation.PhoneValidator;
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import static java.lang.annotation.ElementType.*;
    import static java.lang.annotation.ElementType.TYPE_USE;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    /**
     * 验证手机号是否合法
     * @author ludangxin
     * @date 2021/8/7
     */
    @Documented
    @Constraint(validatedBy = {PhoneValidator.class})
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    public @interface Phone {
       //默认错误消息
       String message() default "不是一个合法的手机号";
    
       //分组
       Class<?>[] groups() default {};
    
       //载荷 将某些元数据信息与给定的注解声明相关联的方法
       Class<? extends Payload>[] payload() default {};
    
       //指定多个时使用
       @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
       @Retention(RUNTIME)
       @Documented
       @interface List {
          Phone[] value();
       }
    }
    

    4.2 编写验证逻辑

    import javax.validation.ConstraintValidator;
    import com.ldx.valid.annotation.Phone;
    import javax.validation.ConstraintValidatorContext;
    import java.util.Objects;
    import java.util.regex.Pattern;
    
    /**
     * 手机号校验器
     * @author ludangxin
     * @date 2021/8/7
     */
    public class PhoneValidator implements ConstraintValidator<Phone, String> {
    
       /**
        * 手机号正则表达式
        */
       private static final String REGEXP_PHONE = "^1[3456789]\d{9}$";
    
       @Override
       public boolean isValid(String value, ConstraintValidatorContext context) {
          if(Objects.isNull(value)) {
             return true;
          }
    
          return Pattern.matches(REGEXP_PHONE, value);
       }
    }
    

    4.3 修改实体

    SysUser.phone 属性添加注解@Phone

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

    4.4 启动测试

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

    4.5 @Pattern

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

    @Pattern(message = "手机号不合法", regexp = "^1[3456789]\d{9}$", groups = {ValidationInterface.add.class})
    @NotEmpty(message = "手机号不能为空", groups = {ValidationInterface.add.class})
    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即可

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

    5.1.2 定义验证方法

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

    5.1.2 启动验证

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

    报错日志如下

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

    5.2 非spring环境验证

    5.2.1 定义验证方法

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

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

    5.2.2 启动验证

    报错信息如下,符合预期

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

    6. 方法参数验证

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

    6.1 修改控制器

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

    @Slf4j
    @Validated
    @RestController
    @RequestMapping("sys/user")
    public class SysUserController {
      ... 省略代码
        /**
         * 根据手机号和邮箱查询用户信息
         * @param phone 手机号
         * @return 用户list
         */
        @GetMapping("selectByPhone")
        public CommonResult queryByPhone(@NotEmpty(message = "手机号不能为空") String phone) {
           List<SysUser> queryResult = USERS.stream()
                 .filter(obj -> obj.getPhone().equals(phone))
                 .collect(Collectors.toList());
           return CommonResult.success(queryResult);
        }
    }
    

    6.2 启动验证

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

    错误日志:

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

    7. 统一异常处理

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

    所以,开撸。

    7.1 创建业务异常类

    /**
     * 业务异常
     * @author ludangxin
     * @date 2021/8/5
     */
    public class BusinessException extends RuntimeException {
        private static final long serialVersionUID = 1L;
    
        protected final String message;
    
        public BusinessException(String message) {
            this.message = message;
        }
    
        public BusinessException(String message, Throwable e) {
            super(message, e);
            this.message = message;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    }
    

    7.2 创建全局异常处理器

    import com.ldx.valid.util.CommonResult;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.BindException;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.FieldError;
    import org.springframework.web.HttpRequestMethodNotSupportedException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import javax.servlet.http.HttpServletRequest;
    import javax.validation.ConstraintViolation;
    import javax.validation.ConstraintViolationException;
    import javax.validation.ValidationException;
    import java.util.Set;
    
    /**
     * 全局异常处理器
     *
     * @author ludangxin
     * @date 2021/8/5
     */
    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        /**
         * 参数绑定异常类 用于表单验证时抛出的异常处理
         */
        @ExceptionHandler(BindException.class)
        public CommonResult validatedBindException(BindException e){
            log.error(e.getMessage(), e);
            BindingResult bindingResult = e.getBindingResult();
            FieldError fieldError = e.getFieldError();
            String message = "[" + e.getAllErrors().get(0).getDefaultMessage() + "]";
            return CommonResult.error(message);
        }
    
        /**
         * 用于方法形参中参数校验时抛出的异常处理
         * @param e
         * @return
         */
        @ExceptionHandler(ConstraintViolationException.class)
        public CommonResult handle(ValidationException e) {
            log.error(e.getMessage(), e);
            String errorInfo = "";
            if(e instanceof ConstraintViolationException){
                ConstraintViolationException exs = (ConstraintViolationException) e;
                Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
                for (ConstraintViolation<?> item : violations) {
                    errorInfo = errorInfo + "[" + item.getMessage() + "]";
                }
            }
            return CommonResult.error(errorInfo);
        }
    
        /**
         * 请求方式不支持
         */
        @ExceptionHandler({ HttpRequestMethodNotSupportedException.class })
        public CommonResult handleException(HttpRequestMethodNotSupportedException e){
            log.error(e.getMessage(), e);
            return CommonResult.error("不支持' " + e.getMethod() + "'请求");
        }
    
        /**
         * 拦截未知的运行时异常
         */
        @ExceptionHandler(RuntimeException.class)
        public CommonResult notFount(RuntimeException e) {
            log.error("运行时异常:", e);
            return CommonResult.error("运行时异常:" + e.getMessage());
        }
    
        /**
         * 系统异常
         */
        @ExceptionHandler(Exception.class)
        public CommonResult handleException(Exception e) {
            log.error(e.getMessage(), e);
            return CommonResult.error("服务器错误,请联系管理员");
        }
    
        /**
         * 业务异常
         */
        @ExceptionHandler(BusinessException.class)
        public CommonResult businessException(HttpServletRequest request, BusinessException e) {
            log.error(e.getMessage());
            return CommonResult.error(e.getMessage());
        }
    }
    

    7.3 修改控制器

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

    import com.ldx.valid.exception.BusinessException;
    import com.ldx.valid.model.SysUser;
    import com.ldx.valid.model.ValidationInterface;
    import com.ldx.valid.service.UserService;
    import com.ldx.valid.util.CommonResult;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.FieldError;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    import javax.annotation.Resource;
    import javax.validation.*;
    import javax.validation.constraints.NotEmpty;
    import java.text.MessageFormat;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * 用户管理控制器
     * @author ludangxin
     * @date 2021/8/5
     */
    @Slf4j
    @Validated
    @RestController
    @RequestMapping("sys/user")
    public class SysUserController {
       private static final List<SysUser> USERS = new ArrayList<>();
    
       // 数据初始化
       static {
          SysUser user = new SysUser();
          user.setId(1L);
          user.setUsername("zhangsan");
          user.setPhone("13566666666");
          user.setEmail("example@qq.com");
          USERS.add(user);
          SysUser user1 = new SysUser();
          user1.setId(2L);
          user1.setUsername("lisi");
          user1.setPhone("13588888888");
          user1.setEmail("example1@qq.com");
          USERS.add(user1);
       }
    
       /**
        * 根据手机号或邮箱查询用户信息
        * @param sysUser 查询条件
        * @return 用户list
        */
       @GetMapping
       public CommonResult queryList(@Validated(value = ValidationInterface.select.class) SysUser sysUser) {
          String phone = sysUser.getPhone();
          String email = sysUser.getEmail();
    
          if(phone == null && email == null) {
             return CommonResult.success(USERS);
          }
    
          List<SysUser> queryResult = USERS.stream()
                .filter(obj -> obj.getPhone().equals(phone) || obj.getEmail().equals(email))
                .collect(Collectors.toList());
          return CommonResult.success(queryResult);
       }
    
       /**
        * 根据手机号和邮箱查询用户信息
        * @param phone 手机号
        * @return 用户list
        */
       @GetMapping("selectByPhone")
       public CommonResult queryByPhone(@NotEmpty(message = "手机号不能为空") String phone) {
          List<SysUser> queryResult = USERS.stream()
                .filter(obj -> obj.getPhone().equals(phone))
                .collect(Collectors.toList());
          return CommonResult.success(queryResult);
       }
    
       /**
        * 新增用户信息
        * @param sysUser 用户信息
        * @return 成功标识
        */
       @PostMapping
       public CommonResult add(@Validated(value = ValidationInterface.add.class) @RequestBody SysUser sysUser) {
          Long id = (long) (USERS.size() + 1);
          sysUser.setId(id);
          USERS.add(sysUser);
          return CommonResult.success("新增成功");
       }
    
       /**
        * 根据Id更新用户信息
        * @param sysUser 用户信息
        * @return 成功标识
        */
       @PutMapping("{id}")
       public CommonResult updateById(@PathVariable("id") Long id,
                                      @Validated(value = ValidationInterface.update.class)
                                      @RequestBody SysUser sysUser)
       {
          for(int i = 0; i < USERS.size(); i++) {
             if(USERS.get(i).getId().equals(id)) {
                USERS.set(i,sysUser);
             }
          }
          return CommonResult.success("更新成功");
       }
    
       /**
        * 根据Id删除用户信息
        * @param id 主键
        * @return 成功标识
        */
       @DeleteMapping("{id}")
       public CommonResult deleteById(@PathVariable Long id) {
          USERS.removeIf(obj -> obj.getId().equals(id));
          return CommonResult.success("删除成功");
       }
    
       /**
        * 测试业务异常
        */
       @GetMapping("testException")
       public CommonResult testException(String name) {
          if(!"张三".equals(name)){
             throw new BusinessException("只有张三才可以访问");
          }
          return CommonResult.success();
       }
    }
    

    7.4 启动测试

    查询:

    ​ 输出错误的邮箱

    根据手机号查询:

    ​ 输入空值手机号

    新增:

    ​ 输入错误的手机号

    测试主动抛出业务异常:

    8. 自定义验证信息源

    8.1 修改配置文件

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.support.ReloadableResourceBundleMessageSource;
    import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
    import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
    import javax.validation.Validator;
    import java.util.Properties;
    
    /**
     * 配置 Hibernate 参数校验
     * @author ludangxin
     * @date 2021/8/5
     */
    @Configuration
    public class ValidatorConfig {
        @Bean
        public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
            MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
            postProcessor.setValidator(validator);
            return postProcessor;
        }
    
        /**
         * 实体类字段校验国际化引入
         */
        @Bean
        public Validator validator() {
            LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
            // 设置messages资源信息
            ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
            // 多个用逗号分割
            messageSource.setBasenames("classpath:/messages/validation/messages");
            // 设置字符集编码
            messageSource.setDefaultEncoding("UTF-8");
            validator.setValidationMessageSource(messageSource);
            // 设置验证相关参数
            Properties properties = new Properties();
            // 快速失败,只要有错马上返回
            properties.setProperty("hibernate.validator.fail_fast", "true");
            validator.setValidationProperties(properties);
            return validator;
        }
    }
    

    8.2 添加信息源文件

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

    8.3 修改实体类

    import com.ldx.valid.annotation.Phone;
    import lombok.Data;
    import org.hibernate.validator.constraints.Range;
    import javax.validation.constraints.*;
    import java.io.Serializable;
    
    /**
     * 用户信息管理
     * @author ludangxin
     * @date 2021/8/5
     */
    @Data
    public class SysUser implements Serializable {
        private static final long serialVersionUID = 1L;
        /**
         * 主键
         */
        @NotNull(message = "{id.not.empty}", groups = {ValidationInterface.update.class})
        private Long id;
    
        /**
         * 用户名
         */
        @NotEmpty(message = "{name.not.empty}", groups = {
                  ValidationInterface.update.class,
                  ValidationInterface.add.class})
        private String username;
    
        /**
         * 密码
         */
        @Size(min = 6, max = 16, message = "{password.size.valid}", groups = {
              ValidationInterface.update.class,
              ValidationInterface.add.class})
        private String password = "123456";
    
        /**
         * 邮箱地址
         */
        @Email(message = "{email.not.valid}",
               groups = {
               ValidationInterface.update.class,
               ValidationInterface.add.class,
               ValidationInterface.select.class})
        @NotEmpty(message = "{email.not.empty}", groups = ValidationInterface.add.class)
        private String email;
    
        /**
         * 电话
         */
        @Pattern(message = "{phone.not.valid}", regexp = "^1[3456789]\d{9}$", 
                 groups = {ValidationInterface.add.class})
        @NotEmpty(message = "{phone.not.empty}", groups = {ValidationInterface.add.class})
        private String phone;
    }
    

    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格式
  • 相关阅读:
    JavaSript数组扁平化去重
    宝塔面板忘记登陆账号和密码怎么办
    宝塔shell脚本执行thinkphp命令行
    laravel设置中国时区
    Laravel-admin左侧菜单栏怎么默认展开打开
    install.sh: 115: install.sh: Syntax error: "(" unexpected (expecting "}")
    git生成密钥
    git 忽略提交某个指定的文件(不从版本库中删除)
    larael-admin汉化配置中文
    Nginx PHP-Fcgi中因PHP执行时间导致504无限循环中断
  • 原文地址:https://www.cnblogs.com/ludangxin/p/15113954.html
Copyright © 2020-2023  润新知