• 商城项目10_JSR303常用注解、在项目中如何使用、统一处理异常、分组校验功能、自定义校验注解


    ①. JSR303校验概述、注解

    • ①. JSR是Java Specification Requests的缩写,意思是Java规范提案,JSR-303是JAVA EE6中的一项子规范,叫做Bean Validation即,JSR 303,Bean Validation规范 ,为Bean验证定义了元数据模型和API。默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展。

    • ②. 如何使用
      在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-validationartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • ③. 常用的注解说明
    注解解释
    @NotNull属性不能为null,无法查检长度为0的字符串
    @NotBlank检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格
    @NotEmpty该字段不能为null或""
    @Email被注释的元素必须是电子邮箱地址
    @Length被注释的字符串的大小必须在指定的范围内
    @Range被注释的元素必须在合适的范围内
    @Pattern(value)被注释的元素必须符合指定的正则表达式
    @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值

    ②. 项目中JSR303如何使用

    • ①. 在类中使用JSR303注解
      在message写上我们自己的内容,这样通过测试就返回的数据就是我们自己填写的
    @Data
    @TableName("pms_brand")
    public class BrandEntity implements Serializable {
    	private static final long serialVersionUID = 1L;
    
    	/**
    	 * 品牌id
    	 */
    	@TableId
    	private Long brandId;
    	/**
    	 * 品牌名
    	 */
    	@NotBlank(message = "品牌名称不能为空")
    	private String name;
    	/**
    	 * 品牌logo地址
    	 */
    	@NotEmpty
    	@URL(message = "logo必须是一个合法的url地址")
    	private String logo;
    	/**
    	 * 介绍
    	 */
    	private String descript;
    	/**
    	 * 显示状态[0-不显示;1-显示]
    	 */
    	private Integer showStatus;
    	/**
    	 * 检索首字母
    	 */
    	@NotEmpty
    	@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母")
    	private String firstLetter;
    	/**
    	 * 排序
    	 */
    	@NotNull
    	@Min(value = 0)
    	private Integer sort;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    在这里插入图片描述

    • ②. controller中加校验注解@Valid,开启校验
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand){
        brandService.save(brand);
        return R.ok();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • ③. 第一次测试:http://localhost:88/api/product/brand/save
      在postman种发送上面的请求,可以看到返回的甚至不是R对象
    {
        "timestamp": "2020-04-29T09:36:04.125+0000",
        "status": 400,
        "error": "Bad Request",
        "errors": [
            {
                "codes": [
                    "NotBlank.brandEntity.name",
                    "NotBlank.name",
                    "NotBlank.java.lang.String",
                    "NotBlank"
                ],
                "arguments": [
                    {
                        "codes": [
                            "brandEntity.name",
                            "name"
                        ],
                        "arguments": null,
                        "defaultMessage": "name",
                        "code": "name"
                    }
                ],
                "defaultMessage": "品牌名称不能为空",
                "objectName": "brandEntity",
                "field": "name",
                "rejectedValue": "",
                "bindingFailure": false,
                "code": "NotBlank"
            }
        ],
        "message": "Validation failed for object='brandEntity'. Error count: 1",
        "path": "/product/brand/save"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • ④. 修改代码,进行第二次测试
    1. 给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装
    2. 这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand,
                  BindingResult result){ // 手动处理异常
        if( result.hasErrors()){
            Map<String,String> map=new HashMap<>();
            //1.获取错误的校验结果
            result.getFieldErrors().forEach((item)->{
                //获取发生错误时的message
                String message = item.getDefaultMessage();
                //获取发生错误的字段
                String field = item.getField();
                map.put(field,message);
            });
            return R.error(400,"提交的数据不合法").put("data",map);
        }
        brandService.save(brand);
        return R.ok();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    ③. 统一异常处理@ExceptionHandler

    • ①. 抽取一个异常处理类
    1. @ControllerAdvice标注在类上,通过“basePackages”能够说明处理哪些路径下的异常。
    2. @ExceptionHandler(value = 异常类型.class)标注在方法上4
    3. @RestControllerAdvice=@ControllerAdvice+@ResponseBody
    • ②. 代码展示 掌握
    @Slf4j
    @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")//管理的controller
    public class GulimallExceptionControllerAdvice {
    
        @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
        public R handleValidException(MethodArgumentNotValidException exception){
    
            Map<String,String> map=new HashMap<>();
            // 获取数据校验的错误结果
            BindingResult bindingResult = exception.getBindingResult();
            // 处理错误
            bindingResult.getFieldErrors().forEach(fieldError -> {
                String message = fieldError.getDefaultMessage();
                String field = fieldError.getField();
                map.put(field,message);
            });
    
            log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
    
            return R.error(400,"数据校验出现问题").put("data",map);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • ③. 测试 http://localhost:88/api/product/brand/save
      在这里插入图片描述
    • ④. 默认异常处理
    //如果在异常类上这些都没有进行匹配,最后都会由这个异常进行处理
    @ExceptionHandler(value = Throwable.class)//异常的范围更大
    public R handleException(Throwable throwable){
        log.error("未知异常{},异常类型{}",
                  throwable.getMessage(),
                  throwable.getClass());
        return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),
                       BizCodeEnum.UNKNOW_EXEPTION.getMsg());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ④. 处理错误状态码

    • ①. 上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
      上面的用法主要是通过@RestControllerAdvice+@ExceptionHandler来进行异常拦截处理

    • ②. 为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

    package com.atguigu.common.exception;
    /***
     * 错误码和错误信息定义类
     * 1. 错误码定义规则为5为数字
     * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
     * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
     * 错误码列表:
     *  10: 通用
     *      001:参数格式校验
     *  11: 商品
     *  12: 订单
     *  13: 购物车
     *  14: 物流
     */
    public enum BizCodeEnum {
        UNKNOW_EXEPTION(10000,"系统未知异常"),
    
        VALID_EXCEPTION( 10001,"参数格式校验失败");
        private Integer code;
        private String msg;
        BizCodeEnum(Integer code,String msg){
            this.code=code;
            this.msg=msg;
        }
        public int getCode() {
            return code;
        }
    
        public String getMsg() {
            return msg;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • ③. 修改处理异常类代码
    /**
     * 集中处理所有的异常
     */
    @Slf4j
    //@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
    @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
    //@RestControllerAdvice=@ControllerAdvice+@ResponseBody
    public class GulimallExceptionControllerAdvice {
    
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public R handleValidException(MethodArgumentNotValidException exception){
            log.error("数据校验出现问题{},异常类型{}", exception.getMessage(), exception.getClass());
            if (exception.getBindingResult().hasErrors()){
                Map<String,String>map=new HashMap<>();
                exception.getBindingResult().getFieldErrors().forEach((result)->{
                    map.put(result.getField(),result.getDefaultMessage());
                });
                return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(),BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data",map);
            }
            return R.error();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • ④. 测试: http://localhost:88/api/product/brand/save
      在这里插入图片描述

    ⑤. 分组校验功能(多场景校验)

    • ①. 前面解决了统一异常处理,但是现状有新的需求是对同一实体类参数也要区分场景
      如果新增和修改两个接口需要验证的字段不同,比如id字段,新增可以不传递,但是修改必须传递id,我们又不可能写两个vo来满足不同的校验规则。所以就需要用到分组校验来实现。

    • ②.分组校验步骤:

    1. 创建分组接口AddGroup.java和UpdateGroup.java
    2. 在VO的属性中标注@NotBlank等注解,并指定要使用的分组,如@NotNull(message = “用户姓名不能为空”,groups = {AddGroup.class,UpdateGroup.class})
      (这个意思是:在新增、修改的时候该属性不能为空)
    3. controller的方法上或者方法参数上写要处理的分组的接口信息,如@Validated(AddGroup.class)
    /**
     * 品牌id
     */
    @NotNull(message ="修改必须指定品牌id",groups ={UpdateGroup.class} )
    @Null(message ="新增不能指定品牌id",groups ={AddGroup.class}  )
    @TableId
    private Long brandId;
    /**
    * 品牌logo地址 修改可以不带上logoURL
    */
    //注意下面因为@NotBlank没有指定UpdateGroup分组,所以不生效。
    //此时update时可以不携带,但带了一定得是合法的url地址
    @NotBlank(groups = {AddGroup.class})
    @URL(message = "logo必须是一个合法的URL地址", groups={AddGroup.class, UpdateGroup.class})
    private String logo;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
      @RequestMapping("/save")
        //public R save(@Valid @RequestBody BrandEntity brand/*, BindingResult result*/){
        public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand/*, BindingResult result*/){
    //		if(result.hasErrors()){
    //		    Mapmap=new HashMap<>();
    //            //1.获取错误的校验结果
    //		    result.getFieldErrors().forEach((item)->{
    //                //获取发生错误的字段
    //                String field = item.getField();
    //                //获取发生错误时的message
    //                String message = item.getDefaultMessage();
    //                map.put(field,message);
    //            });
    //            return R.error(400,"提交的数据不合法").put("data",map);
    //        }
            brandService.save(brand);
            return R.ok();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    ⑥. 自定义校验注解

    • ①. 场景:要校验showStatus的0/1状态,可以用正则,但我们可以利用其他方式解决复杂场景。比如我们想要下面的场景,当前台输入0或者1的时候是合法的,当前台输入不是0或者1就要给出相对于的提示
    /**
     * 显示状态[0-不显示;1-显示]
     */
    //自定义注解
    @ListValue(vals={0,1},groups = AddGroup.class)
    private Integer showStatus;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • ②. 导入依赖,自定义校验注解有三步:
    1. 编写一个自定义的校验注解
    2. 编写一个自定义的校验器ConstraintValidator
    3. 关联自定义的校验器和自定义的校验注解
    4. 注意{ListValueConstraintValidator.class(可以指定多个不同的校验器,适配不同类型的校验)}
    <!--校验-->
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • ③. 自定义校验注解,必须有3个属性
      (@Constraint(validatedBy = {ListValueConstraintValidator.class}) ListValueConstraintValidator会在第二步进行定义)
    1. message()错误信息
    2. groups()分组校验
    3. payload()自定义负载信息
    4. 因为上面的message值对应的最终字符串需要去ValidationMessages.properties中获得,所以我们在common中新建文件ValidationMessages.properties(如果出现了乱码删除文件重新定义)
    @Documented
    @Constraint(validatedBy = {ListValueConstraintValidator.class})
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ListValue {
        String message() default "{com.atguigu.common.valid.ListValue.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        int []vals()default {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    com.atguigu.common.valid.ListValue.message=必须提交指定的值[0,1]
    
    • 1
    • ④. 自定义校验器ConstraintValidator
    1. 上面只是定义了异常消息,但是怎么验证是否异常还没说,下面的ConstraintValidator就是说的比如我们要限定某个属性值必须在一个给定的集合里,那么就通过重写initialize()方法,指定可以有哪些元素而controller接收到的数据用isValid(验证)
    2. 具体的校验类需要实现ConstraintValidator接口,第一个泛型参数是所对应的校验注解类型,第二个是校验对象类型。在初始化方法initialize中,我们可以先做一些别的初始化工作,例如这里我们获取到注解上的value并保存下来,然后生成set对象。
    3. 真正的验证逻辑由isValid完成,如果传入形参的属性值在这个set里就返回true,否则返回false
    public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {//<注解,校验值类型>
    
        private Set<Integer> set = new HashSet<>();
        //初始化方法
        @Override
        // 存储所有可能的值
        public void initialize(ListValue constraintAnnotation) {
            // 获取后端写好的限制 // 这个value就是ListValue里的value,我们写的注解是@ListValue(value={0,1})
            int[] vals = constraintAnnotation.vals();
            for (int val : vals) {
                set.add(val);
            }
    
        }
    
        //判断是否校验成功
    
        /**
         *
         * @param value 需要校验的值
         * @param context
         * @return
         */
        @Override
        public boolean isValid(Integer value, ConstraintValidatorContext context) {
            // 看是否在限制的值里
            return set.contains(value);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在这里插入图片描述

    • ⑤. 关联校验器和校验注解(一个校验注解可以匹配多个校验器)
    @Constraint(validatedBy = { ListValueConstraintValidator.class})
    
    • 1
    • ⑥. 使用实例
    /**
     * 显示状态[0-不显示;1-显示]
     */
    //自定义注解
    @ListValue(vals={0,1},groups = AddGroup.class)
    private Integer showStatus;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • ⑦. 使用postman进行测试,当我们将showStatus字段输入数字3,显示乱码。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • 相关阅读:
    Javaweb基础-前端工程化学习笔记
    Docker Desktop更改镜像存储位置
    【SSM框架】Mybatis详解08(源码自取)之优化注册,#{}与¥{}区别,返回主键,UUID
    Laravel 的事件监听器与服务提供者和服务容器的二三事
    Linux下“多线程”相关内容整理总结
    Mysql之增删改查案例【进阶篇】
    供销社物资仓库管理系统-RFID供销社物资管理系统
    康耐视深度学习ViDi-Database菜单介绍
    List<HashMap<String,String>>实现自定义字符串排序(key排序、Value排序)
    《吐血整理》高级系列教程-吃透Fiddler抓包教程(25)-Fiddler如何优雅地在正式和测试环境之间来回切换-下篇
  • 原文地址:https://blog.csdn.net/TZ845195485/article/details/126797721