程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 詳解利用 JDK6 動態編譯組件搭建 OSGi 運行時編譯環境

詳解利用 JDK6 動態編譯組件搭建 OSGi 運行時編譯環境

編輯:關於JAVA

但是我們知道,在開發 OSGi 環境下的 Bundle 時最麻煩的步驟之一就是搭建編譯環境。即便利用 Eclipse 這樣高效的 開發工具,由於 Bundle 個數的龐大以及同一 Bundle 的版本多樣性,維護一個編譯環境變得非常繁瑣。常常我們需要對一 個 OSGi 的 Bundle 進行二次開發時,僅僅一個很小的改動都需要花大量的時間去搭建專為這套程序的編譯環境。我們迫切 希望可以有一個運行時的編譯環境來簡化這些步驟,利用環境既有的依賴項來對代碼進行編譯。

本篇文章介紹 OSGi 的運行特性,Bundle 間運行時的依賴方式以及 JDK6 所提供動態編譯功能,接著一步步介紹如何利用這些特性搭建可以運 行時編譯的開發環境,最後通過 IBM System Director 作為示例,將開發 Bundle 安裝在上面上演示以給讀者一個直觀的 體驗。本文假設讀者具備一定的 OSGi 基礎知識和 java 基礎知識。讀完本文後,如果讀者有一個 OSGi 的運行環境和想要 編譯的運行在這個環境上的 java 源代碼,就可以自行搭建出利用這個運行環境編譯出字節碼文件出來以供發布及二次開發 使用的模塊。

圖 1 描述了傳統基於 OSGi 的應用程序架構:

圖 1. 基於 OSGi 的應用程序架構

要實現文章所介 紹的功能,應用程序只需滿足如下系統需求:

基於任意 OSGi4 的實現版本,如 Knopflerfish 或 Equinox。

運行 JDK 版本 6.0 或以上。

圖 2 概括描述了本文所要描述的目的:

圖 2. 運行時編譯環境模型

其中 Bundle1、2 等等分別是 OSGi 運行環 境中既存的 Bundle,即 OSGi 已經將它們對應的 jar 包或是字節碼文件加載了進來。棕色框圖則是我們要制作的 OSGi 運 行時動態編譯 Bundle。

用戶輸入 Java 源代碼(以命令行或是文件形式)

動態編譯模塊掃描所有 Bundle,將運行時環境裡所有的 jar 及 class 文件作為編譯時依賴保存。

利用 JDK6 提供的動態編譯組件進行動態編譯

編寫自己的 JavaFileObject 對象,並且指定 StandardJavaFileManager 的編譯目的地(這些都是 JDK6 動態編譯組件 的相關內容會在後續詳細說明),即可編譯出 Java 字節碼文件

[ 可選 ] 發送 class 文件到指定郵箱完成整個運行時編譯過程。

接下來就讓我們來看看怎樣利用 JDK6 動態編譯組件和 OSGi 提供的相關功能來搭建這個運行時編譯 Bundle。不過在這 之前,還是有必要了解下 Bundle 之間的通訊方式。

Bundle 間通訊方式

大家都知道 OSGi 是以 Bundle 為 程序模塊單位組成的。每一個 Bundle 獨立開發,各自成章,有自己分配的內存空間以及生命周期。Bundle 也分很多種類 型,有些 Bundle 只提供功能 Service,有些 Bundle 消費 Service 並呈現給用戶(比如 UI Bundle),還有些 Bundle 自己不單獨啟動作為其他 Bundle 的 fragment Bundle 存在(比如資源文件 Bundle)等等。

Bundle 的狀態也有很 多種,啟動成功的 Active 狀態,解析成功等待啟動的 Resolved 狀態(可能是啟動失敗),安裝成功等待啟動的 Installed 狀態等等,關於 Bundle 的類型,生命周期及對應的狀態信息這裡不做詳細闡述,讀者可以參見 OSGi 官方說明 文檔。

既然 Bundle 是 OSGi 的功能單元,服務提供者和消費者 Bundle 之間,功能包提供者和消費者之間當然需 要進行相互通信。概括有以下三種通信方式:

Import Package 方式,在服務提供 Bundle 用 Export Package 導出。需要通過配置 MANIFEST.MF 來實現。

Required Bundle 方式,在服務提供 Bundle 用 Export Package 導出。需要通過配置 MANIFEST.MF 來實現。

Register Service 方式,在服務提供 Bundle 通過 BundleContext 進行 Service 注冊,在消費者 Bundle 通過 ServiceTracker 取得 Service,需要通過寫代碼來實現。

另外如果是新開發 Bundle,還可以采用聲明式方式取得 Service,聲明式方式取得 service 的方法通過簡單配置能非 常方便地獲取 Service,開發者只需關注自己的邏輯,而不必關心 OSGi 較為復雜的搜索 Service 方法。

現在我們 對 OSGi Bundle 之間的通訊依賴方式有了基本了解之後,就可以來了解下如何動態編譯了。好在 JDK6 提供了一套專門用 於動態編譯文件的組件來幫我們簡化工作,我們只要繼承簡單的基礎類就可以實現內存中動態編譯出 class 文件(好比 javac 命令)的功能來了。

JDK6 動態編譯組件

Java SE 6 之後自身集成了運行時編譯的組件:javax.tools ,存放在 tools.jar 包裡,可以實現 Java 源代碼編譯,幫助擴展靜態應用程序。該包中提供主要類可以從 Java String 、StringBuffer 或其他 CharSequence 中獲取源代碼並進行編譯。

接下來通過代碼一步步講述如何利用 JDK6 特性 進行運行時編譯。

清單 1. JDK6 動態編譯代碼片段

// 通過 ToolProvider 取得 JavaCompiler 對象,

JavaCompiler 對象是動態編譯工具的主要對象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 
    
// 通過 JavaCompiler 取得標准 StandardJavaFileManager 對象,StandardJavaFileManager 對象主要負責
// 編譯文件對象的創建,編譯的參數等等,我們只對它做些基本設置比如編譯 CLASSPATH 等。
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); 
    
// 因為是從內存中讀取 Java 源文件,所以需要創建我們的自己的 JavaFileObject,即 InMemoryJavaFileObject 
JavaFileObject fileObject = new InMemoryJavaFileObject(className, codeString); 
Iterable<? extends JavaFileObject> files = Arrays.asList(fileObject); 
    
// 編譯結果信息的記錄
StringWriter sw = new StringWriter(); 
    
// 編譯目的地設置
Iterable options = Arrays.asList("-d", classOutputFolder); 
    
// 通過 JavaCompiler 對象取得編譯 Task 
JavaCompiler.CompilationTask task = 
 compiler.getTask(sw, fileManager, null, options, null, files); 
    
// 調用 call 命令執行編譯,如果不成功輸出錯誤信息
if (!task.call()) { 
String failedMsg = sw.toString(); 
System.out.println(“Build Error:” + failedMsg); 
} 
    
// 自定義 JavaFileObject 實現了 SimpleJavaFileObject,指定 string 為 java 源代碼,這樣就不用將源代碼
// 存在內存中,直接從變量中讀入即可。
public static class InMemoryJavaFileObject extends SimpleJavaFileObject { 
private String contents = null; 
    
public InMemoryJavaFileObject(String className, String contents) { 
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), 
   Kind.SOURCE); 
this.contents = contents; 
} 
    
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 
return contents; 
} 
}

說明:注釋詳細說明了每一步的作用,通過 JDK 提供的 API,我們可以很簡單也很靈活的進行動態運行時編譯 。

JRE 運行環境中如何進行動態編譯

JRE 環境不像 JDK 環境,不帶 javac 命令,也省略了 tools.jar 功 能組件,所以自然也不具備運行時編譯的功能。如果是這樣讀者可以用如下方式來解決這個問題:

找到 tools.jar ,手動導入到 bundle 中並在 MANIFEST.MF 中添加依賴。

更改 JavaCompiler 的取得方式:

因為 ToolProvider.getSystemJavaCompiler() 還是通過運行時環境取得,JRE 環境下為 null,所以需要更改為直接從 Bundle -ClassPath 實例化:

JavaCompiler compiler = new com.sun.tools.javac.api.JavacTool();

這樣就可以 正常取得 JavaCompiler 進行運行時編譯了。

搭建運行時 OSGi 編譯運行環境

現在我們可以討論如何利用 JDK6 的動態編譯工具在 OSGi 特定的環境環境下進行動態編譯了。想要編譯 OSGi 應用程序內任意 Java Code,會遇到如 下的編譯依賴問題:

如何取得 OSGi 容器內正在使用的 Bundle 的作為 CLASSPATH 的一部分。

如何掃描取 得 OSGi 環境下所有的 jar 包以作為 CLASSPATH 的一部分。

如何取得某些 jar 包打包在特定 jar 包中的那些依 賴項目作為 CLASSPATH 的一部分。

下節我們就來具體討論如何解決這些個編譯依賴問題。

解決編譯依賴

我們都知道在使用 javac 命令進行編譯時最為復雜的便是設置 CLASSPATH 了,任意一個 jar 包或是 class 文件 找不到就會拋出 ClassNotFoundException。Eclipse IDE 中為我們省去了這一繁瑣的步驟,只要略微配置 Build Path 就 能編譯出項目所有的 class 文件來。

不幸的是,我們需要制作的動態編譯 Bundle 不知道依賴項是什麼,也是需要 我們自己來設置 CLASSPATH,而且因為我們不知道以後會編譯什麼樣的文件,會用到什麼樣的依賴,所以如何設置,設置多 少就成了一個難題。

在這裡筆者提出了一個解決方案,就是 掃描應用程序文件夾中所有的 Bundle以及 調用 OSGi 中所有的 BundleLocation聯合作為 CLASSPATH 來滿足編譯依賴的完備性。另外還要滿足規則如下表所示:

注意:

因為搜索到全部的這些 jar 文件 的時間視 OSGi 應用程序的規模大小而定,所以盡量在實現時將其保存於靜態變量中以節省重復操作時間。在演示章節中會 示例在一個大型項目中各個階段的具體所消耗的時間。

下面給出各個階段的示例代碼。

清單 2. 取得運行時 Bundle Location 的依賴項

//將所有找到的Bundle Location添加入List<File>中
private static List<File> allReferences = new ArrayList<File>();;
//從BundleContext對象中取得所有的bundles
Bundle[] bundles = bundleContext.getBundles();
//遍歷所有的Bundle,取得其location
for (Bundle bundle : bundles) {
    //取得location的示例String如下:
//initial@reference:file:plugins/org.eclipse.core.jobs_3.4.1.R34x_v20081128.jar/
//update@plugins/org.eclipse.core.net_1.1.0.I20080604.jar
    String bundleLocation = bundle.getLocation();
    
    
    //需要進行字符串截取方能找到真實的相對路徑:
    String[] splitAt = bundleLocation.split("@");
    bundleLocation = splitAt[splitAt.length - 1];
    String[] splitColon = bundleLocation.split(":");
    bundleLocation = splitColon[splitColon.length - 1];
    //拼上運行時路徑就可以生成其File對象了
    File f = new File("./eclipse/" + bundleLocation);
    //添加至依賴List
    allReferences.add(f);
}

說明:保留下 allReferences 對象,繼續添加第二第三階段的依賴項。

清單 3. 掃描 OSGi 運行目錄下 所有的 jar 包作為依賴項

// 取得所有的 jar 文件絕對路徑,並且挑選版本最新的生成 File 對象添加到 CLASSPATH 中
List<String> allJarFiles = searchFiles("..", ".jar"); 
for (String jarFile : allJarFiles) { 
File addFile = new File(jarFile); 
for (File existFile : allReferences) { 
if (isElderVersion(addFile.getName(), existFile.getName())) { 
continue; 
} 
} 
allReferences.add(addFile); 
} 
    
// 記錄所有 jar 文件的絕對路徑 String 
static List<String> allFiles = new ArrayList<String>(); 
    
// 返回所有 jar 文件絕對路徑的 List 
public static synchronized List<String> searchFiles(String startPath, String suffix) { 
allFiles = new ArrayList<String>(); 
    
// start to recurse 
recurse(startPath, suffix); 
    
return allFiles; 
} 
    
// 使用遞歸來取得所有 .jar 結尾的文件
private static void recurse(String startPath, String suffix) { 
File dir = new File(startPath); 
File[] files = dir.listFiles(); 
if (files == null) 
return; 
for (int i = 0; i < files.length; i++) { 
if (files[i].isDirectory()) { 
recurse(files[i].getAbsolutePath(), suffix); 
} else { 
String strFileName = files[i].getAbsolutePath().toLowerCase(); 
if (null == suffix || "".equals(suffix)) { 
allFiles.add(files[i].getAbsolutePath()); 
} else if (strFileName.endsWith(suffix)) { 
allFiles.add(files[i].getAbsolutePath()); 
} 
} 
} 
} 
    
// 判斷是否 allReferences 已經存在了比其更新的 jar,如果有更新的就返回 false 不再添加了。
private static boolean isElderVersion(String addFile, String existFile) { 
if (!isSameJarFile(addFile, existFile)) { 
return false; 
} 
String addVersion = getVersionString(addFile); 
String existVersion = getVersionString(existFile); 
    
if (addVersion == null || existVersion == null) { 
return false; 
} 
String[] addVs = addVersion.split("\\."); 
String[] existVs = existVersion.split("\\."); 
    
int length = addVs.length > existVs.length ? existVs.length : addVs.length; 
try { 
int i; 
for (i = 0; i < length; i++) { 
int addDigit = Integer.parseInt(addVs[i]); 
int existDigit = Integer.parseInt(existVs[i]); 
if (addDigit > existDigit) { 
return false; 
} else if (addDigit < existDigit) { 
return true; 
} 
} 
    
if (addVs.length > existVs.length) { 
return false; 
} 
} catch (Exception ex) { 
return false; 
} 
    
return true; 
}

說明:階段二搜索當前 OSGi 運行文件夾下所有 jar 文件作為 CLASSPATH 一部分添加,其中過濾了同文件的舊版本。 在三個步驟中,階段二較為暴力也最為費時,但由於用戶可能會編譯與 OSGi 容器及代碼相關的任意文件,所以為了保證依 賴項的完備性而犧牲第一次加載的時間也是有必要的。

清單 4. 深度掃描指定的 jar 包,將其包含所有的 jar 文 件也作為 CLASSPATH 的一部分

import java.util.jar.JarEntry; 
import java.util.jar.JarFile; 
    
List<String> deepSearchJarNames = new ArrayList<String>(); 
// deepSearchJarNames 是用戶配置的需要進行深度搜索的 jar 包名。
deepSearchJarNames.add("com.mycompany.libs"); 
// 遍歷已經添加的所有 reference 找到需要進行深度搜索的 jar 包
for (File jarFile : allReferences) { 
for (String deepJarName : deepSearchJarNames) { 
if (jarFile.getName().startsWith(deepJarName)) { 
try { 
JarFile jarF = new JarFile(jarFile); 
Enumeration<JarEntry> jarEntries = jarF.entries(); 
    
while (jarEntries.hasMoreElements()) { 
JarEntry jarEntry = jarEntries.nextElement(); 
if (jarEntry.getName().endsWith(".jar")) { 
String filePath = jarFile.getAbsolutePath(); 
    
// 將 jar 文件解壓縮到 temp 文件夾中
if (tempOutputFolder != null && !"".equals(tempOutputFolder)) { 
// Make folder if dest folder not existed. 
File destF = new File(tempOutputFolder); 
if (!destF.exists()) { 
destF.mkdirs(); 
} 
    
String classFile = tempOutputFolder + File.separator + jarEntry.getName(); 
File f = new File(classFile); 
if (!f.getParentFile().exists()) { 
f.getParentFile().mkdirs(); 
} 
InputStream is = jarF.getInputStream(jarEntry); 
FileOutputStream fos = new java.io.FileOutputStream(f); 
byte[] buf = new byte[1024]; 
int len; 
while ((len = is.read(buf)) > 0) { 
fos.write(buf, 0, len); 
} 
fos.close(); 
is.close(); 
    
// 添加至依賴 List 
allReferences.add(f); 
} 
} 
} 
} catch (Exception ex) { 
} 
} 
} 
}

說明:階段三將根據用戶指定的 jar 包名(可以 hard coding 也可以從配置文件中讀取),在已經整理好的 allReferences中進行搜索。並將其中所有的 jar 文件解壓縮到臨時目錄並添加進依賴 List 中。

至此,我們的依 賴 List:allReferences 就完成了,將這個 List 作為 CLASSPATH 編譯選項進行編譯,只要源代碼中所有的依賴都在容器 內,就不會出現編譯依賴缺失的問題了。

在查看最終代碼前我們再看看還有什麼可以做的。

添加發送郵件功 能

這是一個額外的功能。用戶當然可以通過設定的 classOutputFolder 中自己去取編譯好的 class 文件。我們也 可以做的更自動化點,讓工具在編譯後為我們發送到指定的郵箱。當然這需要用戶指定 smtp 服務器。

清單 5. 發 送郵件示例代碼

import javax.mail.Message; 
import javax.mail.MessagingException; 
import javax.mail.Multipart; 
import javax.mail.Session; 
import javax.mail.Transport; 
import javax.mail.internet.InternetAddress; 
import javax.mail.internet.MimeBodyPart; 
import javax.mail.internet.MimeMessage; 
import javax.mail.internet.MimeMultipart; 
    
public static boolean sendMail(String host, String from, String to, List<File> files) { 
Properties props = System.getProperties(); 
// 設置 smtp 服務器地址
props.put("mail.smtp.host", host); 
    
// 取得 Mail Session 
Session session = Session.getInstance(props, null); 
    
try { 
MimeMessage message = new MimeMessage(session); 
message.setFrom(new InternetAddress(from)); 
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to)); 
message.setSubject("Compiled bytecode files from DirectorDebugTool"); 
    
MimeBodyPart messageBodyPart = new MimeBodyPart(); 
    
// 填充郵件內容
StringBuffer sb = new StringBuffer(); 
sb.append("Compiled successfully!! 
  Fetch the attachments for compiled bytecode files.\n\n\n\n"); 
sb.append("This mail is sent automatically by DirectorDebugTool, 
please do not reply. Any question or problem please contact [email protected]."); 
messageBodyPart.setText(sb.toString()); 
    
Multipart multipart = new MimeMultipart(); 
multipart.addBodyPart(messageBodyPart); 
    
// 添加附件
if (files != null && files.size() > 0) { 
for (File fileAttachment : files) { 
if (fileAttachment.isFile()) { 
messageBodyPart = new MimeBodyPart(); 
DataSource source = new FileDataSource(fileAttachment); 
messageBodyPart.setDataHandler(new DataHandler(source)); 
messageBodyPart.setFileName(fileAttachment.getName()); 
multipart.addBodyPart(messageBodyPart); 
} 
} 
} 
message.setContent(multipart); 
    
// 發送郵件
Transport.send(message); 
} catch (MessagingException mex) { 
return false; 
} 
    
return true; 
}

說明:代碼示例了如果用 javax.mail 發送郵件並把編譯後的 class 文件作為附件添加到郵件中。

最終運行時 編譯模塊

大功告成了。我們完成了 JDK 動態編譯的功能模塊,也找到了所有編譯要用到的依賴項。這樣就可以根據 用戶輸入的源代碼進行運行時編譯,並且發送到用戶指定的郵箱了。

經過調整的動態編譯模塊最終示例代碼如下所 示:

清單 6. 動態編譯部分的最終代碼

JavaCompiler compiler = 

ToolProvider.getSystemJavaCompiler();
    
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
//設置編譯依賴的CLASSPATH。
//setLocation的value為Iterable<? extends File>,只要將allReferences返回即可完成設置編譯依賴。
fileManager.setLocation(StandardLocation.CLASS_PATH, Utils.getCompileClassPath());
    
JavaFileObject fileObject = new InMemoryJavaFileObject(className, codeString);
Iterable<? extends JavaFileObject> files = Arrays.asList(fileObject);
    
StringWriter sw = new StringWriter();
    
Iterable options = Arrays.asList("-d", classOutputFolder);
    
JavaCompiler.CompilationTask task = 
    compiler.getTask(sw, fileManager, null, options, null, files);
    
if (!task.call()) {
    String failedMsg = sw.toString();
    System.out.println(“Build Error:” + failedMsg);
    Return;
}
    
//取得classOutputFolder中所有的.class文件,並作為參數調用發送郵件模塊
File dir = new File(classOutputFolder);
File[] allClassFiles = dir.listFiles();
    
List<File> sendAttachments = new ArrayList<File>();
for (File file : allClassFiles) {
    if (file.getName().endsWith(".class")) {
        sendAttachments.add(file);
    }
}
    
//發送郵件
Utils.sendMail(host, "DynamicCompileTool", to, sendAttachments);

OSGi 運行時編譯組件小試牛刀

以上列出了基於 OSGi 的運行時動態編譯組件的主要代碼。筆者也花了一些時間開發完成了一個特定的 Bundle,它包含 GUI 用來接收用戶輸入(復制)源代碼,也包含了所有上述的源代碼,並將它部署到已經發布的 IBM System Director 產 品中實際使用了起來,效果比較理想。接下來將根據這個特定的場景演示下這個運行時編譯組件是如何使用的。

演 示環境:IBM System Director

IBM Systems Director 是一款綜合的系統管理平台,為復雜的 IT 環境提供單點管 理和自動化功能。它基於 OSGi 平台的 Equinox 實現,經過幾年的開發維護,目前已經有超過 800 個 Bundle,系統目錄 數千個 jar 包。在這裡作為一個示例,看看這個自己開發的運行時編譯組件在成熟的企業級 OSGi 產品環境中表現如何。

在 IBM System Director 運行時編譯源代碼並發送郵件

將 Bundle 發布到 IBM System Director 中隨環境 啟動。我們可以打開如下的用戶交互界面:

圖 3. 工具主界面

這裡可以看到一個 TextArea 用於輸入源代 碼,Email 和 Mail Server 的 TextBox 用於配置發送郵件功能。

嘗試編譯源代碼,這裡使用工具 Bundle 裡的 Utils.java 來做演示。

圖 4. 運行時編譯源代碼

點擊 Compile and Mail Class 按鈕後提示 編譯成功,僅編譯部分花了 2.5s。郵件發送到了 [email protected],去看看 .class 文件是否作為附件發送了過來。

圖 5. 運行時編譯源代碼

郵件已經收到。信息內容無誤並且編譯後的 class 字節碼文件和內部類都發送了過來。運行時編譯工具成功得完成了容器內編譯過程,幫我們省下了搭建環境的時間。 不過還沒完,讓我們來整理下 log 看看在第一次執行時收集所有依賴項的性能如何。

表 2. 生成依賴項各階段所消 耗的時間

注:總共生成 1790 個依賴項。

可以注意到第二階段遞歸掃描所有 jar 包最為耗時,但正如前文所說, 由於用戶可能會編譯任意文件,為了保證依賴項的完備性這是必需消耗的時間。好在只要 OSGi 不重啟,這樣的搜索操作只 會執行一次,以後的編譯中就不會再消耗這麼多時間了。

小結

本文先是介紹了 OSGi 的應用程序架構和 Bundle 間依賴關系。再是介紹了 JDK6 中提供的動態編譯功能,並提供了示例代碼。在您有了相應的背景知識後,提出了 在 OSGi 運行時容器內動態編譯的概念,並提供了解決編譯依賴項的解決方案,將這些功能組件結合,理論上就可以在任意 OSGi 容器內進行運行時編譯了。再次結合了發送郵件功能,在 IBM System Director 上演示了工具的使用效果,給讀者更 直觀的認識。

本文僅針對編譯單個 Java 文件,您完全可以根據自己項目情況,結合 ant 或 maven,開發可以在容 器內編譯和部署整個項目的工具。同時適用范圍也可以加以拓展,不僅是 OSGi 環境,所有基於 Java 平台的都可以進行運 行時動態編譯。希望本文能給讀者帶來啟發,也期待能與讀者進一步交流探討相關延伸領域的技術。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved