動態執行程式碼邏輯
- 2019 年 12 月 19 日
- 筆記
動態執行邏輯的方法據我所知有一下兩種方式
- 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再實例化成對象
動手實現
依賴
<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.定義腳本模板
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.腳本執行器
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 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); } } }
執行結果
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
思考
通過groovy動態載入Class,再結合Spring的生命周期,是否可以實現動態添加Bean?是否可以實現動態添加Controller?