学习记录 springboot使用Aop进行参数校验
2025-01-15 09:02 阅读(113)

前言

在项目练习中。对前端传递的参数后端会进行一个校验,一般情况下都是使用if对参数进行校验太麻烦,重复的代码太多,这个时候我们就需要使用aop。

在Spring Boot中,参数校验通常使用javax.validation.constraints包中的注解,这里我们就使用aop进行一个参数校验

www.zuocode.com

一、导入aop的包到项目中

在项目中导入一个aop的包,版本根据你的sprigboot版本来

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

二、aop实现

在项目目录中新建一个aspect包,在后在这个包文件夹下新建一个切面类,切面类的名字可以自定义,我这边的名字就叫做OperationAspect

我们需要将创建的类定义成一个切面类,并且需要将其交给spring管理,我们需要在这个类上添加一些注解来实现 @Aspect @Component

@Aspect
@Component("operationAspect")
public class OperationAspect {
    
}

我们需要设置切入点,来告诉程序我们从哪个地方进行切入,就好比一个西瓜横着还是竖着切,需要告诉程序,切入点设置完成后,我们使用自定义注解的形式来使用

@Pointcut("@annotation()")
private void pointcut() {

}

创建一个annotation的包,在里面定义一个annotation的类,之后使用注解来定义切入点,这个自定义注解还需要设置俩个注解,一个用来告诉程序在什么地方使用,一个什么时候运行,在里面自定义我们的方法和默认参数

@Target({ElementType.METHOD}),@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalInterceptor {
    // 是否校验参数
    boolean checkParams() default true;
}

将这个注解的包名和方法添加到我上面切入点空缺的地方

@Aspect
@Component("operationAspect")
public class OperationAspect {
    @Pointcut("@annotation(com.easyjob.annotation.GlobalInterceptor)")
    private void pointcut() {

    }
}

这样我们的一个aop就创建成功了,在需要使用的地方加上我们自定义的注解就可以了,接下来我们就可以来写aop需要处理的逻辑了

三、 实现aop参数校验

我们使用前置通知来实现一些我们需要实现的功能,我们通过joinPoint来获取,我们的方法名和方法上的其他参数,在通过获取的method来拿到方法上的注解,在通过判断注解上checkParams是否是需要校验,通过定义的validateParams来校验参数,这个方法传递俩个参数,我们获取的method,和方法上面的参数arguments

private Logger logger = LoggerFactory.getLogger(OperationAspect.class);

//    前置通知
@Before("@annotation(com.easyjob.annotation.GlobalInterceptor)")
public void interceptorDo(JoinPoint joinPoint) {
    // 获取参数
    Object[] arguments = joinPoint.getArgs();
    //TODO  使用反射来获取方法的参数名和其他信息 例如方法名、参数和返回值等
    Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    //获取方法上的注解
    GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
    if (interceptor == null) {
        return;
    }

    // 参数校验
    if (interceptor.checkParams()) {
        validateParams(method, arguments);
    }
}

// 参数校验
private void validateParams(Method method, Object[] arguments) {

}

我们在定义一个VerifyParams注解来校验数据的最小最大长度和是否需要正则校验


如果需要其他条件可以自行添加,这里我们的正则校验使用的是一个枚举类,这个类里面定义了一些正则规则,如果需要其他的可以自行查找添加

@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VerifyParams {
    // 最小长度
    int min() default -1;

    // 最大长度
    int max() default -1;

    // 是否必填
    boolean required() default false;

    // 校验正则
    VerifyRegexEnum regex() default VerifyRegexEnum.NO;
}

正则枚举类

public enum VerifyRegexEnum {
    NO("", "不校验"),
    IP("([1-9]|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])(\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])){3}", "IP地址"),
    POSITIVE_INTEGER("^[0-9]*[1-9][0-9]*$", "正整数"),
    NUMBER_LETTER_UNDER_LINE("^\w+$", "由数字、26个英文字母或者下划线组成的字符串"),
    EMAIL("^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$", "邮箱"),
    PHONE("(1[0-9])\d{9}$", "手机号码"),
    COMMON("^[a-zA-Z0-9_\u4e00-\u9fa5]+$", "数字,字母,中文,下划线"),
    PASSWORD("^(?=.*\d)(?=.*[a-zA-Z])[\da-zA-Z~!@#$%^&*_]{8,}$", "只能是数字,字母,特殊字符 8-18位"),
    ACCOUNT("^[0-9a-zA-Z_]{1,}$", "字母开头,由数字、英文字母或者下划线组成"),
    MONEY("^[0-9]+(.[0-9]{1,2})?$", "金额");

    private String regex;
    private String desc;

    VerifyRegexEnum(String regex, String desc) {
        this.regex = regex;
        this.desc = desc;
    }

    public String getRegex() {
        return regex;
    }

    public String getDesc() {
        return desc;
    }
}

编写回到我们定义的validateParams方法里面来编写代码

private void validateParams(Method method, Object[] arguments) {
    //method.getParameters() 它提供了关于参数的类型、名称和其他属性的信息。类型是一个 Parameter[]数组
    Parameter[] parameters = method.getParameters();
    // 获取方法上的参数
    for (int i = 0; i < parameters.length; i++) {
        Parameter parameter = parameters[i];
        //这个对象的值跟我们的方法参数是一样的
        Object value = arguments[i];
        // 获取参数上的注解
        VerifyParams verifyParams = parameter.getAnnotation(VerifyParams.class);
        if(verifyParams==null){
            continue;
        }
    }

}

Parameter[] parameters = method.getParameters();这个地方获取我们的参数名称,循环遍历出我们的参数 这个地方的parameters跟我们的arguments是一一对应的



校验的过程中,我们要根据类型来进行一个校验,如果传递的是一个对象要怎么校验如果是基础数据类型就直接校验,定义基本数据类型


添加基础数据类型,判断是否是对象,对象有对象的判断方法

private static final String[] TYPE_BASE = {"java.lang.String", "java.lang.Integer", "java.lang.Long"};

我们将处理参数的逻辑在单独抽出,在这个方法中处理校验参数的逻辑,让一个方法里面的代码尽量简洁,看起来

也舒服

新建一个方法,这个方法用来真正处理校验字段的逻辑,这里我们还需要校验正则方法,使用我们使用了一个正则校验的工具类VerifyUtils,有了这个工具类后我们就可以开始校验正则了

public class VerifyUtils {
    /**
     * 验证字符串是否符合指定的正则表达式
     *
     * @param regs 正则表达式
     * @param value 待验证的字符串
     * @return 如果字符串符合正则表达式,返回 true,否则返回 false
     */
    public static boolean verify(String regs, String value) {
        // 如果值为空,则返回 false
        if (StringTools.isEmpty(value)) {
            return false;
        }
        // 编译正则表达式
        Pattern pattern = Pattern.compile(regs);
        // 创建匹配器
        Matcher matcher = pattern.matcher(value);
        // 返回匹配结果
        return matcher.matches();
    }

    /**
     * 验证字符串是否符合指定的 VerifyRegexEnum 枚举类型中的正则表达式
     *
     * @param regs 正则表达式枚举
     * @param value 待验证的字符串
     * @return 如果字符串符合正则表达式,返回 true,否则返回 false
     */
    public static boolean verify(VerifyRegexEnum regs, String value) {
        // 调用 verify(String regs, String value) 方法进行验证
        return verify(regs.getRegex(), value);
    }

}

在代码中添加好校验正则的代码,如果有误抛出异常提醒

/**
 * 校验正则
 */
if (!isEmpty && !StringTools.isEmpty(verifyParams.regex().getRegex()) && VerifyUtils.verify(verifyParams.regex(), String.valueOf(value))) {
    throw new BusinessException(ResponseCodeEnum.CODE_600);
}

这样我们的基本数据类型校验就已经可以实现了!在要校验的方法参数前加上我们的@VerifyParams注解就可以了!

接下来我们实现如何根据对象类型进行一个校验

定义一个checkObjValue方法我们在这个方法里面编写校验对象参数的逻辑,这里需要使用反射获取到对象之后根据获取到的对象来进行处理


private void checkObjValue(Parameter parameter, Object obj) {
    /**
     * 1 、获取参数类型 传递的是对象 例如 com.xxx.entity.po.SysAccount
     */
    String typeName = parameter.getParameterizedType().getTypeName();

   
    try {
        /**
         *  2、 根据反射获取类和类的字段
         *      Class.forName 是一个静态方法,用于根据类的完全限定名(包括包名)获取 Class 对象。
         */
        Class classz = Class.forName(typeName);
        Field[] fields = classz.getDeclaredFields();
        
        /**
         *  3. 遍历反射获取到的字段
         */
        for (Field field : fields) {
            // 3.1 拿到对象字段上的注解
            VerifyParams fieldVerifyParams = field.getAnnotation(VerifyParams.class);
            if (fieldVerifyParams == null) {
                continue;
            }

            /**
             * 3.2 设置字段可访问
             */
            field.setAccessible(true);

            /**
             *  4. 获取字段的值
             */
            Object resultValue = field.get(obj);
            
            // 5. 校验
            checkValue(resultValue, fieldVerifyParams);
        }
    } catch (Exception e) {
        logger.error("校验参数失败", e);
        throw new BusinessException(ResponseCodeEnum.CODE_600);
    }
}

到这一步已经完成了,可以进行一个参数校验了。


四 完整代码

@Aspect
@Component("operationAspect")
public class OperationAspect {
//    @Pointcut("@annotation(com.easyjob.annotation.GlobalInterceptor)")
//    private void pointcut() {
//
//    }
    // 前置通知
//    @Before("pointcut()")
//    public void interceptorDo(JoinPoint joinPoint) {
//
//    }

    private Logger logger = LoggerFactory.getLogger(OperationAspect.class);

    // 基本数据类型
    private static final String[] TYPE_BASE = {"java.lang.String", "java.lang.Integer", "java.lang.Long"};
//    private static final String TYPE_STRING = "java.lang.String";
//    private static final String TYPE_INTEGER = "java.lang.Integer";
//    private static final String TYPE_LONG = "java.lang.Long";

    //    前置通知
    @Before("@annotation(com.easyjob.annotation.GlobalInterceptor)")
    public void interceptorDo(JoinPoint joinPoint) {
        // 获取参数
        Object[] arguments = joinPoint.getArgs();
        //TODO  使用反射来获取方法的参数名和其他信息 例如方法名、参数和返回值等
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取方法上的注解
        GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
        if (interceptor == null) {
            return;
        }

        // 参数校验
        if (interceptor.checkParams()) {
            validateParams(method, arguments);
        }
    }

    // 参数校验
    private void validateParams(Method method, Object[] arguments) {
        //method.getParameters() 它提供了关于参数的类型、名称和其他属性的信息。类型是一个 Parameter[]数组
        Parameter[] parameters = method.getParameters();
        // 获取方法上的参数
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            // 这个对象的值跟我们的方法参数是一样的(就是我们传递来的参数值)
            Object value = arguments[i];
            // 获取参数上的注解
            VerifyParams verifyParams = parameter.getAnnotation(VerifyParams.class);
            if (verifyParams == null) {
                continue;
            }

            /**
             * 获取参数类型: 例如 java.lang.String
             *  parameter.getParameterizedType().getTypeName();
             *
             */
            String paramTypeName = parameter.getParameterizedType().getTypeName();

            // 判断参数是否为基本数据类型
            if (ArrayUtils.contains(TYPE_BASE, paramTypeName)) {
                checkValue(value, verifyParams);
            } else {
                checkObjValue(parameter, verifyParams);
            }
        }
    }

    /**
     * 校验对象参数
     */

    private void checkObjValue(Parameter parameter, Object obj) {
        /**
         * 1 、获取参数类型 传递的是对象 例如 com.xxx.entity.po.SysAccount
         */
        String typeName = parameter.getParameterizedType().getTypeName();


        try {
            /**
             *  2、 根据反射获取类和类的字段
             *      Class.forName 是一个静态方法,用于根据类的完全限定名(包括包名)获取 Class 对象。
             */
            Class classz = Class.forName(typeName);
            Field[] fields = classz.getDeclaredFields();

            /**
             *  3. 遍历反射获取到的字段
             */
            for (Field field : fields) {
                // 3.1 拿到对象字段上的注解
                VerifyParams fieldVerifyParams = field.getAnnotation(VerifyParams.class);
                if (fieldVerifyParams == null) {
                    continue;
                }

                /**
                 * 3.2 设置字段可访问
                 */
                field.setAccessible(true);

                /**
                 *  4. 获取字段的值也就是 例如:18666666666
                 */
                Object resultValue = field.get(obj);

                // 5. 校验
                checkValue(resultValue, fieldVerifyParams);
            }
        } catch (Exception e) {
            logger.error("校验参数失败", e);
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }
    }


    /**
     * 开始校验基本参数
     *
     * @param value        方法传递的校验参数
     * @param verifyParams 这个的作用是用来区分是否是必须校验的,或者是使用了什么正则
     */
    private void checkValue(Object value, VerifyParams verifyParams) {
        //是否是空
        Boolean isEmpty = value == null || StringTools.isEmpty(value.toString());
        //长度
        Integer length = value == null ? 0 : value.toString().length();

        /**
         * 校验空
         */
        if (isEmpty && verifyParams.required()) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }

        /**
         * 校验长度 作用:如果值不为空,并且其长度不在指定的最大和最小长度范围内,就抛出一个业务异常
         */
        if (!isEmpty &&
                (verifyParams.max() != -1 && verifyParams.max() < length || verifyParams.min() != -1 &&
                        verifyParams.min() > length)
        ) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }

        /**
         * 校验正则
         */
        if (!isEmpty &&
                !StringTools.isEmpty(verifyParams.regex().getRegex())
                && VerifyUtils.verify(verifyParams.regex(), String.valueOf(value))
        ) {
            throw new BusinessException(ResponseCodeEnum.CODE_600);
        }
    }
}

作者:想努力找到前端实习的呆呆鸟

链接:https://juejin.cn