当javassist遇见Spring boot
最近在开发自己的开源项目REACTIVE DUBBO过程中,需要修改Dubbo的一个工具类
RpcUtils
,通过选型决定用字节码工具javassist对一个静态方法进行魔改。在编码阶段很顺利实现了我想要的效果,但是当打包进行验证时问题出现了。
问题分析
首先来到事发地点
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(RPCUTILS_CLASS_NAME);
CtMethod ctMethod = ctClass.getDeclaredMethod("getReturnTypes");
//rename from `getReturnTypes` to `getReturnTypes0`
ctClass.removeMethod(ctMethod);
ctMethod.setName("getReturnTypes0");
ctClass.addMethod(ctMethod);
//add new `getReturnTypes` method according to RpcUtilsCracker.getReturnTypes
CtClass ctClass1 = classPool.get(RpcUtilsCracker.class.getName());
ctMethod = new CtMethod(ctClass1.getDeclaredMethod("getReturnTypes"),ctClass,null);
ctClass.addMethod(ctMethod);
ctClass.toClass();
} catch (NotFoundException|CannotCompileException e) {
logger.warn("crack RpcUtils failed",e);
}
这段代码的用途是将RpcUtils
的getReturnTypes
方法重命名,并增加自定义的方法。在开发阶段运行正常,然而在使用spring-boot:run运行(或者用Uber jar运行)时会报如下错误:
javassist.NotFoundException: com.alibaba.dubbo.rpc.support.RpcUtils
at javassist.ClassPool.get(ClassPool.java:452) ~[javassist-3.20.0-GA.jar:na]
at com.github.cherrythefatbunny.reactive.dubbo.extensions.rpc.support.RpcUtilsCracker.hack(RpcUtilsCracker.java:28) ~[reactive-dubbo-extensions-1.0.2-SNAPSHOT.jar:na]
at com.github.cherrythefatbunny.reactive.dubbo.boot.ReactiveApplicationContextInitializer.initialize(ReactiveApplicationContextInitializer.java:14) [reactive-dubbo-starter-1.0.2-SNAPSHOT.jar:na]
at org.springframework.boot.SpringApplication.applyInitializers(SpringApplication.java:649) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:373) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at com.github.cherrythefatbunny.demo.consumer.ConsumerApplication.main(ConsumerApplication.java:12) [classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_121]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_121]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_121]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_121]
at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:558) [spring-boot-maven-plugin-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_121]
报错语句为classPool.get(RPCUTILS_CLASS_NAME)
,由于这段代码是要修改存量的类,javassist需要首先读class文件。debug发现执行到Object.class.getResource(jarname)
时,用IDE启动能返回该类的URL,而Spring boot启动则返回空。
继续跟断点发现,最终是使用类加载器Launcher$AppClassLoader
完成加载操作,这样问题定位是出在ClassLoader身上,要想解决这个问题首先要从JVM类加载机制以及Spring boot的启动原理说起。
成功获取Class:
成功获取Class
未能获取Class:
未能获取Class
JVM类加载
类加载器
类加载器用来动态加载Java类到Java虚拟机的内存空间中,分为Bootstrap、Extension和System以及User-Defined。其中Bootstrap负责加载<JAVA_HOME>/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的类;Extension负责加载<JAVA_HOME>/lib/ext
目录下或者由系统变量-Djava.ext.dir
指定位路径中的类;System负责加载ClassPath下的类。
双亲委派模型
除了Bootstrap,每个类加载器都有父类加载器,当类加载器接收到类加载请求时它会先将请求发给父类加载器处理,如果加载不成功才自己尝试加载。所以通过子类加载器可以找到父类加载器加载的类,反之不可以。
JVM ClassLoaderSpring boot启动原理导致的差异
通过IDE(IntelliJ)启动
开发阶段可以通过项目的主函数启动Spring boot,通过启动命令我们发现IDE会自动将依赖加入classpath,这样的启动方式和普通Java项目并无二致,javassist也能顺利找到类RpcUtils
。
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/bin/java
-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59051,suspend=y,server=n
-Dvisualvm.id=202724856185364 -XX:TieredStopAtLevel=1 -noverify
-Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote
-Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true
-javaagent:/Users/cherry/Library/Caches/IntelliJIdea2018.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8
-classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/charsets.jar:
...
/Users/cherry/IdeaProjects/reactive-dubbo/demo/consumer/target/classes:
/Users/cherry/IdeaProjects/reactive-dubbo/demo/facade/target/classes:
...
/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-starter/2.1.2.RELEASE/spring-boot-starter-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot/2.1.2.RELEASE/spring-boot-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.1.2.RELEASE/spring-boot-autoconfigure-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.1.2.RELEASE/spring-boot-starter-logging-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar:/Users/cherry/.m2/repository/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar:/Users/cherry/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.11.1/log4j-to-slf4j-2.11.1.jar:/Users/cherry/.m2/repository/org/apache/logging/log4j/log4j-api/2.11.1/log4j-api-2.11.1.jar:/Users/cherry/.m2/repository/org/slf4j/jul-to-slf4j/1.7.25/jul-to-slf4j-1.7.25.jar:/Users/cherry/.m2/repository/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar:/Users/cherry/.m2/repository/org/springframework/spring-core/5.1.4.RELEASE/spring-core-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/spring-jcl/5.1.4.RELEASE/spring-jcl-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/yaml/snakeyaml/1.23/snakeyaml-1.23.jar:/Users/cherry/.m2/repository/com/alibaba/spring/spring-context-support/1.0.2/spring-context-support-1.0.2.jar:/Users/cherry/.m2/repository/com/alibaba/boot/dubbo-spring-boot-starter/0.2.1.RELEASE/dubbo-spring-boot-starter-0.2.1.RELEASE.jar:/Users/cherry/.m2/repository/com/alibaba/boot/dubbo-spring-boot-autoconfigure/0.2.1.RELEASE/dubbo-spring-boot-autoconfigure-0.2.1.RELEASE.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo/2.6.5/dubbo-2.6.5.jar:/Users/cherry/.m2/repository/org/springframework/spring-context/5.1.4.RELEASE/spring-context-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/spring-aop/5.1.4.RELEASE/spring-aop-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/spring-beans/5.1.4.RELEASE/spring-beans-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/spring-expression/5.1.4.RELEASE/spring-expression-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/javassist/javassist/3.20.0-GA/javassist-3.20.0-GA.jar:/Users/cherry/.m2/repository/org/jboss/netty/netty/3.2.5.Final/netty-3.2.5.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-all/4.1.31.Final/netty-all-4.1.31.Final.jar:/Users/cherry/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.9.8/jackson-databind-2.9.8.jar:/Users/cherry/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.9.0/jackson-annotations-2.9.0.jar:/Users/cherry/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.9.8/jackson-core-2.9.8.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-starter-webflux/2.1.2.RELEASE/spring-boot-starter-webflux-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.1.2.RELEASE/spring-boot-starter-json-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.9.8/jackson-datatype-jdk8-2.9.8.jar:/Users/cherry/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.9.8/jackson-datatype-jsr310-2.9.8.jar:/Users/cherry/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.9.8/jackson-module-parameter-names-2.9.8.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-starter-reactor-netty/2.1.2.RELEASE/spring-boot-starter-reactor-netty-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/io/projectreactor/netty/reactor-netty/0.8.4.RELEASE/reactor-netty-0.8.4.RELEASE.jar:/Users/cherry/.m2/repository/io/netty/netty-codec-http/4.1.31.Final/netty-codec-http-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-codec/4.1.31.Final/netty-codec-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-codec-http2/4.1.31.Final/netty-codec-http2-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-handler/4.1.31.Final/netty-handler-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-buffer/4.1.31.Final/netty-buffer-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-transport/4.1.31.Final/netty-transport-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-resolver/4.1.31.Final/netty-resolver-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-handler-proxy/4.1.31.Final/netty-handler-proxy-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-codec-socks/4.1.31.Final/netty-codec-socks-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-transport-native-epoll/4.1.31.Final/netty-transport-native-epoll-4.1.31.Final-linux-x86_64.jar:/Users/cherry/.m2/repository/io/netty/netty-common/4.1.31.Final/netty-common-4.1.31.Final.jar:/Users/cherry/.m2/repository/io/netty/netty-transport-native-unix-common/4.1.31.Final/netty-transport-native-unix-common-4.1.31.Final.jar:/Users/cherry/.m2/repository/org/hibernate/validator/hibernate-validator/6.0.14.Final/hibernate-validator-6.0.14.Final.jar:/Users/cherry/.m2/repository/javax/validation/validation-api/2.0.1.Final/validation-api-2.0.1.Final.jar:/Users/cherry/.m2/repository/org/jboss/logging/jboss-logging/3.3.2.Final/jboss-logging-3.3.2.Final.jar:/Users/cherry/.m2/repository/com/fasterxml/classmate/1.4.0/classmate-1.4.0.jar:/Users/cherry/.m2/repository/org/springframework/spring-web/5.1.4.RELEASE/spring-web-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/spring-webflux/5.1.4.RELEASE/spring-webflux-5.1.4.RELEASE.jar:/Users/cherry/.m2/repository/org/synchronoss/cloud/nio-multipart-parser/1.1.0/nio-multipart-parser-1.1.0.jar:/Users/cherry/.m2/repository/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar:/Users/cherry/.m2/repository/org/synchronoss/cloud/nio-stream-storage/1.1.3/nio-stream-storage-1.1.3.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-registry-zookeeper/2.6.5/dubbo-registry-zookeeper-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-registry-api/2.6.5/dubbo-registry-api-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-cluster/2.6.5/dubbo-cluster-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-rpc-api/2.6.5/dubbo-rpc-api-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-serialization-api/2.6.5/dubbo-serialization-api-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-container-api/2.6.5/dubbo-container-api-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-remoting-zookeeper/2.6.5/dubbo-remoting-zookeeper-2.6.5.jar:/Users/cherry/.m2/repository/com/alibaba/dubbo-common/2.6.5/dubbo-common-2.6.5.jar:/Users/cherry/.m2/repository/commons-logging/commons-logging/1.2/commons-logging-1.2.jar:/Users/cherry/.m2/repository/log4j/log4j/1.2.16/log4j-1.2.16.jar:/Users/cherry/.m2/repository/com/alibaba/hessian-lite/3.2.4/hessian-lite-3.2.4.jar:/Users/cherry/.m2/repository/com/alibaba/fastjson/1.2.46/fastjson-1.2.46.jar:/Users/cherry/.m2/repository/com/esotericsoftware/kryo/4.0.1/kryo-4.0.1.jar:/Users/cherry/.m2/repository/com/esotericsoftware/reflectasm/1.11.3/reflectasm-1.11.3.jar:/Users/cherry/.m2/repository/com/esotericsoftware/minlog/1.3.0/minlog-1.3.0.jar:/Users/cherry/.m2/repository/de/javakaffee/kryo-serializers/0.42/kryo-serializers-0.42.jar:/Users/cherry/.m2/repository/de/ruedigermoeller/fst/2.48-jdk-6/fst-2.48-jdk-6.jar:/Users/cherry/.m2/repository/com/cedarsoftware/java-util/1.9.0/java-util-1.9.0.jar:/Users/cherry/.m2/repository/com/cedarsoftware/json-io/2.5.1/json-io-2.5.1.jar:/Users/cherry/.m2/repository/org/apache/zookeeper/zookeeper/3.4.9/zookeeper-3.4.9.jar:/Users/cherry/.m2/repository/org/slf4j/slf4j-log4j12/1.7.25/slf4j-log4j12-1.7.25.jar:/Users/cherry/.m2/repository/jline/jline/0.9.94/jline-0.9.94.jar:/Users/cherry/.m2/repository/io/netty/netty/3.10.5.Final/netty-3.10.5.Final.jar:/Users/cherry/.m2/repository/com/101tec/zkclient/0.2/zkclient-0.2.jar:/Users/cherry/.m2/repository/org/apache/curator/curator-framework/2.12.0/curator-framework-2.12.0.jar:/Users/cherry/.m2/repository/org/apache/curator/curator-client/2.12.0/curator-client-2.12.0.jar:/Users/cherry/.m2/repository/com/google/guava/guava/16.0.1/guava-16.0.1.jar:/Users/cherry/IdeaProjects/reactive-dubbo/reactive-dubbo-starter/target/classes:/Users/cherry/IdeaProjects/reactive-dubbo/reactive-dubbo-extensions/target/classes:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-autoconfigure-processor/2.1.2.RELEASE/spring-boot-autoconfigure-processor-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/org/springframework/boot/spring-boot-configuration-processor/2.1.2.RELEASE/spring-boot-configuration-processor-2.1.2.RELEASE.jar:/Users/cherry/.m2/repository/io/projectreactor/reactor-core/3.2.5.RELEASE/reactor-core-3.2.5.RELEASE.jar:/Users/cherry/.m2/repository/org/reactivestreams/reactive-streams/1.0.2/reactive-streams-1.0.2.jar:/Users/cherry/.m2/repository/org/projectlombok/lombok/1.18.4/lombok-1.18.4.jar:/Users/cherry/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar:/Users/cherry/.m2/repository/org/objenesis/objenesis/2.6/objenesis-2.6.jar:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar" com.github.cherrythefatbunny.demo.consumer.ConsumerApplication
通过jar启动
Spring boot定义了一套打包标准,将依赖的jar都打包成一个Uber jar,启动类变成了Spring boot打包进去的JarLauncher
,JarLauncher
会通过自定义的LaunchedURLClassLoader
加载Uber jar内部的lib jar内部的class😂,
由于所寻找的类不在classpath内,因此通过之前的Launcher$AppClassLoader
是无法找到的,这正是报错的原因。
── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── com
│ │ └── github
│ │ └── cherrythefatbunny
│ │ └── demo
│ │ └── consumer
│ │ ├── ConsumerApplication.class
│ │ └── PersonController.class
│ └── lib
│ ├── asm-5.0.4.jar
│ ...
│ ├── zkclient-0.2.jar
│ └── zookeeper-3.4.9.jar
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.github.cherrythefatbunny.demo
│ └── consumer
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
...
解决问题
找到了问题的根源是javassist默认使用Object
类的Launcher$AppClassLoader
,而所依赖的jar都是通过Spring boot的LaunchedURLClassLoader
加载的。根据JVM的双亲委派模型可知,使用Launcher$AppClassLoader
是无法查找子类加载器LaunchedURLClassLoader
所加载类的。幸好javassist为我们留下了扩展的方法,classPool.appendClassPath(new LoaderClassPath(RpcUtilsCracker.class.getClassLoader()));
,通过向ClassPool添加一个能访问到类RpcUtils
的ClassLoader就可以解决问题。这里我选了代码所在的类RpcUtilsCracker
,由于我开发的是一个Spring boot starter,因此该类会被LaunchedURLClassLoader
所加载。经过编译打包发现,报错不再出现😎。
总结
Spring boot是Spring生态重要的组成,给程序员带来了极大便利,但不代表这个项目问题就少,而且底层框架不出问题则已,一有问题就让你怀疑人生。之前就曾经遇到过使用Spring boot热部署工具,导致类型判定出现问题。
最近在学习响应式编程,最开始有一定门槛,一旦迈过去就欲罢不能。趁热写了一个小项目REACTIVE DUBBO,Make your Dubbo reactive!