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

摘要:
AOP+自定义注解实现全局参数校验在开发过程中,用户传递的数据不一定合法,虽然可以通过前端进行一些校验,但是为了确保程序的安全性,保证数据的合法,在后台进行数据校验也是十分必要的。思想是通过自定义注解来完成对实体类属性的标注,在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再进行反射判断

免责声明:文章转载自《AOP+自定义注解实现全局参数校验》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇使用Git在Mac和Windows系统之间进行同步数据JS日历控件集合----附效果图、源代码下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

java 类型转换

在java项目的实际开发和应用中,常常需要用到将对象转为String这一基本功能。本文将对常用的转换方法进行一个总结。常用的方法有Object.toString(),(String)要转换的对象,String.valueOf(Object)等。下面对这些方法一一进行分析。方法1:采用 Object.toString()方法请看下面的例子:      Obj...

c#调用存储过程两种方法

摘要 存储过程的调用在B/S系统中用的很多。传统的调用方法不仅速度慢,而且代码会随着存储过程的增多不断膨胀,难以维护。新的方法在一定程度上解决了这些问题。   关键词 ASP.NET;存储过程     在使用.NET的过程中,数据库访问是一个很重要的部分,特别是在B/S系统的构建过程中,数据库操作几乎成为了一个必不可少的操作。调用存储过程实现数据库操作使很...

C# POST multipart/form-data 方式提交数据

一.提交方法 /// <summary> /// MultipartFormData Post方式提交 /// </summary> /// <param name="url"></param> /// <param name="kVD...

UTL_FILE 的用法

UTL_FILE 是用来进行文件IO处理的专用包,使用这外包的注意事项如下: 1. 生成的文件好象只能放置在DATABASE所在的服务器路径中. 2. 生成的文件如何DOWNLOAD到本地来,还有待研究. Coding步骤: 1. 注册文件输出路径 Create directory path[例如: C:\AA]as 'pathname'; 此命令应由数...

免费的天气Web Service接口

免费的天气Web Service接口 在android应用当中很多时候需要获取天气的信息,这里提供怎么获取天气信息: 1. http://www.ayandy.com/Service.asmx?wsdl 官网:http://www.ayandy.com 2. http://webservice.webxml.com.cn/WebServices/Weat...

波场TRX 钱包开发,看这篇就够了

波场TRON作为一种基于区块链的去中心化内容协议,其目标意为通过区块链与分布式存储技术,构建一个全球范围内的自由内容娱乐体系,这个协议可以让每个用户自由发布,存储,拥有数据,并通过去中心化的自治形式,以数字资产发行,流通,交易方式决定内容的分发、订阅、推送,赋能内容创造者,形成去中心化的内容娱乐生态。   TRX是TRON区块链上账户的基本单位,所有其他代...