Spring AOP+反射实现自定义动态配置校验规则,让校验规则飞起来


1. 场景描述

之前项目都是使用hibernate-validator来校验参数,但是实际上会出现一些小问题,就是校验规则都是通过注解的方式来完成,这样如果项目上线了,这个参数校验规则就没办法修改,如果出现校验规则问题,就必须修改后重新紧急上线(之前手机号码格式校验就出现过这个问题,因为新的号段不支持)。为了适应动态配置校验规则,在新起的项目我们就不再使用hibernate-validator校验规则,而是自己写个小功能来实现。

附加说明:如果在项目中对校验规则修改要求不是很高的,建议不要使用动态,乖乖使用hibernate-validator开发效率更高,更实用。

2. 实现思路

1、实现这种动态配置,就要能随时修改规则,并应用到实际业务逻辑中,直接在代码中写是不行的,因此这里采用数据库记录的方式是一个不错的选择;
2、需要对所有 controller 进入的参数校验,不能每个方法中加调用逻辑,这个必须写一个公共的方法,使用 Spring AOP 做切面切入所有的 controller 方法;
3、服务的请求方式,使用这种方式,最方便的就是使用 post 请求,入参后,参数都在一个类中封装,拿到类,使用反射,拿出参数的参数名和参数值。
基本就是以上思路,切面切入 controller 类中所有方法,拿到请求 Dto 类,利用反射技术拿出所有的参数名和参数值,从数据库中获取当前 Dto 类下所有参数的校验规则,依次对参数进行校验。

3. 项目构建

3.1 项目结构

项目结构
aspect:切面(DynamicCheckAspect)和校验引擎(DynamicCheckEngine),切面中反射出字段,查询校验规则,然后将字段交给检验引擎完成校验动作;
controller:接口入口,DynamicCheckController 提供校验测试;
dao:dao 下有两个目录,分别是 mapper 和 model,用于存放 Mapper 接口类和查询结果数据封装类;
dto:请求参数封装类(DynamicCheckReqDto),响应参数封装类(DynamicCheckRespDto);
exception:自定义异常类存放位置;
service:业务逻辑代码;
ApplicationStart:Spring Boot 启动入口;
resource:存放 mapper.xml 文件和 application.properties 配置以及日志配置 logback.xml。

3.2 数据库准备

数据库需要建三张表,校验模板表(t_template_info),校验模板规则表(t_template_rule_info),实体规则关联表(t_bean_rule_info),只说表的基本字段,需要 SQL 可以到码云或者 git 上现在原代码,项目中有 datasql.sql 文件中很详细,还包含初始数据。

3.2.1 t_template_info:

`template_id` varchar(16) NOT NULL COMMENT '模板编号',
`template_desc` varchar(64) DEFAULT NULL COMMENT '模板描述',
`template_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '模板状态(0:不使用,1:使用)',
`check_level` int(11) NOT NULL COMMENT '检查优先级'

3.2.2 t_template_rule_info:

`rule_id` varchar(16) NOT NULL COMMENT '规则编号',
`template_id` varchar(16) NOT NULL COMMENT '模板编号',
`rule_express` varchar(128) NOT NULL COMMENT '规则表达式',
`toast_msg` varchar(128) NOT NULL COMMENT '提示信息',
`rule_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '规则状态'

3.2.3 t_bean_rule_info:

`bean_id` varchar(32) NOT NULL COMMENT '实体类编号',
`rule_id` varchar(16) NOT NULL COMMENT '规则编号',
`field_name` varchar(32) NOT NULL COMMENT '字段名',
`field_desc` varchar(128) DEFAULT NULL COMMENT '字段描述',
`check_status` tinyint(4) DEFAULT '1' COMMENT '是否校验'

3.3 上手代码

3.3.1 pom.xml 配置

<!-- 统一制定spring boot版本 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.6.RELEASE</version>
</parent>

<!-- 版本配置信息 -->
<properties>
    <java.version>1.8</java.version>
    <lombok.version>1.16.10</lombok.version>
    <druid.version>1.1.0</druid.version>
    <mybatis.version>1.3.0</mybatis.version>
    <mysql.version>5.1.35</mysql.version>
    <commons-lang3.version>3.5</commons-lang3.version>
</properties>

<!-- 所需依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!-- 数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>${druid.version}</version>
    </dependency>
    <!-- spring AOP包含aspectj等依赖,不需要单独引入 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- spring+mybatis整合依赖 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.version}</version>
    </dependency>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
    </dependency>
    <!-- lombok注解(注意在这里使用需要在idea上安装lombok插件) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
        <scope>provided</scope>
    </dependency>
    <!-- 工具类 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>${commons-lang3.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- maven编译插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

3.3.2 DynamicCheckAspect 核心代码

@Component
@Slf4j
@Aspect
public class DynamicCheckAspect {

    @Autowired
    private DynamicCheckRuleService dynamicCheckRuleService;
    @Autowired
    private DynamicCheckEngine paramCheckEngine;

    /**
     * 定义切点
     */
    @Pointcut("execution(* com.minuor.dynamic.check.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 定义环切
     */
    @Around("pointcut()")
    public void check(ProceedingJoinPoint joinPoint) {
        try {
            // 查询获取请求参数封装类(dto)的类名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
            String beanName = null;
            if (parameterTypes != null && parameterTypes.length > 0) {
                beanName = parameterTypes[0].getSimpleName();
            }
            //查询当前beanName下字段的所有校验规则
            List<DynamicCheckRuleModel> modelList = null;
            if (StringUtils.isNotBlank(beanName)) {
                modelList = dynamicCheckRuleService.queryRuleByBeanName(beanName);
            }
            if (modelList != null && !modelList.isEmpty()) {
                //规则分类(根据字段名分类)
                Map<String, List<DynamicCheckRuleModel>> ruleMap = new HashMap<>();
                for (DynamicCheckRuleModel ruleModel : modelList) {
                    List<DynamicCheckRuleModel> fieldRules = ruleMap.get(ruleModel.getFieldName());
                    if (fieldRules == null) fieldRules = new ArrayList<>();
                    fieldRules.add(ruleModel);
                    ruleMap.put(ruleModel.getFieldName(), fieldRules);
                }
                //获取请求参数
                Object[] args = joinPoint.getArgs();
                if (args != null && args.length > 0) {
                    Object reqDto = args[0];
                    Field[] fields = reqDto.getClass().getDeclaredFields();
                    if (fields != null && fields.length > 0) {
                        for (Field field : fields) {
                            String fieldName = field.getName();
                            boolean isCheck = ruleMap.containsKey(fieldName);
                            if (!isCheck) continue;
                            field.setAccessible(true);
                            List<DynamicCheckRuleModel> paramRules = ruleMap.get(fieldName);
                            for (DynamicCheckRuleModel ruleModel : ruleMap.get(fieldName)) {
                                ruleModel.setFieldValue(field.get(reqDto));
                            }
                            //校验
                            paramCheckEngine.checkParamter(paramRules);
                        }
                    }
                }
            }
            joinPoint.proceed();
        } catch (Exception e) {
            throw new DynamicCheckException(e.getMessage());
        } catch (Throwable throwable) {
            throw new DynamicCheckException(throwable.getMessage());
        }
    }
}

这里首先是获取 Dto 的名称,然后到数据库中查询校验规则列表,如果没有,就不需要校验,中间的校验逻辑就无需再走。

3.3.3 DynamicCheckEngine 核心代码

@Slf4j
@Component
public class DynamicCheckEngine {

    /**
     * 综合校验分发器
     *
     * @param paramRules
     */
    public void checkParamter(List<DynamicCheckRuleModel> paramRules) throws Exception {
        paramRules.sort(Comparator.comparing(DynamicCheckRuleModel::getCheckLevel));
        for (DynamicCheckRuleModel ruleModel : paramRules) {
            Method method = this.getClass().getMethod(ruleModel.getTemplateId(), DynamicCheckRuleModel.class);
            Object result = method.invoke(this, ruleModel);
            if (result != null) {
                throw new DynamicCheckException((String) result);
            }
        }
    }

    /**
     * 检查非空
     * 模板编号:notBlank
     */
    public String notBlank(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        Object fieldValue = roleModel.getFieldValue();
        if (fieldValue == null) {
            return generateToastMsg(roleModel);
        } else {
            if ((fieldValue instanceof String) && StringUtils.isBlank((String) fieldValue)) {
                return generateToastMsg(roleModel);
            }
        }
        return null;
    }

    /**
     * 检查非空
     * 模板编号:notNull
     */
    public String notNull(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        if (roleModel.getFieldValue() == null) return generateToastMsg(roleModel);
        return null;
    }

    /**
     * 检查长度最大值
     * 模板编号:lengthMax
     */
    public String lengthMax(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        String fieldValue = (String) roleModel.getFieldValue();
        if (fieldValue.length() > Integer.valueOf(roleModel.getRuleExpress().trim())) {
            return generateToastMsg(roleModel);
        }
        return null;
    }

    /**
     * 检查长度最小值
     * 模板编号:lengthMin
     */
    public String lengthMin(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        String fieldValue = (String) roleModel.getFieldValue();
        if (fieldValue.length() < Integer.valueOf(roleModel.getRuleExpress().trim())) {
            return generateToastMsg(roleModel);
        }
        return null;
    }

    /**
     * 检查值最大值
     * 模板编号:valueMax
     */
    public String valueMax(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        Double fieldValue = Double.valueOf(roleModel.getFieldValue().toString());
        if (fieldValue > Double.valueOf(roleModel.getRuleExpress())) {
            return generateToastMsg(roleModel);
        }
        return null;
    }

    /**
     * 检查值最小值
     * 模板编号:valueMin
     */
    public String valueMin(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        Double fieldValue = Double.valueOf(roleModel.getFieldValue().toString());
        if (fieldValue < Double.valueOf(roleModel.getRuleExpress())) {
            return generateToastMsg(roleModel);
        }
        return null;
    }

    /**
     * 正则格式校验
     * 模板编号:regex
     */
    public String regex(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        String value = (String) roleModel.getFieldValue();
        if (!Pattern.matches(roleModel.getRuleExpress(), value)) {
            return generateToastMsg(roleModel);
        }
        return null;
    }

    /**
     * 构建结果信息
     */
    private String generateToastMsg(DynamicCheckRuleModel roleModel) throws DynamicCheckException {
        String[] element = new String[]{StringUtils.isNotBlank(roleModel.getFieldDesc())
                ? roleModel.getFieldDesc() : roleModel.getFieldName(), roleModel.getRuleExpress()};
        String toast = roleModel.getToastMsg();
        int index = 0;
        while (index < element.length) {
            String replace = toast.replace("{" + index + "}", element[index] + "");
            if (toast.equals(replace)) break;
            toast = replace;
            index++;
        }
        return toast;
    }
}

在校验方法 checkParameter 中,并不是去 if else 取判断校验模板名称,而是使用反射的方式执行方法,当然这里执行的校验的方法名要和模板名称相同,如校验非空,模板名是 notBlank,那么对应的检验方法名就是 notBlank。

4. 总结

1、这里没有列出项目中的所有代码,感觉没有必要,太冗余,主要思路和核心代码足矣,其他的代码下面会提供 git 和码云上的下载链接地址;
2、这里校验及基于 post 请求,如果你所在的项目中必须有 get 请求,那么就需要重新筹划一下这个校验规则如何定义,如 get 采用方法名,post 采用 Dto 名称;
3、这里代码作为 demo 展示,记得使用根据自己项目做优化;
4、这里面校验的异常都是往外抛出的,实际是不会把异常抛给用户,可以在 controller 中做异常的统一过滤封装。

5. 源代码

Github:https://github.com/itcrud/dynamic-check


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
死磕Java并发:深入分析ThreadLocal原理 死磕Java并发:深入分析ThreadLocal原理
ThreadLocal是啥?以前面试别人时就喜欢问这个,有些伙伴喜欢把它和线程同步机制混为一谈,事实上ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢?API是这样介绍它的:This class ……
2018-06-24
下一篇 
JDK7、8对ConcurrentHashMap的实现和总结 JDK7、8对ConcurrentHashMap的实现和总结
ConcurrentHashMap是一个经常被使用的数据结构,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力,但同时降低了对读一致性的要求(这点好像CAP理论啊 O(∩_∩)O)。ConcurrentHashMap的……
2018-06-20
  目录