分享

一种基于 JEP 和可配置公式实现用户自定义字段的解决方案

 CevenCheng 2013-08-27

一种基于 JEP 和可配置公式实现用户自定义字段的解决方案

肖 凯, 软件工程师, IBM
沈 伟伟, 软件工程师, IBM

简介: 本文介绍了一种基于 JEP(英文全称为 Java Math Expression Parser)并结合可配置公式实现用户自定义字段的解决方案,该方案灵活高效的满足了不同用户对报表展现的个性化需求,具有良好的维护性和扩展性。

发布日期: 2013 年 8 月 26 日 
访问情况 : 203 次浏览 
评论: 0 (查看 | 添加评论 - 登录)

平均分 0 星 共 0 个评分 平均分 (0个评分)
为本文评分

背景

随着 IT 技术的普及和发展,用户的信息化水平越来越高,软件产品除了满足用户的基本需求之外,还必须越来越照顾到用户的个性化需求,为用户提供深层次的个性化服务。以一个包含报表展示功能的产品为例,默认呈现给所有用户完全相同的报表,即同一个报表的字段内容和标签对所有用户完全相同。而在实际中,我们常常会遇到不同的用户由于其业务需求的不同,对于同一张报表,除基本数据字段之外,还要求额外增加符合该用户特定业务含义的字段,我们称之为用户自定义字段(Custom Metric)。这类需求在财务报表,数据分析报表中是比较常见。对于用户自定义字段,不同的用户给定不同的计算公式,甚至对于同一个用户的同一个字段,其计算公式也可能会随着时间推移而改变。一种直观的方法就是将所有用户有可能用到的字段都存储起来,然后再对不同的用户实现不同的字段,这样不仅会造成存储空间的浪费,而且后期的维护成本也十分高昂。本文将介绍一种基于 JEP 和可配置公式的解决方案,在不增加额外存储空间的情况下,灵活快速的解决用户的该类需求,并且具有良好的维护性和扩展性。

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 工作流程图
图 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. 整体设计图
图 2. 整体设计图

在本方案中,JEP 提供了一套用于对数学表达式的解析和计算类库,可以对用户配置的计算公式进行解析,并快速计算求值。通过将计算公式设计拥有按用户隔离、配置化管理以及运行时载入三个特性,我们便可以对同一字段针对不同的用户配置不同的计算公式。对于计算后的用户的自定义字段,我们可以根据不同的业务需求,可以直接在 UI 上展示,或者存储如数据,或者作为中间结果供其他用途。

JEP

JEP 为整个功能设计的核心,主要对公式进行读取和解析,并为计算中遇到的变量赋值,并且计算结果。

可配置化公式

对于计算公式,考虑到灵活性和可扩展性,我们将各个用户的自定义公式保存在配置文件中,其具备如下特性:

  1. 按用户隔离

每一个用户都使用独立文件存放计算公式,用户之间不会相互干扰,实现用户公式的个性化配置。

  1. 配置化管理

提供修改功能,保持程序的灵活性和可扩展性。在新增自定义字段或者改变计算公式时,无需修改代码,只需要重新对计算公式进行配置即可。

  1. 运行时载入

修改配置后,无需重启应用,也可将配置的公式载入运行时系统中。

在系统启动时会读取配置文件,在系统运行过程中,提供对用户的自定义公式的再配置功能并重新加载,在无需重启服务器的情况下让新配置的公式生效。

另外,由于计算公式不同而带来的字段业务含义不同,如用户自定义字段需要显示在 UI 上,为了使显示内容更加友好,我们可以为用户自定义字段提供可配置化的标签,最终使用户的自定义字段的标签与内容相匹配。下面的示例中也包含了这部分的实现。

详细代码实现

下面将以一个实际的例子并结合代码的方式,来具体说明该方案。

案例需求

假设有一个消费者到一家快餐店,由于又渴又饿让他食欲大增,他告诉商家他需要 5 杯可乐和 10 个汉堡。庆幸的是,正好这时快餐店在搞活动,他在享用大餐的同时,也可以节省一点费用。另外,对于供应商来说,他也会关心在这笔交易中的利益。于是有了如下两个表格:


表 1. 交易清单
产品编号产品名称原售价折扣数量经营成本进货价供货成本
001可乐3.20.250.22.52
002汉堡10210165

对于这一笔生意,不同的用户关心的内容不同,于是有如下的需求表:


表 2. 角色需求表
角色需求说明字段 A字段 B
消费者我关心付了多少钱,节省了多少钱,能省钱当然开心了支出节省
商家我关心我的收入和利润收入利润
供应商我也关心我的收入和利润 
(请注意:虽然我和商家关心的内容看似一样,不过我们的计算方法不同哦!)
收入利润

如何来解决不同角色关心不同内容的需求呢?下面通过展示具体得代码来说明实现过程。

代码实现

首先我们列出整体的代码结构图,让您有一个整体的认识,两部分主程序和配置文件。如下图:


图 3. 主程序代码结构图
图 3. 主程序代码结构图

图 4. 配置文件结构图
图 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.rar705kHTTP

关于下载方法的信息


参考资料

学习

  • 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 用户交流。 

作者简介

肖凯,现任职于 IBM CDL 部门,负责 DemandTec 产品的开发,对于敏捷开发、设计模式、代码重用以及软件设计有浓厚兴趣。

沈伟伟,软件工程师,现任职于 IBM 中国开发中心 DemandTec 项目组, 对构建基于 JavaEE 的系统有着丰富的经验, 主要关注于后台技术。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多