成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深度探討Java字節(jié)代碼的操縱方法

開發(fā) 后端
本文為IBM工程師成富編寫的《Java深度歷險(xiǎn)》的第一部分Java字節(jié)代碼的操縱,像這樣Java語言的深度理解和運(yùn)用還沒有很多文章,我們把他奉獻(xiàn)給讀者,希望讀者們喜歡。

51CTO編者按:我們?cè)o大家介紹過Java字節(jié)碼文件操作技巧。Java作為業(yè)界應(yīng)用最為廣泛的語言之一,深得眾多軟件廠商和開發(fā)者的推崇,更是被包括Oracle在內(nèi)的眾多JCP成員積極地推動(dòng)發(fā)展。但是對(duì)于Java語言的深度理解和運(yùn)用,畢竟是很少會(huì)有人涉及的話題。本文為IBM工程師成富編寫的《Java深度歷險(xiǎn)》的***部分Java字節(jié)代碼的操縱,希望讀者們喜歡。

在一般的Java應(yīng)用開發(fā)過程中,開發(fā)人員使用Java的方式比較簡單。打開慣用的IDE,編寫Java源代碼,再利用IDE提供的功能直接運(yùn)行Java 程序就可以了。這種開發(fā)模式背后的過程是:開發(fā)人員編寫的是Java源代碼文件(.java),IDE會(huì)負(fù)責(zé)調(diào)用Java的編譯器把Java源代碼編譯成平臺(tái)無關(guān)的字節(jié)代碼(byte code),以類文件的形式保存在磁盤上(.class)。Java虛擬機(jī)(JVM)會(huì)負(fù)責(zé)把Java字節(jié)代碼加載并執(zhí)行。Java通過這種方式來實(shí)現(xiàn)其“編寫一次,到處運(yùn)行(Write once, run anywhere)” 的目標(biāo)。Java類文件中包含的字節(jié)代碼可以被不同平臺(tái)上的JVM所使用。Java字節(jié)代碼不僅可以以文件形式存在于磁盤上,也可以通過網(wǎng)絡(luò)方式來下載,還可以只存在于內(nèi)存中。JVM中的類加載器會(huì)負(fù)責(zé)從包含字節(jié)代碼的字節(jié)數(shù)組(byte[])中定義出Java類。在某些情況下,可能會(huì)需要?jiǎng)討B(tài)的生成 Java字節(jié)代碼,或是對(duì)已有的Java字節(jié)代碼進(jìn)行修改。這個(gè)時(shí)候就需要用到本文中將要介紹的相關(guān)技術(shù)。首先介紹一下如何動(dòng)態(tài)編譯Java源文件。

動(dòng)態(tài)編譯Java源文件

在一般情況下,開發(fā)人員都是在程序運(yùn)行之前就編寫完成了全部的Java源代碼并且成功編譯。對(duì)有些應(yīng)用來說,Java源代碼的內(nèi)容在運(yùn)行時(shí)刻才能確定。這個(gè)時(shí)候就需要?jiǎng)討B(tài)編譯源代碼來生成Java字節(jié)代碼,再由JVM來加載執(zhí)行。典型的場景是很多算法競賽的在線評(píng)測系統(tǒng)(如PKU JudgeOnline),允許用戶上傳Java代碼,由系統(tǒng)在后臺(tái)編譯、運(yùn)行并進(jìn)行判定。在動(dòng)態(tài)編譯Java源文件時(shí),使用的做法是直接在程序中調(diào)用Java編譯器。

JSR 199引入了Java編譯器API。如果使用JDK 6的話,可以通過此API來動(dòng)態(tài)編譯Java代碼。比如下面的代碼用來動(dòng)態(tài)編譯最簡單的Hello World類。該Java類的代碼是保存在一個(gè)字符串中的。

 

  1. public class CompilerTest {  
  2.    public static void main(String[] args) throws Exception {        
  3.       String source = "public class Main { public static void main(String[] args) {System.out.println(\"Hello World!\");} }";  
  4.       JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();  
  5.       StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);  
  6.       StringSourceJavaObject sourceObject = new CompilerTest.StringSourceJavaObject("Main", source);  
  7.       Iterable< extends JavaFileObject> fileObjects = Arrays.asList(sourceObject);  
  8.       CompilationTask task = compiler.getTask(null, fileManager, null, null, null, fileObjects);  
  9.       boolean result = task.call();  
  10.       if (result) {  
  11.          System.out.println("編譯成功。");  
  12.       }  
  13.    }  
  14.  
  15.    static class StringSourceJavaObject extends SimpleJavaFileObject {  
  16.  
  17.       private String content = null;  
  18.       public StringSourceJavaObject(String name, String content) ??throws URISyntaxException {  
  19.          super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);  
  20.          this.content = content;  
  21.       }  
  22.  
  23.       public CharSequence getCharContent(boolean ignoreEncodingErrors) ??throws IOException {  
  24.          return content;  
  25.       }  
  26.    }  
  27. }  
  28.  

如果不能使用JDK 6提供的Java編譯器API的話,可以使用JDK中的工具類com.sun.tools.javac.Main,不過該工具類只能編譯存放在磁盤上的文件,類似于直接使用javac命令。

另外一個(gè)可用的工具是Eclipse JDT Core提供的編譯器。這是Eclipse Java開發(fā)環(huán)境使用的增量式Java編譯器,支持運(yùn)行和調(diào)試有錯(cuò)誤的代碼。該編譯器也可以單獨(dú)使用。Play框架在內(nèi)部使用了JDT的編譯器來動(dòng)態(tài)編譯Java源代碼。在開發(fā)模式下,Play框架會(huì)定期掃描項(xiàng)目中的Java源代碼文件,一旦發(fā)現(xiàn)有修改,會(huì)自動(dòng)編譯 Java源代碼。因此在修改代碼之后,刷新頁面就可以看到變化。使用這些動(dòng)態(tài)編譯的方式的時(shí)候,需要確保JDK中的tools.jar在應(yīng)用的 CLASSPATH中。

下面介紹一個(gè)例子,是關(guān)于如何在Java里面做四則運(yùn)算,比如求出來(3+4)*7-10的值。一般的做法是分析輸入的運(yùn)算表達(dá)式,自己來模擬計(jì)算過程。考慮到括號(hào)的存在和運(yùn)算符的優(yōu)先級(jí)等問題,這樣的計(jì)算過程會(huì)比較復(fù)雜,而且容易出錯(cuò)。另外一種做法是可以用JSR 223引入的腳本語言支持,直接把輸入的表達(dá)式當(dāng)做JavaScript或是JavaFX腳本來執(zhí)行,得到結(jié)果。下面的代碼使用的做法是動(dòng)態(tài)生成Java源代碼并編譯,接著加載Java類來執(zhí)行并獲取結(jié)果。這種做法完全使用Java來實(shí)現(xiàn)。

  1. private static double calculate(String expr) throws CalculationException  {  
  2.    String className = "CalculatorMain";  
  3.    String methodName = "calculate";  
  4.    String source = "public class " + className   
  5.       + " { public static double " + methodName + "() { return " + expr + "; } }";  
  6.    //省略動(dòng)態(tài)編譯Java源代碼的相關(guān)代碼,參見上一節(jié)  
  7.    boolean result = task.call();  
  8.    if (result) {  
  9.       ClassLoader loader = Calculator.class.getClassLoader();   
  10.       try {              
  11.          Class<?> clazz = loader.loadClass(className);  
  12.          Method method = clazz.getMethod(methodName, new Class<?>[] {});  
  13.          Object value = method.invoke(null, new Object[] {});  
  14.          return (Double) value;  
  15.       } catch (Exception e) {  
  16.          throw new CalculationException("內(nèi)部錯(cuò)誤。");          
  17.       }      
  18.    } else {  
  19.       throw new CalculationException("錯(cuò)誤的表達(dá)式。");      
  20.    }  
  21. }  
  22.  

上面的代碼給出了使用動(dòng)態(tài)生成的Java字節(jié)代碼的基本模式,即通過類加載器來加載字節(jié)代碼,創(chuàng)建Java類的對(duì)象的實(shí)例,再通過Java反射API來調(diào)用對(duì)象中的方法。

Java字節(jié)代碼增強(qiáng)

Java 字節(jié)代碼增強(qiáng)指的是在Java字節(jié)代碼生成之后,對(duì)其進(jìn)行修改,增強(qiáng)其功能。這種做法相當(dāng)于對(duì)應(yīng)用程序的二進(jìn)制文件進(jìn)行修改。在很多Java框架中都可以見到這種實(shí)現(xiàn)方式。Java字節(jié)代碼增強(qiáng)通常與Java源文件中的注解(annotation)一塊使用。注解在Java源代碼中聲明了需要增強(qiáng)的行為及相關(guān)的元數(shù)據(jù),由框架在運(yùn)行時(shí)刻完成對(duì)字節(jié)代碼的增強(qiáng)。Java字節(jié)代碼增強(qiáng)應(yīng)用的場景比較多,一般都集中在減少冗余代碼和對(duì)開發(fā)人員屏蔽底層的實(shí)現(xiàn)細(xì)節(jié)上。用過JavaBeans的人可能對(duì)其中那些必須添加的getter/setter方法感到很繁瑣,并且難以維護(hù)。而通過字節(jié)代碼增強(qiáng),開發(fā)人員只需要聲明Bean中的屬性即可,getter/setter方法可以通過修改字節(jié)代碼來自動(dòng)添加。用過JPA的人,在調(diào)試程序的時(shí)候,會(huì)發(fā)現(xiàn)實(shí)體類中被添加了一些額外的 域和方法。這些域和方法是在運(yùn)行時(shí)刻由JPA的實(shí)現(xiàn)動(dòng)態(tài)添加的。字節(jié)代碼增強(qiáng)在面向方面編程(AOP)的一些實(shí)現(xiàn)中也有使用。

在討論如何進(jìn)行字節(jié)代碼增強(qiáng)之前,首先介紹一下表示一個(gè)Java類或接口的字節(jié)代碼的組織形式。

類文件

  1.  {  
  2.    0xCAFEBABE,小版本號(hào),大版本號(hào),常量池大小,常量池?cái)?shù)組,  
  3.    訪問控制標(biāo)記,當(dāng)前類信息,父類信息,實(shí)現(xiàn)的接口個(gè)數(shù),實(shí)現(xiàn)的接口信息數(shù)組,域個(gè)數(shù),  
  4.    域信息數(shù)組,方法個(gè)數(shù),方法信息數(shù)組,屬性個(gè)數(shù),屬性信息數(shù)組  
  5. }  

如上所示,一個(gè)類或接口的字節(jié)代碼使用的是一種松散的組織結(jié)構(gòu),其中所包含的內(nèi)容依次排列。對(duì)于可能包含多個(gè)條目的內(nèi)容,如所實(shí)現(xiàn)的接口、域、方法和屬性等,是以數(shù)組來表示的。而在數(shù)組之前的是該數(shù)組中條目的個(gè)數(shù)。不同的內(nèi)容類型,有其不同的內(nèi)部結(jié)構(gòu)。對(duì)于開發(fā)人員來說,直接操縱包含字節(jié)代碼的字節(jié)數(shù)組的話,開發(fā)效率比較低,而且容易出錯(cuò)。已經(jīng)有不少的開源庫可以對(duì)字節(jié)代碼進(jìn)行修改或是從頭開始創(chuàng)建新的Java類的字節(jié)代碼內(nèi)容。這些類庫包括ASM、cglib、serp和BCEL等。使用這些類庫可以在一定程度上降低增強(qiáng)字節(jié)代碼的復(fù)雜度。比如考慮下面一個(gè)簡單的需求,在一個(gè)Java類的所有方法執(zhí)行之前輸出相應(yīng)的日志。熟悉AOP的人都知道,可以用一個(gè)前增強(qiáng)(before advice)來解決這個(gè)問題。如果使用ASM的話,相關(guān)的代碼如下:

  1. ClassReader cr = new ClassReader(is);  
  2. ClassNode cn = new ClassNode();  
  3. cr.accept(cn, 0);  
  4. for (Object object : cn.methods) {      
  5.    MethodNode mn = (MethodNode) object;     
  6.    if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {          
  7.       continue;      
  8.    }      
  9.    InsnList insns = mn.instructions;      
  10.    InsnList il = new InsnList();     
  11.    il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));      
  12.    il.add(new LdcInsnNode("Enter method -> " + mn.name));     
  13.    il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));      
  14.    insns.insert(il);  mn.maxStack += 3;  
  15. }  
  16. ClassWriter cw = new ClassWriter(0);  
  17. cn.accept(cw);  
  18. byte[] b = cw.toByteArray();  
  19.  

從ClassWriter就可以獲取到包含增強(qiáng)之后的字節(jié)代碼的字節(jié)數(shù)組,可以把字節(jié)代碼寫回磁盤或是由類加載器直接使用。上述示例中,增強(qiáng)部分的邏輯比較簡單,只是遍歷Java類中的所有方法并添加對(duì)System.out.println方法的調(diào)用。在字節(jié)代碼中,Java方法體是由一系列的指令組成的。而要做的是生成調(diào)用System.out.println方法的指令,并把這些指令插入到指令集合的最前面。ASM對(duì)這些指令做了抽象,不過熟悉全部的指令比較困難。ASM提供了一個(gè)工具類ASMifierClassVisitor,可以打印出Java類的字節(jié)代碼的結(jié)構(gòu)信息。當(dāng)需要增強(qiáng)某個(gè)類的時(shí)候,可以先在源代碼上做出修改,再通過此工具類來比較修改前后的字節(jié)代碼的差異,從而確定該如何編寫增強(qiáng)的代碼。

對(duì)類文件進(jìn)行增強(qiáng)的時(shí)機(jī)是需要在Java源代碼編譯之后,在JVM執(zhí)行之前。比較常見的做法有:

由IDE在完成編譯操作之后執(zhí)行。如Google App Engine的Eclipse插件會(huì)在編譯之后運(yùn)行DataNucleus來對(duì)實(shí)體類進(jìn)行增強(qiáng)。

在構(gòu)建過程中完成,比如通過Ant或Maven來執(zhí)行相關(guān)的操作。

實(shí)現(xiàn)自己的Java類加載器。當(dāng)獲取到Java類的字節(jié)代碼之后,先進(jìn)行增強(qiáng)處理,再從修改過的字節(jié)代碼中定義出Java類。

通過JDK 5引入的java.lang.instrument包來完成。

  1. java.lang.instrument 

由于存在著大量對(duì)Java字節(jié)代碼進(jìn)行修改的需求,JDK 5引入了java.lang.instrument包并在JDK 6中得到了進(jìn)一步的增強(qiáng)。基本的思路是在JVM啟動(dòng)的時(shí)候添加一些代理(agent)。每個(gè)代理是一個(gè)jar包,其清單(manifest)文件中會(huì)指定一個(gè)代理類。這個(gè)類會(huì)包含一個(gè)premain方法。JVM在啟動(dòng)的時(shí)候會(huì)首先執(zhí)行代理類的premain方法,再執(zhí)行Java程序本身的main方法。在 premain方法中就可以對(duì)程序本身的字節(jié)代碼進(jìn)行修改。JDK 6中還允許在JVM啟動(dòng)之后動(dòng)態(tài)添加代理。java.lang.instrument包支持兩種修改的場景,一種是重定義一個(gè)Java類,即完全替換一個(gè) Java類的字節(jié)代碼;另外一種是轉(zhuǎn)換已有的Java類,相當(dāng)于前面提到的類字節(jié)代碼增強(qiáng)。還是以前面提到的輸出方法執(zhí)行日志的場景為例,首先需要實(shí)現(xiàn)java.lang.instrument.ClassFileTransformer接口來完成對(duì)已有Java類的轉(zhuǎn)換。

  1. static class MethodEntryTransformer implements ClassFileTransformer {  
  2.    public byte[] transform(ClassLoader loader, String className,  
  3.      Class<?> classBeingRedefined, ?ProtectionDomain protectionDomain, byte[] classfileBuffer)   
  4.      throws  IllegalClassFormatException {  
  5.         try {  
  6.            ClassReader cr = new ClassReader(classfileBuffer);  
  7.            ClassNode cn = new ClassNode();              
  8.            //省略使用ASM進(jìn)行字節(jié)代碼轉(zhuǎn)換的代碼              
  9.            ClassWriter cw = new ClassWriter(0);  
  10.            cn.accept(cw);   
  11.            return cw.toByteArray();         
  12.         } catch (Exception e){              
  13.            return null;  
  14.         }  
  15.    }  

有了這個(gè)轉(zhuǎn)換類之后,就可以在代理的premain方法中使用它。

  1. public static void premain(String args, Instrumentation inst) {      
  2.    inst.addTransformer(new MethodEntryTransformer());  

把該代理類打成一個(gè)jar包,并在jar包的清單文件中通過Premain-Class聲明代理類的名稱。運(yùn)行Java程序的時(shí)候,添加JVM啟動(dòng)參數(shù)-javaagent:myagent.jar。這樣的話,JVM會(huì)在加載Java類的字節(jié)代碼之前,完成相關(guān)的轉(zhuǎn)換操作。

總結(jié)

操縱Java字節(jié)代碼是一件很有趣的事情。通過它,可以很容易的對(duì)二進(jìn)制分發(fā)的Java程序進(jìn)行修改,非常適合于性能分析、調(diào)試跟蹤和日志記錄等任務(wù)。另外一個(gè)非常重要的作用是把開發(fā)人員從繁瑣的Java語法中解放出來。開發(fā)人員應(yīng)該只需要負(fù)責(zé)編寫與業(yè)務(wù)邏輯相關(guān)的重要代碼。對(duì)于那些只是因?yàn)檎Z法要求而添加的,或是模式固定的代碼,完全可以將其字節(jié)代碼動(dòng)態(tài)生成出來。字節(jié)代碼增強(qiáng)和源代碼生成是不同的概念。源代碼生成之后,就已經(jīng)成為了程序的一部分,開發(fā)人員需要去維護(hù)它:要么手工修改生成出來的源代碼,要么重新生成。而字節(jié)代碼的增強(qiáng)過程,對(duì)于開發(fā)人員是完全透明的。妥善使用Java字節(jié)代碼的操縱技術(shù),可以更好的解決某一類開發(fā)問題。

【編輯推薦】

  1. Java的簡單數(shù)據(jù)類型
  2. Java字節(jié)碼文件操作技巧
  3. Java編譯過程與C/C++編譯過程有何不同
  4. Java代碼的靜態(tài)編譯和動(dòng)態(tài)編譯中的問題比較
  5. 防止Java字節(jié)碼反編譯問題解決方案
責(zé)任編輯:佚名 來源: InfoQ
相關(guān)推薦

2011-03-15 14:16:20

JsonObject

2024-03-07 12:40:28

Python*args開發(fā)

2023-12-22 08:46:15

useEffectVueMobx

2010-05-24 17:13:34

Linux SNMP

2009-11-05 09:01:41

WinForm

2010-01-27 16:10:32

C++靜態(tài)構(gòu)造函數(shù)

2018-03-14 08:10:44

深度學(xué)習(xí)

2010-03-19 09:12:05

JRuby

2010-03-17 14:33:44

云計(jì)算

2024-03-15 09:00:00

2016-10-14 13:46:26

2009-12-02 19:42:24

PHP頁面自動(dòng)跳轉(zhuǎn)

2009-12-01 16:34:21

PHP表單

2010-03-17 15:54:42

Java線程控制權(quán)

2009-12-15 13:59:42

Ruby對(duì)象操作

2010-01-04 17:29:00

Silverlight

2010-02-03 10:50:33

C++多態(tài)

2017-06-09 15:58:23

人工智能AI深度學(xué)習(xí)

2021-06-15 09:10:41

漏洞網(wǎng)絡(luò)安全程序員

2023-08-30 11:03:47

Java工具
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 国产高潮好爽受不了了夜色 | av激情在线| 国产在线精品一区二区三区 | 午夜影院| 性一交一乱一透一a级 | 国产一区二区视频在线 | 久久另类 | 一区二区三区四区免费观看 | 精品亚洲一区二区三区 | 成人综合一区 | 亚洲97| 一区二区小视频 | 国产精品视频在线观看 | 在线播放亚洲 | 99精品欧美一区二区三区综合在线 | 国产精品日韩欧美 | 一级二级三级在线观看 | 精品一区二区不卡 | 欧美高清视频在线观看 | 欧美激情精品久久久久久 | 精品久久久久久亚洲精品 | 日本在线免费观看 | 麻豆91av | 亚洲精品九九 | 国产精品欧美一区二区 | 国产综合在线视频 | 久久久久久一区 | 国产黄色精品在线观看 | 久草成人 | 欧美日韩国产精品激情在线播放 | 日韩1区 | 国产一区二区精品 | 亚洲精品九九 | 91一区| 日韩羞羞 | 日韩1区 | 人人干人人草 | 亚洲国产精品一区二区久久 | 午夜精品久久久久久不卡欧美一级 | 激情 亚洲 | 又黄又色|