随着 IT 技术的普及和发展,用户的信息化水平越来越高,软件产品除了满足用户的基本需求之外,还必须越来越照顾到用户的个性化需求,为用户提供深层次的个性化服务。以一个包含报表展示功能的产品为例,默认呈现给所有用户完全相同的报表,即同一个报表的字段内容和标签对所有用户完全相同。而在实际中,我们常常会遇到不同的用户由于其业务需求的不同,对于同一张报表,除基本数据字段之外,还要求额外增加符合该用户特定业务含义的字段,我们称之为用户自定义字段(Custom Metric)。这类需求在财务报表,数据分析报表中是比较常见。对于用户自定义字段,不同的用户给定不同的计算公式,甚至对于同一个用户的同一个字段,其计算公式也可能会随着时间推移而改变。一种直观的方法就是将所有用户有可能用到的字段都存储起来,然后再对不同的用户实现不同的字段,这样不仅会造成存储空间的浪费,而且后期的维护成本也十分高昂。本文将介绍一种基于 JEP 和可配置公式的解决方案,在不增加额外存储空间的情况下,灵活快速的解决用户的该类需求,并且具有良好的维护性和扩展性。
考虑到很多人对 JEP 还比较陌生,在介绍整个实现方案之前,有必要先让您对 JEP 有一个初步的了解。
JEP(Java Math Expression Parser)是一个第三方的 Java 工具包,提供了一套用于解析和计算数学表达式的类库,其核心功能就是计算公式的解析和结果的计算。在 JEP2.4.1 版本之前为符合 GPLv3 协议的开源免费包,你可以在 sourceforge 网站上下载和使用。使用 JEP 提供的 API,可以根据用户给定的公式来即时计算结果。JEP 支持用户自定义变量、常量和函数。在 JEP 中,已经预先包含大量的可使用的通用数学函数和常量,可满足日常的绝大部分数学计算需求。其官方网站是http://www./jep/,大家可以在该网站上下载试用版本和相关文档。
JEP 具有如下的特性:
- 文件小巧(jar archive 文件大小在 300k 以下)
- 快速求值
- 精度高,计算中使用 BigDecimal
- 包含常见的数学函数和运算符
- 支持布尔型表达式
- 良好的可扩展和可配置性
- 支持字符串,向量和复杂数值
- 支持隐式乘法
- 允许声明的或者未声明的变量
- 兼容 JAVA1.5
- 支持 Unicode 字符
- 大量的开发文档供参考
- 包含 JAVACC 语法分析生成器可自动生成 main class
JEP 对一个表达式的计算分为两个步骤。JEP 首先会对表达式进行解析,解析后会生成一个树形结构,接下来会基于这个树形结构进行快速求值。其工作流程图如下:
图 1. JEP 工作流程图
从上图可以看出,JEP 的工作过程十分简单。下面举一个简单的例子进行进一步说明,让您对 JEP 有一个更加直观的了解。
清单 1. JEP 简单示例
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 为整个功能设计的核心,主要对公式进行读取和解析,并为计算中遇到的变量赋值,并且计算结果。
对于计算公式,考虑到灵活性和可扩展性,我们将各个用户的自定义公式保存在配置文件中,其具备如下特性:
- 按用户隔离
每一个用户都使用独立文件存放计算公式,用户之间不会相互干扰,实现用户公式的个性化配置。
- 配置化管理
提供修改功能,保持程序的灵活性和可扩展性。在新增自定义字段或者改变计算公式时,无需修改代码,只需要重新对计算公式进行配置即可。
- 运行时载入
修改配置后,无需重启应用,也可将配置的公式载入运行时系统中。
在系统启动时会读取配置文件,在系统运行过程中,提供对用户的自定义公式的再配置功能并重新加载,在无需重启服务器的情况下让新配置的公式生效。
另外,由于计算公式不同而带来的字段业务含义不同,如用户自定义字段需要显示在 UI 上,为了使显示内容更加友好,我们可以为用户自定义字段提供可配置化的标签,最终使用户的自定义字段的标签与内容相匹配。下面的示例中也包含了这部分的实现。
下面将以一个实际的例子并结合代码的方式,来具体说明该方案。
假设有一个消费者到一家快餐店,由于又渴又饿让他食欲大增,他告诉商家他需要 5 杯可乐和 10 个汉堡。庆幸的是,正好这时快餐店在搞活动,他在享用大餐的同时,也可以节省一点费用。另外,对于供应商来说,他也会关心在这笔交易中的利益。于是有了如下两个表格:
表 1. 交易清单
产品编号 | 产品名称 | 原售价 | 折扣 | 数量 | 经营成本 | 进货价 | 供货成本 |
---|---|---|---|---|---|---|---|
001 | 可乐 | 3.2 | 0.2 | 5 | 0.2 | 2.5 | 2 |
002 | 汉堡 | 10 | 2 | 10 | 1 | 6 | 5 |
对于这一笔生意,不同的用户关心的内容不同,于是有如下的需求表:
表 2. 角色需求表
角色 | 需求说明 | 字段 A | 字段 B |
---|---|---|---|
消费者 | 我关心付了多少钱,节省了多少钱,能省钱当然开心了 | 支出 | 节省 |
商家 | 我关心我的收入和利润 | 收入 | 利润 |
供应商 | 我也关心我的收入和利润 (请注意:虽然我和商家关心的内容看似一样,不过我们的计算方法不同哦!) | 收入 | 利润 |
如何来解决不同角色关心不同内容的需求呢?下面通过展示具体得代码来说明实现过程。
首先我们列出整体的代码结构图,让您有一个整体的认识,两部分主程序和配置文件。如下图:
图 3. 主程序代码结构图
图 4. 配置文件结构图
定义产品交易类 ProductDeal.java,包含如下字段及相应的 getter 和 setter 方法:
清单 2. 数据对象定义
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
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
customMetricA=formulaA customMetricB=formulaB |
对于同一个公式名称,不同的用户可以在其单独的配置文件中配置计算表达式,以实现其个性化需求。
清单 5. 消费者的公式配置文件 config/customer/formula.properties
formulaA= (unitPrice-unitPriceOff) * volume formulaB= unitPriceOff * volume |
清单 6. 消费者对应的字段标签配置 config/customer/label.properties
customMetricA= 消费者支出 customMetricB= 消费者节省 |
清单 7. 商家的公式配置文件 config/seller/formula.properties
formulaA=(unitPrice-unitPriceOff) * volume formulaB=customMetricA - (unitOperationCost + unitSupplierPrice) * volume |
注意:对于商家的自定义字段的计算,customMetricB 的计算需要使用到 customMetricA 的数值,所以在计算的时候需要考虑计算顺序,确保 customMetricB 在 customMetricA 之前计算。该功能在实际应用中可以避免重复计算。
清单 8. 商家对应的字段标签配置 config/seller/label.properties
customMetricA= 商家收入 customMetricB= 商家利润 |
清单 9. 供应商的公式配置文件 config/supplier/formula.properties
formulaA=customMetricB - unitSupplierCost * volume formulaB=unitSupplierPrice * volume |
清单 10. 供应商对应的字段标签配置 config/supplier/label.properties
customMetricA= 供应商利润 customMetricB= 供应商收入 |
注意:对于供应商的自定义字段的计算,customMetricA 的计算需要使用到 customMetricB 的数值,所以在计算的时候需要考虑计算顺序,确保 customMetricB 在 customMetricA 之前计算。
创建并配置好这些配置文件之后,需要将这些配置文件中的内容载入,于是我们需要用到配置管理工具类。其分别有三个方法,用于载入前述的三类配置文件。 目前的这些配置信息是以 properties 文件的形式存储。当然,在实际应用中,采用数据库或者 xml 文件的形式进行存储,也可以达到同样的效果,只需要相应的修改配置管理类的实现即可。
清单 11. 配置管理类 ConfigurationUtil.java
/** * 载入自定义字段和公式名称的映射信息 */ 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
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
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
/** * 初始化 */ 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
/** 构造函数 */ 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
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. 输出结果
我是消费者,我关心支出和节省金额 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 | HTTP |
学习
- 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 编程各个方面的文章。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。