解决javassist在SpringBoot环境下找不到类的问题

本文介绍了在SpringBoot环境下使用javassist遇到找不到类的问题及其解决方案。通过理解类加载器的工作原理,特别是SpringBoot的类加载器,以及javassist的ClassPool和LoaderClassPath,展示了如何获取SpringBoot的类加载器并使其帮助查找和修改目标类,从而避免找不到类的异常。

问题

最近在玩javassit的时候(利用java代理实现对代码的运行时修改),碰到了一个问题。

目标应用是一个SpringBoot应用,我需要修改Spring MVC中的一个类InterceptorRegisty,动态增加一个拦截器。

当我直接在IDE中带agent参数运行这个应用时,没有问题,可当打包成jar后运行时,却抛出找不到类的异常:

javassist.NotFoundException: org.springframework.web.servlet.config.annotation.InterceptorRegistry
	at javassist.ClassPool.get(ClassPool.java:422)
	at cn.alfredzhang.agent.springservlet.Agent.modify(Agent.java:63)
	at cn.alfredzhang.agent.springservlet.Agent.premain(Agent.java:24)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386)
	at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:401)

思考

明明是一个肯定存在的类,你告诉我找不到?作为一个老司机,自然很快联想到类装载器的问题。

关于类装载器,具体的这里不展开,只简单说一下要点:

  • 类装载器(以下简称CL)负责将类装入虚拟机
  • java内置三种CL:application应用、extension扩展和bootstrap引导
    • 引导CL:负责装载JDK内部类,包括rt.jar和jre/lib/目录下其他核心库中的类,它也是所有装载器的爸爸
    • 扩展CL:负责装载标准核心java类的扩展类(lib/ext等),它是引导CL的儿子
    • 应用CL:或称系统CL,负责装载所有应用级的类,它是扩展CL的儿子
  • 委托模型:要装载某个类时,CL会先委托给自己的爸爸,最后才会由自己来装载
  • 自定义CL:当内置的CL无法满足需求时,可以自定义CL,例如SpringBoot就有自己的CL,专门用来从它那个结构特殊的jar包中装载类
  • 类的可见性:儿子装载的类可以看到爸爸装载的类,但反过来不行——爸爸装载的类看不到儿子装载的类(可怜天下父母心)

好了,那么上面问题的根源,就是javassist想要找的这个类,其实是放在SpringBoot那个特殊的包里,而它用的装载器(应用CL)却只会在类路径里(-classpth)里去找一圈,结果当然是找不到。

那么怎么解决呢?

解决

既然这个类是在SpringBoot特殊的jar包里,那自然只有SpringBoot自己的CL知道怎么去找它,只要想办法让javassist能拜托SpringBoot的CL帮忙找就行了。

让javassist拜托其他CL帮忙找类

javasssit显然也考虑到了此类情况,所以ClassPool类提供了一个方法:

ClassPath appendClassPath​(java.lang.String pathname)

以及一个类LoaderClassPath。

我们只要:

ClassPool classPool = new ClassPool();
classPool.appendClassPath(new LoaderClassPath(classLoader));

就可以让其他的CL帮我们找类。

那么问题就变成了怎么拿到SpringBoot的CL。

获取SpringBoot的类装载器

这个就比较容易了,因为这个装载器本身是由应用CL来装载的,所以javassist默认情况下就能看到。

那么问题就简单了,找到这个CL的类,修改下它的构造函数,让它主动来调用一下我们的代码,把自己的实例作为参数传过来。

CtClass ctClass = classPool.get("org.springframework.boot.loader.LaunchedURLClassLoader");
CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();
ctConstructors[0].insertAfter("cn.alfredzhang.agent.springservlet.Agent.modifyClass(this);");
ctClass.toClass();

修改目标类

现在我们已经可以让SpringBoot的CL把自己的实例传过来,那么我们即可以用上面的让javassist拜托其他CL的方法,让SpringBoot的CL帮忙找出我们的目标类,然后去修改它了,代码串起来:

public static void modifyClass(ClassLoader loader) {

    try {

        ClassPool classPool = ClassPool.getDefault();

        classPool.appendClassPath(new LoaderClassPath(loader));

        ctClass = classPool.get("org.springframework.web.servlet.config.annotation.InterceptorRegistry");
        CtMethod ctMethod = ctClass.getDeclaredMethod("getInterceptors");
        
        // 修改目标方法,略
            
        ctClass.toClass(loader, ctClass.getClass().getProtectionDomain());

    } catch (Exception e) {
        e.printStackTrace();
    }
}

最后,当要调用toClass方法来创建修改后的Class对象时,记得一定要用指定CL的版本:

toClass​(java.lang.ClassLoader loader, java.security.ProtectionDomain domain)

否则这个类就被我们的默认CL(应用CL)装了,这本身没什么问题(因为SpringBoot的CL作为儿子是可以看到爸爸装的类的),但这个类引用的其他类却仍然是由SpringBoot的CL装的,所以它看不到(爸爸装载的类看不到儿子装载的类)!然后又会有找不到类的错误等着你!

到这里为止,大功告成。当然还有很多地方可以优化,比如判断是否是SpringBoot环境还是普通环境,再分别处理,这里就不赘述了。

Spring Boot 2.5.4 中,“复制”这个说法可能有多种理解方式。常见的场景包括: - **复制一个 Java (如 DTO、Entity)的属性到另一个**,比如将 `User` 实体复制到 `UserDTO`。 - **深拷贝或浅拷贝对象实例**。 - **在运行时动态复制结构(字节码操作)** —— 这种较少见,通常使用 CGLIB、ASM 或 Javassist。 由于你提到的是 Spring Boot 2.5.4,我假设你最可能的需求是:**如何安全高效地将一个对象的属性复制到另一个对象中**,这是开发中非常常见的需求(例如 Controller 层返回 DTO,而数据来自 Entity)。 --- ### ✅ 推荐解决方案:使用 MapStruct 实现对象属性复制 MapStruct 是一个注解处理器,用于在编译期生成型安全、高性能的对象映射代码,非常适合 Spring Boot 项目。 #### 1. 添加依赖(Maven) ```xml <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.4</version> </dependency> <!-- MapStruct 核心 --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>11</source> <!-- 根据你的 JDK 版本调整 --> <target>11</target> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> ``` --- #### 2. 定义源和目标 ```java // User.java public class User { private Long id; private String name; private String email; private Integer age; // 构造函数、getter、setter 省略,建议用 Lombok } ``` ```java // UserDTO.java public class UserDTO { private Long id; private String name; private String email; private Integer age; // getter/setter } ``` --- #### 3. 创建 MapStruct 映射接口 ```java import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @Mapper public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); UserDTO userToDto(User user); User dtoToUser(UserDTO dto); } ``` --- #### 4. 在 Service 中使用 ```java import org.springframework.stereotype.Service; @Service public class UserService { public UserDTO getUserDTO() { User user = new User(); user.setId(1L); user.setName("Alice"); user.setEmail("alice@example.com"); user.setAge(25); return UserMapper.INSTANCE.userToDto(user); } } ``` --- #### 5. 控制器返回 DTO ```java import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/user") public UserDTO getUser() { return userService.getUserDTO(); } } ``` --- ### 🔍 解释说明 - **MapStruct 在编译时生成实现**,没有反射开销,性能接近手写代码。 - 使用 `@Mapper` 注解标记接口,MapStruct 自动生成 `.java` 文件(位于 `target/generated-sources/...`)。 - `Mappers.getMapper()` 是获取实例的简便方式,在 Spring 中也可以通过组件扫描自动注册为 Bean(加上 `componentModel = "spring"`)。 修改注解为: ```java @Mapper(componentModel = "spring") ``` 然后通过 `@Autowired` 注入即可。 --- ### ⚠️ 其他方式对比 | 方法 | 是否推荐 | 说明 | |------|----------|------| | 手动 set/get | ❌ 不推荐 | 冗余、易错 | | BeanUtils.copyProperties (Spring) | ⚠️ 谨慎使用 | 基于反射,慢;且会复制所有同名字段,容易出错 | | ModelMapper | △ 可用 | 功能强但运行时反射,性能较差 | | MapStruct | ✅ 强烈推荐 | 编译期生成,型安全,性能高 | --- ### 示例:使用 Spring 的 BeanUtils(不推荐但常见) ```java import org.springframework.beans.BeanUtils; UserDTO dto = new UserDTO(); BeanUtils.copyProperties(user, dto); // 注意参数顺序:src, target ``` ⚠️ 风险:字段型不同可能静默失败,或复制了不该复制的字段。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值