动态执行逻辑的方法据我所知有一下两种方式
- QLExpress
- Groovy
QLExpress
QLExpress是阿里开源的动态脚本执行的项目。 由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。 在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。
https://github.com/alibaba/QLExpress
这种方案在配置上感觉不太方便,原因是没有IDE支持、某些JAVA语法不支持。。。
Groovy
来着百度百科
Groovy 是 用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。
Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行 Java 编程),使用方式基本与使用 Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使 Groovy 与 Java 代码的互操作很容易。(注意:不是指Groovy替代java,而是指Groovy和java很好的结合编程。
原理
通过Groovy提供的GroovyClassLoader把源代码动态加载编译成Class,Class再实例化成对象
动手实现
依赖
代码语言:javascript复制<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.0-rc-1</version>
</dependency>
<!--hutool 工具包,不是核心-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.0.3</version>
</dependency>
1.创建动态脚本工厂,inject
方法用于扩展。
package cn.dhbin.dynamic;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import groovy.lang.GroovyClassLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
* 动态脚本工厂
* 作用:
* 通过字符串源码生成Class
* Class -> 实例
*
* @author donghaibin
* @date 2019/11/19
*/
public class DynamicFactory {
/**
* 单例
*/
private static DynamicFactory dynamicFactory = new DynamicFactory();
/**
* groovy类加载器
*/
private GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
/**
* 缓存Class
*/
private ConcurrentHashMap<String, Class<?>> classCache = new ConcurrentHashMap<>();
/**
* 获取单例
*
* @return 实例
*/
public static DynamicFactory getInstance() {
return dynamicFactory;
}
/**
* 加载创建实例,prototype
*
* @param codeSource 源代码
* @return 实例
* @throws Exception 异常
*/
public IScript loadNewInstance(String codeSource) throws Exception {
if (StrUtil.isNotBlank(codeSource)) {
Class<?> aClass = getCodeSourceClass(codeSource);
if (aClass != null) {
Object instance = aClass.newInstance();
if (instance != null) {
if (instance instanceof IScript) {
this.inject((IScript) instance);
return (IScript) instance;
} else {
throw new IllegalArgumentException(StrUtil.format("创建实例失败,[{}]不是IScript的子类", instance.getClass()));
}
}
}
}
throw new IllegalArgumentException("创建实例失败,instance is null");
}
/**
* code text -> class
* 通过类加载器生成class
*
* @param codeSource 源代码
* @return class
*/
private Class<?> getCodeSourceClass(String codeSource) {
String md5 = SecureUtil.md5(codeSource);
Class<?> aClass = classCache.get(md5);
if (aClass == null) {
aClass = groovyClassLoader.parseClass(codeSource);
classCache.putIfAbsent(md5, aClass);
}
return aClass;
}
/**
* 对script对象处理
*
* @param script {@link IScript}
*/
public void inject(IScript script) {
// to do something
}
}
2.定义脚本模板
代码语言:javascript复制package cn.dhbin.dynamic;
/**
* 脚本接口,所有脚本实现该接口的{@link IScript#run(String)}方法
*
* @author donghaibin
* @date 2019/11/19
*/
public interface IScript {
/**
* 具体逻辑
*
* @param param 参数
* @return 执行结果
*/
String run(String param);
}
3.脚本执行器
代码语言:javascript复制package cn.dhbin.dynamic;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author donghaibin
* @date 2019/11/19
*/
public class ScriptExecutor {
/**
* 缓存实例
*/
private ConcurrentHashMap<String, IScript> objCache = new ConcurrentHashMap<>();
/**
* 执行脚本
*
* @param id 实例Id
* @return 运行结果
*/
public String run(String id, String param) {
IScript script = objCache.get(id);
if (script == null) {
throw new IllegalArgumentException("未找到实例, id = [" id "]");
} else {
return script.run(param);
}
}
/**
* 注册实例
*
* @param id 实例id
* @param script 实例
* @return 返回前一个实例,如果为null,则是新插入
*/
public IScript register(String id, IScript script) {
return objCache.put(id, script);
}
/**
* 移除实例
*
* @param id 实例id
* @return 移除的实例
*/
public IScript remove(String id) {
return objCache.remove(id);
}
}
到这里,就基本实现了脚本的加载-实例化-执行。下面测试
编写脚本
代码语言:javascript复制package cn.dhbin.dynamic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author donghaibin
* @date 2019/11/19
*/
public class SimpleScript implements IScript{
private static final Logger log = LoggerFactory.getLogger(SimpleScript.class);
@Override
public String run(String param) {
log.info("输入的参数是:[{}]", param);
log.info("你好世界");
return "hello world";
}
}
测试用例
代码语言:javascript复制package com.pig4cloud.pig.sms.dynamic;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
/**
* @author donghaibin
* @date 2019/11/19
*/
@Slf4j
class DynamicFactoryTest {
@Test
void runWithExecutor() throws Exception {
DynamicFactory dynamicFactory = DynamicFactory.getInstance();
ScriptExecutor executor = new ScriptExecutor();
String codeSource = "package cn.dhbin.dynamic;n"
"n"
"import org.slf4j.Logger;n"
"import org.slf4j.LoggerFactory;n"
"n"
"/**n"
" * @author donghaibinn"
" * @date 2019/11/19n"
" */n"
"public class SimpleScript implements IScript{n"
"n"
"tprivate static final Logger log = LoggerFactory.getLogger(SimpleScript.class);n"
"n"
"t@Overriden"
"tpublic String run(String param) {n"
"ttlog.info("输入的参数是:[{}]", param);n"
"ttlog.info("你好世界");n"
"ttreturn "hello world";n"
"t}n"
"n"
"}n";
IScript script = dynamicFactory.loadNewInstance(codeSource);
String id = "1";
executor.register(id, script);
for (int i = 0; i < 10; i ) {
String result = executor.run(id, "abc");
log.info("结果:[{}]", result);
}
}
@Test
void runWithoutExecutor() throws Exception{
DynamicFactory dynamicFactory = DynamicFactory.getInstance();
String codeSource = "package cn.dhbin.dynamic;n"
"n"
"import org.slf4j.Logger;n"
"import org.slf4j.LoggerFactory;n"
"n"
"/**n"
" * @author donghaibinn"
" * @date 2019/11/19n"
" */n"
"public class SimpleScript implements IScript{n"
"n"
"tprivate static final Logger log = LoggerFactory.getLogger(SimpleScript.class);n"
"n"
"t@Overriden"
"tpublic String run(String param) {n"
"ttlog.info("输入的参数是:[{}]", param);n"
"ttlog.info("你好世界");n"
"ttreturn "hello world";n"
"t}n"
"n"
"}n";
for (int i = 0; i < 10; i ) {
IScript script = dynamicFactory.loadNewInstance(codeSource);
String result = script.run("abc");
log.info("结果:[{}]", result);
}
}
}
执行结果
代码语言:javascript复制11:19:32.243 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.255 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.255 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.255 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.255 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.255 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.255 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.255 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.256 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.256 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.256 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 输入的参数是:[abc]
11:19:32.256 [main] INFO cn.dhbin.dynamic.SimpleScript - 你好世界
11:19:32.256 [main] INFO cn.dhbin.dynamic.DynamicFactoryTest - 结果:[hello world]
两个用例执行的结果都一样,区别就是一个使用了执行器。这样做的目的是提高运行效率,执行器缓存了实例对象,不用每次执行都实例化。
总结
Groovy这种方案其实是从xxl-job
这个定时任务项目中提取出来的。它还扩展了Spring的几个注解,能从Spring的容器中加载Bean并使用。项目链接: https://gitee.com/xuxueli0323/xxl-job
SpringGlueFactory
思考
通过groovy动态加载Class,再结合Spring的生命周期,是否可以实现动态添加Bean?是否可以实现动态添加Controller?