背景
随着 IT 技术的普及和发展,用户的信息化水平越来越高,软件产品除了满足用户的基本需求之外,还必须越来越照顾到用户的个性化需求,为用户提供深层次的个性化服务。以一个包含报表展示功能的产品为例,默认呈现给所有用户完全相同的报表,即同一个报表的字段内容和标签对所有用户完全相同。而在实际中,我们常常会遇到不同的用户由于其业务需求的不同,对于同一张报表,除基本数据字段之外,还要求额外增加符合该用户特定业务含义的字段,我们称之为用户自定义字段(Custom Metric)。这类需求在财务报表,数据分析报表中是比较常见。对于用户自定义字段,不同的用户给定不同的计算公式,甚至对于同一个用户的同一个字段,其计算公式也可能会随着时间推移而改变。一种直观的方法就是将所有用户有可能用到的字段都存储起来,然后再对不同的用户实现不同的字段,这样不仅会造成存储空间的浪费,而且后期的维护成本也十分高昂。本文将介绍一种基于 JEP 和可配置公式的解决方案,在不增加额外存储空间的情况下,灵活快速的解决用户的该类需求,并且具有良好的维护性和扩展性。
JEP 介绍
考虑到很多人对 JEP 还比较陌生,在介绍整个实现方案之前,有必要先让您对 JEP 有一个初步的了解。
JEP(Java Math Expression Parser)是一个第三方的 Java 工具包,提供了一套用于解析和计算数学表达式的类库,其核心功能就是计算公式的解析和结果的计算。在 JEP2.4.1 版本之前为符合 GPLv3 协议的开源免费包,你可以在 sourceforge 网站上下载和使用。使用 JEP 提供的 API,可以根据用户给定的公式来即时计算结果。JEP 支持用户自定义变量、常量和函数。在 JEP 中,已经预先包含大量的可使用的通用数学函数和常量,可满足日常的绝大部分数学计算需求。其官方网站是 http://www.singularsys.com/jep/,大家可以在该网站上下载试用版本和相关文档。
JEP 具有如下的特性:
- 文件小巧(jar archive 文件大小在 300k 以下)
- 快速求值
- 精度高,计算中使用 BigDecimal
- 包含常见的数学函数和运算符
- 支持布尔型表达式
- 良好的可扩展和可配置性
- 支持字符串,向量和复杂数值
- 支持隐式乘法
- 允许声明的或者未声明的变量
- 兼容 JAVA1.5
- 支持 Unicode 字符
- 大量的开发文档供参考
- 包含 JAVACC 语法分析生成器可自动生成 main class
JEP 对一个表达式的计算分为两个步骤。JEP 首先会对表达式进行解析,解析后会生成一个树形结构,接下来会基于这个树形结构进行快速求值。其工作流程图如下:
图 1. JEP 工作流程图
从上图可以看出,JEP 的工作过程十分简单。下面举一个简单的例子进行进一步说明,让您对 JEP 有一个更加直观的了解。
清单 1. JEP 简单示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Jep jep = new Jep();
try {
int x = 10;
//1. 设置变量的值
jep.addVariable("x", x);
//2. 载入并解析公式
jep.parse("x+1");
//3. 计算结果
Object result = jep.evaluate();
//4. 输出显示
System.out.println("x + 1 = " + result + " (When x="+x+")");
} catch (Exception e) {
System.out.println("An error occured: " + e.getMessage());
}
输出结果:x + 1 = 11.0 (When x=10)
|
经过以上的介绍,想必您已经对 JEP 有一个初步的认识,那么接下来就可以开始进入本文的主题了。
整体设计说明
本方案的整体设计如下图所示:
图 2. 整体设计图
在本方案中,JEP 提供了一套用于对数学表达式的解析和计算类库,可以对用户配置的计算公式进行解析,并快速计算求值。通过将计算公式设计拥有按用户隔离、配置化管理以及运行时载入三个特性,我们便可以对同一字段针对不同的用户配置不同的计算公式。对于计算后的用户的自定义字段,我们可以根据不同的业务需求,可以直接在 UI 上展示,或者存储如数据,或者作为中间结果供其他用途。
JEP
JEP 为整个功能设计的核心,主要对公式进行读取和解析,并为计算中遇到的变量赋值,并且计算结果。
可配置化公式
对于计算公式,考虑到灵活性和可扩展性,我们将各个用户的自定义公式保存在配置文件中,其具备如下特性:
- 按用户隔离
每一个用户都使用独立文件存放计算公式,用户之间不会相互干扰,实现用户公式的个性化配置。
- 配置化管理
提供修改功能,保持程序的灵活性和可扩展性。在新增自定义字段或者改变计算公式时,无需修改代码,只需要重新对计算公式进行配置即可。
- 运行时载入
修改配置后,无需重启应用,也可将配置的公式载入运行时系统中。
在系统启动时会读取配置文件,在系统运行过程中,提供对用户的自定义公式的再配置功能并重新加载,在无需重启服务器的情况下让新配置的公式生效。
另外,由于计算公式不同而带来的字段业务含义不同,如用户自定义字段需要显示在 UI 上,为了使显示内容更加友好,我们可以为用户自定义字段提供可配置化的标签,最终使用户的自定义字段的标签与内容相匹配。下面的示例中也包含了这部分的实现。
详细代码实现
下面将以一个实际的例子并结合代码的方式,来具体说明该方案。
案例需求
假设有一个消费者到一家快餐店,由于又渴又饿让他食欲大增,他告诉商家他需要 5 杯可乐和 10 个汉堡。庆幸的是,正好这时快餐店在搞活动,他在享用大餐的同时,也可以节省一点费用。另外,对于供应商来说,他也会关心在这笔交易中的利益。于是有了如下两个表格:
表 1. 交易清单
对于这一笔生意,不同的用户关心的内容不同,于是有如下的需求表:
表 2. 角色需求表
如何来解决不同角色关心不同内容的需求呢?下面通过展示具体得代码来说明实现过程。
代码实现
首先我们列出整体的代码结构图,让您有一个整体的认识,两部分主程序和配置文件。如下图:
图 3. 主程序代码结构图
图 4. 配置文件结构图
定义产品交易类 ProductDeal.java,包含如下字段及相应的 getter 和 setter 方法:
清单 2. 数据对象定义
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
|
public class ProductDeal {
/** 标识 */
private String productId;
/** 名称 */
private String productName;
/** 售价 */
private Double unitPrice;
/** 单位减价 */
private Double unitPriceOff;
/** 销量 */
private Integer volume;
/** 商家经营成本 */
private Double unitOperationCost;
/** 供应商价格 */
private Double unitSupplierPrice;
/** 供应商成本 */
private Double unitSupplierCost;
/** 自定义字段 A*/
private Double customMetricA;
/** 自定义字段 B*/
private Double customMetricB;
/**getter/setter 方法略 */
...
...
}
|
本案例中设计到三个类用户,分别为他们创建用户账号,见如下清单。
清单 3. 用户账号管理类 UserAccount.java
1
2
3
4
5
6
7
8
|
public class UserAccount {
/** 消费者 */
public static String USER_CUSTOMER= "CUSTOMER";
/** 商家 */
public static String USER_SELLER= "SELLER";
/** 供应商 */
public static String USER_SUPPLIER= "SUPPLIER";
}
|
对于本例中的两个自定义字段 customMetricA 和 customMetricB,每个字段都对应一个公式名称。
清单 4. 配置文件 metricFormulaConfig.properties
1
2
|
customMetricA=formulaA
customMetricB=formulaB
|
对于同一个公式名称,不同的用户可以在其单独的配置文件中配置计算表达式,以实现其个性化需求。
清单 5. 消费者的公式配置文件 config/customer/formula.properties
1
2
|
formulaA= (unitPrice-unitPriceOff) * volume
formulaB= unitPriceOff * volume
|
清单 6. 消费者对应的字段标签配置 config/customer/label.properties
1
2
|
customMetricA= 消费者支出
customMetricB= 消费者节省
|
清单 7. 商家的公式配置文件 config/seller/formula.properties
1
2
|
formulaA=(unitPrice-unitPriceOff) * volume
formulaB=customMetricA - (unitOperationCost + unitSupplierPrice) * volume
|
注意:对于商家的自定义字段的计算,customMetricB 的计算需要使用到 customMetricA 的数值,所以在计算的时候需要考虑计算顺序,确保 customMetricB 在 customMetricA 之前计算。该功能在实际应用中可以避免重复计算。
清单 8. 商家对应的字段标签配置 config/seller/label.properties
1
2
|
customMetricA= 商家收入
customMetricB= 商家利润
|
清单 9. 供应商的公式配置文件 config/supplier/formula.properties
1
2
|
formulaA=customMetricB - unitSupplierCost * volume
formulaB=unitSupplierPrice * volume
|
清单 10. 供应商对应的字段标签配置 config/supplier/label.properties
1
2
|
customMetricA= 供应商利润
customMetricB= 供应商收入
|
注意:对于供应商的自定义字段的计算,customMetricA 的计算需要使用到 customMetricB 的数值,所以在计算的时候需要考虑计算顺序,确保 customMetricB 在 customMetricA 之前计算。
创建并配置好这些配置文件之后,需要将这些配置文件中的内容载入,于是我们需要用到配置管理工具类。其分别有三个方法,用于载入前述的三类配置文件。 目前的这些配置信息是以 properties 文件的形式存储。当然,在实际应用中,采用数据库或者 xml 文件的形式进行存储,也可以达到同样的效果,只需要相应的修改配置管理类的实现即可。
清单 11. 配置管理类 ConfigurationUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* 载入自定义字段和公式名称的映射信息
*/
public static void loadMetricFormulaMapping(){ …… }
/**
* 载入用户的自定义公式信息
*/
public static void loadFormulas(String user){ …… }
/**
* 载入用户自定义字段标签信息
*/
public static void loadLabels(String user){ …… }
|
JEP 在计算之前需要提前设置好所有用于计算的变量的值,在 JEP 计算完成之后,将计算结果存储起来供进一步使用。JEP 对这些变量的值的来源并不关心,一般来讲,变量的值会来自于 Java VO 对象,也可以来源于 ResultSet 对象。为了统一的获取变量的值和存放计算结果,在这里创建了一个 IValueable.java 接口类,其实现非常简单:
清单 12. 数据转换接口 IValuable.java
1
2
3
4
5
6
7
8
9
10
|
public interface IValuable{
/**
* 通过字段名称获取字段值
*/
public Double getValue(String fieldName) throws Exception;
/**
* 设置字段名称和计算结果
*/
public void setValue(String fieldName,Double result) throws Exception;
}
|
下面给出一个基于 Java VO 的实现,其中利用 Apache 的 BeanUtils 来根据字段名称获取字段的值:
清单 13. IValuable 接口的 Java VO 实现类 ObjectValueBean.java
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
|
public class ObjectValueBean implements IValuable{
public Object object;
public Map<
String
,Double> resultMap = new HashMap<
String
,Double>();
public ObjectValueBean(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
@Override
public Double getValue(String fieldName) throws Exception{
//TODO Auto-generated method stub
if (resultMap.containsKey(fieldName))
return resultMap.get(fieldName);
else
return new Double(BeanUtils.getProperty(object,fieldName));
}
@Override
public void setValue(String fieldName, Double result) throws Exception {
// TODO Auto-generated method stub
BeanUtils.setProperty(object, fieldName, result);
resultMap.put(fieldName, result);
}
}
|
类似的我们也可以实现以 ResultSet 为数据源的 IValuable 接口实现类。
JEP 部分是整个实现的核心,在用户公式给定之后,JEP 要根据用户信息读入用户的公式,对公式进行校验(确保计算表达式配置正确,无字段的循环引用等),并根据字段之间的依赖关系设定好字段的计算顺序,对给定数据进行计算和处理。
清单 14. Jep 工具类 JepUtil.java
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
|
/**
* 初始化
*/
public void init(String user) throws Exception
{
setUser(user);
formulaEvaluatorMap= new HashMap<
String
,JepFormulaEvaluator>();
calculationOrder = new LinkedHashSet<
String
>();
initCalculatorOrder();
}
/**
* 初始化计算顺序,当存在字段的循环引用时会抛出异常
*/
private void initCalculatorOrder() throws Exception
{
if (ConfigurationUtil.getMetricFormulaMap()== null
|| ConfigurationUtil.getMetricFormulaMap().keySet().isEmpty()
|| ConfigurationUtil.getFormulaMap(user)== null
|| ConfigurationUtil.getFormulaMap(user).keySet().isEmpty())
return ;
List<
String
> order = new ArrayList<
String
>();
for (Object fieldName:ConfigurationUtil.getMetricFormulaMap().keySet()) {
this.addFormulaFields((String)fieldName, order,(String)fieldName);
}
calculationOrder.addAll(order);
}
/**
* 对每一行记录进行处理,包括如下步骤
* a) 公式解析
* b) 变量赋值
* c) 计算
* d) 存储计算结果
*/
public void processRow(IValuable valuable) throws Exception {
for (String fieldName : calculationOrder){
if(ConfigurationUtil.getMetricFormulaMap().keySet().contains(fieldName)){
String formulaKey = (String)ConfigurationUtil.getMetricFormulaMap().get(fieldName);
JepFormulaEvaluator jep = formulaEvaluatorMap.get(formulaKey);
jep.addVariables(valuable);
Double result = jep.evaluate();
valuable.setValue(fieldName, result);
}
}
}
|
公式的解析、赋值和计算的工作,最终由 JepFormulaEvaluator.java 来实现。在 JepFormulaEvaluator 类中,构造函数需要传入 formula 表达式,在设置变量值时需要传入 IValuable 对象实例作为数据来源。
清单 15. Jep 公式计算类 JepFormulaEvaluator.java
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
|
/** 构造函数 */
public JepFormulaEvaluator(String formula){
this .formula = formula;
}
/**
* 解析公式表达式
*/
public boolean parse() throws ParseException {
if (formula == null ) {
return false ;
}
node = jep.parse(formula);
return true ;
}
/**
* 变量赋值
*/
public void addVariables(IValuable valuable) throws Exception{
Set<
String
> children = this .findChildren();
for (String child : children) {
jep.addVariable(child, valuable.getValue(child));
}
}
/**
* 计算结果
*/
public Double evaluate() throws EvaluationException {
result = (Double)jep.evaluate();
return result;
}
|
在编写完上述代码之后,我们就可以应用它来解决我们在一开始提出的案例需求,见下面 CustomMetricExample.java 类的实现。请注意,我们定义了一个 postAction() 方法,此处我们将结果直接打印在控制台,在实际应用中可以将结果显示在 UI 上,也可以保存到数据库,另外也可以作为中间结果供其他用途,也只需要编写相应代码就可实现。
清单 16. 案例实现 CustomMetricExample.java
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
|
public class CustomMetricExample {
JepUtil jepUtil = new JepUtil();
List<
IValuable
> valueList;
/**
* 载入测试数据
*/
public void loadData() {
List<
IValuable
> valueList = new ArrayList<
IValuable
>();
ProductDeal productDeal1 = new ProductDeal();
productDeal1.setProductId("001");
productDeal1.setProductName("可乐");
productDeal1.setUnitPrice(3.2);
productDeal1.setUnitPriceOff(0.2);
productDeal1.setUnitOperationCost(0.2);
productDeal1.setUnitSupplierPrice(2.5);
productDeal1.setUnitSupplierCost(2.0);
productDeal1.setVolume(5);
valueList.add( new ObjectValueBean(productDeal1));
ProductDeal productDeal2 = new ProductDeal();
productDeal2.setProductId("002");
productDeal2.setProductName("汉堡");
productDeal2.setUnitPrice(10.0);
productDeal2.setUnitPriceOff(2.0);
productDeal2.setUnitOperationCost(1.0);
productDeal2.setUnitSupplierPrice(6.0);
productDeal2.setUnitSupplierCost(5.0);
productDeal2.setVolume(10);
valueList.add( new ObjectValueBean(productDeal2));
this.valueList = valueList;
}
/**
* 计算用户的自定义字段
*/
public void calculateCustomMetricForUser(String user) throws Exception {
jepUtil.init(user);
for (IValuable valuable : valueList) {
jepUtil.processRow(valuable);
}
this.postAction(user);
}
/**
* 后处理操作
* 可以直接显示在 UI 上,也可以存储到数据库中,或作为中间结果供其他用途
*/
private void postAction(String user) {
//Display on UI
for(IValuable valuable : valueList) {
ProductDeal productDeal = (ProductDeal)((ObjectValueBean)valuable).getObject();
System.out.println(user +" "+ productDeal.getProductName());
System.out.println("customMetricA" + " ==> "+ ConfigurationUtil.getLabel(
user,"customMetricA")+"=" + productDeal.getCustomMetricA());
System.out.println("customMetricB" + " ==> "+ ConfigurationUtil.getLabel(
user,"customMetricB")+"=" + productDeal.getCustomMetricB());
}
System.out.println("--------------------------------------------------");
}
public static void main(String[] args) throws Exception {
CustomMetricExample customMetricExample = new CustomMetricExample();
//1.载入原始数据
customMetricExample.loadData();
//2.载入公共配置信息(字段和公式名的映射)
ConfigurationUtil.loadCommonConfiguration();
//3.载入用户配置信息(公式和标签)
ConfigurationUtil.loadUserConfiguration(UserAccount.USER_CUSTOMER);
ConfigurationUtil.loadUserConfiguration(UserAccount.USER_SELLER);
ConfigurationUtil.loadUserConfiguration(UserAccount.USER_SUPPLIER);
//4.应用
System.out.println("我是消费者,我关心支出和节省金额");
customMetricExample.calculateCustomMetricForUser(UserAccount.USER_CUSTOMER);
System.out.println("我是消费者,我关心收入和利润");
customMetricExample.calculateCustomMetricForUser(UserAccount.USER_SELLER);
System.out.println("我是供应商,我关心收入和利润");
customMetricExample.calculateCustomMetricForUser(UserAccount.USER_SUPPLIER);
}
}
|
在执行 CustomMetricExample.java 的 main 方法后,我们得到如下的输出结果:
清单 17. 输出结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
我是消费者,我关心支出和节省金额
CUSTOMER 可乐
customMetricA ==> 消费者支出 =15.0
customMetricB ==> 消费者节省 =1.0
CUSTOMER 汉堡
customMetricA ==> 消费者支出 =80.0
customMetricB ==> 消费者节省 =20.0
--------------------------------------------------
我是商家,我关心收入和利润
SELLER 可乐
customMetricA ==> 商家收入 =15.0
customMetricB ==> 商家利润 =1.5
SELLER 汉堡
customMetricA ==> 商家收入 =80.0
customMetricB ==> 商家利润 =10.0
--------------------------------------------------
我是供应商,我关心收入和利润
SUPPLIER 可乐
customMetricA ==> 供应商利润 =2.5
customMetricB ==> 供应商收入 =12.5
SUPPLIER 汉堡
customMetricA ==> 供应商利润 =10.0
customMetricB ==> 供应商收入 =60.0
--------------------------------------------------
|
通过以上的实现可以看出,该实现充分考虑到需求的可变性,用户提出的关于自定义字段业务含义改变时,我们只需要重新配置其计算公式和对应的字段标签即可,非常的方便。
总结
本文对第三方 Java 类库 JEP 做了一个简单入门介绍,在此基础上介绍了一种基于 JEP 和配置化管理公式来解决用户自定义字段的通用方案,实现过程简洁清晰,并且具备良好的可维护性和扩展性,具有很强的应用价值。
下载资源
- Jep 公式自定义字段解决方案示例 (JEPsrc.rar | 705k)
相关主题
- JEP2.4.1:符合 GPLv3 协议的开源免费版(JEP2.4.1), 可以从 sourceforge 网站上下载。
- JEP 的官方网站:更多内容可通过访问 JEP 的官方网站获取。目前 JEP 的最新版本为 3.4,本文使用的是官方试用版 jep-java-3.4-trial.zip。另外 JEP 还提供 .NET 版本,是基于 JEP Java Release 3 的基础上移植产生。
- developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。