作者:京東科技 宋慧超
一、前言
最近在開發(fā)一個功能模塊時,在功能自測階段,通過使用單測測試功能的完整性,在測試單測聯(lián)通性使用到靜態(tài)方法測試時,發(fā)現(xiàn)單測報錯,通過查閱解決方案發(fā)現(xiàn)需要對Javaassist包進(jìn)行排包或者升版本處理。通過排包解決掉單測報錯,在部署項目時發(fā)現(xiàn)頻繁報bean注入失敗問題,最終定位發(fā)現(xiàn)是因為對Javaassist包排包引起的bean加載失敗。故而對Javaassist包相關(guān)知識進(jìn)行學(xué)習(xí)整理文章如下。
單測相關(guān)報錯信息如下:
Powermock - java.lang.IllegalStateException: Failed to transform class
解決單測報錯的文章鏈接:
https://stackoverflow.com/questions/32854688/powermock-java-lang-illegalstateexception-failed-to-transform-class
二、問題復(fù)現(xiàn)
1、前期準(zhǔn)備
首先使用了Spring框架新建一個demo,并寫一個簡單測試類對問題進(jìn)行復(fù)現(xiàn)。
public interface UserService { void save(User user); }
UserServiceImpl的實現(xiàn)代碼:
@Service public class UserServiceImpl implements UserService { private UserDao userDao; @Autowired public UserServiceImpl(UserDao userDao) { this.userDao = userDao; } @Override public void save(User user) { userDao.save(user); } }
這里我們使用了Spring框架的@Service和@Autowired注解,以便讓Spring框架自動裝配UserDao實例。
但是,在我們的POM文件中,雖然我們添加了對Spring框架的依賴,但是并沒有添加Javaassist庫的依賴。而UserServiceImpl中確實使用了Javaassist庫來進(jìn)行字節(jié)碼操作, UserServiceImpl的具體實現(xiàn)代碼:
public class UserServiceImpl implements UserService { // ... private static final String USER_CLASS_NAME = "com.example.User"; private static final Class??> USER_CLASS; static { try { USER_CLASS = Class.forName(USER_CLASS_NAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } public void save(User user) { try { // 創(chuàng)建一個ClassPool對象 ClassPool cp = ClassPool.getDefault(); // 從ClassPool中獲取一個CtClass對象 CtClass ctClass = cp.get(USER_CLASS_NAME); // 獲取無參構(gòu)造器 CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{}); // 獲取save方法 CtMethod saveMethod = ctClass.getDeclaredMethod("save"); // 生成代碼 saveMethod.insertBefore("{System.out.println("插入代碼前");}"); saveMethod.insertAfter("{System.out.println("插入代碼后");}"); // 生成新的字節(jié)碼并裝載到內(nèi)存 Class??> targetClass = ctClass.toClass(); Object instance = targetClass.newInstance(); // 調(diào)用save方法 Method method = targetClass.getMethod("save", USER_CLASS); method.invoke(instance, user); } catch (Exception e) { throw new RuntimeException(e); } } }
在這段代碼中,我們通過Javaassist庫生成了一個新的字節(jié)碼,并使用反射機(jī)制將其實例化,并在調(diào)用save()方法前后插入了一些代碼。但是,由于Javaassist庫缺失,導(dǎo)致項目在啟動過程中無法正確加載UserServiceImpl的實例,從而出現(xiàn)了下述錯誤信息。
2、報錯信息
在部署程序時發(fā)現(xiàn),應(yīng)用無法正常啟動,并出現(xiàn)如下錯誤信息:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in file [C:workspaceprojecttargetclassescomexampleUserServiceImpl.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.UserService]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.example.UserService.()
從錯誤信息中我們可以看到,應(yīng)用在創(chuàng)建UserService的實例時遇到了問題,無法實例化成功。
3、解決方案
為了修復(fù)這個問題,我們需要在POM文件中加入對Javaassist庫的依賴:
org.javassist/groupId?> javassist/artifactId?> 3.27.0-GA/version?> /dependency?>
添加依賴后,重新編譯并部署應(yīng)用程序即可正常運行。
三、Javaassist包
1、什么是Javaassist?
Javaassist 是由東京工業(yè)大學(xué)數(shù)學(xué)和計算機(jī)科學(xué)系的 Shigeru Chiba (千葉滋)教授創(chuàng)造的。Javaassist 作為實現(xiàn)動態(tài)字節(jié)碼生成的一個開源類庫,極大地簡化了 Java 開發(fā)者對底層字節(jié)碼操作的難度,讓開發(fā)者能夠更加輕松地在運行時動態(tài)生成類、修改類文件來達(dá)到輕量級 AOP、ORM、基于代理的遠(yuǎn)程方法調(diào)用等功能。
(Javaassist已加入了開放源代碼JBoss 應(yīng)用服務(wù)器項目,通過使用Javaassist對字節(jié)碼操作為JBoss實現(xiàn)動態(tài)AOP框架。)
2、什么是動態(tài)編程?
動態(tài)編程是相對于靜態(tài)編程而言的,平時我們討論比較多的就是靜態(tài)編程語言,例如Java,與動態(tài)編程語言,例如JavaScript。那二者有什么明顯的區(qū)別呢?簡單的說就是在靜態(tài)編程中,類型檢查是在編譯時完成的,而動態(tài)編程中類型檢查是在運行時完成的。所謂動態(tài)編程就是繞過編譯過程在運行時進(jìn)行操作的技術(shù),在Java中有如下幾種方式:
?反射
這個搞Java的應(yīng)該比較熟悉,原理也就是通過在運行時獲得類型信息然后做相應(yīng)的操作。由于Java執(zhí)行過程中是將類型載入虛擬機(jī)中的,在運行時我們就可以動態(tài)獲取到所有類型的信息。只能獲取卻不能修改類型信息。
?動態(tài)編譯
動態(tài)編譯是從Java 6開始支持的,主要是通過一個JavaCompiler接口來完成的。通過這種方式我們可以直接編譯一個已經(jīng)存在的java文件,也可以在內(nèi)存中動態(tài)生成Java代碼,動態(tài)編譯執(zhí)行。
?調(diào)用JavaScript引擎
早在Java 6就加入了對Script(JSR223)的支持。這是一個腳本框架,提供了讓腳本語言來訪問Java內(nèi)部的方法。你可以在運行的時候找到腳本引擎,然后調(diào)用這個引擎去執(zhí)行腳本。這個腳本API允許你為腳本語言提供Java支持。
?動態(tài)生成字節(jié)碼
這種技術(shù)通過操作Java字節(jié)碼的方式在JVM中生成新類或者對已經(jīng)加載的類動態(tài)添加元素。
3、動態(tài)編程解決什么問題?
在靜態(tài)語言中引入動態(tài)特性,主要是為了解決一些使用場景的痛點。其實完全使用靜態(tài)編程也辦的到,只是付出的代價比較高,沒有動態(tài)編程來的優(yōu)雅。例如依賴注入框架Spring使用了反射,而Dagger2 卻使用了代碼生成的方式(APT)。
例如:
a: 在那些依賴關(guān)系需要動態(tài)確認(rèn)的場景: b: 需要在運行時動態(tài)插入代碼的場景,比如動態(tài)代理的實現(xiàn)。 c: 通過配置文件來實現(xiàn)相關(guān)功能的場景
4、Javassit使用方法
javassist是jboss的一個子項目,其主要的優(yōu)點,在于簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機(jī)指令,就能動態(tài)改變類的結(jié)構(gòu),或者動態(tài)生成類。
操作java字節(jié)碼的工具有兩個比較流行,一個是ASM,一個是Javassit 。
?ASM :直接操作字節(jié)碼指令,執(zhí)行效率高,要求使用者掌握Java類字節(jié)碼文件格式及指令,對使用者的要求比較高。
?Javassit 提供了更高級的API,執(zhí)行效率相對較差,但無需掌握字節(jié)碼指令的知識,對使用者要求較低。
應(yīng)用層面來講一般使用建議優(yōu)先選擇Javassit,如果后續(xù)發(fā)現(xiàn)Javassit 成為了整個應(yīng)用的效率瓶頸的話可以再考慮ASM。當(dāng)然如果開發(fā)的是一個基礎(chǔ)類庫,或者基礎(chǔ)平臺,還是直接使用ASM吧,相信從事這方面工作的開發(fā)者能力應(yīng)該比較高。
Javassist中最為重要的是ClassPool,CtClass ,CtMethod 以及 CtField這幾個類。
?ClassPool:一個基于HashMap實現(xiàn)的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認(rèn)的ClassPool使用與底層JVM相同的類路徑,因此在某些情況下,可能需要向ClassPool添加類路徑或類字節(jié)。
? getDefault (): 返回默認(rèn)的ClassPool ,單例模式,一般通過該方法創(chuàng)建我們的ClassPool;
? appendClassPath(ClassPath cp), insertClassPath(ClassPath cp) : 將一個ClassPath加到類搜索路徑的末尾位置或插入到起始位置。通常通過該方法寫入額外的類搜索路徑,以解決多個類加載器環(huán)境中找不到類問題;
? importPackage(String packageName):導(dǎo)入包;
? makeClass(String classname):創(chuàng)建一個空類,沒有變量和方法,后序通過CtClass的函數(shù)進(jìn)行添加;
? get(String classname)、getCtClass(String classname) : 根據(jù)類路徑名獲取該類的CtClass對象,用于后續(xù)的編輯。
?CtClass:表示一個類,這些CtClass對象可以從ClassPool獲得。
?debugDump; String類型,如果生成.class文件,保存在這個目錄下。
?setName(String name): 給類重命名;
?setSuperclass(CtClass clazz): 設(shè)置父類;
?addField(CtField f, Initializer init): 添加字段(屬性),初始值見CtField;
?addMethod(CtMethod m): 添加方法(函數(shù));
?toBytecode(): 返回修改后的字節(jié)碼。需要注意的是一旦調(diào)用該方法,則無法繼續(xù)修改CtClass;
?toClass(): 將修改后的CtClass加載至當(dāng)前線程的上下文類加載器中,CtClass的toClass方法是通過調(diào)用本方法實現(xiàn)。需要注意的是一旦調(diào)用該方法,則無法繼續(xù)修改已經(jīng)被加載的CtClass;
?writeFile(String directoryName): 根據(jù)CtClass生成 .class 文件;
?defrost(): 解凍類,用于使用了toclass()、toBytecode、writeFile(),類已經(jīng)被JVM加載,Javassist凍結(jié)CtClass后;
?detach(): 避免內(nèi)存溢出,從ClassPool中移除一些不需要的CtClass。
?CtMethods:表示類中的方法。
?insertBefore(String src):在方法的起始位置插入代碼;
?insertAfter(String src):在方法的所有 return 語句前插入代碼以確保語句能夠被執(zhí)行,除非遇到exception;
?insertAt(int lineNum, String src):在指定的位置插入代碼;
?addCatch(String src, CtClass exceptionType):將方法內(nèi)語句作為try的代碼塊,插入catch代碼塊src;
?setBody(String src):將方法的內(nèi)容設(shè)置為要寫入的代碼,當(dāng)方法被 abstract修飾時,該修飾符被移除;
?setModifiers(int mod):設(shè)置訪問級別,一般使用Modifier調(diào)用常量;
?invoke(Object obj, Object... args):反射調(diào)用字節(jié)碼生成類的方法。
?CtFields :表示類中的字段。
?CtField(CtClass type, String name, CtClass declaring) :構(gòu)造函數(shù),添加字段類型,名稱,所屬的類;
?CtField.Initializer constant():CtClass使用addField時初始值的設(shè)置;
?setModifiers(int mod):設(shè)置訪問級別,一般使用Modifier調(diào)用常量。
?$開頭的特殊字符
符號 | 具體含義 |
---|---|
$0, $1, $2, … | $0=this,$1表示方法的第一個參數(shù),依次類推,如果方法是靜態(tài)的,則 $0 不可用 |
$args | 方法參數(shù)數(shù)組.它的類型為 Object[],$args[0]=1 , 1,1,args[1]=$2 |
$r | 返回結(jié)果的類型,用于強(qiáng)制類型轉(zhuǎn)換 |
$w | 包裝器類型,用于強(qiáng)制類型轉(zhuǎn)換,當(dāng)返回值是包裝類型時,可以用此來強(qiáng)轉(zhuǎn) |
$_ | 返回值,一般在insertAfter中用到,用于得到原方法的返回值 |
$slg | 參數(shù)類型數(shù)組,$sig[0]表示第一個參數(shù)類型 |
$type | 返回值類型,一般在insertAfter中用到,即$_的類型 |
$class | $0或this的類型 |
$e | 異常類型 |
5、常用的Java插樁工具有哪些?
Java 插樁工具是一種能夠修改 Java 字節(jié)碼的工具,通過在應(yīng)用程序運行時動態(tài)修改字節(jié)碼來實現(xiàn)對程序的監(jiān)控、跟蹤、調(diào)試和優(yōu)化等功能。
工具 | 字節(jié)碼抽象級別 | 具體描述 |
---|---|---|
ASM、BCEL | 低級 | 庫需要直接在字節(jié)碼級別上進(jìn)行操作。通常,它們提供大多數(shù)功能豐富的功能,但與其他字節(jié)碼操作工具相比,它們的使用也最復(fù)雜。 |
Javaassist | 中級 | 庫提供了字節(jié)碼的某種抽象級別,并簡化了其修改。例如,代替修改字節(jié)碼,可以使用類似于Java的語法進(jìn)行更改,然后將其編譯為字節(jié)碼,然后由使用的庫修改為原始字節(jié)碼。通常,它們?nèi)鄙傩薷暮蟮拇a驗證的功能-這意味著,錯誤可能在修改準(zhǔn)備過程中被忽略,然后在運行時被發(fā)現(xiàn)。 |
AspectJ、CGLib | 高級 | 庫使用高級指令進(jìn)行操作,并且通常配備有用于語法驗證的工具集。不幸的是,從修改后的字節(jié)碼進(jìn)行的最高抽象化通常會導(dǎo)致某些功能的喪失,這些功能僅在直接修改字節(jié)碼時可用。 |
四、總結(jié)
本文通過對由于Javaassist包缺失導(dǎo)致項目啟動過程中bean加載失敗的問題進(jìn)行復(fù)現(xiàn),并通過demo進(jìn)行實例分析,解釋了因為缺失Javaassist庫導(dǎo)致的應(yīng)用程序啟動失敗問題。并對Javaassist包相關(guān)知識進(jìn)行介紹,后續(xù)會繼續(xù)對Javaassist相關(guān)知識進(jìn)行學(xué)習(xí)補充。
建議大家在構(gòu)建Maven項目時,仔細(xì)檢查POM文件中的依賴,確保沒有漏掉任何必要的庫,以免因為遺漏而引起不必要的問題。
審核編輯 黃宇
-
測試
+關(guān)注
關(guān)注
8文章
5367瀏覽量
126923 -
spring
+關(guān)注
關(guān)注
0文章
340瀏覽量
14368
發(fā)布評論請先 登錄
相關(guān)推薦
評論