zoukankan      html  css  js  c++  java
  • AOP+自定义注解实现全局参数校验

    AOP+自定义注解实现全局参数校验

    在开发过程中,用户传递的数据不一定合法,虽然可以通过前端进行一些校验,但是为了确保程序的安全性,保证数据的合法,在后台进行数据校验也是十分必要的。

    后台的参数校验

    在controller方法中校验:

    后台的参数是通过controller方法获取的,所以最简单的参数校验的方法,就是在controller方法中进行参数校验。在controller方法中如果进行参数校验会有大量重复、没有太大意义的代码。

    使用拦截器、过滤器校验

    为了保证controller中的代码有更好的可读性,可以将参数校验的工作交由拦截器(Interceptor)或者过滤器(Filter)来完成,但是此时又存在一个问题:非共性的参数需要每个方法都创建一个与之对应的拦截器(或者过滤器)。

    实现对Entity的统一校验

    鉴于上述解决方案的缺点,我们可以借助AOP的思想来进行统一的参数校验。思想是通过自定义注解来完成对实体类属性的标注,在AOP中扫描加了自定义注解的属性,对其进行注解属性标注的校验。对于不满足的参数直接抛出自定义异常,交由全局异常处理来处理并返回友好的提示信息。

    在介绍此方法之前,我们先来介绍一下使用其会用到的一些内容。

    自定义异常

    在开发过程中,经常需要抛出一些异常,但是异常中没有状态码,自定义描述等属性。所以可以自定义一个异常。抛出异常时,使用全局异常处理,通过全局异常来处理此异常。

    注意:Aspect中的异常只有RuntimeException(及其子类)能被全局异常处理。

    所以我们通常将自定义异常定义为运行时异常。

    package cn.rayfoo.common.exception;
    
    import lombok.*;
    
    /**
     * @Author: rayfoo@qq.com
     * @Date: 2020/7/20 9:26 下午
     * @Description: 自定义的异常...
     */
    @Getter@Setter@Builder@NoArgsConstructor@AllArgsConstructor
    public class MyException extends RuntimeException{
    
        private int code;
    
        private String msg;
    
    }
    
    

    断言类

    在代码的执行过程中,我们经常需要在特定条件下(一般为是否满足某条件)抛出异常,此时需要加入抛异常、返回状态码、错误信息、记录日志等操作,此操作是大量重复的操作,所以借助Junit中Assert的思想,创建了下述的断言工具类,用于在指定条件下抛出一个自定义异常。

    package cn.rayfoo.common.exception;
    
    import cn.rayfoo.common.response.HttpStatus;
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p>断言类</p>
     * @date 2020/8/7 9:43
     */
    @Slf4j
    public class MyAssert {
    
        /**
         * 如果为空直接抛出异常 类似于断言的思想
         * @param status 当status为false 就会抛出异常 不继续执行后续语句
         * @param msg  异常描述
         */
        public static void assertMethod(boolean status, String msg) throws Exception {
            //为false抛出异常
            if (!status) {
                //记录错误信息
                log.error(msg);
                //抛出异常
                throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(msg).build();
            }
        }
    
        /**
         * 如果为空直接抛出异常 类似于断言的思想
         * @param status 当status为false 就会抛出异常 不继续执行后续语句
         * @param code 状态码
         * @param msg  异常描述
         */
        public static void assertMethod(boolean status,Integer code, String msg) throws Exception {
            //为false抛出异常
            if (!status) {
                //记录错误信息
                log.error(msg);
                //抛出异常
                throw MyException.builder().code(code).msg(msg).build();
            }
        }
    
        /**
         * 如果为空直接抛出异常 类似于断言的思想
         * @param status 当status为false 就会抛出异常 不继续执行后续语句
         */
        public static void assertMethod(boolean status) throws Exception {
            //为false抛出异常
            if (!status) {
                //记录错误信息
                log.error(HttpStatus.INTERNAL_SERVER_ERROR.name());
                //抛出异常
                throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg(HttpStatus.INTERNAL_SERVER_ERROR.name()).build();
            }
        }
    }
    
    

    当调用断言方法时,只要传递一个boolean表达式,当表达式为false,就会抛出一个异常,提前结束方法。这个异常,通常由全局异常处理类来拦截。

    全局异常处理拦截断言抛出的方法

    package cn.rayfoo.common.exception;
    
    import cn.rayfoo.common.response.HttpStatus;
    import cn.rayfoo.common.response.Result;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.io.FileNotFoundException;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * @date 2020/8/5 14:55
     * @description 全局异常处理
     */
    @ControllerAdvice@Slf4j
    public class ServerExceptionResolver {
    
        /**
         * 对某种异常进行处理,如果非前后端分离的项目此处可以使用ModelAndView 返回错误页面
         * @param ex
         * @return
         */
        @ExceptionHandler(Exception.class)@ResponseBody
        public Result<String> resolveException(Exception ex) {
    
            //打印完整的异常信息
            ex.printStackTrace();
    
            //创建result
            Result<String> result = new Result<>();
    
            //设置result属性
            result.setData(ex.getMessage());
            result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            //判断异常类型
            if(ex instanceof FileNotFoundException){
                log.error("文件问找到异常。。。");
                // TODO 自定义一个Status
                result.setMsg("文件未找到,请检查文件是否存在!");
            }
            else if(ex instanceof RuntimeException){
                log.error("服务器内部发生了异常");
                result.setMsg(HttpStatus.INTERNAL_SERVER_ERROR.name());
            }else{
                log.error("服务器内部发生了异常");
                result.setMsg(HttpStatus.INTERNAL_SERVER_ERROR.name());
            }
            //.....
            return result;
        }
    
    
        /**
         * 处理自定义的异常
         * @param ex
         * @return
         */
        @ExceptionHandler(MyException.class)@ResponseBody
        public Result<String> resolveMyException(MyException ex){
            //打印完整的异常信息
            ex.printStackTrace();
            //创建result
            Result<String> result = new Result<>();
            //设置result属性
            result.setData(ex.getMsg());
            result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            result.setMsg(ex.getMsg());
            //保存自定义异常日志
            log.error(ex.getMsg());
            return result;
        }
    }
    
    

    定义Verify注解

    准备好上面的内容,我们就可以使用自定义注解+Aspect来完成全局的参数校验了~

    此注解用于注解实体类的属性,这个注解中,创建了以下几个属性:

    • name:用于描述修饰的字段,当校验失败时,提示用户字段的具体名称。
    • maxLength:最大的长度,对字符串长度进行校验,如果是默认值代表不进行长度校验
    • minLength:最小的长度,同样进行字符串长度的校验,如果是默认值代表不进行长度校验
    • required:是否是必填属性,即进行非空判断
    • notNull:进行非空和非空串的判断
    • regular:指定用于校验的正则表达式,如果为RegexOption.DEFAULT表示不进行正则校验
    package cn.rayfoo.common.annotation;
    
    import cn.rayfoo.common.enums.RegexOption;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/7 15:33
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD,ElementType.PARAMETER})
    public @interface Verify {
    
        /** 参数名称 */
        String name();
    
        /** 参数最大长度 */
        int maxLength() default Integer.MAX_VALUE;
    
        /** 是否必填 这里只是判断是否为null */
        boolean required() default true;
    
        /** 是否为非空 是否为null和空串都判断 */
        boolean notNull() default true;
    
        /** 最小长度 */
        int minLength() default Integer.MIN_VALUE;
    
        /** 正则匹配 */
        RegexOption regular() default RegexOption.DEFAULT;
    
    }
    
    

    上面的自定义注解中使用到了RegexOption枚举,此注解只写了常见的正则校验方法,如果需要拓展可以自定添加,下面是此枚举的代码:

    package cn.rayfoo.common.enums;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/7 15:51
     */
    public enum RegexOption {
    
        /**
         * 缺省,表示不进行正则校验
         */
        DEFAULT(""),
    
        /**
         * 邮箱正则
         */
        EMAIL_REGEX("^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$"),
    
        /**
         * 手机号正则
         */
        PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\d{8}$"),
    
        /**
         * 身份证正则
         */
        IDENTITY_CARD_REGEX("(^\d{18}$)|(^\d{15}$)"),
    
        /**
         * URL正则
         */
        URL_REGEX("http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"),
    
        /**
         * IP地址正则
         */
        IP_ADDR_REGEX("(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)"),
    
        /**
         * 用户名正则
         */
        USERNAME_REGEX("^[a-zA-Z]\w{5,20}$"),
    
        /**
         * 密码正则
         */
        PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
    
        /**
         * 正则
         */
        private String regex;
    
        /**
         * 构造方法
         *
         * @param regex
         */
        private RegexOption(String regex) {
            this.regex = regex;
        }
    
    
        public String getRegex() {
            return regex;
        }
    
        public void setRegex(String regex) {
            this.regex = regex;
        }
    }
    

    使用Aspect进行全局参数校验

    前面的准备工作做好,就可以进行全局的参数校验了~

    package cn.rayfoo.common.aspect;
    
    import cn.rayfoo.common.annotation.Verify;
    import cn.rayfoo.common.enums.RegexOption;
    import cn.rayfoo.common.exception.MyAssert;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.util.StringUtils;
    
    import java.lang.reflect.Field;
    import java.util.regex.Pattern;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p>Controller中的全局参数校验</p>
     * @date 2020/8/7 14:03
     */
    @Aspect
    //@Component
    @Slf4j
    public class EntityValidatorAspect {
    
        /**
         * 定义一个方法,用于声明切入表达式。
         */
        @Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
        public void validatorPointcut() {
        }
    
        @Before("validatorPointcut()")
        public void parameterVerify(JoinPoint point) throws Exception {
    
            //迭代所有参数
            for (int i = 0; i < point.getArgs().length; i++) {
                //切点对象
                Object obj = point.getArgs()[i];
                Class clazz = obj.getClass();
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                    //需要做校验的参数
                    if (field.isAnnotationPresent(Verify.class)) {
                        //获取注解对象
                        Verify verify = field.getAnnotation(Verify.class);
                        //取出注解的属性
                        String name = verify.name();
                        int maxLength = verify.maxLength();
                        int minLength = verify.minLength();
                        boolean required = verify.required();
                        boolean notNull = verify.notNull();
                        RegexOption regular = verify.regular();
                        //属性值
                        Object fieldObj = field.get(obj);
                        //是否时必传 断言判断
                        if (required) {
                            MyAssert.assertMethod(fieldObj != null, String.format("【%s】为必传参数", name));
                        }
                        //字符串的 非空校验
                        if (notNull) {
                            MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能为空", name));
                        }
                        //是否有最大长度限制 断言判断
                        if (Integer.MAX_VALUE != maxLength) {
                            MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最大长度为【%s】", name, maxLength));
                        }
                        //是否有最小长度限制 断言判断
                        if (Integer.MIN_VALUE != minLength) {
                            MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最小长度为【%s】", name, minLength));
                        }
                        //是否有正则校验
                        if (!"".equals(regular.getRegex())) {
                            Pattern pattern = Pattern.compile(regular.getRegex());
                            //断言判断正则
                            MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("参数【%s】的请求数据不符合规则", name));
                        }
                    }
                }
            }
        }
    }
    
    

    上述的校验适用于Controller方法中参数为自定义的实体类,但是对于Map类型、普通类型(包括包装类型)的参数还无法完成校验。后续可以考虑增加对自定义注解的拓展,即可以允许加在方法参数上。

    对于Map类型的参数进行校验

    上述的校验完成后,又发现了一个问题:如果Controller方法的参数是Map类型,如何完成参数的校验?

    经过一番思考,结合上面案例的解决方案,最终也实现了对map的校验,但是要求比较严苛:由于其原理是通过key来匹配校验规则,所以map中的key,必须是后端指定的key才能自动完成校验。

    下面介绍以下具体的实现方法:

    创建校验枚举

    这个枚举是不是很眼熟呀,没错 就是基于上面的注解编写的,增加了一个key属性。通过key属性可以判断map中指定的key进行何种正则校验。

    package cn.rayfoo.common.enums;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/7 15:51
     */
    public enum JSONRegexOption {
    
        /**
         * 缺省,表示不进行正则校验
         */
        DEFAULT("",""),
    
        /**
         * 邮箱正则
         */
        EMAIL_REGEX("email","^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$"),
    
        /**
         * 手机号正则
         */
        PHONE_NUMBER_REGEX("phoneNumber","^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\d{8}$"),
    
        /**
         * 身份证正则
         */
        IDENTITY_CARD_REGEX("identityCard","(^\d{18}$)|(^\d{15}$)"),
    
        /**
         * URL正则
         */
        URL_REGEX("url","http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"),
    
        /**
         * IP地址正则
         */
        IP_ADDR_REGEX("ipAddr","(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)"),
    
        /**
         * 用户名正则
         */
        USERNAME_REGEX("username","^[a-zA-Z]\w{5,20}$"),
    
        /**
         * 密码正则
         */
        PASSWORD_REGEX("password","^[a-zA-Z0-9]{6,20}$");
    
    
        /**
         * JSON的key
         */
        private String key;
    
        /**
         * 正则
         */
        private String regex;
    
    
    
        /**
         * 构造方法
         *
         * @param regex
         */
        private JSONRegexOption(String key,String regex) {
            this.key = key;
            this.regex = regex;
        }
    
        public String getKey() {
            return key;
        }
    
        public void setKey(String key) {
            this.key = key;
        }
    
        public String getRegex() {
            return regex;
        }
    
        public void setRegex(String regex) {
            this.regex = regex;
        }
    
    }
    
    

    在Aspect中进行全局校验

    经过反复的断点测试发现,Map类型的参数在JoinPoint中获取时是通过java.util.LinkedHashMap类型来接受的。所以我们可以通过判断参数的类型来判断当前参数是否为map,如果为Map通过遍历Map的key来实现全局的校验:

    package cn.rayfoo.common.aspect;
    
    import cn.rayfoo.common.enums.JSONRegexOption;
    import cn.rayfoo.common.exception.MyAssert;
    import cn.rayfoo.common.exception.MyException;
    import cn.rayfoo.common.response.HttpStatus;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.util.LinkedHashMap;
    import java.util.Set;
    import java.util.regex.Pattern;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p>Controller中的JSON全局参数校验</p>
     * @date 2020/8/7 14:03
     */
    @Aspect
    @Component
    @Slf4j
    public class JsonValidatorAspect {
    
        /**
         * 定义一个方法,用于声明切入表达式。
         */
        @Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
        public void validatorPointcut() {
        }
    
        @Before("validatorPointcut()")
        public void parameterVerify(JoinPoint point) throws Exception {
    
            //迭代所有参数
            for (int i = 0; i < point.getArgs().length; i++) {
                //切点对象
                Object obj = point.getArgs()[i];
                //将数据转换为json
                Class clazz = obj.getClass();
                //如果是map接收参数
                if ("java.util.LinkedHashMap".equals(clazz.getName())) {
                    //获取集合
                    LinkedHashMap map = (LinkedHashMap) obj;
                    //获取key列表
                    Set set = map.keySet();
                    //迭代key
                    for (Object key : set) {
                        //如果有空值 或者空字符串
                        if (StringUtils.isEmpty(map.get(key))) {
                            throw MyException.builder().code(HttpStatus.INTERNAL_SERVER_ERROR.value()).msg("数据中存在空值!").build();
                        }
                        //用户名校验
                        valueValidate(JSONRegexOption.USERNAME_REGEX, map, key, "您输入的用户名不符合规范");
                        //密码校验
                        valueValidate(JSONRegexOption.PASSWORD_REGEX, map, key, "您输入的密码不符合规范");
                        //邮箱校验
                        valueValidate(JSONRegexOption.EMAIL_REGEX, map, key, "您输入的邮箱不符合规范");
                        //手机号校验
                        valueValidate(JSONRegexOption.PHONE_NUMBER_REGEX, map, key, "您输入的手机号不符合规范");
                        //身份证号校验
                        valueValidate(JSONRegexOption.IDENTITY_CARD_REGEX, map, key, "您输入的身份证号不符合规范");
                        //ip校验
                        valueValidate(JSONRegexOption.IP_ADDR_REGEX, map, key, "您输入的IP不符合规范");
                        //url校验
                        valueValidate(JSONRegexOption.URL_REGEX, map, key, "您输入的URL不符合规范");
                    }
                }
            }
        }
    
        /**
         * 正则校验
         *
         * @param regex 正则
         * @param param 需要校验的值
         * @return 校验结果
         */
        public boolean regexValidate(String regex, String param) {
            Pattern pattern = Pattern.compile(regex);
            return param.matches(regex);
        }
    
        /**
         * @param regexOption 校验类型
         * @param map         数据集
         * @param key         校验的key
         * @param msg         如果出错返回的信息
         */
        public void valueValidate(JSONRegexOption regexOption, LinkedHashMap map, Object key, String msg) throws Exception {
            //密码校验
            if (regexOption.getKey().equals(key.toString())) {
                //根据key获取值
                String value = map.get(key).toString();
                //值校验
                MyAssert.assertMethod(regexValidate(regexOption.getRegex(), value), msg);
            }
        }
    }
    
    

    对Map类型参数校验的优化

    对于Map类型参数的校验还有优化的办法,能够解决key的硬编码问题。想到了一种解决思路,稍后可以尝试一下。

    思路

    1. 创建一个注解加在方法的参数上,其可以指定一个或一组Entity类的全路径。
    2. 在Aspect中通过获取此注解获取所有Entity。
    3. 再使用反射来获取这些Entity中加入注解的属性。
    4. 通过属性名(匹配key)属性上注解的实例(匹配校验规则)
    5. 从而实现全局值校验。

    对于普通类型(包括包装类型)的优化

    对于普通类型(包括包装类型),可以编写一些单独的校验注解。当参数上增加了这些注解,就进行相关的校验。

    对于List、Set、List的校验

    经过上面的一些解决方案,其实写出这样的校验已经不是什么难题,只需要在Aspect中进行相关的判断即可,具体的实现大家可以多尝试哈~~

    有什么更好的解决方案欢迎留言一起交流

    来自一小时后的更新。。。。

    完善Map类型的校验~

    对于上述的想法立马进行了实践,完善了对Map类型参数的校验,再说一遍思路:

    首先要在map参数前加上一个自定义注解,此注解只有一个属性,用于声明此map中要校验的数据来自哪些实体类。(实体类需要指定全类名,因为要对其进行反射)

    package cn.rayfoo.common.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/8 19:50
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface VerifyEntity {
    
        /**
         * 实体类全类名列表
         */
        String[] baseEntityList();
    
    }
    
    

    在方法上加上注解:

        @PutMapping("/updatePhone")
        public Result<Object> updatePhone(@RequestBody @VerifyEntity(baseEntityList = {"cn.rayfoo.modules.base.entity.User"}) Map<String, Object> record) {
            return null;
        }
    

    校验Aspect代码:

    package cn.rayfoo.common.aspect;
    
    import cn.hutool.core.util.ArrayUtil;
    import cn.rayfoo.common.annotation.Verify;
    import cn.rayfoo.common.annotation.VerifyEntity;
    import cn.rayfoo.common.exception.MyAssert;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.lang.reflect.Parameter;
    import java.util.LinkedHashMap;
    import java.util.Set;
    import java.util.regex.Pattern;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p>Controller中的JSON全局参数校验</p>
     * @date 2020/8/7 14:03
     */
    @Aspect
    @Component
    @Slf4j
    public class JsonValidatorAspectPlus {
    
        /**
         * 校验的类型
         */
        private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
    
        /**
         * 定义一个方法,用于声明切入表达式。
         */
        @Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
        public void validatorPointcut() {
        }
    
        @Before("validatorPointcut()")
        public void parameterVerify(JoinPoint point) throws Exception {
    
            //获取参数列表
            Object[] args = point.getArgs();
    
            //通过签名 获取方法签名
            MethodSignature signature = (MethodSignature) point.getSignature();
            //通过方法签名获取执行方法
            Method method = signature.getMethod();
            //获取参数上的所有注解
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            //获取参数列表
            Parameter[] parameters = method.getParameters();
    
            //拆分方法,提高阅读性
            isVerifyEntity(parameterAnnotations, args);
    
        }
    
    
        /**
         * 判断是否加了@VerifyEntity注解 加了再进行下一步的操作
         * @param parameterAnnotations 所有参数前的注解列表
         * @param args 所有的参数列表
         */
        private void isVerifyEntity(Annotation[][] parameterAnnotations, Object[] args) throws Exception {
            //判断是否加了VerifyEntity注解
            for (Annotation[] parameterAnnotation : parameterAnnotations) {
                //获取当前参数的位置
                int index = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation);
                for (Annotation annotation : parameterAnnotation) {
                    //获取注解的全类名
                    String verifyEntityName = VerifyEntity.class.getName();
                    //获取当前注解的全类名
                    String name = annotation.annotationType().getName();
                    //匹配是否相同
                    if (verifyEntityName.equals(name)) {
                        //获取此注解修饰的具体的参数
                        Object param =  args[index];
                        //如果存在此注解,执行方法
                        isLinkedHashMap(annotation,param);
                    }
                }
            }
        }
    
        /**
         * 判断是否为LinkedHashMap,如果是,进行进一步的操作
         * @param annotation 参数上的注解
         * @param param 注解所修饰的参数
         */
        private void isLinkedHashMap(Annotation annotation,Object param) throws Exception {
            //获取注解
            VerifyEntity verifyEntity = (VerifyEntity) annotation;
            //获取要校验的所有entity
            String[] entitys = verifyEntity.baseEntityList();
            //如果是map接收参数
            if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
                //如果存在Verify注解
                hasVerify(entitys, param);
            }
        }
    
        /**
         * 如果EntityList中的实体存在Verify注解
         * @param entityList 实体列表
         * @param param 加入@verifyEntity的注解 的参数
         */
        private void hasVerify(String[] entityList, Object param) throws Exception {
    
            //迭代entityList
            for (int i = 0; i < entityList.length; i++) {
                Field[] fields = Class.forName(entityList[i]).getDeclaredFields();
                //迭代字段
                for (Field field : fields) {
                    //判断是否加入了Verify注解
                    if (field.isAnnotationPresent(Verify.class)) {
                        //如果有 获取注解的实例
                        Verify verify = field.getAnnotation(Verify.class);
                        //校验
                        validateMap(param, verify, field.getName());
                    }
                }
            }
        }
    
        /**
         * 真正进行校验的类
         * @param param 增加@VerifyEntity注解的参数
         * @param verify Verify注解的实例
         * @param fieldName 加了Verify的属性name值
         */
        public void validateMap(Object param, Verify verify, String fieldName) throws Exception {
            //获取集合
            LinkedHashMap map = (LinkedHashMap) param;
            //获取key列表
            Set set = map.keySet();
            //迭代key
            for (Object key : set) {
                //如果key和注解的fieldName一致
                if (fieldName.equals(key)) {
                    //当前值
                    Object fieldObj = map.get(key);
                    //获取verify的name
                    String name = verify.name();
                    //是否时必传 断言判断
                    if (verify.required()) {
                        MyAssert.assertMethod(fieldObj != null, String.format("【%s】为必传参数", name));
                    }
                    //字符串的 非空校验
                    if (verify.notNull()) {
                        MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能为空", name));
                    }
                    //是否有最大长度限制 断言判断
                    int maxLength = verify.maxLength();
                    if (Integer.MAX_VALUE != maxLength) {
                        MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最大长度为【%s】", name, maxLength));
                    }
                    //是否有最小长度限制 断言判断
                    int minLength = verify.minLength();
                    if (Integer.MIN_VALUE != minLength) {
                        MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最小长度为【%s】", name, minLength));
                    }
                    //是否有正则校验
                    if (!"".equals(verify.regular().getRegex())) {
                        //初始化Pattern
                        Pattern pattern = Pattern.compile(verify.regular().getRegex());
                        //断言判断正则
                        MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("参数【%s】的请求数据不符合规则", name));
                    }
                }
            }
        }
    }
    
    

    此时,解决了Map和Entity两种常见参数的统一校验~

    已经解决了常见的参数校验啦~

    再次更新,完成普通类型、map、eitity三种校验的整合

    • 增强了@verify对于普通类型参数的支持

    • 增加了@RequestEntity、@RequestMap注解

    • 可以实现对Map、Entity、普通类型(包括包装类型)的全局校验

    • 对原有的多个Aspect进行了整合,JSONRegexOption、EntityValidatorAspect、JsonValidatorAspect都可以Deprecated了

    • 具有一定的拓展性,如需增加校验规则,只需要拓展RegexOption即可

    废话不多说,直接上代码:

    适用于普通参数和属性的检验注解:

    package cn.rayfoo.common.annotation;
    
    import cn.rayfoo.common.enums.RegexOption;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/7 15:33
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD,ElementType.PARAMETER})
    public @interface Verify {
    
        /** 参数名称 */
        String name();
    
        /** 参数最大长度 */
        int maxLength() default Integer.MAX_VALUE;
    
        /** 是否必填 这里只是判断是否为null */
        boolean required() default true;
    
        /** 是否为非空 是否为null和空串都判断 */
        boolean notNull() default true;
    
        /** 最小长度 */
        int minLength() default Integer.MIN_VALUE;
    
        /** 正则匹配 */
        RegexOption regular() default RegexOption.DEFAULT;
    
    }
    
    

    适用于Controller参数中的Map类型:

    package cn.rayfoo.common.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p>对Map</p>
     * @date 2020/8/8 19:50
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface RequestMap {
    
        /**
         * 实体类全类名列表
         */
        String[] baseEntityList();
    
    }
    
    

    适用于Controller方法中的Entity参数:

    package cn.rayfoo.common.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/8 22:43
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface RequestEntity {
    
        String value() default "";
    
    }
    
    

    适用于@Verify注解的枚举,如果需要新增校验,可以对此枚举进行拓展:

    package cn.rayfoo.common.enums;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p></p>
     * @date 2020/8/7 15:51
     */
    public enum RegexOption {
    
        /**
         * 缺省,表示不进行正则校验
         */
        DEFAULT(""),
    
        /**
         * 邮箱正则
         */
        EMAIL_REGEX("^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$"),
    
        /**
         * 手机号正则
         */
        PHONE_NUMBER_REGEX("^((13[0-9])|(14[0|5|6|7|9])|(15[0-3])|(15[5-9])|(16[6|7])|(17[2|3|5|6|7|8])|(18[0-9])|(19[1|8|9]))\d{8}$"),
    
        /**
         * 身份证正则
         */
        IDENTITY_CARD_REGEX("(^\d{18}$)|(^\d{15}$)"),
    
        /**
         * URL正则
         */
        URL_REGEX("http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?"),
    
        /**
         * IP地址正则
         */
        IP_ADDR_REGEX("(25[0-5]|2[0-4]\d|[0-1]\d{2}|[1-9]?\d)"),
    
        /**
         * 用户名正则
         */
        USERNAME_REGEX("^[a-zA-Z]\w{5,20}$"),
    
        /**
         * 密码正则
         */
        PASSWORD_REGEX("^[a-zA-Z0-9]{6,20}$");
    
        /**
         * 正则
         */
        private String regex;
    
        /**
         * 构造方法
         *
         * @param regex
         */
        private RegexOption(String regex) {
            this.regex = regex;
        }
    
    
        public String getRegex() {
            return regex;
        }
    
        public void setRegex(String regex) {
            this.regex = regex;
        }
    }
    
    

    Aspect:

    package cn.rayfoo.common.aspect;
    
    import cn.hutool.core.util.ArrayUtil;
    import cn.rayfoo.common.annotation.RequestEntity;
    import cn.rayfoo.common.annotation.RequestMap;
    import cn.rayfoo.common.annotation.Verify;
    import cn.rayfoo.common.exception.MyAssert;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.LinkedHashMap;
    import java.util.Set;
    import java.util.regex.Pattern;
    
    /**
     * @author rayfoo@qq.com
     * @version 1.0
     * <p>Controller中的JSON全局参数校验</p>
     * @date 2020/8/7 14:03
     */
    @Aspect
    @Component
    @Slf4j
    public class JsonValidatorAspectPlus {
    
        /**
         * 校验的类型
         */
        private static final String LINK_HASH_MAP_TYPE = "java.util.LinkedHashMap";
    
        /**
         * 定义一个方法,用于声明切入表达式。
         */
        @Pointcut("execution(* cn.rayfoo.modules..controller..*(..))")
        public void validatorPointcut() {
        }
    
        @Before("validatorPointcut()")
        public void parameterVerify(JoinPoint point) throws Exception {
    
    
    
            //通过签名 获取方法签名
            MethodSignature signature = (MethodSignature) point.getSignature();
            //通过方法签名获取执行方法
            Method method = signature.getMethod();
            //获取参数上的所有注解
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            //获取参数列表
            Object[] args = point.getArgs();
    
            //判断是否加了RequestMap注解
            for (Annotation[] parameterAnnotation : parameterAnnotations) {
                //获取当前参数的位置
                int index = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation);
                for (Annotation annotation : parameterAnnotation) {
                    //获取此注解修饰的具体的参数
                    Object param = args[index];
                    //如果有@RequestEntity注解
                    hasRequestEntity(annotation, param);
                    //如果有Verify注解 由于是参数上的注解 注意:此处传递的是具体的param 而非args
                    hasVerify(annotation, param);
                    //如果有RequestMap注解  由于是参数上的注解  注意:此处传递的是具体的param 而非args
                    hasRequestMap(annotation, param);
                }
    //            }
            }
    
        }
    
        /**
         * 如果参数存在RequestEntity注解
         *
         * @param annotation 参数上的注解
         * @param param      具体的参数
         */
        private void hasRequestEntity(Annotation annotation, Object param) throws Exception {
            //获取注解的全类名
            String requestEntityName = RequestEntity.class.getName();
            //获取当前注解的全类名
            String name = annotation.annotationType().getName();
            //匹配是否相同
            if (requestEntityName.equals(name)) {
                //获取参数的字节码
                Class clazz = param.getClass();
                //获取当前参数对应类型的所有属性
                Field[] fields = clazz.getDeclaredFields();
                //遍历属性
                for (Field field : fields) {
                    //获取私有属性值
                    field.setAccessible(true);
                    //需要做校验的参数
                    if (field.isAnnotationPresent(Verify.class)) {
                        //获取注解对象
                        Verify verify = field.getAnnotation(Verify.class);
                        //校验的对象
                        Object fieldObj = field.get(param);
                        //校验
                        validate(verify, fieldObj);
                    }
                }
            }
        }
    
    
        /**
         * 如果参数上加的是Verify注解
         *
         * @param annotation 参数上的注解
         * @param param      参数
         */
        private void hasVerify(Annotation annotation, Object param) throws Exception {
            //获取注解的全类名
            String verifyName = Verify.class.getName();
            //获取当前注解的全类名
            String name = annotation.annotationType().getName();
            //匹配是否相同
            if (verifyName.equals(name)) {
                //获取此注解修饰的具体的参数
                //获取当前注解的具值
                Verify verify = (Verify) annotation;
                //进行校验
                validate(verify, param);
            }
        }
    
        /**
         * 判断是否加了@RequestMap注解 加了再进行下一步的操作
         *
         * @param annotation 所有参数前的注解
         * @param param      当前参数
         */
        private void hasRequestMap(Annotation annotation, Object param) throws Exception {
            //获取注解的全类名
            String RequestMapName = RequestMap.class.getName();
            //获取当前注解的全类名
            String name = annotation.annotationType().getName();
            //匹配是否相同
            if (RequestMapName.equals(name)) {
                //如果存在此注解,执行方法
                isLinkedHashMap(annotation, param);
            }
        }
    
        /**
         * 判断是否为LinkedHashMap,如果是,进行进一步的操作
         *
         * @param annotation 参数上的注解
         * @param param      注解所修饰的参数
         */
        private void isLinkedHashMap(Annotation annotation, Object param) throws Exception {
            //获取注解
            RequestMap RequestMap = (RequestMap) annotation;
            //获取要校验的所有entity
            String[] entitys = RequestMap.baseEntityList();
            //如果是map接收参数
            if (LINK_HASH_MAP_TYPE.equals(param.getClass().getName())) {
                //如果存在Verify注解
                hasVerify(entitys, param);
            }
        }
    
        /**
         * 如果EntityList中的实体存在Verify注解
         *
         * @param entityList 实体列表
         * @param param      加入@RequestMap的注解 的参数
         */
        private void hasVerify(String[] entityList, Object param) throws Exception {
    
            //迭代entityList
            for (int i = 0; i < entityList.length; i++) {
                //获取所有字段
                Field[] fields = Class.forName(entityList[i]).getDeclaredFields();
                //迭代字段
                for (Field field : fields) {
                    field.setAccessible(true);
                    //判断是否加入了Verify注解
                    if (field.isAnnotationPresent(Verify.class)) {
                        //如果有 获取注解的实例
                        Verify verify = field.getAnnotation(Verify.class);
                        //校验
                        fieldIsNeedValidate(param, verify, field.getName());
                    }
                }
            }
        }
    
        /**
         * 字段是否需要校验
         *
         * @param param     增加@RequestMap注解的参数
         * @param verify    Verify注解的实例
         * @param fieldName 加了Verify的属性name值
         */
        private void fieldIsNeedValidate(Object param, Verify verify, String fieldName) throws Exception {
            //获取集合
            LinkedHashMap map = (LinkedHashMap) param;
            //获取key列表
            Set set = map.keySet();
            //迭代key
            for (Object key : set) {
                //如果key和注解的fieldName一致
                if (fieldName.equals(key)) {
                    //当前值
                    Object fieldObj = map.get(key);
                    //真正的进行校验
                    validate(verify, fieldObj);
                }
            }
        }
    
    
        /**
         * 正则的校验方法
         *
         * @param verify   校验规则
         * @param fieldObj 校验者
         */
        private void validate(Verify verify, Object fieldObj) throws Exception {
            //获取verify的name
            String name = verify.name();
            //是否时必传 断言判断
            if (verify.required()) {
                MyAssert.assertMethod(fieldObj != null, String.format("【%s】为必传参数", name));
            }
            //字符串的 非空校验
            if (verify.notNull()) {
                MyAssert.assertMethod(!StringUtils.isEmpty(fieldObj), String.format("【%s】不能为空", name));
            }
            //是否有最大长度限制 断言判断
            int maxLength = verify.maxLength();
            if (Integer.MAX_VALUE != maxLength) {
                MyAssert.assertMethod(maxLength > String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最大长度为【%s】", name, maxLength));
            }
            //是否有最小长度限制 断言判断
            int minLength = verify.minLength();
            if (Integer.MIN_VALUE != minLength) {
                MyAssert.assertMethod(minLength < String.valueOf(fieldObj).length(), String.format("【%s】长度不合理,最小长度为【%s】", name, minLength));
            }
            //是否有正则校验
            if (!"".equals(verify.regular().getRegex())) {
                //初始化Pattern
                Pattern pattern = Pattern.compile(verify.regular().getRegex());
                //断言判断正则
                MyAssert.assertMethod(pattern.matcher(String.valueOf(fieldObj)).matches(), String.format("参数【%s】的请求数据不符合规则", name));
            }
        }
    }
    
    

    在controller类中使用上述注解:

        @PutMapping("/updatePhone")
        public Result<Object> updatePhone(@RequestBody @RequestMap(baseEntityList = {"cn.rayfoo.modules.base.entity.User"}) Map<String, Object> record) {
            return null;
        }
    
    
        @PostMapping("/test")
        public Result<Object> test(@RequestBody @RequestEntity User user) {
            return Result.builder().msg("ok").code(200).data("success").build();
        }
    
        @GetMapping("/username")
        public Result<Object> usernameTest(@Verify(name = "用户名",regular = RegexOption.USERNAME_REGEX) String username) {
            return Result.builder().msg("ok").code(200).data("success").build();
        }
    

    对于组合Entity、List这类的数据还需要继续优化,目前已经有一些头绪。后续可能还会更新

    思路:

    对于组合Entity可以在@Verify增加一个属性 修饰是否该属性是一个Entity,进行递归式判断

    对于List可以先迭代list,再在list中的每个Object再进行反射判断

  • 相关阅读:
    JAVA写入文本文件
    oracle误删数据闪回
    Myeclipese :Creation of element failed解决方法
    Hibernate的四种状态
    java中list、set和map 的区别<转>
    C#操作mysql中临时表不自动删除
    WPF 实现地图的移动和滚动放大
    c# 将十六进制字符串写入注册表
    ASP.NET 视图状态概述:初步了解
    vs好用插件
  • 原文地址:https://www.cnblogs.com/zhangruifeng/p/13460060.html
Copyright © 2011-2022 走看看