动态执行代码逻辑
动态执行逻辑的方法据我所知有一下两种方式
- QLExpress
- Groovy
QLExpress
QLExpress是阿里开源的动态脚本执行的项目。 由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。 在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。
https://github.com/alibaba/QL...
这种方案在配置上感觉不太方便,原因是没有IDE支持、某些JAVA语法不支持。。。
Groovy
来着百度百科
Groovy 是 用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。
Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行 Java 编程),使用方式基本与使用 Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使 Groovy 与 Java 代码的互操作很容易。(注意:不是指Groovy替代java,而是指Groovy和java很好的结合编程。
原理
通过Groovy提供的GroovyClassLoader把源代码动态加载编译成Class,Class再实例化成对象
动手实现
依赖
<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>
- 创建动态脚本工厂,
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 } }
- 定义脚本模板
package cn.dhbin.dynamic; /** * 脚本接口,所有脚本实现该接口的{@link IScript#run(String)}方法 * * @author donghaibin * @date 2019/11/19 */ public interface IScript { /** * 具体逻辑 * * @param param 参数 * @return 执行结果 */ String run(String param); }
- 脚本执行器
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); } }
到这里,就基本实现了脚本的加载-实例化-执行。下面测试
编写脚本
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"; } }
测试用例
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 donghaibin\n" + " * @date 2019/11/19\n" + " */\n" + "public class SimpleScript implements IScript{\n" + "\n" + "\tprivate static final Logger log = LoggerFactory.getLogger(SimpleScript.class);\n" + "\n" + "\t@Override\n" + "\tpublic String run(String param) {\n" + "\t\tlog.info(\"输入的参数是:[{}]\", param);\n" + "\t\tlog.info(\"你好世界\");\n" + "\t\treturn \"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 donghaibin\n" + " * @date 2019/11/19\n" + " */\n" + "public class SimpleScript implements IScript{\n" + "\n" + "\tprivate static final Logger log = LoggerFactory.getLogger(SimpleScript.class);\n" + "\n" + "\t@Override\n" + "\tpublic String run(String param) {\n" + "\t\tlog.info(\"输入的参数是:[{}]\", param);\n" + "\t\tlog.info(\"你好世界\");\n" + "\t\treturn \"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); } } }
执行结果
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...
思考
通过groovy动态加载Class,再结合Spring的生命周期,是否可以实现动态添加Bean?是否可以实现动态添加Controller?