【统一规则校验的设计与实现】-自定义匹配规则
业务规则校验作为IT系统中重要组成部分之一,是对数据的有效性的重要保障手段,而作为业务规则校验相关在相关的开源框架中都存在对应部分内容,比如spring与hibernate等是基于jsr-303 validation规范进行数据验证的,考虑到日常业务规则校验有时候对配置灵活性有较大要求(比如动态校验支持),本文自定义一组业务规则(字符串格式),同时相应提供校验工具类来满足业务的需要。
校验过程可以理解为:被匹配字符串(string)匹配规则字符串(rule),以及最终结果(ture/false)
规则设计
关键词 | 格式 | 值约束 | 举例 | |
数据类型 | typeof-类型 | typeof[*] | int-整数 | typeof[float] |
float-浮点数 | ||||
string-字符串 | ||||
date-日期 | ||||
datetime-时间 | ||||
关系运算符 | eq-等于 | eq[*] | 无限制 | eq[你好] |
lt-小于 | lt[*] | 无限制 | lt[100] | |
gt-大于 | gt[*] | 无限制 | gt[200] | |
ne-不等于 | ne[*] | 无限制 | ne[100] | |
le-小于等于 | le[*] | 无限制 | le[100] | |
ge-大于等于 | ge[*] | 无限制 | ge[100] | |
in-在之内 | in[*] | 无限制 | in[100,200,300,400] | |
ni-不在之内 | ni[*] | 无限制 | ni[100,200,300,400] | |
bt-在之间 | bt[*] | 无限制 | bt[100,400] | |
nb-不在之间 | nb[*] | 无限制 | nb[100,400] | |
逻辑运算符 | &&-且 | *&&* | ne[NULL] && in[5,4,3,2,1] | |
||-或 | *||* | ne[NULL] || in[5,4,3,2,1] | ||
!-非 | !* | !(ne[NULL] || in[5,4,3,2,1]) | ||
长度 | length-长度 | length[*,*] | length[100,200] | |
替代符 | NULL-空 | |||
NOW-当前时间 |
举例:(!(ne[NULL] && in[5,4,3,2,1] && true)||eq[NULL]) && (typeof[datetime]&>[NOW])&&true
校验算法
假定规则:(!(ne[NULL] && in[5,4,3,2,1] && true)||eq[NULL])||(typeof[datetime]&&rl[NOW])
算法一,替换计算法(替换原子表达式结果,最后计算只有true/false的逻辑表达式)
- 将ne[NULL] = ture;in[5,4,3,2,1] = false;eq[NULL] =true;typeof[datetime] = true; rl[NOW] = false直接替换进入表达式
- 计算替换后(!true && false && true) || true || (true || false )表达式值
- 得出结果:true
算法二,递归计算法 (从左到右开始计算表达式,如果提前确定返回结果,不再计算后面值)
- (ne[NULL]&&in[5,4,3,2,1]) ||(typeof[datetime]&&rl[NOW])
- (false&&in[5,4,3,2,1]) ||(typeof[datetime]&&rl[NOW])
- false||(typeof[datetime]&&rl[NOW])
- typeof[datetime]&&rl[NOW]
- false&&&&rl[NOW]
- false
算法二相对于算法一能够减少不必要的计算,因此本文工具类选择算法二进行实现
工具类
public static ValidInfo validate(String data, String rule) { if (StringUtils.isBlank(rule)) { ValidInfo validInfo = new ValidInfo(); validInfo.setResult(true); return validInfo; } rule = rule.replaceAll("[ ]+", ""); ValidInfo validInfo = new ValidInfo(); validInfo.setData(data); boolean result = excuteCascade(data, null, rule, validInfo); validInfo.setResult(result); validInfo.setResultMessage(ValidationUtil.transferResultMessage(data, validInfo.getRule())); return validInfo; } private static String getFirstExp(String rule) { Pattern pattern = Pattern.compile("[^\\(\\)!(\\|\\|)(\\&\\&)]+"); Matcher matches = pattern.matcher(rule); if(matches.find()) { return matches.group(); } return null; } private static boolean excuteCascade(String data, String dataType, String rule, ValidInfo validInfo) { String headExp = getFirstExp(rule); if(dataType == null) { dataType = parseDataType(headExp); } //获得表达式执行结果 boolean headResult = excuteExp(data, dataType, headExp, validInfo); //将表达式替换为执行结果,只可能为true或者false rule = rule.replace(headExp,String.valueOf(headResult)); //前后关联将该表达式前后多余的括号与否计算掉,并替换为最终结果 System.out.print("==> rule : " + rule); rule = trimHeadResult(rule, headResult); System.out.println(" ==> : " + rule); headResult = Boolean.parseBoolean(getFirstExp(rule)); //判断该表达式后面的逻辑运算符,根据逻辑运算符做相应替换 Pattern groupPattern = Pattern.compile("(true|false)[\\|\\&]*"); Matcher matches = groupPattern.matcher(rule); if(matches.find()) {//一定能匹配 String headExpExt = matches.group(); // System.out.println(headExpExt); //后续没有逻辑运算符,说明已是最终结果,直接返回结果 if(headExpExt.equals(String.valueOf(headResult))) { return headResult; } if(headExpExt.endsWith("&&") && !headResult) { int beginIndex = rule.indexOf(headExpExt); int endIndex = getNextExpEndIndex(rule,headExpExt); rule = rule.substring(0,beginIndex) + false + rule.substring(endIndex); return excuteCascade(data, dataType, rule, validInfo); }else if(headExpExt.endsWith("||") && headResult) { int beginIndex = rule.indexOf(headExpExt); int endIndex = getNextExpEndIndex(rule,headExpExt); rule = rule.substring(0,beginIndex) + true + rule.substring(endIndex); return excuteCascade(data, dataType, rule, validInfo); }else { rule = rule.replace(headExpExt,""); return excuteCascade(data, dataType, rule, validInfo); } } System.out.println("该处原则上不会执行到!"); return false; } private static String parseDataType(String headExp) { if(headExp.startsWith(RelationOperator.TYPEOF)) { return headExp.substring(headExp.indexOf("[") + 1, headExp.indexOf("]")); } return null; } private static String trimHeadResult(String rule, boolean headResult) { Pattern groupPattern = Pattern.compile("(true|false)\\)*"); Matcher matches = groupPattern.matcher(rule); if(matches.find()) { String temp = matches.group(); rule = rule.replace(temp,String.valueOf(headResult)); int count = temp.replace("true", "").replace("false", "").length(); groupPattern = Pattern.compile("!?(\\(!?){" + count + "}(true|false)"); matches = groupPattern.matcher(rule); if(matches.find()) { temp = matches.group(); int length = temp.replace("true", "").replace("false", "").replaceAll("\\(", "").length(); if(length % 2 == 1) { headResult = !headResult; } rule = rule.replace(temp, String.valueOf(headResult)); } } return rule; } private static int getNextExpEndIndex(String rule, String headExpExt) { int count = 0 ; int beginIndex = rule.indexOf(headExpExt); int endIndex = rule.length(); for(int i = beginIndex; i < rule.length(); i ++) { if('(' == rule.charAt(i)) { count ++; } if(')' == rule.charAt(i)) { count --; } if(count < 0) { endIndex = i; break; } } return endIndex; } @SuppressWarnings("unused") private static String trans2RegexText(String curExp) { return curExp.replaceAll("\\[","\\\\\\[").replaceAll("\\]", "\\\\\\]"); } private static boolean excuteExp(String data, String dataType, String curExp,ValidInfo validInfo) { if("true".equals(curExp)) { return true; } if("false".equals(curExp)) { return false; } String ruleType = curExp.substring(0,curExp.indexOf("[")); String paramStr = curExp.substring(curExp.indexOf("[") + 1, curExp.indexOf("]")); String[] params = paramStr.split(","); boolean result = excuteExp(data, dataType, ruleType, params); validInfo.setRule(curExp); validInfo.setRuleResult(result); System.out.println("==> data : " + data + " ; curExp : " + curExp + " ; result : " + result); return result; } private static boolean excuteExp(String data, String dataType, String ruleType, String[] params) { if(TypeOf.DATETIME.equals(dataType) || TypeOf.DATE.equals(dataType)) { if(ReplacementCharacter.NOW.equals(params[0])) { params[0] = DateUtil.getCurrentDateString(DateUtil.DATETIME_PATTERN); } } switch (ruleType) { case RelationOperator.EQ://等于 if(ReplacementCharacter.NULL.equals(params[0])) { return data == null; }else { return params[0].equals(data); } case RelationOperator.LT: //小于 return compareLessThan(data,dataType,params); case RelationOperator.GT: //大于 return compareGreaterThan(data, dataType, params); case RelationOperator.NE://不等于 if(ReplacementCharacter.NULL.equals(params[0])) { return !(data == null); }else { return !params[0].equals(data); } case RelationOperator.LE://小于等于 return !compareGreaterThan(data, dataType, params); case RelationOperator.GE://大于等于 return !compareLessThan(data, dataType, params); case RelationOperator.IN://在之内 return Arrays.binarySearch(params,data)> -1; case RelationOperator.NI://不在之内 return Arrays.binarySearch(params,dataType)< -1; case RelationOperator.BT://在之间 return compareBetween(data, dataType, params); case RelationOperator.NB://不在之间 return !compareBetween(data, dataType, params); case RelationOperator.LENGTH://长度 int param1 = Integer.parseInt(params[0]); int param2 = -1; if(params.length > 1) { param2 = Integer.parseInt(params[1]); } if(TypeOf.FLOAT.equals(data)) { int precision = new BigDecimal(data).precision(); int intLength = new BigDecimal(data).toBigInteger().bitLength(); if(param1 > intLength){ if(param2 != -1 && param2 < precision) { return false; } return true; }else { return false; } } return data.length() < Integer.valueOf(param1); case RelationOperator.TYPEOF://类型 if(TypeOf.INT.equals(dataType)) { try { Long.parseLong(data); return true; }catch (Exception e) { return false; } }else if(TypeOf.FLOAT.equals(dataType)) { try { new BigDecimal(data); return true; }catch (Exception e) { return false; } }else if(TypeOf.DATE.equals(dataType)) { return DateUtil.stringToDate(data, DateUtil.ISO_EXPANDED_DATE_FORMAT) != null; }else if(TypeOf.DATETIME.equals(dataType)) { return DateUtil.stringToDate(data, DateUtil.DATETIME_PATTERN) != null; }else if(TypeOf.STRING.equals(dataType)) { return true; }else { System.out.println("【ERROR】该类型不支持:" + ruleType); return false; } default: System.out.println("不支持类型:" + ruleType); } return false; } private static boolean compareBetween(String data, String dataType, String[] params) { if(TypeOf.INT.equals(data)) { return Long.parseLong(data) >= Long.parseLong(params[0]) && Long.parseLong(data) <= Long.parseLong(params[1]); }else if(TypeOf.FLOAT.equals(data)) { return new BigDecimal(data).compareTo(new BigDecimal(params[0])) >= 0 && new BigDecimal(data).compareTo(new BigDecimal(params[1])) <= 0; }else if(TypeOf.DATE.equals(data)) { return DateUtil.stringToDate(data, DateUtil.ISO_EXPANDED_DATE_FORMAT) .compareTo(DateUtil.stringToDate(params[0], DateUtil.ISO_EXPANDED_DATE_FORMAT)) >= 0 && DateUtil.stringToDate(data, DateUtil.ISO_EXPANDED_DATE_FORMAT) .compareTo(DateUtil.stringToDate(params[1], DateUtil.ISO_EXPANDED_DATE_FORMAT)) <= 0; }else if(TypeOf.DATETIME.equals(data)) { return DateUtil.stringToDate(data, DateUtil.DATETIME_PATTERN) .compareTo(DateUtil.stringToDate(params[0], DateUtil.DATETIME_PATTERN)) >= 0 && DateUtil.stringToDate(data, DateUtil.DATETIME_PATTERN) .compareTo(DateUtil.stringToDate(params[1], DateUtil.DATETIME_PATTERN)) <= 0; }else if(TypeOf.STRING.equals(data)) { System.out.println("【ERROR】该类型不支持!"); return false; }else { System.out.println("【ERROR】该类型不支持!"); return false; } } private static boolean compareGreaterThan(String data, String dataType, String[] params) { if(TypeOf.INT.equals(dataType)) { return Long.parseLong(data) > Long.parseLong(params[0]); }else if(TypeOf.FLOAT.equals(dataType)) { return new BigDecimal(data).compareTo(new BigDecimal(params[0])) > 0; }else if(TypeOf.DATE.equals(dataType)) { return DateUtil.stringToDate(data, DateUtil.ISO_EXPANDED_DATE_FORMAT) .after(DateUtil.stringToDate(params[0], DateUtil.ISO_EXPANDED_DATE_FORMAT)); }else if(TypeOf.DATETIME.equals(dataType)) { return DateUtil.stringToDate(data, DateUtil.DATETIME_PATTERN) .after(DateUtil.stringToDate(params[0], DateUtil.DATETIME_PATTERN)); }else if(TypeOf.STRING.equals(dataType)) { System.out.println("【ERROR】该类型不支持!"); return false; }else { System.out.println("【ERROR】该类型不支持!"); return false; } } private static boolean compareLessThan(String data, String dataType, String[] params) { if(TypeOf.INT.equals(dataType)) { return Long.parseLong(data) < Long.parseLong(params[0]); }else if(TypeOf.FLOAT.equals(dataType)) { return new BigDecimal(data).compareTo(new BigDecimal(params[0])) < 0; }else if(TypeOf.DATE.equals(dataType)) { return DateUtil.stringToDate(data, DateUtil.ISO_EXPANDED_DATE_FORMAT) .before(DateUtil.stringToDate(params[0], DateUtil.ISO_EXPANDED_DATE_FORMAT)); }else if(TypeOf.DATETIME.equals(dataType)) { return DateUtil.stringToDate(data, DateUtil.DATETIME_PATTERN) .before(DateUtil.stringToDate(params[0], DateUtil.DATETIME_PATTERN)); }else if(TypeOf.STRING.equals(dataType)) { System.out.println("【ERROR】该类型不支持!"); return false; }else { System.out.println("【ERROR】该类型不支持!"); return false; } } public static String transferResultMessage(String data, String rule) { String ruleType = rule.substring(0, rule.indexOf("[")); String paramStr = rule.substring(rule.indexOf("[") + 1, rule.indexOf("]")); String[] params = paramStr.split(","); switch (ruleType) { case RelationOperator.EQ://等于 if(ReplacementCharacter.NULL.equals(params[0])) { return "只能为空"; }else { return "只能为" + params[0]; } case RelationOperator.LT: //小于 return "小于" + params[0]; case RelationOperator.GT: //大于 return "大于" + params[0]; case RelationOperator.NE://不等于 return "不等于" + params[0]; case RelationOperator.LE://小于等于 return "小于等于" + params[0]; case RelationOperator.GE://大于等于 return "大于等于" + params[0]; case RelationOperator.IN://在之内 return "在" + Arrays.toString(params) + "之内"; case RelationOperator.NI://不在之内 return "不在" + Arrays.toString(params) + "之内"; case RelationOperator.BT://在之间 return "在" + Arrays.toString(params) + "之间"; case RelationOperator.NB://不在之间 return "不在" + Arrays.toString(params) + "之间"; case RelationOperator.LENGTH://长度 int param1 = Integer.parseInt(params[0]); int param2 = -1; if(params.length > 1) { param2 = Integer.parseInt(params[1]); } if(TypeOf.FLOAT.equals(data)) { String tmp = "小数前最多" + param1 + "位"; if(param2 > -1) { tmp += ",小数点后最多" + param2 + "位"; } return tmp; } return "长度不能大于" + params[0] + "位"; case RelationOperator.TYPEOF://类型 String dataType = params[0]; if(TypeOf.INT.equals(dataType)) { return "必须为数字类型"; }else if(TypeOf.FLOAT.equals(dataType)) { return "必须为浮点类型"; }else if(TypeOf.DATE.equals(dataType)) { return "必须为日期类型"; }else if(TypeOf.DATETIME.equals(dataType)) { return "必须为时间类型"; }else if(TypeOf.STRING.equals(dataType)) { return "必须为字符类型"; } default: } return null; }
测试
-
public static void main(String[] args) { String exp = "(!(ne[NULL] && in[5,4,3,2,1] && true)||eq[NULL]) && (typeof[datetime]&>[NOW])&&true"; // exp = "ne[NULL]"; // exp = "(typeof[date]) && gt[NOW]"; exp = "(true&&typeof[date])||eq[]"; ValidInfo result = ValidationUtil.validate("", exp); System.out.println(result.isResult() + ":"+ result.getResultMessage()); }