之前项目都是使用hibernate-validator
来校验参数,但是实际上会出现一些小问题,就是校验规则都是通过注解的方式来完成,这样如果项目上线了,这个参数校验规则就没办法修改,如果出现校验规则问题,就必须修改后重新紧急上线(之前手机号码格式校验就出现过这个问题,因为新的号段不支持)。为了适应动态配置校验规则,在新起的项目我们就不再使用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
:请求参数封装类(DynamicCheckReqDto),响应参数封装类(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文件中很详细,还包含初始数据。
`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 '检查优先级'
`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 '规则状态'
`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 '是否校验'
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.5.6.RELEASEversion>
parent>
<properties>
<java.version>1.8java.version>
<lombok.version>1.16.10lombok.version>
<druid.version>1.1.0druid.version>
<mybatis.version>1.3.0mybatis.version>
<mysql.version>5.1.35mysql.version>
<commons-lang3.version>3.5commons-lang3.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>${commons-lang3.version}version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.1version>
<configuration>
<source>${java.version}source>
<target>${java.version}target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
@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的名称,然后到数据库中查询校验规则列表,如果没有,就不需要校验,中间的校验逻辑就无需再走。
@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。
1、这里没有列出项目中的所有代码,感觉没有必要,太冗余,主要思路和核心代码足矣,其他的代码下面会提供git和码云上的下载链接地址;
2、这里校验及基于post请求,如果你所在的项目中必须有get请求,那么就需要重新筹划一下这个校验规则如何定义,如get采用方法名,post采用Dto名称;
3、这里代码作为demo展示,记得使用根据自己项目做优化;
4、这里面校验的异常都是往外抛出的,实际是不会把异常抛给用户,可以在controller中做异常的统一过滤封装。
码云(gitee):https://gitee.com/itcrud/dynamic-check