轉(zhuǎn)帖|其它|編輯:郝浩|2011-02-01 09:46:11.000|閱讀 1857 次
概述:Java字節(jié)代碼不僅可以以文件形式存在于磁盤上,也可以通過網(wǎng)絡(luò)方式來下載,還可以只存在于內(nèi)存中。JVM中的類加載器會負責從包含字節(jié)代碼的字節(jié)數(shù)組(byte[])中定義出Java類。在某些情況下,可能會需要動態(tài)的生成 Java字節(jié)代碼,或是對已有的Java字節(jié)代碼進行修改。這個時候就需要用到本文中將要介紹的相關(guān)技術(shù)。首先介紹一下如何動態(tài)編譯Java源文件。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
Java作為業(yè)界應(yīng)用最為廣泛的語言之一,深得眾多軟件廠商和開發(fā)者的推崇,更是被包括Oracle在內(nèi)的眾多JCP成員積極地推動發(fā)展。但是對于Java語言的深度理解和運用,畢竟是很少會有人涉及的話題。InfoQ中文站特地邀請IBM高級工程師成富為大家撰寫這個《Java深度歷險》專欄,旨在就Java的一些深度和高級特性分享他的經(jīng)驗。
在一般的Java應(yīng)用開發(fā)過程中,開發(fā)人員使用Java的方式比較簡單。打開慣用的IDE,編寫Java源代碼,再利用IDE提供的功能直接運行 Java 程序就可以了。這種開發(fā)模式背后的過程是:開發(fā)人員編寫的是Java源代碼文件(.java),IDE會負責調(diào)用Java的編譯器把Java源代碼編譯成平臺無關(guān)的字節(jié)代碼(byte code),以類文件的形式保存在磁盤上(.class)。Java虛擬機(JVM)會負責把Java字節(jié)代碼加載并執(zhí)行。Java通過這種方式來實現(xiàn)其 “編寫一次,到處運行(Write once, run anywhere)” 的目標。Java類文件中包含的字節(jié)代碼可以被不同平臺上的JVM所使用。Java字節(jié)代碼不僅可以以文件形式存在于磁盤上,也可以通過網(wǎng)絡(luò)方式來下載,還可以只存在于內(nèi)存中。JVM中的類加載器會負責從包含字節(jié)代碼的字節(jié)數(shù)組(byte[])中定義出Java類。在某些情況下,可能會需要動態(tài)的生成 Java字節(jié)代碼,或是對已有的Java字節(jié)代碼進行修改。這個時候就需要用到本文中將要介紹的相關(guān)技術(shù)。首先介紹一下如何動態(tài)編譯Java源文件。
動態(tài)編譯Java源文件
在一般情況下,開發(fā)人員都是在程序運行之前就編寫完成了全部的Java源代碼并且成功編譯。對有些應(yīng)用來說,Java源代碼的內(nèi)容在運行時刻才能確定。這個時候就需要動態(tài)編譯源代碼來生成Java字節(jié)代碼,再由JVM來加載執(zhí)行。典型的場景是很多算法競賽的在線評測系統(tǒng)(如PKU JudgeOnline),允許用戶上傳Java代碼,由系統(tǒng)在后臺編譯、運行并進行判定。在動態(tài)編譯Java源文件時,使用的做法是直接在程序中調(diào)用Java編譯器。
JSR 199引入了Java編譯器API。如果使用JDK 6的話,可以通過此API來動態(tài)編譯Java代碼。比如下面的代碼用來動態(tài)編譯最簡單的Hello World類。該Java類的代碼是保存在一個字符串中的。
public class CompilerTest {
public static void main(String[] args) throws Exception {
String source = "public class Main { public static void main(String[] args) {System.out.println(\"Hello World!\");} }";
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
StringSourceJavaObject sourceObject = new CompilerTest.StringSourceJavaObject("Main", source);
Iterable< extends JavaFileObject> fileObjects = Arrays.asList(sourceObject);
CompilationTask task = compiler.getTask(null, fileManager, null, null, null, fileObjects);
boolean result = task.call();
if (result) {
System.out.println("編譯成功。");
}
}
static class StringSourceJavaObject extends SimpleJavaFileObject {
private String content = null;
public StringSourceJavaObject(String name, String content) ??throws URISyntaxException {
super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
this.content = content;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors) ??throws IOException {
return content;
}
}
}
如果不能使用JDK 6提供的Java編譯器API的話,可以使用JDK中的工具類com.sun.tools.javac.Main,不過該工具類只能編譯存放在磁盤上的文件,類似于直接使用javac命令。
另外一個可用的工具是Eclipse JDT Core提供的編譯器。這是Eclipse Java開發(fā)環(huán)境使用的增量式Java編譯器,支持運行和調(diào)試有錯誤的代碼。該編譯器也可以單獨使用。Play框架在內(nèi)部使用了JDT的編譯器來動態(tài)編譯Java源代碼。在開發(fā)模式下,Play框架會定期掃描項目中的Java源代碼文件,一旦發(fā)現(xiàn)有修改,會自動編譯 Java源代碼。因此在修改代碼之后,刷新頁面就可以看到變化。使用這些動態(tài)編譯的方式的時候,需要確保JDK中的tools.jar在應(yīng)用的 CLASSPATH中。
下面介紹一個例子,是關(guān)于如何在Java里面做四則運算,比如求出來(3+4)*7-10的值。一般的做法是分析輸入的運算表達式,自己來模擬計算過程。考慮到括號的存在和運算符的優(yōu)先級等問題,這樣的計算過程會比較復(fù)雜,而且容易出錯。另外一種做法是可以用JSR 223引入的腳本語言支持,直接把輸入的表達式當做JavaScript或是JavaFX腳本來執(zhí)行,得到結(jié)果。下面的代碼使用的做法是動態(tài)生成Java源代碼并編譯,接著加載Java類來執(zhí)行并獲取結(jié)果。這種做法完全使用Java來實現(xiàn)。
private static double calculate(String expr) throws CalculationException {
String className = "CalculatorMain";
String methodName = "calculate";
String source = "public class " + className
+ " { public static double " + methodName + "() { return " + expr + "; } }";
//省略動態(tài)編譯Java源代碼的相關(guān)代碼,參見上一節(jié)
boolean result = task.call();
if (result) {
ClassLoader loader = Calculator.class.getClassLoader();
try {
Class<?> clazz = loader.loadClass(className);
Method method = clazz.getMethod(methodName, new Class<?>[] {});
Object value = method.invoke(null, new Object[] {});
return (Double) value;
} catch (Exception e) {
throw new CalculationException("內(nèi)部錯誤。");
}
} else {
throw new CalculationException("錯誤的表達式。");
}
}
上面的代碼給出了使用動態(tài)生成的Java字節(jié)代碼的基本模式,即通過類加載器來加載字節(jié)代碼,創(chuàng)建Java類的對象的實例,再通過Java反射API來調(diào)用對象中的方法。
Java字節(jié)代碼增強
Java 字節(jié)代碼增強指的是在Java字節(jié)代碼生成之后,對其進行修改,增強其功能。這種做法相當于對應(yīng)用程序的二進制文件進行修改。在很多Java框架中都可以見到這種實現(xiàn)方式。Java字節(jié)代碼增強通常與Java源文件中的注解(annotation)一塊使用。注解在Java源代碼中聲明了需要增強的行為及相關(guān)的元數(shù)據(jù),由框架在運行時刻完成對字節(jié)代碼的增強。Java字節(jié)代碼增強應(yīng)用的場景比較多,一般都集中在減少冗余代碼和對開發(fā)人員屏蔽底層的實現(xiàn)細節(jié)上。用過JavaBeans的人可能對其中那些必須添加的getter/setter方法感到很繁瑣,并且難以維護。而通過字節(jié)代碼增強,開發(fā)人員只需要聲明Bean中的屬性即可,getter/setter方法可以通過修改字節(jié)代碼來自動添加。用過JPA的人,在調(diào)試程序的時候,會發(fā)現(xiàn)實體類中被添加了一些額外的 域和方法。這些域和方法是在運行時刻由JPA的實現(xiàn)動態(tài)添加的。字節(jié)代碼增強在面向方面編程(AOP)的一些實現(xiàn)中也有使用。
在討論如何進行字節(jié)代碼增強之前,首先介紹一下表示一個Java類或接口的字節(jié)代碼的組織形式。
類文件 {
0xCAFEBABE,小版本號,大版本號,常量池大小,常量池數(shù)組,
訪問控制標記,當前類信息,父類信息,實現(xiàn)的接口個數(shù),實現(xiàn)的接口信息數(shù)組,域個數(shù),
域信息數(shù)組,方法個數(shù),方法信息數(shù)組,屬性個數(shù),屬性信息數(shù)組
}
如上所示,一個類或接口的字節(jié)代碼使用的是一種松散的組織結(jié)構(gòu),其中所包含的內(nèi)容依次排列。對于可能包含多個條目的內(nèi)容,如所實現(xiàn)的接口、域、方法和屬性等,是以數(shù)組來表示的。而在數(shù)組之前的是該數(shù)組中條目的個數(shù)。不同的內(nèi)容類型,有其不同的內(nèi)部結(jié)構(gòu)。對于開發(fā)人員來說,直接操縱包含字節(jié)代碼的字節(jié)數(shù)組的話,開發(fā)效率比較低,而且容易出錯。已經(jīng)有不少的開源庫可以對字節(jié)代碼進行修改或是從頭開始創(chuàng)建新的Java類的字節(jié)代碼內(nèi)容。這些類庫包括ASM、cglib、serp和BCEL等。使用這些類庫可以在一定程度上降低增強字節(jié)代碼的復(fù)雜度。比如考慮下面一個簡單的需求,在一個Java類的所有方法執(zhí)行之前輸出相應(yīng)的日志。熟悉AOP的人都知道,可以用一個前增強(before advice)來解決這個問題。如果使用ASM的話,相關(guān)的代碼如下:
ClassReader cr = new ClassReader(is);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
for (Object object : cn.methods) {
MethodNode mn = (MethodNode) object;
if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
continue;
}
InsnList insns = mn.instructions;
InsnList il = new InsnList();
il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("Enter method -> " + mn.name));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));
insns.insert(il); mn.maxStack += 3;
}
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] b = cw.toByteArray();
從ClassWriter就可以獲取到包含增強之后的字節(jié)代碼的字節(jié)數(shù)組,可以把字節(jié)代碼寫回磁盤或是由類加載器直接使用。上述示例中,增強部分的邏輯比較簡單,只是遍歷Java類中的所有方法并添加對System.out.println方法的調(diào)用。在字節(jié)代碼中,Java方法體是由一系列的指令組成的。而要做的是生成調(diào)用 System.out.println方法的指令,并把這些指令插入到指令集合的最前面。ASM對這些指令做了抽象,不過熟悉全部的指令比較困難。ASM 提供了一個工具類ASMifierClassVisitor,可以打印出Java類的字節(jié)代碼的結(jié)構(gòu)信息。當需要增強某個類的時候,可以先在源代碼上做出修改,再通過此工具類來比較修改前后的字節(jié)代碼的差異,從而確定該如何編寫增強的代碼。
對類文件進行增強的時機是需要在Java源代碼編譯之后,在JVM執(zhí)行之前。比較常見的做法有:
由IDE在完成編譯操作之后執(zhí)行。如Google App Engine的Eclipse插件會在編譯之后運行DataNucleus來對實體類進行增強。
在構(gòu)建過程中完成,比如通過Ant或Maven來執(zhí)行相關(guān)的操作。
實現(xiàn)自己的Java類加載器。當獲取到Java類的字節(jié)代碼之后,先進行增強處理,再從修改過的字節(jié)代碼中定義出Java類。
通過JDK 5引入的java.lang.instrument包來完成。
java.lang.instrument
由于存在著大量對Java字節(jié)代碼進行修改的需求,JDK 5引入了java.lang.instrument包并在JDK 6中得到了進一步的增強。基本的思路是在JVM啟動的時候添加一些代理(agent)。每個代理是一個jar包,其清單(manifest)文件中會指定一個代理類。這個類會包含一個premain方法。JVM在啟動的時候會首先執(zhí)行代理類的premain方法,再執(zhí)行Java程序本身的main方法。在 premain方法中就可以對程序本身的字節(jié)代碼進行修改。JDK 6中還允許在JVM啟動之后動態(tài)添加代理。java.lang.instrument包支持兩種修改的場景,一種是重定義一個Java類,即完全替換一個 Java類的字節(jié)代碼;另外一種是轉(zhuǎn)換已有的Java類,相當于前面提到的類字節(jié)代碼增強。還是以前面提到的輸出方法執(zhí)行日志的場景為例,首先需要實現(xiàn)java.lang.instrument.ClassFileTransformer接口來完成對已有Java類的轉(zhuǎn)換。
static class MethodEntryTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ?ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
try {
ClassReader cr = new ClassReader(classfileBuffer);
ClassNode cn = new ClassNode();
//省略使用ASM進行字節(jié)代碼轉(zhuǎn)換的代碼
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
return cw.toByteArray();
} catch (Exception e){
return null;
}
}
}
有了這個轉(zhuǎn)換類之后,就可以在代理的premain方法中使用它。
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new MethodEntryTransformer());
}
把該代理類打成一個jar包,并在jar包的清單文件中通過Premain-Class聲明代理類的名稱。運行Java程序的時候,添加JVM啟動參數(shù)-javaagent:myagent.jar。這樣的話,JVM會在加載Java類的字節(jié)代碼之前,完成相關(guān)的轉(zhuǎn)換操作。
總結(jié)
操縱Java字節(jié)代碼是一件很有趣的事情。通過它,可以很容易的對二進制分發(fā)的Java程序進行修改,非常適合于性能分析、調(diào)試跟蹤和日志記錄等任務(wù)。另外一個非常重要的作用是把開發(fā)人員從繁瑣的Java語法中解放出來。開發(fā)人員應(yīng)該只需要負責編寫與業(yè)務(wù)邏輯相關(guān)的重要代碼。對于那些只是因為語法要求而添加的,或是模式固定的代碼,完全可以將其字節(jié)代碼動態(tài)生成出來。字節(jié)代碼增強和源代碼生成是不同的概念。源代碼生成之后,就已經(jīng)成為了程序的一部分,開發(fā)人員需要去維護它:要么手工修改生成出來的源代碼,要么重新生成。而字節(jié)代碼的增強過程,對于開發(fā)人員是完全透明的。妥善使用Java字節(jié)代碼的操縱技術(shù),可以更好的解決某一類開發(fā)問題。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉(zhuǎn)載自:網(wǎng)絡(luò)轉(zhuǎn)載