作者:京東科技 宋慧超
一、前言
最近在開發一個功能模塊時,在功能自測階段,通過使用單測測驗功能的完整性,在測驗單測聯通性使用到靜態方法測驗時,發現單測報錯,通過查閱解決方案發現需要對Javaassist包進行排包或者升版本處理,通過排包解決掉單測報錯,在部署專案時發現頻繁報bean注入失敗問題,最終定位發現是因為對Javaassist包排包引起的bean加載失敗,故而對Javaassist包相關知識進行學習整理文章如下,
單測相關報錯資訊如下:
Powermock - java.lang.IllegalStateException: Failed to transform class
解決單測報錯的文章鏈接:
https://stackoverflow.com/questions/32854688/powermock-java-lang-illegalstateexception-failed-to-transform-class
二、問題復現
1、前期準備
首先使用了Spring框架新建一個demo,并寫一個簡單測驗類對問題進行復現,
UserService的定義:
public interface UserService {
void save(User user);
}
UserServiceImpl的實作代碼:
@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庫來進行位元組碼操作, UserServiceImpl的具體實作代碼:
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 {
// 創建一個ClassPool物件
ClassPool cp = ClassPool.getDefault();
// 從ClassPool中獲取一個CtClass物件
CtClass ctClass = cp.get(USER_CLASS_NAME);
// 獲取無參構造器
CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[]{});
// 獲取save方法
CtMethod saveMethod = ctClass.getDeclaredMethod("save");
// 生成代碼
saveMethod.insertBefore("{System.out.println(\"插入代碼前\");}");
saveMethod.insertAfter("{System.out.println(\"插入代碼后\");}");
// 生成新的位元組碼并裝載到記憶體
Class<?> targetClass = ctClass.toClass();
Object instance = targetClass.newInstance();
// 呼叫save方法
Method method = targetClass.getMethod("save", USER_CLASS);
method.invoke(instance, user);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
在這段代碼中,我們通過Javaassist庫生成了一個新的位元組碼,并使用反射機制將其實體化,并在呼叫save()方法前后插入了一些代碼,但是,由于Javaassist庫缺失,導致專案在啟動程序中無法正確加載UserServiceImpl的實體,從而出現了下述錯誤資訊,
2、報錯資訊
在部署程式時發現,應用無法正常啟動,并出現如下錯誤資訊:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in file [C:\workspace\project\target\classes\com\example\UserServiceImpl.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.<init>()
從錯誤資訊中我們可以看到,應用在創建UserService的實體時遇到了問題,無法實體化成功,
3、解決方案
為了修復這個問題,我們需要在POM檔案中加入對Javaassist庫的依賴:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
添加依賴后,重新編譯并部署應用程式即可正常運行,
三、Javaassist包
1、什么是Javaassist?
Javaassist 是由東京工業大學數學和計算機科學系的 Shigeru Chiba (千葉滋)教授創造的,Javaassist 作為實作動態位元組碼生成的一個開源類別庫,極大地簡化了 Java 開發者對底層位元組碼操作的難度,讓開發者能夠更加輕松地在運行時動態生成類、修改類檔案來達到輕量級 AOP、ORM、基于代理的遠程方法呼叫等功能,
(Javaassist已加入了開放源代碼JBoss 應用服務器專案,通過使用Javaassist對位元組碼操作為JBoss實作動態AOP框架,)
2、什么是動態編程?
動態編程是相對于靜態編程而言的,平時我們討論比較多的就是靜態編程語言,例如Java,與動態編程語言,例如JavaScript,那二者有什么明顯的區別呢?簡單的說就是在靜態編程中,型別檢查是在編譯時完成的,而動態編程中型別檢查是在運行時完成的,所謂動態編程就是繞過編譯程序在運行時進行操作的技術,在Java中有如下幾種方式:
?反射
這個搞Java的應該比較熟悉,原理也就是通過在運行時獲得型別資訊然后做相應的操作,由于Java執行程序中是將型別載入虛擬機中的,在運行時我們就可以動態獲取到所有型別的資訊,只能獲取卻不能修改型別資訊,
?動態編譯
動態編譯是從Java 6開始支持的,主要是通過一個JavaCompiler介面來完成的,通過這種方式我們可以直接編譯一個已經存在的java檔案,也可以在記憶體中動態生成Java代碼,動態編譯執行,
?呼叫JavaScript引擎
早在Java 6就加入了對Script(JSR223)的支持,這是一個腳本框架,提供了讓腳本語言來訪問Java內部的方法,你可以在運行的時候找到腳本引擎,然后呼叫這個引擎去執行腳本,這個腳本API允許你為腳本語言提供Java支持,
?動態生成位元組碼
這種技術通過操作Java位元組碼的方式在JVM中生成新類或者對已經加載的類動態添加元素,
3、動態編程解決什么問題?
在靜態語言中引入動態特性,主要是為了解決一些使用場景的痛點,其實完全使用靜態編程也辦的到,只是付出的代價比較高,沒有動態編程來的優雅,例如依賴注入框架Spring使用了反射,而Dagger2 卻使用了代碼生成的方式(APT),
例如:
a: 在那些依賴關系需要動態確認的場景: b: 需要在運行時動態插入代碼的場景,比如動態代理的實作, c: 通過組態檔來實作相關功能的場景
4、Javassit使用方法
javassist是jboss的一個子專案,其主要的優點,在于簡單,而且快速,直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類,
操作java位元組碼的工具有兩個比較流行,一個是ASM,一個是Javassit ,
?ASM :直接操作位元組碼指令,執行效率高,要求使用者掌握Java類位元組碼檔案格式及指令,對使用者的要求比較高,
?Javassit 提供了更高級的API,執行效率相對較差,但無需掌握位元組碼指令的知識,對使用者要求較低,
應用層面來講一般使用建議優先選擇Javassit,如果后續發現Javassit 成為了整個應用的效率瓶頸的話可以再考慮ASM,當然如果開發的是一個基礎類別庫,或者基礎平臺,還是直接使用ASM吧,相信從事這方面作業的開發者能力應該比較高,
Javassist中最為重要的是ClassPool,CtClass ,CtMethod 以及 CtField這幾個類,

?ClassPool:一個基于HashMap實作的CtClass物件容器,其中鍵是類名稱,值是表示該類的CtClass物件,默認的ClassPool使用與底層JVM相同的類路徑,因此在某些情況下,可能需要向ClassPool添加類路徑或類位元組,
? getDefault (): 回傳默認的ClassPool ,單例模式,一般通過該方法創建我們的ClassPool;
? appendClassPath(ClassPath cp), insertClassPath(ClassPath cp) : 將一個ClassPath加到類搜索路徑的末尾位置或插入到起始位置,通常通過該方法寫入額外的類搜索路徑,以解決多個類加載器環境中找不到類問題;
? importPackage(String packageName):匯入包;
? makeClass(String classname):創建一個空類,沒有變數和方法,后序通過CtClass的函式進行添加;
? get(String classname)、getCtClass(String classname) : 根據類路徑名獲取該類的CtClass物件,用于后續的編輯,
?CtClass:表示一個類,這些CtClass物件可以從ClassPool獲得,
?debugDump; String型別,如果生成.class檔案,保存在這個目錄下,
?setName(String name): 給類重命名;
?setSuperclass(CtClass clazz): 設定父類;
?addField(CtField f, Initializer init): 添加欄位(屬性),初始值見CtField;
?addMethod(CtMethod m): 添加方法(函式);
?toBytecode(): 回傳修改后的位元組碼,需要注意的是一旦呼叫該方法,則無法繼續修改CtClass;
?toClass(): 將修改后的CtClass加載至當前執行緒的背景關系類加載器中,CtClass的toClass方法是通過呼叫本方法實作,需要注意的是一旦呼叫該方法,則無法繼續修改已經被加載的CtClass;
?writeFile(String directoryName): 根據CtClass生成 .class 檔案;
?defrost(): 解凍類,用于使用了toclass()、toBytecode、writeFile(),類已經被JVM加載,Javassist凍結CtClass后;
?detach(): 避免記憶體溢位,從ClassPool中移除一些不需要的CtClass,
?CtMethods:表示類中的方法,
?insertBefore(String src):在方法的起始位置插入代碼;
?insertAfter(String src):在方法的所有 return 陳述句前插入代碼以確保陳述句能夠被執行,除非遇到exception;
?insertAt(int lineNum, String src):在指定的位置插入代碼;
?addCatch(String src, CtClass exceptionType):將方法內陳述句作為try的代碼塊,插入catch代碼塊src;
?setBody(String src):將方法的內容設定為要寫入的代碼,當方法被 abstract修飾時,該修飾符被移除;
?setModifiers(int mod):設定訪問級別,一般使用Modifier呼叫常量;
?invoke(Object obj, Object... args):反射呼叫位元組碼生成類的方法,
?CtFields :表示類中的欄位,
?CtField(CtClass type, String name, CtClass declaring) :建構式,添加欄位型別,名稱,所屬的類;
?CtField.Initializer constant():CtClass使用addField時初始值的設定;
?setModifiers(int mod):設定訪問級別,一般使用Modifier呼叫常量,
?$開頭的特殊字符
| 符號 | 具體含義 |
|---|---|
| $0, $1, $2, … | $0=this,$1表示方法的第一個引數,依次類推,如果方法是靜態的,則 $0 不可用 | |
| \(args | 方法引數陣列.它的型別為 Object\[\],\)args[0]=1 , 1,1,args[1]=$2 | |
| $r | 回傳結果的型別,用于強制型別轉換 |
| $w | 包裝器型別,用于強制型別轉換,當回傳值是包裝型別時,可以用此來強轉 |
| $_ | 回傳值,一般在insertAfter中用到,用于得到原方法的回傳值 |
| \(slg | 引數型別陣列,\)sig[0]表示第一個引數型別 | |
| \(type | 回傳值型別,一般在insertAfter中用到,即\)_的型別 | |
| $class | $0或this的型別 | |
| $e | 例外型別 |
5、常用的Java插樁工具有哪些?
Java 插樁工具是一種能夠修改 Java 位元組碼的工具,通過在應用程式運行時動態修改位元組碼來實作對程式的監控、跟蹤、除錯和優化等功能,
| 工具 | 位元組碼抽象級別 | 具體描述 |
|---|---|---|
| ASM、BCEL | 低級 | 庫需要直接在位元組碼級別上進行操作,通常,它們提供大多數功能豐富的功能,但與其他位元組碼操作工具相比,它們的使用也最復雜, |
| Javaassist | 中級 | 庫提供了位元組碼的某種抽象級別,并簡化了其修改,例如,代替修改位元組碼,可以使用類似于Java的語法進行更改,然后將其編譯為位元組碼,然后由使用的庫修改為原始位元組碼,通常,它們缺少修改后的代碼驗證的功能-這意味著,錯誤可能在修改準備程序中被忽略,然后在運行時被發現, |
| AspectJ、CGLib | 高級 | 庫使用高級指令進行操作,并且通常配備有用于語法驗證的工具集,不幸的是,從修改后的位元組碼進行的最高抽象化通常會導致某些功能的喪失,這些功能僅在直接修改位元組碼時可用, |
四、總結
本文通過對由于Javaassist包缺失導致專案啟動程序中bean加載失敗的問題進行復現,并通過demo進行實體分析,解釋了因為缺失Javaassist庫導致的應用程式啟動失敗問題,并對Javaassist包相關知識進行介紹,后續會繼續對Javaassist相關知識進行學習補充,
建議大家在構建Maven專案時,仔細檢查POM檔案中的依賴,確保沒有漏掉任何必要的庫,以免因為遺漏而引起不必要的問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/551816.html
標籤:其他
上一篇:解密Elasticsearch:深入探究這款搜索和分析引擎
下一篇:返回列表
