Spring AOP+反射实现自定义动态配置校验规则,让校验规则飞起来
原文作者:弥诺R
原文地址:http://www.minuor.com/1524369999/article
转载声明:转载请注明原文地址,注意版权维护,谢谢!
场景小计
之前项目都是使用hibernate-validator来校验参数,但是实际上会出现一些小问题,就是校验规则都是通过注解的方式来完成,这样如果项目上线了,这个参数校验规则就没办法修改,如果出现校验规则问题,就必须修改后重新紧急上线(之前因为手机号码格式校验就出现过这个问题,因为新的号段不支持)。为了适应动态配置校验规则,在新起的项目我们就不再使用hibernate-validator校验规则,而是自己写个小功能来实现。
实现思路
1、实现这种动态配置,就要能随时修改规则,并应用到实际业务逻辑中,直接在代码中写是不行的,因此这里采用数据库记录的方式是一个不错的选择;
2、需要对所有controller进入的参数校验,不能每个方法中加调用逻辑,这个必须写一个公共的方法,使用Spring AOP做切面切入所有的controller方法;
3、服务的请求方式,使用这种方式,最方便的就是使用post请求,入参后,参数都在一个类中封装,拿到类,使用反射,拿出参数的参数名和参数值。
基本都是以上思路,切面切入controller类中所有方法,拿到请求Dto类,利用反射技术拿出所有的参数名和参数值,从数据库中获取当前Dto类下所有参数的校验规则,依次对参数进行校验。
项目构建
项目结构
aspect:切面(DynamicCheckAspect)和校验引擎(DynamicCheckEngine),切面中反射出字段,查询校验规则,然后将字段交给检验引擎完成校验动作;
controller:接口入口,DynamicCheckController提供校验测试;
dao:dao下有两个目录,分别是mapper和model,用于存放Mapper接口类和查询结果数据封装类;
dto:请求参数封装类(DynamicCheckR
5b4
eqDto),响应参数封装类(DynamicCheckRespDto);
exception:自定义异常类存放位置;
service:业务逻辑代码;
ApplicationStart:Spring Boot启动入口;
resource:存放mapper.xml文件和application.properties配置以及日志配置logback.xml。
数据库准备
数据库需要建三张表,校验模板表(t_template_info),校验模板规则表(t_template_rule_info),实体规则关联表(t_bean_rule_info),只说表的基本字段,需要SQL可以到码云或者git上现在原代码,项目中有datasql.sql文件中很详细,还包含初始数据。
t_template_info:template_idvarchar(16) NOT NULL COMMENT '模板编号',
template_descvarchar(64) DEFAULT NULL COMMENT '模板描述',
template_statustinyint(4) NOT NULL DEFAULT '1' COMMENT '模板状态(0:不使用,1:使用)',
check_levelint(11) NOT NULL COMMENT '检查优先级' t_template_rule_info:
rule_idvarchar(16) NOT NULL COMMENT '规则编号',
template_idvarchar(16) NOT NULL COMMENT '模板编号',
rule_expressvarchar(128) NOT NULL COMMENT '规则表达式',
toast_msgvarchar(128) NOT NULL COMMENT '提示信息',
rule_statustinyint(4) NOT NULL DEFAULT '1' COMMENT '规则状态' t_bean_rule_info:
bean_id varchar(32) NOT NULL COMMENT '实体类编号',
rule_id
varchar(16) NOT NULL COMMENT '规则编号',field_namevarchar(32) NOT NULL COMMENT '字段名',
field_descvarchar(128) DEFAULT NULL COMMENT '字段描述',
check_statustinyint(4) DEFAULT '1' COMMENT '是否校验'
上手代码
#####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>
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 { // 查询获取请求参数封装类(dt 5b4 o)的类名 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 req 111c Dto = 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的名称,然后到数据库中查询校验规则列表,如果没有,就不需要校验,中间的校验逻辑就无需再走。
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 ( b60 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。
总结
1、这里没有列出项目中的所有代码,感觉没有必要,太冗余,主要思路和核心代码足矣,其他的代码下面会提供git和码云上的下载链接地址;
2、这里校验及基于post请求,如果你所在的项目中必须有get请求,那么就需要重新筹划一下这个校验规则如何定义,如get采用方法名,post采用Dto名称;
3、这里代码作为demo展示,记得使用根据自己项目做优化;
4、这里面校验的异常都是往外抛出的,实际是不会把异常抛给用户,可以在controller中做异常的统一过滤封装。
项目代码
码云:https://gitee.com/minuor/dynamic-check
gitHub:https://github.com/minuor/dynamic-check
- SpringBoot+ZooKeeper+ZKUI+Drools 实现应用配置中心及业务规则动态加载
- shiro 实现自定义权限规则校验
- Spring动态对Quartz定时任务的管理,实现动态加载,停止的配置实例代码
- 利用spring+ibatiS技术,在spring中配置多个数据源,并实现动态切换。
- Spring动态对Quartz定时任务的管理,实现动态加载,停止的配置实例代码
- Spring 配置多个数据源,并实现动态切换
- Spring整合quart 实现动态配置定时任务
- Spring Boot 自动配置理解 以及实现自定义Starter
- Spring 配置多个数据源,并实现动态切换
- Spring 配置多个数据源,并实现动态切换
- Spring配置多个数据源并实现动态切换示例
- SpringBoot+Docker 实现属性动态配置
- 让IoC动态解析自定义配置(提供基于Unity的实现)
- springboot整合Quartz实现动态配置定时任务
- 利用spring+ibatiS技术,在spring中配置多个数据源,并实现动态切换。
- Spring整合Mybatis实现动态数据源切换教程配置
- JAVAWEB开发之Struts2详解(二)——Action接受请求参数、类型转换器、使用Struts2的输入校验、以及遵守约定规则实现Struts2的零配置
- 如何动态实现spring 配置更新
- [置顶] spring整合quartz实现动态定时任务的前台网页配置与管理
- Spring动态对Quartz定时任务的管理,实现动态加载,停止的配置实例代码