目录
- 一、问题描述
- 二、原因分析
- 三、解决方案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可以作为一个备选方案。