動態執行程式碼邏輯

  • 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

SpringGlueFactory

思考

通過groovy動態載入Class,再結合Spring的生命周期,是否可以實現動態添加Bean?是否可以實現動態添加Controller?