基于Groovy的规则脚本引擎实战
规则引擎由推理引擎发展而来,一种嵌入在应用程序中的组件,实现将业务决策从应用程序代码中分离出来并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。
把规则和核心业务拆开,规则单独配置。这样当我们的规则变化的时候,就可以通过修改规则文件而不用修改核心的代码了。
Groovy是动态语言,依靠反射方式动态执行表达式的求值,并且依靠JIT编译器,在执行次数够多以后,编译成本地字节码,因此性能非常的高,适应于反复执行的表达式,用Groovy脚本动态调整线上代码,无须发版。
Groovy字符串
使用单引号或双引号在Groovy中创建字符串。当使用单引号时,字符串被看作为java.lang.String的一个实例,而当使用双引号时,它被会被看为groovy.lang.Gstring的一个实例,支持字符串变量值。
def name = "mickjoust" def amount = 250 println('My name is ${name}') println("My name is ${name}") println("He paid \$${amount}")
会打印下面的输出:
My name is ${name}
My name is mickjoust
He paid $250
在Groovy中,我们直接通过声明属性来创建bean,然后使用object.propertyName语法访问它们,而无需创建setter和getters。如下代码片段:
class Person { def id def name def email } def p = new Person() p.id = 1 p.name = 'mickjoust' p.email = 'mickjoust@test.com' println("Id: ${p.id}, Name: ${p.name}, Email: ${p.email}")
使用范围运算符(..)进行迭代,如下例子:
for(i in 0..5) { print "${i}" }
使用upto()的来确定下限和上限:
0.upto(3) { print "$it " }
输出:0 1 2 3
使用times()从0开始迭代:
5.times { print "$it " }
输出
0 1 2 3 4
使用step()的下限和上限,来迭代并使用步长值:
0.step(10, 2) { print "$it "}
输出
0 2 4 6 8
在java中运行Groovy脚本,有三种比较常用的类支持:GroovyShell、GroovyClassLoader 以及GroovyScriptEngine
GroovyShell
GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。
通常用来运行"script片段"或者一些零散的表达式(Expression)
GroovyClassLoader
用 Groovy 的 GroovyClassLoader ,会动态地加载一个脚本并执行它。GroovyClassLoader是一个Groovy定制的类装载器,负责解析加载Java类中用到的Groovy类。
如果脚本是一个完整的文件,特别是有API类型的时候,比如有类似于JAVA的接口,面向对象设计时,通常使用GroovyClassLoader.
GroovyScriptEngine
GroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从您指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许您传入参数值,并能返回脚本的值。
以GroovyClassLoader为例:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.5.6</version>
<type>pom</type>
</dependency>
定义Groovy执行的java接口
@Data public class RuleEngineExecuteContext { /** * 业务元id,例如订单id,案件id等 */ private String bizId; /** * 业务id,比如产品id或者产品组id */ private String proId; /** * 策略组ID,供使用方使用一个产品对应多个策略组的情况下使用(比如进行分流策略时) */ private Integer groupId; /** * */ private Map<String, Object> data=new HashMap<>(); private String nextScenario; /** * 是否是初始节点,用于第一个节点自动迭代 */ private boolean init = false; }
抽象出一个Groovy模板文件,放在resource下面以便加载:
import com.smile.loader.*; import com.smile.loader.domain.RuleEngineExecuteContext; class %s implements EngineGroovyModuleRule{ boolean run(RuleEngineExecuteContext context){ %s } }
解析Groovy的模板文件,可以将模板文件缓存起来,解析我是通过spring的PathMatchingResourcePatternResolver进行的;下面的StrategyLogicUnit这个String就是具体的业务规则的逻辑,把这一部分的逻辑进行一个配置化。 例如:我们假设执行的逻辑是:申请订单的金额大于20000时,走流程A,代码简单实例如下:
@Slf4j @Service public class GroovyScriptTemplate implements InitializingBean { private static final Map<String, String> SCRIPT_TEMPLATE_MAP = new ConcurrentHashMap<>(); public String getScript(String fileName) { String template = SCRIPT_TEMPLATE_MAP.get(fileName); if (StringUtils.isEmpty(template)) { String errorMessage = String.format("请添加脚本模板:%s到resource目录下", fileName); throw new RuntimeException(errorMessage); } return template; } @Override public void afterPropertiesSet() throws Exception { final String path = "classpath*:*.groovy_template"; PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Arrays.stream(resolver.getResources(path)).parallel().forEach(resource -> { try { String filename = resource.getFilename(); InputStream inputStream = resource.getInputStream(); BufferedReader buff = new BufferedReader(new InputStreamReader(inputStream)); StringBuffer template = new StringBuffer(); String line; while ((line = buff.readLine()) != null) { template.append(line).append("\n"); } SCRIPT_TEMPLATE_MAP.put(filename, template.toString()); log.info("load script template: {}", resource.getURL()); } catch (Exception e) { log.error("read file error: {}", e.getMessage()); } }); } }
执行逻辑接口
public interface RuleEngineGroovyExecutor<T> { /** * 获取脚本执行 * * @param name * @return */ T getInstance(String name); /** * 编译脚本并缓存 * * @param name * @param script */ void parseAndCache(String name, String script); }
实现类
@Slf4j @Component public class RuleEngineGroovyModuleRuleExecutor implements RuleEngineGroovyExecutor<EngineGroovyModuleRule> { private Map<String, Class<EngineGroovyModuleRule>> nameAndClass = Maps.newConcurrentMap(); @Autowired private GroovyScriptTemplate groovyScriptTemplate; @Override public EngineGroovyModuleRule getInstance(String name) { try { Class<EngineGroovyModuleRule> moduleRuleClass = nameAndClass.get(name); if (moduleRuleClass == null) { throw new IllegalArgumentException(String.format("script: %s not load", name)); } return moduleRuleClass.newInstance(); } catch (Exception e) { throw new RuntimeException("创建module rule异常"); } } @Override public void parseAndCache(String name, String script) { String scriptBuilder = groovyScriptTemplate.getScript("ScriptTemplate.groovy_template"); String scriptClassName = RuleEngineGroovyModuleRuleExecutor.class.getSimpleName() + "_" + name; String fullScript = String.format(scriptBuilder, scriptClassName, script); log.info("scriptBuilder:{} scriptClassName:{} fullScript:{}", scriptBuilder, scriptClassName, fullScript); GroovyClassLoader classLoader = new GroovyClassLoader(); Class aClass = classLoader.parseClass(fullScript); log.info("collection-engine load script:{} finish", name); nameAndClass.put(name, aClass); } }
测试:
@Test public void test() { String name = "testGroovy"; String strategyLogicUnit = "if(context.getData().get('amount')>=20000){\n" + " context.nextScenario='A'\n" + " return true\n" + " }"; ruleExecutor.parseAndCache(name, strategyLogicUnit); EngineGroovyModuleRule instance = ruleExecutor.getInstance(name); RuleEngineExecuteContext executeContext = new RuleEngineExecuteContext(); executeContext.getData().put("amount", 30000); Boolean result = instance.run(executeContext); log.info("result:{}", result); }
浙公网安备 33010602011771号