目錄
- 一、問題描述
- 二、原因分析
- 三、解決方案1(推薦):獲取線程上下文中的類加載器
- 擴展
- 四、解決方案2:自定義SpringBoot類加載器
一、問題描述
在使用Byte-Buddy中的TypePool
對類進行擴展后,在本地開發集成環境(Intellij Idea)中可以正常運行,其中被擴展的類com.xx.yourClass
是某個maven 依賴中的類(在開發環境沒法直接進行編輯),具體擴展代碼示例如下:
//使用TypePool + Redefine擴展屬性
TypePool typePool = TypePool.Default.ofSystemLoader();
Class bar = new ByteBuddy().redefine(typePool.describe("com.xx.yourClass").resolve(), ClassFileLocator.ForClassLoader.ofSystemLoader()).defineField("qux", String.class) .make().load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION).getLoaded();
Field quxField = bar.getDeclaredField("qux");
Assert.isTrue(Objects.nonNull(quxField), "qux field is null");
但是通過SpringBoot打包(底層依賴spring-boot-maven-plugin進行打包)成jar后,運行jar報如下錯誤,即無法加載到被擴展的類(某個maven依賴中的類):
# 使用TypePool報錯
net.bytebuddy.pool.TypePool$Resolution$NoSuchTypeException: Cannot resolve type description for com.xxx.YourClass
# 擴展 - 使用Redefine可能會報如下錯誤
java.lang.IllegalStateException: Could not locate class file for com.xxx.YourClass
二、原因分析
可以發現上述示例代碼中有3處使用了系統類加載器:
TypePool typePool = TypePool.Default.ofSystemLoader()
refedine(.., ClassFileLocator.ForClassLoader.ofSystemLoader())
load(ClassLoader.getSystemClassLoader(), ...)
問題就出在這塊,SpingBoot打包后的Jar文件有其特殊的層次結構,無法通過系統的類加載器加載到內嵌jar(即/BOOT-INF/lib/*.jar
)中的類,而是需要通過SpringBoot自身提供的類加載器org.springframework.boot.loader.launch.LaunchedClassLoader
進行加載,所以如上代碼通過系統類加載器SystemClassLoader
是無法加載到內嵌jar中的類的。
SpringBoot打包后的Jar文件結構示例如下:
三、解決方案1(推薦):獲取線程上下文中的類加載器
具體的解決方法就是如何獲取并設置byte-buddy使用SpringBoot自身提供的類加載器org.springframework.boot.loader.launch.LaunchedClassLoader
,可以通過個取巧的方式獲取SpringBoot的類加載器。
在應用中植入如下代碼,即分別獲取線程上下文中的類加載器、系統加載器、平臺加載器:
System.out.println("Thread.CurrentThread.ContextClassLoader=" + Thread.currentThread().getContextClassLoader().getClass().getName());
System.out.println("SystemClassLoader=" + ClassLoader.getSystemClassLoader().getClass().getName());
System.out.println("PlatformClassLoader=" + ClassLoader.getPlatformClassLoader().getClass().getName());
直接在本地開發集成環境(Intellij Idea)中執行,打印結果如下:
Thread.CurrentThread.ContextClassLoader=jdk.internal.loader.ClassLoaders$AppClassLoader
SystemClassLoader=jdk.internal.loader.ClassLoaders$AppClassLoader
PlatformClassLoader=jdk.internal.loader.ClassLoaders$PlatformClassLoader
通過SpringBoot打包成Jar,運行Jar后打印結果如下:
Thread.CurrentThread.ContextClassLoader=org.springframework.boot.loader.launch.LaunchedClassLoader
SystemClassLoader=jdk.internal.loader.ClassLoaders$AppClassLoader
PlatformClassLoader=jdk.internal.loader.ClassLoaders$PlatformClassLoader
可以發現,在運行SpringBoot Jar后SpringBoot會將線程上下文中的類加載器(即Thread.currentThread().getContextClassLoader()
)設置為SpringBoot自身的類加載器LaunchedClassLoader
,如此即可通過獲取線程上下文中的類加載器的方式來兼容本地開發環境和SpringBoot Jar中的類都能被正確加載。
調整最開始的示例代碼,將所有使用系統類加載器
的地方都調整為使用線程上下文中的類加載器
,調整后代碼如下:
//調整1:獲取SpringBoot內置的類加載器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
ClassFileLocator springBootClassFileLocator = ClassFileLocator.ForClassLoader.of(contextClassLoader);
//調整2:使用線程上下文中的SpringBoot內置的類加載器
TypePool typePool = TypePool.Default.of(springBootClassFileLocator);
Class bar = new ByteBuddy().redefine(typePool.describe("com.xx.yourClass").resolve(), //調整3:使用線程上下文中的SpringBoot內置的類加載器springBootClassFileLocator).defineField("qux", String.class) .make()//調整4:使用線程上下文中的SpringBoot內置的類加載器.load(contextClassLoader, ClassLoadingStrategy.Default.INJECTION).getLoaded();
Field quxField = bar.getDeclaredField("qux");
Assert.isTrue(Objects.nonNull(quxField), "qux field is null");
搞定!
擴展
如果不使用TypePool
,而是普通的redefine
、rebase
等操作,此時是可以直接獲取到被擴展的類的(xx.class
而不是類的字符串表示
),注意這時只需將所有跟類加載器
相關的統一調整為根據被擴展類進行獲取即可,如下示例統一根據被擴展類YourClass
獲取類加載器:
ByteBuddyAgent.install();
new ByteBuddy().redefine(YourClass.class, ClassFileLocator.ForClassLoader.of(YourClass.class.getClassLoader()))//省略....make().load(YourClass.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
四、解決方案2:自定義SpringBoot類加載器
大力出奇跡,可以在byte-buddy
中自定義SpringBoot的類加載器實現,
具體參見:https://github.com/raphw/byte-buddy/issues/1470#issuecomment-1617556513,
即自行遍歷"/BOOT-INF/**/*.jar"
的所有jar來加載類,具體自定義SpringBootClassFileLocator
實現代碼如下:
import net.bytebuddy.dynamic.ClassFileLocator;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;/*** SpringBoot復合類加載器** @author luohq* @date 2025-04-24* @link <a href="https://github.com/raphw/byte-buddy/issues/1470#issuecomment-1617556513">https://github.com/raphw/byte-buddy/issues/1470#issuecomment-1617556513</a>*/
public class SpringBootClassFileLocator {private SpringBootClassFileLocator() {}/*** 獲取SpringBoot復合類加載器** @return SpringBoot復合類加載器*/public static ClassFileLocator ofCompound() {try {String basePath = SpringBootClassFileLocator.class.getResource("/").getPath();List<ClassFileLocator> classFileLocators = new ArrayList<ClassFileLocator>();classFileLocators.add(ClassFileLocator.ForClassLoader.ofSystemLoader());if (basePath != null && basePath.contains("BOOT-INF")) {String matchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/BOOT-INF/**/*.jar";ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();Resource[] resources = resourcePatternResolver.getResources(matchPattern);for (Resource resource : resources) {ClassFileLocator classFileLocator = transform(resource);if (classFileLocator != null) {classFileLocators.add(classFileLocator);}}} else {classFileLocators.add(ClassFileLocator.ForClassLoader.ofPlatformLoader());}return new ClassFileLocator.Compound(classFileLocators);} catch (IOException ioe) {throw new RuntimeException("Init SpringBoot ClassFileLocator Exception!", ioe);}}/*** 轉換資源為ClassFileLocator** @param resource 資源* @return ClassFileLocator* @throws IOException IO異常*/private static ClassFileLocator transform(Resource resource) throws IOException {try (InputStream inputStream = resource.getInputStream()) {if (inputStream != null) {if (resource.getFilename().endsWith("jar")) {File tempFile = File.createTempFile("temp/jar/" + resource.getFilename(), ".jar");Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);return ClassFileLocator.ForJarFile.of(tempFile);}}}return null;}
}
SpringBootClassFileLocator
集成示例如下:
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//調整:使用自定義的SpringBoot類加載器
ClassFileLocator springBootClassFileLocator = SpringBootClassFileLocator.ofCompound();
TypePool typePool = TypePool.Default.of(springBootClassFileLocator);
Class bar = new ByteBuddy().redefine(typePool.describe("com.xx.yourClass").resolve(), springBootClassFileLocator).defineField("qux", String.class) .make().load(contextClassLoader, ClassLoadingStrategy.Default.INJECTION).getLoaded();
Field quxField = bar.getDeclaredField("qux");
Assert.isTrue(Objects.nonNull(quxField), "qux field is null");
實際測試加載速度不如解決方案1,且還需額外維護SpringBootClassFileLocator
實現,綜合對比還是更推薦解決方案1,解決方案2可以作為一個備選方案。