业务规则引擎浅析
在CRM(客户关系管理)系统或者其他业务支撑型系统的开发过程中,最经常多变的就是复杂的业务规则。因为这些规则要迎合、顺应市场的变化,如何能有效到做到业务规则和整体的系统支撑架构解耦分离,这个是开发过程中必须考虑的一个问题。每当客户要求改变一个业务规则的时候,我们又如何能做到在最短的时间内完成需求的开发提交,提高系统的灵活度?业务规则引擎无非是一个比较好的解决方案。它把复杂、冗余的业务规则同整个支撑系统分离开,做到架构的可复用移植,这个就是我们的终极目标。
那规则引擎又是什么东西?严格来说,它是一种嵌入到应用程序中的一个组件,能很好的把业务决策从应用程序框架中分离出来,然后使用预定义的方言(dialect)编写语义模块和业务决策模块,使用约定好的语法规范,接受用户的输入,然后解析用户的业务规则,然后根据解析好的业务规则,作出业务决策。可以说,一个好的支撑系统,离不开一个灵活的业务规则引擎,在某种意义上可以做到“以不变应万变”。
言归正传,那既然业务规则引擎这么好?那要如何设计实现呢?写到这里,我忽然就想起我大学的时候,我的启蒙老师跟我说的一句话:我们可以不要重复发明轮子,但是要能很好的运用和理解如何使用别人造好的“轮子”。这句话一直是我的座右铭。现在就有一个开源的业务规则引擎Drools,就能很好的满足我们的要求。以此为基础,站在巨人的肩膀上,何乐而不为呢?
Drools是什么?
简单来说,Drools是基于Java的规则引擎框架,是JBoss开源社区中的一个为Java量身定制的、基于RETE算法的产生式规则引擎的实现。大致的工作原理是,基于XML、DRL(Drools规则配置文件)的基础上,通过一个内置的解析器,把业务规则翻译成AST(Abstract Syntax Tree),最终会映射编译成Java的代码包,然后在程序运行的时候,加载这些代码包中的业务规则,并把在工作内存空间的规则和事实进行匹配,看下事实是否符合业务规则的约定。
业务规则引擎的架构设计
主要从两方面考虑,把常用的业务规则脚本放置到数据库进行存储,后续为了节省程序的IO开销,可以通过缓存机制从数据库从增量拷贝业务规则的镜像,程序客户端直接同缓存打交道。目前基于Java可以考虑的缓存框架也有很多,比如JCS(Java Caching System)。另外一个方面如果一个系统是分布式架构,可以考虑通过Zookeeper上面的节点进行业务规则的分布式部署,也可以实现规则的灰度版本发布、业务规则的动态事件监控等等操作。同样的,程序客户端可以直接同Zookeeper的某个节点通信,获得业务规则的数据,Zookeeper本身也会自动同步数据库中的业务规则。综上所述,得到如下图所示的业务规则引擎架构
业务规则引擎主要包含如下模块
- BusinessRuleExecutor 规则引擎配置执行器接口:主要用来定义获取业务规则配置的动作方式。
- BusinessRuleExecutorImpl 规则引擎配置加载模块:实现了BusinessRuleExecutor接口,目前演示的是基于Oracle数据库,加载业务规则配置的动作方式。
- BusinessRule 业务规则元素:定义业务规则的接口,主要包含:业务规则标识、业务规则内容。
- BusinessRuleRunner 业务规则引擎执行器:主要是执行具体的业务规则脚本代码,触发相应的事件判断。
类图层次结构如下所示
现在我们就用一个实际的例子,配合上面的框架来进行一下讲解。
在移动业务支撑型系统中,为了留住更多的在网移动用户,业务规定,凡是现有用户中,有订购家庭产品和VPN产品,并且他是动感地带品牌的,都认为是移动的幸运用户。如果我们遇到这种业务规则,又应该如何来实现?
我们首先梳理一下业务类图实体结构:
我们发现,这里实际上有三个实体,一个是用户,一个是用户订购的产品,一个是幸运客户。并且一个用户是可以订购多个产品的,用户的品牌有动感地带,此外还有全球通。主要针对的产品是VPN产品(VPNPRODUCT)和家庭产品(FAIMILYPROUDCT),于是我们画出如下的类图结构:
现在理清楚了业务规则和潜在实体,我们现在来看下如何实现?
首先在数据库中,我们可以简单设计如下表结构进行规则存储,表结构如下所示(基于Oracle)
create table pms.ng_business_rule
(
rule_id number(4),
drl_content varchar2(1024)
);
其中rule_id表示规则编码,drl_content表示规则的内容,具体的JavaBean映射结构如下
/** * @filename:BusinessRule.java * * Newland Co. Ltd. All rights reserved. * * @Description:业务规则定义 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import java.io.Serializable; public class BusinessRule implements Serializable { private Integer ruleId; private String drlContent; public Integer getRuleId() { return ruleId; } public void setRuleId(Integer ruleId) { this.ruleId = ruleId; } public String getDrlContent() { return drlContent; } public void setDrlContent(String drlContent) { this.drlContent = drlContent; } @Override public String toString() { return "BusinessRule{id:" + getRuleId() + "|rule:" + getDrlContent() + "}"; } }
现在再来看下,规则引擎配置执行器接口定义的内容
/** * @filename:BusinessRuleExecutor.java * * Newland Co. Ltd. All rights reserved. * * @Description:规则引擎配置执行器接口定义 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import java.util.List; public interface BusinessRuleExecutor { List<BusinessRule> findAll(); List<BusinessRule> findAllByRuleId(Integer ruleId); }
然后是基于Oracle数据库加载业务规则配置模块,当然你还可以继续实现BusinessRuleExecutor接口的方法,完成缓存读取、Zookeeper方式读取业务规则的相应模块,这里就不再复述实现细节,后续有时间可以补上。
/** * @filename:BusinessRuleExecutorImpl.java * * Newland Co. Ltd. All rights reserved. * * @Description:规则引擎配置加载模块(DB方式) * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; import org.springframework.jdbc.core.simple.ParameterizedBeanPropertyRowMapper; import java.util.List; import java.util.Map; public class BusinessRuleExecutorImpl extends NamedParameterJdbcDaoSupport implements BusinessRuleExecutor { private static final RowMapper<BusinessRule> ruleMapper = ParameterizedBeanPropertyRowMapper.newInstance(BusinessRule.class); private Map<String, String> ruleList; protected String getRuleList(String key) { return (ruleList != null) ? ruleList.get(key) : null; } public void setRuleList(Map<String, String> ruleList) { this.ruleList = ruleList; } protected <T> List<T> query(String queryId, RowMapper<T> rowMapper, Object... args) { return getJdbcTemplate().query(getRuleList(queryId), rowMapper, args); } public List<BusinessRule> findAll() { return query("select-rule", ruleMapper); } public List<BusinessRule> findAllByRuleId(Integer ruleId) { return query("select-rule-by-id", ruleMapper, ruleId); } }
接下来就是关键的模块,业务规则引擎执行器,是基于Drools的实现,drlContent是指业务规则的内容,elements是指业务规则中关注的事实对象。具体代码如下
/** * @filename:BusinessRuleRunner.java * * Newland Co. Ltd. All rights reserved. * * @Description:业务规则引擎执行器 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import org.drools.builder.ResourceType; import org.drools.definition.KnowledgePackage; import org.drools.io.ResourceFactory; import org.drools.runtime.StatefulKnowledgeSession; import org.drools.KnowledgeBase; import org.drools.KnowledgeBaseFactory; import org.drools.builder.KnowledgeBuilder; import org.drools.builder.KnowledgeBuilderFactory; public class BusinessRuleRunner { public BusinessRuleRunner() {} public void notify(String drlContent, ArrayList<Object> elements) { //构建知识库引擎 KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); try { kbuilder.add(ResourceFactory.newInputStreamResource(getDrlStream(drlContent)), ResourceType.DRL); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } Collection<KnowledgePackage> pkgs = kbuilder.getKnowledgePackages(); kbase.addKnowledgePackages(pkgs); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); //drl脚本有编译问题要提示 if (kbuilder.hasErrors()) { System.out.println(kbuilder.getErrors().toString()); throw new RuntimeException("Unable to compile: " + drlContent+ "\n"); } //插入WorkingMemory for (int i = 0; i < elements.size(); i++) { Object fact = elements.get(i); ksession.insert(fact); } //激活规则 ksession.fireAllRules(); } private InputStream getDrlStream(String drlContent) throws Exception{ ByteArrayInputStream is = new ByteArrayInputStream(drlContent.getBytes()); return is; } }
然后用Spring框架实现业务规则配置的自动装配,首先是business-rule-config-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource" /> </bean> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="newlandframework/ruleengine/db.properties" /> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="ruleengine-config" class="newlandframework.ruleengine.BusinessRuleExecutorImpl"> <property name="jdbcTemplate" ref="jdbcTemplate" /> <property name="ruleList"> <map> <entry key="select-rule"> <value><![CDATA[ select rule_id,drl_content from ng_business_rule ]]></value> </entry> <entry key="select-rule-by-id"> <value><![CDATA[ select rule_id,drl_content from ng_business_rule where rule_id = ? ]]></value> </entry> </map> </property> </bean> </beans>
数据库方式实现业务规则配置读取的事务控制Spring配置:business-rule-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd">
<import resource="business-rule-config-spring.xml" /> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="servicePointcut" expression="execution(* newlandframework.ruleengine.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/> </aop:config> </beans>
再看下用户、用户产品、幸运用户的实体类定义
package newlandframework.ruleengine; /** * @filename:Users.java * * Newland Co. Ltd. All rights reserved. * * @Description:用户定义 * @author tangjie * @version 1.0 * */ import java.util.List; public class Users { // 全球通品牌 public static final Integer GOTONE = 1000; // 动感地带品牌 public static final Integer MZONE = 1016; // 用户归属地市编码(591表示福州/592表示厦门) private Integer homeCity; // 用户的手机号码 private Integer msisdn; // 用户标识 private Integer userId; // 用户品牌标识 private Integer userBrand; private List<UserProduct> userProduct; public List<UserProduct> getUserProduct() { return userProduct; } public void setUserProduct(List<UserProduct> userProduct) { this.userProduct = userProduct; } public Integer getHomeCity() { return homeCity; } public void setHomeCity(Integer homeCity) { this.homeCity = homeCity; } public Integer getMsisdn() { return msisdn; } public void setMsisdn(Integer msisdn) { this.msisdn = msisdn; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public Integer getUserBrand() { return userBrand; } public void setUserBrand(Integer userBrand) { this.userBrand = userBrand; } @Override public String toString() { return "Users [homeCity=" + homeCity + ", msisdn=" + msisdn + ", userId=" + userId + ", userBrand=" + userBrand + ", userProduct=" + userProduct + "]"; } } /** * @filename:UserProduct.java * * Newland Co. Ltd. All rights reserved. * * @Description:用户产品定义 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; public class UserProduct { // VPN产品编码 public static final Integer VPNPRODUCT = 1000000001; // 家庭产品编码 public static final Integer FAIMILYPROUDCT = 1000000002; // 用户归属地市编码(591表示福州/592表示厦门) private Integer homeCity; // 用户标识 private Integer userId; // 产品编码 private Integer productId; // 产品名称描述 private String productName; public Integer getHomeCity() { return homeCity; } public void setHomeCity(Integer homeCity) { this.homeCity = homeCity; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public Integer getProductId() { return productId; } public void setProductId(Integer productId) { this.productId = productId; } public String getProductName() { return productName; } public void setProductName(String productName) { this.productName = productName; } @Override public String toString() { return "UserProduct [homeCity=" + homeCity + ", userId=" + userId + ", productId=" + productId + ", productName=" + productName + "]"; } } /** * @filename:LuckUsers.java * * Newland Co. Ltd. All rights reserved. * * @Description:幸运用户定义 * @author tangjie * @version 1.0 * */ package newlandframework.ruleengine; public class LuckUsers { // 用户归属地市编码(591表示福州/592表示厦门) private Integer homeCity; // 用户的手机号码 private Integer msisdn; // 用户标识 private Integer userId; public LuckUsers() { } public LuckUsers(Integer homeCity, Integer msisdn, Integer userId) { super(); this.homeCity = homeCity; this.msisdn = msisdn; this.userId = userId; } public Integer getHomeCity() { return homeCity; } public void setHomeCity(Integer homeCity) { this.homeCity = homeCity; } public Integer getMsisdn() { return msisdn; } public void setMsisdn(Integer msisdn) { this.msisdn = msisdn; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } @Override public String toString() { return "LuckUsers [homeCity=" + homeCity + ", msisdn=" + msisdn + ", userId=" + userId + "]"; } }
最后是Drools的业务规则配置的内容,我们把它入库到数据库表pms.ng_business_rule中的drl_content字段,其中规则标识可以用序列自动生成。
业务规则的主要逻辑就是判断这个用户是不是幸运用户?业务判断标准是:该用户订购了家庭产品和VPN产品,并且他的品牌是动感地带。
//created on: 2016-3-26
//业务规则决策配置 by tangjie
package newlandframework.ruleengine //list any import classes here. import newlandframework.ruleengine.UserProduct; import newlandframework.ruleengine.Users; import newlandframework.ruleengine.LuckUsers; //declare any global variables here rule "genLuckyUsersRule" dialect "mvel" when $luck : LuckUsers() $userFamilyProduct : UserProduct( productId == UserProduct.FAIMILYPROUDCT ) $userVpnProduct : UserProduct( productId == UserProduct.VPNPRODUCT ) $user : Users(userProduct contains $userFamilyProduct) and Users(userProduct contains $userVpnProduct) eval($user.userBrand == Users.MZONE) then //actions System.out.println("family:"+$userFamilyProduct.productId); System.out.println("vpn:"+$userVpnProduct.productId); System.out.println("msisdn:"+$user.msisdn); $luck.homeCity = $user.homeCity; $luck.msisdn = $user.msisdn; $luck.userId = $user.userId; end
最后我们可以在客户端中初始化一个属性(homeCity、msisdn、userId)都是空(null)的幸运用户对象LuckyUsers,然后传入一个用户刚好符合上述业务规则决策条件的“事实”用户,调用方式参考代码如下:
//创建一个默认的幸运用户对象 LuckUsers luck = new LuckUsers(); //业务规定的vpn产品对象 UserProduct vpn = new UserProduct(); vpn.setProductId(UserProduct.VPNPRODUCT); //业务规定的家庭产品对象 UserProduct family = new UserProduct(); family.setProductId(UserProduct.FAIMILYPROUDCT); //存放用户已经订购的产品列表 List<UserProduct> listProduct = new ArrayList<UserProduct>(); //创建测试用户,用户号码119,用户归属地市591,用户标识1240 Integer homeCity = new Integer(591); Integer userId = new Integer(1240); Integer msisdn = new Integer(119); //假设用户还订购了其他的4G飞享套餐,产品编码是1000000003 Integer otherProductId = new Integer(1000000003); UserProduct userProduct1 = new UserProduct(); userProduct1.setHomeCity(homeCity); userProduct1.setProductId(otherProductId); userProduct1.setProductName("4G飞享套餐"); userProduct1.setUserId(userId); listProduct.add(userProduct1); UserProduct userProduct2 = new UserProduct(); userProduct2.setHomeCity(homeCity); userProduct2.setProductId(UserProduct.VPNPRODUCT); userProduct2.setProductName("VPN产品"); userProduct2.setUserId(userId); listProduct.add(userProduct2); UserProduct userProduct3 = new UserProduct(); userProduct3.setHomeCity(homeCity); userProduct3.setProductId(UserProduct.FAIMILYPROUDCT); userProduct3.setProductName("家庭产品"); userProduct3.setUserId(userId); listProduct.add(userProduct3); Users user = new Users(); user.setHomeCity(homeCity); user.setMsisdn(msisdn); user.setUserBrand(Users.MZONE); user.setUserId(userId); user.setUserProduct(listProduct); //业务规则关注的事实对象 ArrayList<Object> elements = new ArrayList<Object>(); elements.add(vpn); elements.add(family); elements.add(userProduct1); elements.add(userProduct2); elements.add(userProduct3); elements.add(user); elements.add(luck); //加入业务规则引擎中执行决策 new BusinessRuleRunner().notify(drlContent, elements);
好了,我们执行一下代码,看下运行结果:
可以很清楚的看到,符合条件的用户,果然被我们找到,并且打印出来了。就是上面homeCity = 591,msisdn = 119,userId=1240的记录。
并且后续如果业务部门又改变业务规则,我们只要重新编写或者修改一个规则配置,然后重新发布、刷新缓存,既可以符合要求,又省去了很多代码编译、发布、上线等等一系列繁琐的中间步骤。最关键的是我们的代码框架也会变得非常灵活。
本文的内容是基于本人日常开发工作中应对复杂多变的业务规则的一种解决方式的设想,当然其中肯定有很多需要完善优化的地方,希望在此抛砖引玉,不吝赐教!