JAVA表达式解析器———EvalEx

java表达式解析器(支持自定义函数以及操作符)

说明

下面的工具类适用于类似这种表达式 VAR(‘001d’)+VAR(‘test1’) 的解析,需要自定义函数并且函数还附带字符串等非数字类型的参数

执行逻辑

  1. 接受一个需要计算的表达式以及计算表达式所需要的参数信息(参数可空),获取传入参数的方式FormalAnalysis.param.get()
  2. 将函数中的字符串参数替换为纯数字参数 VAR(‘001d’)+VAR(‘test1’) -> VAR(1)+VAR(2),并且记录改对应关系,在计算是自定替换为原始字符串参数
  3. 创建解析器Expression
  4. 使用SpringBoot的ApplicationContext获取对应的自定义函数和操作符,并添加到所创建的解析器中

优化

  1. 工具类中使用的都是线程变量ThreadLocal,确保每个线程的参数以及映射信息都是独立的,并且在每次公式计算完毕进行清空
  2. 公式中还定义了搜集异常信息的list变量warnContent,如果想保留异常信息调用FormalAnalysis.warnContent.get().add(异常信息) 进行存储,公式解析完毕会将该异常信息抛出
  3. 使用ApplicationContext自动获取自定义的函数和操作符,添加到表达式解析器中,无需手动添加

注意

一定要严格按照你自定义函数所配置的函数名称、参数个数以及自定义的操作符来编写表达式,也就是严格控制表达式的内容
如果表达式中出现了没有定义的其他函数或操作符,会直接报错

1.引入依赖

1
2
3
4
5
<dependency>
<groupId>com.udojava</groupId>
<artifactId>EvalEx</artifactId>
<version>2.7</version>
</dependency>

2.关于自定义函数介绍

如果你解析的公式函数不包含参数或者你的参数为数字,那么你可以直接继承AbstractFunction
如果你的自定义函数中包含的参数是字符串或者其他类型的信息,那么我建议你看下我写的自定义类CustomAbstractFunction

重写自定义函数父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public abstract class CustomAbstractFunction extends AbstractFunction {
@Override
public Expression.LazyNumber lazyEval(List<Expression.LazyNumber> lazyParams) {
return super.lazyEval(lazyParams);
}

public String restoreOriginalString(Integer s) {
return FormalAnalysis.restoreOriginalString(s);
}

protected CustomAbstractFunction(String name, int numParams) {
super(name, numParams);
}

@Override
public BigDecimal eval(List<BigDecimal> parameters) {
List<String> param = new ArrayList<>();
for (int i = 0; i < parameters.size(); i++) {

//兼容VAL BEG 和 END
if (StrUtil.isEmpty(restoreOriginalString(parameters.get(i).intValue()))) {
param.add(i, parameters.get(i).toString());
} else {
String s = restoreOriginalString(parameters.get(i).intValue());
param.add(i, s);
}

}
return customEval(param);
}

/**
* 自定义表达式取值(自动将参数转换为原始字符串)
*
* @param parameters 函数参数信息
* @return 计算值
*/
public BigDecimal customEval(List<String> parameters) {
return null;
}
}

自定义函数示例

  • customEval方法会自动将你的函数替换为原始字符串
  • 创建完类你需要声明你的函数名称以及你的函数参数个数,-1代表参数个数不确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class VAR extends CustomAbstractFunction {
public VAR() {
super("VAR", 1);
}
@Override
public BigDecimal customEval(List<String> parameters) {

String p1 = parameters.get(0);

//这里写你自己的函数计算逻辑

return BigDecimal.ZERO;
}
}

3.关于自定义操作符的介绍

自定义操作符需要继承AbstractOperator

自定义操作符构造方法参数介绍

参数名 类型 含义
oper String 操作符符号,例如 “+”、“-”、“*”、“/”。
precedence int 操作符优先级,值越高优先级越高。例如 * 的优先级高于 +。
leftAssoc boolean 操作符是否是左结合性,例如:+ 和 * 是左结合,^(幂运算)是右结合。
booleanOperator boolean 是否是布尔操作符,布尔操作符处理 true/false 值,例如 && 和
unaryOperator boolean 是否是一元操作符(只有一个操作数),例如 -a 中的 - 是一元操作符。

默认的符号优先级

操作符 描述 优先级 (precedence) 结合性 (leftAssoc)
+ 加法 20 true (左结合)
- 减法 20 true (左结合)
* 乘法 30 true (左结合)
/ 除法 30 true (左结合)
^ 幂运算 40 false(右结合)

自定义操作符示例

1
2
3
4
5
6
7
8
9
10
11
12
13

@Component
public class MaxOperator extends AbstractOperator {
public MaxOperator() {
super(">>", 30, true);
}

@Override
public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
return v1.max(v2);
}
}

4.工具类

方法介绍

  • analysisCal
    • 公式解析工具类
  • preprocessExpression
    • 用于将表达式函数中参数是字符串的参数替换为数字 ,并且将对应关系存储到mapping 变量中
  • restoreOriginalString
    • 还原被替换的参数信息

工具类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
@Component
public class FormalAnalysis {

/**
* key->id,value->字符串参数
* 公式中不能包含字符串
*/
public static final ThreadLocal<Map<Integer, String>> mapping = ThreadLocal.withInitial(HashMap::new);

/**
* 测量点映射id
*/
private static final ThreadLocal<Integer> placeholderCounter = ThreadLocal.withInitial(() -> 0);

/**
* 计算公式所需额外参数信息
*/
public static final ThreadLocal<Map<String, Object>> param = ThreadLocal.withInitial(HashMap::new);

/**
* 错误信息
*/
public static final ThreadLocal<List<String>> warnContent = ThreadLocal.withInitial(ArrayList::new);

/**
* 自动注册自定义函数
*/
@Resource
private ApplicationContext applicationContext;


/**
* 公式解析计算
*
* @param formula 公式
* @param param 取值所需要的参数
*/
public BigDecimal analysisCal(String formula, Map<String, Object> param) {
BigDecimal eval = null;
try {
FormalAnalysis.param.set(param);

formula = preprocessExpression(formula);
// 创建表达式
Expression expression = new Expression(formula);


//添加自定义函数
Map<String, AbstractFunction> fun = applicationContext.getBeansOfType(AbstractFunction.class);
for (AbstractFunction abstractFunction : fun.values()) {
expression.addFunction(abstractFunction);
}
//添加自定义操作符
Map<String, AbstractOperator> oper = applicationContext.getBeansOfType(AbstractOperator.class);
for (AbstractOperator abstractFunction : oper.values()) {
expression.addOperator(abstractFunction);
}

// 计算公式
eval = expression.eval();
if (CollUtil.isNotEmpty(warnContent.get())) {
throw new RuntimeException(StringUtils.join(warnContent.get().stream().distinct().collect(Collectors.toList()), System.lineSeparator()));
}
} catch (ArithmeticException e) {//除零异常
if (CollUtil.isNotEmpty(warnContent.get())) {
throw new RuntimeException(StringUtils.join(warnContent.get().stream().distinct().collect(Collectors.toList()), System.lineSeparator()));
}
eval = BigDecimal.ZERO;
} finally {
// 计算结束,清空当前线程的局部变量
FormalAnalysis.mapping.remove();
FormalAnalysis.placeholderCounter.remove();
FormalAnalysis.warnContent.remove();
FormalAnalysis.param.remove();
}
return eval.setScale(4, RoundingMode.HALF_UP);
}



/**
* 预处理公式,将引号包裹的字符串替换为占位符
*/
private static String preprocessExpression(String formula) {

formula = formula.replace(ValParamEnum.BEG.getName(), ValParamEnum.BEG.getId().toString());
formula = formula.replace(ValParamEnum.END.getName(), ValParamEnum.END.getId().toString());

Pattern pattern = Pattern.compile("'([^']*)'");
Matcher matcher = pattern.matcher(formula);
StringBuffer sb = new StringBuffer();

// 每找到一个匹配项,就生成一个占位符,并将原始字符串存入映射
while (matcher.find()) {
String originalString = matcher.group(1);
placeholderCounter.set(placeholderCounter.get() + 1);
mapping.get().put(placeholderCounter.get(), originalString);
matcher.appendReplacement(sb, placeholderCounter.get() + "");
}
matcher.appendTail(sb);
System.out.println(sb);
return sb.toString();
}

/**
* 在函数调用时,将占位符恢复为原始字符串
*/
public static String restoreOriginalString(Integer placeholder) {
return mapping.get().get(placeholder);
}
}

5.使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("formula")
@Api(tags = "公式测试")
public class FormalTestController {

@Resource
FormalAnalysis formalAnalysis;

@GetMapping
@ApiOperation("test")
public ApiResponse test() {
String formal = "VAR('001d')+VAR('test1')";
BigDecimal bigDecimal = formalAnalysis.analysisCal(formal, new HashMap<>());
System.out.println(bigDecimal);
return ApiResponse.succeed(bigDecimal);
}
}