国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

Java 如何實現動態腳本?

開發 開發工具
在平臺級的 Java 系統中,動態腳本技術是不可或缺的一環。本文分享了一種 Java 動態腳本實現方案,給出了其中的關鍵技術點,并就類重名問題、生命周期、安全問題等做出進一步討論,歡迎同學們共同交流。

在平臺級的 Java 系統中,動態腳本技術是不可或缺的一環。本文分享了一種 Java 動態腳本實現方案,給出了其中的關鍵技術點,并就類重名問題、生命周期、安全問題等做出進一步討論,歡迎同學們共同交流。

前言

繁星是一個數據服務平臺,其核心功能是:用戶配置一段 SQL,繁星產出對應的 HSF/TR/SOA/Http 取數接口。

繁星引擎流程圖如下:

?

??

??


 

一次查詢請求經過引擎的管道,被各個閥門處理后就得到了相應的結果數據。圖中高亮的兩個閥門就是本文討論的重點:前置腳本與后置腳本。

溫馨提示:動態腳本就意味著代碼發布跳過了公司內部發布平臺,做不到監控、灰度、回滾三板斧,容易引發線上故障,因此業務系統中強烈不推薦使用該技術。

當然 Java 動態腳本技術一般使用場景也比較少,主要在平臺性質的系統中可能用到,比如 leetcode 平臺,D2 平臺,繁星數據服務平臺等。本文權當技術探索和交流。

功能描述

對 Javascript 熟悉的同學知道,eval() 函數,例如:

eval('console.log(2+3)')

就會在控制臺中打出 5。

這里我們要做的和 eval 類似,就是希望輸入一段 Java 代碼,服務器按照代碼中的邏輯執行。在繁星中前置腳本的功能就是可以對用戶的輸入參數進行自定義的處理,后置腳本的功能就是可以對數據庫中查詢到的結果做進一步加工。

為什么是 Java 腳本?

Groovy

要實現動態腳本的需求,首先可能會想到 Groovy,但是使用 Groovy 有幾大缺點:

  • Groovy 雖然也是運行在 JVM,但是語法和 Java 有一些差異,對于只會 Java 的同學來說有一定學習成本。
  • 動態類型,缺乏約束。有時候太過于靈活自由也是缺點,尤其是對于平臺說來。
  • 需要額外引入 Groovy 的引擎 jar 包,大小 6.2M,屬實不小,對于有代碼強迫癥的我來說這會是一個重要考慮因素。

Java

采用 Java 來實現動態腳本的功能有以下優點:

  • 學習成本低,在阿里最主要的語言就是 Java,會 Java 幾乎是每個工程師必備的技能,因此上手難度幾乎為零。
  • Java 可以規定接口約束,從而使得用戶寫的前后置腳本整齊劃一,方便管理和治理。
  • 可以實時編譯和錯誤提示,方便用戶及時訂正問題。

實現方式

代碼工程說明

本文的代碼工程:https://kbtdatacenter-read.oss-cn-zhangjiakou.aliyuncs.com/fusu-share/dynamic-script.zip

--dynamic-script 
------advance-discuss //深度討論腳本動態化技術中的一些細節
------code-javac //使用代碼執行編譯加載運行任務
------command-javac //演示用命令行的方式動態編譯和加載java類
------facade //提供單獨的接口包,方便整個演示過程流暢進行

實現方案設計

我們首先定義好一個接口,例如 Animal,然后用戶在自己的代碼中實現 Animal 接口。相當于用戶提供的是 Animal 的實現類 Cat,這樣系統加載了用戶的 Java 代碼后,可以很方便的利用 Java 多態特性,訪問到對應的方法。這樣既方便了用戶書寫規范,同時平臺使用起來也簡單。

使用控制臺命令行

首先回顧如何使用命令行來編譯 Java 類,并且運行。

首先對 facade 模塊打一個 jar 包,方便后續依賴:

cd 項目根目錄 
mvn install

進入到模塊 command-javac 的 resources 文件夾下(絕對路徑因人而異):

# 進入到Cat.java所在的目錄 
cd /Users/fusu/d/group/fusu-share/dynamic-script/command-javac/src/main/resources
# 使用命令行工具javac編譯,linux/mac 上cp分隔符使用 : windown使用 ;
javac -cp .:/Users/fusu/d/group/fusu-share/dynamic-script/facade/target/facade-1.0.jar Cat.java
# 運行
java -cp .:/Users/fusu/d/group/fusu-share/dynamic-script/facade/target/facade-1.0.jar Cat
# 得到結果
# > I'm Cat Main

使用 Process 調用 javac 編譯

有了上面的控制臺命令行操作,很容易想到用 Java 的 Process 類調用命令行工具執行 javac 命令,然后使用 URLClassLoader 來加載生成的 class 文件。代碼位于模塊 command-javac 下的 ProcessJavac.java 文件中,核心代碼如下:

//項目所在路徑 
String projectPath = PathUtil.getAppHomePath();

Process process = null;

String cmd = String.format("javac -cp .:%s/facade/target/facade-1.0.jar -d %s/command-javac/src/main/resources %s/command-javac/src/main/resources/Cat.java", projectPath, projectPath, projectPath);

System.out.println(cmd);

process = Runtime.getRuntime().exec(cmd);

// 打印程序輸出
readProcessOutput(process);

int exitVal = process.waitFor();
if (exitVal == 0) {
System.out.println("javac執行成功!" + exitVal);
} else {
System.out.println("javac執行失敗" + exitVal);
return;
}

String classFilePath = String.format("%s/command-javac/src/main/resources/Cat.class", projectPath);
String urlFilePath = String.format("file:%s", classFilePath);
URL url = new URL(urlFilePath);
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});

Class<?> catClass = classLoader.loadClass("Cat");
Object obj = catClass.newInstance();
if (obj instanceof Animal) {
Animal animal = (Animal) obj;
animal.hello("Kitty");
}
//會得到結果: Hello,Kitty! 我是Cat。

用編程方式編譯和加載

上面兩種方式都有一個明顯的缺點,就是需要依賴于 Cat.java 文件,以及必須產生 Cat.class 文件。在繁星平臺中,自然希望這個過程都在內存中完成,盡量減少 IO 操作,因此使用編程方式來編譯 Java 代碼就顯得很有必要了。代碼位于模塊 code-javac 下的 CodeJavac.java 文件中,核心代碼如下:

//類名 
String className = "Cat";
//項目所在路徑
String projectPath = PathUtil.getAppHomePath();
String facadeJarPath = String.format(".:%s/facade/target/facade-1.0.jar", projectPath);

//需要進行編譯的代碼
Iterable<? extends JavaFileObject> compilationUnits = new ArrayList<JavaFileObject>() {{
add(new JavaSourceFromString(className, getJavaCode()));
}};

//編譯的選項,對應于命令行參數
List<String> options = new ArrayList<>();
options.add("-classpath");
options.add(facadeJarPath);

//使用系統的編譯器
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();

StandardJavaFileManager standardJavaFileManager = javaCompiler.getStandardFileManager(null, null, null);
ScriptFileManager scriptFileManager = new ScriptFileManager(standardJavaFileManager);

//使用stringWriter來收集錯誤。
StringWriter errorStringWriter = new StringWriter();

//開始進行編譯
boolean ok = javaCompiler.getTask(errorStringWriter, scriptFileManager, diagnostic -> {
if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {

errorStringWriter.append(diagnostic.toString());
}
}, options, null, compilationUnits).call();

if (!ok) {
String errorMessage = errorStringWriter.toString();
//編譯出錯,直接拋錯。
throw new RuntimeException("Compile Error:{}" + errorMessage);
}

//獲取到編譯后的二進制數據。
final Map<String, byte[]> allBuffers = scriptFileManager.getAllBuffers();
final byte[] catBytes = allBuffers.get(className);

//使用自定義的ClassLoader加載類
FsClassLoader fsClassLoader = new FsClassLoader(className, catBytes);
Class<?> catClass = fsClassLoader.findClass(className);
Object obj = catClass.newInstance();
if (obj instanceof Animal) {
Animal animal = (Animal) obj;
animal.hello("Moss");
}

//會得到結果: Hello,Moss! 我是Cat。

代碼中主要使用到了系統編譯器 JavaCompiler,調用它的 getTask 方法就相當于命令行中執行 javac,getTask 方法中使用自定義的 ScriptFileManager 來搜集二進制結果,以及使用 errorStringWriter 來搜集編譯過程中可能出錯的信息。最后借助一個自定義類加載器 FsClassLoader 來從二進制數據中加載出類 Cat。

深入討論

上文介紹了動態腳本的實現關鍵點,但是還有諸多問題需要討論,筆者把主要的幾個問題拋出來,簡單討論一下。

ClassLoader 范圍問題

JVM 的類加載機制采用雙親委派模式,類加載器收到加載請求時,會委派自己的父加載器去執行加載任務,因此所有的加載任務都會傳遞到頂層的類加載器,只有當父加載器無法處理時,子加載器才自己去執行加載任務。下面這幅圖相信大家已經很熟悉了。

?

??

??


 

JVM 對于一個類的唯一標識是 (Classloader,類全名),因此可能出現這種情況,接口 Animal 已經加載了,但是我們用 CustomClassLoader 去加載 Cat 時,提示說 Animal 找不到。這就是因為 Animal 和 Cat 不是被同一個 Classloader 加載的。

由于 defineClass 方法是 protected 的,因此要用 byte[] 來加載 class 就需要自定義一個classloader,如何指定這個 Classloader 的父加載器就比較有講究了。

公司內部的 Java 系統都是采用的 pandora,pandora 有自己的類加載器以及線程加載器,因此我們以接口 Animal 的加載器 animalClassLoader 為標準,將線程 ClassLoader 設置為animalClassLoader,同時將自定義的 ClassLoader 的父加載器指定為 animalClassLoader。代碼位于模塊 advance-discuss 下,參考代碼如下:

/*FsClassLoader.java*/ 
public FsClassLoader(ClassLoader parentClassLoader, String name, byte[] data) {
super(parentClassLoader);
this.fullyName = name;
this.data = data;
}


/*AdvanceDiscuss.java*/

//接口的類加載器
ClassLoader animalClassLoader = Animal.class.getClassLoader();
//設置當前的線程類加載器
Thread.currentThread().setContextClassLoader(animalClassLoader);
//...
//使用自定義的ClassLoader加載類
FsClassLoader fsClassLoader = new FsClassLoader(animalClassLoader, className, cat

通過這些保障,就不會出現找不到類的問題了。

類重名問題

當我們只動態加載一個類時,自然不用擔心類全名重復的問題,但是如果需要加載多個相同類時,就有必要進行特殊處理了,可以利用正則表達式捕獲用戶的類名,然后增加隨機字符串的方式來規避重名問題。

從上文中,我們知道 JVM 對于一個類的唯一標識是(Classloader,類全名),因此只要能保證我們自定義的 Classloader 是不同的對象,也能夠避免類重名的問題。

Class 生命周期問題

Java 腳本動態化必須考慮垃圾回收的問題,否則隨著 Class 被加載的越來越多,系統的內存很快就不夠用了。我們知道在 JVM 中,對象實例在沒有被引用后會被 GC (Garbage Collection 垃圾回收),Class 作為 JVM 中一個特殊的對象,也會被 GC(清空方法區中 Class 的信息和堆區中的 java.lang.Class 對象。這時 Class 的生命周期就結束了)。

Class 要被回收,需要滿足以下三個條件:

  • NoInstance:該類所有的實例都已經被 GC。
  • NoClassLoader:加載該類的 ClassLoader 實例已經被 GC。
  • NoReference:該類的 java.lang.Class 沒有被引用 (XXX.class,使用了靜態變量/方法)。

從上面三個條件可以推出,JVM 自帶的類加載器(Bootstrap 類加載器、Extension 類加載器)所加載的類,在 JVM 的生命周期中始終不會被 GC。自定義的類加載器所加載的 Class 是可以被 GC 的,因此在編碼時,自定義的 Classloader 一定做成局部變量,讓其自然被回收。

為了驗證 Class 的 GC 情況,我們寫一個簡單的循環來觀察,模塊 advance-discuss 下的AdvanceDiscuss.java 文件中:

for (int i = 0; i < 1000000; i++) { 
//編譯加載并且執行
compileAndRun(i);

//10000個回收一下
if (i % 10000 == 0) {
System.gc();
}
}

//強制進行回收
System.gc();
System.out.println("休息10s");
Thread.currentThread().sleep(10 * 1000);

打開 Java 自帶的 jvisualvm 程序(位于 JAVA_HOME/bin/jvisualvm),可以可視化的觀看到 JVM 的情況。

?

??

??


 

在上圖中可以看到加載類的變化圖以及堆大小呈鋸齒狀,說明動態加載類能夠被有效的被回收。

安全問題

讓用戶寫腳本,并且在服務器上運行,光是想想就知道是一件非常危險的事情,因此如何保證腳本的安全,是必須嚴肅對待的一個問題。

類的白名單及黑名單機制

在用戶寫的 Java 代碼中,我們需要規定用戶允許使用的類范圍,試想用戶調用 File 來操作服務器上的文件,這是非常不安全的。javassist 庫可以對 Class 二進制文件進行分析,借助該庫我們可以很容易地得到 Class 所依賴的類。代碼位于模塊 advance-discuss 下的 JavassistUtil.java 文件中,以下是核心代碼:

public static Set<String> getDependencies(InputStream is) throws Exception { 

ClassFile cf = new ClassFile(new DataInputStream(is));
ConstPool constPool = cf.getConstPool();
HashSet<String> set = new HashSet<>();
for (int ix = 1, size = constPool.getSize(); ix < size; ix++) {
int descriptorIndex;
if (constPool.getTag(ix) == ConstPool.CONST_Class) {
set.add(constPool.getClassInfo(ix));
} else if (constPool.getTag(ix) == ConstPool.CONST_NameAndType) {
descriptorIndex = constPool.getNameAndTypeDescriptor(ix);
String desc = constPool.getUtf8Info(descriptorIndex);
for (int p = 0; p < desc.length(); p++) {
if (desc.charAt(p) == 'L') {
set.add(desc.substring(++p, p = desc.indexOf(';', p)).replace('/', '.'));
}
}
}
}
return set;
}

拿到依賴后,就可以首先使用白名單來過濾,以下這些包或類只涉及簡單的數據操作和處理,是被允許的:

java.lang, 
java.util,
com.alibaba.fastjson,
java.text,
[Ljava.lang (java.lang下的數組,例如 `String[]`)
[D (double[])
[F (float[])
[I (int[])
[J (long[])
[C (char[])
[B (byte[])
[Z (boolean[])

但是有個別的包下的類也比較危險,需要過濾掉,這時候就需要用黑名單再做一次篩選,這些包或類是不被允許的:

java.lang.Thread 
java.lang.reflect

線程隔離

有可能用戶的代碼中包含死循環,或者執行時間特別長,對于這種有問題的邏輯在編譯時是無法感知的,因此還需要使用單獨的線程來執行用戶的代碼,當出現超時或者內存占用過大的情況就直接 kill。

緩存問題

上面討論的都是從編譯到執行的完整過程,但是有時候用戶的代碼沒有變更,我們去執行時就沒有必要再次去編譯了,因此可以設計一個緩存策略,當用戶代碼沒有發生變更時,就使用懶加載策略,當用戶的代碼發生了變更就釋放之前加載好的 Class,重新加載新的代碼。

及時加載問題

當系統重啟時,相當于所有的類都被釋放了需要重新加載,對于一些比較重要的腳本,可能短暫的懶加載時間也是難以接受的,對于這種就需要單獨搜集,在系統啟動的時候根據系統一起加載進內存,這樣就可以當健康檢查通過時,保證類已經加載好了,從而有效縮短響應時間。

后記

由于篇幅問題,緩存問題、及時加載問題只做了簡單的討論。當然 Java 動態腳本技術還涉及到很多其他細節,需要在使用過程中不斷總結。也歡迎大家一起交流~

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2009-06-14 21:54:37

動態語言Java腳本API

2011-07-06 15:47:29

SQL Server分區

2010-03-09 19:27:42

Python翻譯腳本

2010-10-14 10:28:18

MySQL動態視圖

2010-03-25 16:31:55

Python代碼

2015-09-28 15:59:00

Java動態代理機制

2025-08-26 08:24:04

2020-10-23 06:56:00

C語言動態字符串

2021-11-19 11:36:42

語言string字符串

2025-02-04 11:30:10

2022-03-16 07:59:54

項目語言包JSON 文件

2009-07-22 08:52:05

Scala動態綁定

2021-10-18 12:04:22

Spring BootJava開發

2021-10-18 10:36:31

Spring Boot插件Jar

2010-05-26 15:07:36

SVN版本庫自動備份

2022-04-02 07:52:47

DubboRPC調用動態代理

2025-09-08 02:00:00

2025-09-09 00:00:01

2024-09-05 09:35:58

CGLIBSpring動態代理

2009-12-09 13:02:18

靜態路由動態路由
點贊
收藏

51CTO技術棧公眾號

欧美12一14sex性hd| 成人激情开心网| 中文字幕亚洲欧美在线不卡| 亚洲精品一区二区三区蜜桃久| 国产精品x8x8一区二区| 国产亚洲精品高潮| 免费在线观看的电影网站| 午夜精品久久一牛影视| 亚色视频在线观看| 99国产精品久久久久久久久久久 | 国产区高清在线| 一区二区免费看| 男人艹女人在线观看| 久久久国际精品| 久久网站免费视频| 成人精品视频一区二区三区| 欧美高清中文字幕| 成人午夜电影小说| 国产男女无遮挡| 久久婷婷国产综合国色天香| 国内外成人免费激情视频| 91丨porny丨蝌蚪视频| av天堂永久资源网| 国产精品美女久久久久aⅴ | 污视频在线看操| 亚洲国产一二三| 777永久免费网站国产| 中文字幕综合网| 美女黄视频在线播放| 岛国av在线不卡| av中文字幕一区二区三区| 欧美三级三级三级| 色av手机在线| 国产小视频国产精品| 国产精品成人国产| 欧美激情2020午夜免费观看| 国产99久久精品一区二区300| 国产精品午夜视频| 亚洲三级视频| 视频一区二区视频| 亚洲国产精品二十页| 麻豆传媒在线播放| 欧美二区乱c少妇| 456亚洲精品成人影院| 久久久国产精品一区| 国产一区在线电影| 国产精品专区一| 国产九九精品| 免费看毛片的网址| 国产日韩欧美麻豆| 你懂的视频在线| 精品99久久久久久| h视频久久久| 国产精品日韩高清| 国产伦精品一区二区三区视频青涩 | 97久久精品人人做人人爽| 国产成人综合美国十次| 欧美影视一区在线| 免费污视频在线一区| 日韩av免费看网站| 六月天综合网| 四虎影视av| 欧美不卡视频一区| 99re8这里有精品热视频免费 | 欧美日韩直播| 蜜桃传媒视频麻豆第一区免费观看| 成人综合婷婷国产精品久久免费| 激情六月婷婷| 日韩精品极品毛片系列视频| 视频一区在线观看| 中文视频一区视频二区视频三区| 亚洲国产激情av| 精品176二区| 69av视频在线播放| 欧美aaaaaa午夜精品| 91九色porny视频| 亚洲精品国产精品国自产在线| 欧美日韩破处| 一区二区三区四区免费视频| 亚洲大尺度视频在线观看| 春暖花开亚洲一区二区三区| 91网站免费看| 国产精品欧美久久久久一区二区| 波多野结衣在线播放| 国产精品香蕉av| 粉嫩一区二区三区性色av| 秋霞av在线| 性欧美视频videos6一9| 久久99热狠狠色一区二区| 性视频一区二区三区| 欧美激情视频网站| 国产一区二区精品在线观看| 国产youjizz在线| 日本精品性网站在线观看| 国产精品一区二区三区网站| 亚洲图片88| 国产精品视频中文字幕91| 91视频观看视频| 无遮挡在线观看| 免费日韩av电影| 午夜视黄欧洲亚洲| 国产精品自在| 丁香花在线影院观看在线播放| 欧美一区二区网站| 欧美黄色大片网站| 日本女优北野望在线电影| 欧美精品亚州精品| 国产成人综合亚洲网站| 伦xxxx在线| 国产一区二区三区免费不卡| 黄色成人av在线| 综合国产视频| 网上成人av| 午夜欧美大片免费观看| 91麻豆福利精品推荐| 日本成人伦理电影| 成人在线观看毛片| 2019日韩中文字幕mv| 五月综合激情婷婷六月色窝| 国产成人免费视频网站视频社区 | 91在线观看免费高清完整版在线观看| aa级大片欧美| 成人免费直播| 亚洲最大免费| 亚洲丁香久久久| 肉丝袜脚交视频一区二区| 国产激情在线视频| 欧美日韩精品一区| 欧美夫妻性生活| 久久久夜夜夜| av毛片在线| 日韩精品一线二线三线| 精品va天堂亚洲国产| 日本人妖一区二区| 丁香花在线影院| 热这里只有精品| 在线播放国产一区二区三区| 成人深夜视频在线观看| 精品乱码一区二区三区四区| 菠萝蜜视频在线观看入口| 亚洲欧美中文日韩在线| 成人少妇影院yyyy| 波多野结衣在线一区二区| 国产黄色片大全| 国产精品日韩一区| 欧美三级蜜桃2在线观看| 亚洲影音一区| 亚洲少妇视频| 亚洲国产精品久久久久爰色欲| 久久久久久久影院| 亚洲成人黄色影院| 合欧美一区二区三区| 日韩三级免费| 少妇人妻在线视频| 青草成人免费视频| 欧美在线观看视频一区二区| 久久一区视频| av成人在线看| 99热热99| 国产成人av一区二区三区| 精品国产一区久久| 91在线观看免费视频| 国产一区二区三区网| 欧美三级理伦电影| 男人添女荫道口图片| 国产成人91久久精品| 91精品国产综合久久香蕉的特点| 久久成人精品无人区| 999久久久久久久久6666| 美女做暖暖视频免费在线观看全部网址91| 欧美日韩一区二区三区在线观看免| 精品在线小视频| 亚洲免费观看高清完整版在线| 亚洲三级色网| 国内精品视频| 亚洲欧美日本免费| 一区二区三区不卡在线| 国内自拍欧美激情| 欧美一区二区三区视频在线| av一区二区三区黑人| 午夜精品久久久久久久四虎美女版| 国产剧情av在线播放| 丝袜国产免费观看| 特级黄色录像片| 国产精品成人aaaaa网站| 精品国精品自拍自在线| 日韩码欧中文字| 久久精品国产77777蜜臀| 免费成人av| 自拍网站在线观看| 在线免费观看黄色片| 国产www免费| 91传媒视频免费| 久久精品免费播放| 欧美日韩国产另类一区| 国产视频一区二区三区在线观看| 亚洲视频1区| 伊甸园亚洲一区| 国产精品高清乱码在线观看| 91亚洲欧美|