解决javassist在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环境还是普通环境,再分别处理,这里就不赘述了。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值