现象
项目使用的框架和插件信息:
框架/插件 | 版本 |
---|---|
dubbo | 3.0.5 |
spring-boot | 1.4.1.RELEASE |
spring-boot-maven-plugin | 1.3.0.RELEASE |
在idea中启动正常,但是通过maven打包后,运行spring-boot repackage之后的jar包在启动过程中将Service往注册中心注册时就会报错
[09/04/22 16:47:44:697 CST] main INFO config.ServiceConfig: [DUBBO] Register dubbo service org.apache.dubbo.springboot.demo.DemoService url dubbo://192.168.1.4:20880/org.apache.dubbo.springboot.demo.DemoService?anyhost=true&application=dubbo-springboot-demo-provider&background=false&bind.ip=192.168.1.4&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.springboot.demo.DemoService&methods=getRes,sayHello,sayHelloAsync&pid=38256&qos.enable=false&release=3.0.8-SNAPSHOT&revision=3.0.8-SNAPSHOT&service-name-mapping=true&side=provider&timeout=5×tamp=1649494062947 to registry 127.0.0.1:2181, dubbo version: 3.0.8-SNAPSHOT, current host: 192.168.1.4
[09/04/22 16:47:44:699 CST] main ERROR javassist.JavassistProxyFactory: [DUBBO] Failed to generate invoker by Javassist failed. Fallback to use JDK proxy success. Interfaces: interface org.apache.dubbo.springboot.demo.DemoService, dubbo version: 3.0.8-SNAPSHOT, current host: 192.168.1.4
java.lang.RuntimeException: javassist.NotFoundException: org.apache.dubbo.springboot.demo.Res
at org.apache.dubbo.common.bytecode.Wrapper.makeWrapper(Wrapper.java:170)
at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
at org.apache.dubbo.common.bytecode.Wrapper.getWrapper(Wrapper.java:122)
at org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory.getInvoker(JavassistProxyFactory.java:65)
at org.apache.dubbo.rpc.proxy.wrapper.StubProxyFactoryWrapper.getInvoker(StubProxyFactoryWrapper.java:119)
at org.apache.dubbo.rpc.ProxyFactory$Adaptive.getInvoker(ProxyFactory$Adaptive.java)
at org.apache.dubbo.config.ServiceConfig.doExportUrl(ServiceConfig.java:637)
at org.apache.dubbo.config.ServiceConfig.exportRemote(ServiceConfig.java:619)
at org.apache.dubbo.config.ServiceConfig.exportUrl(ServiceConfig.java:578)
at org.apache.dubbo.config.ServiceConfig.doExportUrlsFor1Protocol(ServiceConfig.java:410)
at org.apache.dubbo.config.ServiceConfig.doExportUrls(ServiceConfig.java:396)
at org.apache.dubbo.config.ServiceConfig.doExport(ServiceConfig.java:361)
at org.apache.dubbo.config.ServiceConfig.export(ServiceConfig.java:233)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.exportServiceInternal(DefaultModuleDeployer.java:341)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.exportServices(DefaultModuleDeployer.java:313)
at org.apache.dubbo.config.deploy.DefaultModuleDeployer.start(DefaultModuleDeployer.java:145)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener.onContextRefreshedEvent(DubboDeployApplicationListener.java:111)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener.onApplicationEvent(DubboDeployApplicationListener.java:100)
at org.apache.dubbo.config.spring.context.DubboDeployApplicationListener.onApplicationEvent(DubboDeployApplicationListener.java:45)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:404)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:361)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:898)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:554)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
at org.apache.dubbo.springboot.demo.provider.ProviderApplication.main(ProviderApplication.java:36)
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 org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:53)
at java.lang.Thread.run(Thread.java:750)
Caused by: javassist.NotFoundException: org.apache.dubbo.springboot.demo.Res
at javassist.ClassPool.get(ClassPool.java:430)
at javassist.bytecode.Descriptor.toCtClass(Descriptor.java:571)
at javassist.bytecode.Descriptor.getReturnType(Descriptor.java:472)
at javassist.CtBehavior.getReturnType0(CtBehavior.java:331)
at javassist.CtMethod.getReturnType(CtMethod.java:232)
at org.apache.dubbo.common.utils.ReflectUtils.getDesc(ReflectUtils.java:541)
at org.apache.dubbo.common.bytecode.Wrapper.makeWrapper(Wrapper.java:167)
... 38 more
寻找原因
从上面的异常栈来看,是dubbo3在暴露服务通过javassist生成代理类时没有找到类。
为什么在dubbo2中没有出现这个问题呢,将dubbo2跟dubbo3的Wrapper.makeWrapper(Class)方法做了一个对比,在dubbo3中加了一段代码,如下:
private static Wrapper makeWrapper(Class<?> c) {
...
final ClassPool classPool = new ClassPool(ClassPool.getDefault());
classPool.insertClassPath(new LoaderClassPath(cl));
classPool.insertClassPath(new DubboLoaderClassPath());
List<String> allMethod = new ArrayList<>();
try {
final CtMethod[] ctMethods = classPool.get(c.getName()).getMethods();
for (CtMethod method : ctMethods) {
allMethod.add(ReflectUtils.getDesc(method));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
Method[] methods = Arrays.stream(c.getMethods())
.filter(method -> allMethod.contains(ReflectUtils.getDesc(method)))
.collect(Collectors.toList())
.toArray(new Method[] {});
...
}
这段代码加上去不知道有什么作用,但是从上面的异常栈中可以看到其中有调用ReflectUtils.getDesc()方法。
经过debug跟踪后,确实是上面那段代码中的new ClassPool(ClassPool.getDefault()) 以及ReflectUtils.getDesc(method)有关。
到这里虽然找到了出现问题的代码,但是为什么加了这段代码就会报这个错呢? 下面就开始来分析其中的原因。
ClassPool
前面介绍到Wrapper.makeWrapper()方法中新增的代码中有个关键的类:ClassPool,首先得看到,而且在ReflectUtils.getDesc()方法中最终也是调用ClassPool.get()方法报的错;先来看看ClassPool.get()方法:
public CtClass get(String classname) throws NotFoundException {
CtClass clazz;
if (classname == null)
clazz = null;
else
clazz = get0(classname, true);
if (clazz == null)
throw new NotFoundException(classname);
else {
clazz.incGetCounter();
return clazz;
}
}
protected synchronized CtClass get0(String classname, boolean useCache)
throws NotFoundException
{
CtClass clazz = null;
if (useCache) {
clazz = getCached(classname);
if (clazz != null)
return clazz;
}
if (!childFirstLookup && parent != null) {
// parent不为null时,先从parent获取
clazz = parent.get0(classname, useCache);
if (clazz != null)
return clazz;
}
clazz = createCtClass(classname, useCache);
if (clazz != null) {
// clazz.getName() != classname if classname is "[L<name>;".
if (useCache)
cacheCtClass(clazz.getName(), clazz, false);
return clazz;
}
if (childFirstLookup && parent != null)
clazz = parent.get0(classname, useCache);
return clazz;
}
ClassPool类中获取类信息的get()方法会调用get0()方法,并且在get0()方法中,如果parent不为null,会先从parent获取,这里的逻辑有点像ClassLoader的双亲委派,不同的时在ClassPool类中可以通过childFirstLookup类属性进行控制,在Wrapper.makeWrapper()方法中创建的new ClassPool(ClassPool.getDefault()) 对象默认childFirstLookup为false,且有parent,所以会先从parent获取类。
springboot 运行jar结构
在分析原因之前,先把springboot打的jar的结构介绍下
class | module | describe |
---|---|---|
org.apache.dubbo.springboot.demo.Res | dubbo-demo-spring-boot-interface | pojo类 |
org.apache.dubbo.springboot.demo.DemoService | dubbo-demo-spring-boot-interface | dubbo service 接口 |
org.apache.dubbo.springboot.demo.provider.DemoServiceImpl | dubbo-demo-spring-boot-provider | dubbo service provider实现类 |
org.apache.dubbo.springboot.demo.provider.ProviderApplication | dubbo-demo-spring-boot-provider | springboot启动类 |
DemoServiceImpl和ProviderApplication在同一个module中,从上面可以看DemoService和Res在另一个module中,使用maven打包,在本次实验中使用的spring-boot-maven-plugin是1.3.0.RELEASE版本,来看看生成的jar包的结构:
(为了便于查看,lib目录下做了删减)
dubbo-demo-spring-boot-provider module中所在的DemoServiceImpl类和ProviderApplication类都是根据类路径直接存放在当前目录包路径下,而dubbo-demo-spring-boot-interface module则被打包成jar放在lib目录下;
debug springboot jar
以jdwp debug的方式运行jar包:
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5050 dubbo-demo-spring-boot-provider-3.0.8-SNAPSHOT.jar
在IDEA中的Run/Debug Configurations中配置Remote方式的debug配置,并运行进行远程调试
Wrapper.makeWrapper()和ClassPool流程分析
结合上面的springboot 运行jar的结构,看看Wrapper.makeWrapper()那段新增代码的流程。
-
先获取的Dubbo service 实现类DemoServiceImpl的类信息
而在调用到ClassPool.get0()方法时,会先尝试通过parent获取
从new ClassPool(ClassPool.getDefault()) 中可以看到,parent就是ClassPool.getDefault(),而ClassPool.getDefault()在获取类信息时,是通过ClassLoader.getSystemClassLoader()去获取的;SystemClassLoader是sun.misc.Launcher$AppClassLoader,这个ClassLoader只读取一个目录,就是dubbo-demo-spring-boot-provider-3.0.8-SNAPSHOT.jar,如下图:
Launcher$AppClassLoader在获取class资源路径时,是直接将相对路径拼凑起来,然后判断该路径是否存在,如果存在就返回Resource对象,不存在就返加null,如下两张图:
然后ClassPool.getDefault()创建CtClass对象返回。
在这里有个重要信息,DemoServiceImple的类信息是由parent ClassPool.getDefault()获取的,ClassPool.getDefault()使用的是Launcher$AppClassLoader。
接下来是ReflectUtils.getDesc(method)获取方法的信息,在DemoServiceImpl.getRes()方法中有个返回类,org.apache.dubbo.springboot.demo.Res -
获取org.apache.dubbo.springboot.demo.Res
调用栈是从ClassPool.getDefault()创建CtClass对象获取CtMethod对象,CtMethod对象再调用CtMethod.getReturnType()方法获取返回类的CtClass对象。
由于DemoServiceImple对应的CtClass是由ClassPool.getDefault()创建的,所以从它获取的CtMethod对象也是包含了ClassPool.getDefault()的引用,并且也是通过ClassPool.getDefault()去获取返回类信息,获取流程跟获取DemoServiceImple类信息的流程是一样的。
但是通过ClassPool.getDefault()对就是无法获取到org.apache.dubbo.springboot.demo.Res的类信息:
原因是ClassPool.getDefault()使用的是Launcher$AppClassLoader拼装之后的路径是dubbo-demo-spring-boot-provider-3.0.8-SNAPSHOT.jar!org/apache/dubbo/springboot/demo/Res.class, 但是Res在lib/dubbo-demo-spring-boot-interface-3.0.8-SNAPSHOT.jar中,所以是找不到的,这也是抛javassist.NotFoundException: org.apache.dubbo.springboot.demo.Res的原因。
ClassGenerator
ClassGenerator类中也是使用ClassPool用于生成Wrapper类,在Wrapper.makeWrapper()方法中也有调用ClassGenerator使用ClassPool生成Wrapper类,为什么没有报错呢?
带着这个问题来看看相关代码。
Wrapper.makeWrapper()方法中创建ClassGenerator对象:
private static Wrapper makeWrapper(Class<?> c) {
...
// 通过Thread.currentThread().getContextClassLoader()获取,返回的是springboot的org.springframework.boot.loader.LaunchedURLClassLoader
ClassLoader cl = ClassUtils.getClassLoader(c);
...
ClassGenerator cc = ClassGenerator.newInstance(cl);
...
}
ClassUtils.getClassLoader© 代码:
public static ClassLoader getClassLoader(Class<?> clazz) {
ClassLoader cl = null;
if (!clazz.getName().startsWith("org.apache.dubbo")) {
cl = clazz.getClassLoader();
}
if (cl == null) {
try {
cl = Thread.currentThread().getContextClassLoader();
} catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back to system class loader...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = clazz.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
} catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
}
return cl;
}
ClassGenerator中创建ClassPool实例的代码:
private ClassGenerator(ClassLoader classLoader, ClassPool pool) {
mClassLoader = classLoader;
mPool = pool;
}
public static ClassPool getClassPool(ClassLoader loader) {
if (loader == null) {
return ClassPool.getDefault();
}
ClassPool pool = POOL_MAP.get(loader);
if (pool == null) {
// ClassPool的parent属性为null
pool = new ClassPool(true);
// loader是springboot的org.springframework.boot.loader.LaunchedURLClassLoader
pool.insertClassPath(new LoaderClassPath(loader));
pool.insertClassPath(new DubboLoaderClassPath());
POOL_MAP.put(loader, pool);
}
return pool;
}
new ClassPool(true) 的构造方法:
通过上这些代码可以看出,ClassGenerator中的ClassPool有跟Wrapper.makeWrapper()方法中新增代码段里面的new ClassPool(ClassPool.getDefault())有一处不同:没有parent,在获取DemoServiceImple类和org.apache.dubbo.springboot.demo.Res类的类信息时是使用springboot的org.springframework.boot.loader.LaunchedURLClassLoader去查找资源的,所以不会出现找不到的情况。
ClassPool.getDefault()
在测试时,使用高于jdk8版本的jdk时,也没有抛javassist.NotFoundException: org.apache.dubbo.springboot.demo.Res,这里也跟了一上ClassPool.getDefault()的代码。
ClassPool.getDefault():
从上面这段代码中并没有啥问题,那只能继续深入看defaultPool.appendSystemPath()方法的代码:
public ClassPath appendSystemPath() {
return source.appendSystemPath();
}
在defaultPool.appendSystemPath()方法中是调用source.appendSystemPath()方法,source是ClassPoolTail对象,再看ClassPoolTail.appendSystemPath()
public ClassPath appendSystemPath() {
// jdk < 9 添加ClassClassPath(),ClassLoader是Launcher\$AppClassLoader
if (javassist.bytecode.ClassFile.MAJOR_VERSION < javassist.bytecode.ClassFile.JAVA_9)
return appendClassPath(new ClassClassPath());
// jdk>=9 ClassLoader是org.springframework.boot.loader.LaunchedURLClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return appendClassPath(new LoaderClassPath(cl));
}
public synchronized ClassPath appendClassPath(ClassPath cp) {
ClassPathList tail = new ClassPathList(cp, null);
ClassPathList list = pathList;
if (list == null)
pathList = tail;
else {
while (list.next != null)
list = list.next;
list.next = tail;
}
return cp;
}
在ClassPoolTail.appendSystemPath()方法中可以看出,当jdk版本<=8时,使用的ClassLoader是Launcher$AppClassLoader;当jdk版本>=9 时,使用的ClassLoader是org.springframework.boot.loader.LaunchedURLClassLoader。
所以使用高于jdk8的jdk版本也能正常启动。
spring-boot-maven-plugin
现在jdk8还是比较常用的,如果是这样的话,dubbo3的采用springboot打包的jar运行报错应该早就暴露出来修复了,为什么还存在这样的问题呢?
带着这个问题,开始怀疑springboot重新打包的jar,尤其是dubbo3推荐使用的是springboot 2.3.1.RELEASE,是否两个版本的spring-boot-maven-plugin重新生成的jar包有哪里不同呢?
分别使用spring-boot-maven-plugin 1.3.0.RELEASE版本和2.3.1RELEASE版本打包生成jar包,使用**jar -xvf **命令解压缩之后,对比发现确是有不同
在spring-boot-maven-plugin:1.3.0.RELEASE版本打包生成的jar的目录结构中,有几个处不同于spring-boot-maven-plugin:2.3.1.RELEASE版本打包生成的jar目录结构
item | 1.3.0 | 2.3.1 |
---|---|---|
org/apache/dubbo/springboot/demo/provider | org.apache.dubbo.springboot.demo.provider包直接在根目录下(该路径下包含DemoServiceImpl.class文件) | org.apache.dubbo.springboot.demo.provider包在BOOT-INF/classes目录下(该路径下包含DemoServiceImpl.class文件) |
lib | lib目录直接在根目录下,Res.class所在的dubbo-demo-spring-boot-interface-3.0.8-SNAPSHOT.jar也在./lib目录下 | lib目录在BOOT-INF目录下,Res.class所在的dubbo-demo-spring-boot-interface-3.0.8-SNAPSHOT.jar也在./BOOT-INF/lib目录下 |
log4j.properties | 在根目录下 | 在./BOOT-INF目录下 |
application.yml | 在根目录下 | 在./BOOT-INF目录下 |
INDEX.LIST | 在META-INF目录下 | 无 |
从上表中可以看到,DemoServiceImpl.class文件在1.3.0版本打包的jar中的路径是org/apache/dubbo/springboot/demo/provider/DemoServiceImpl.class,Launcher$AppClassLoader获取资源时是直接拼装路径的,拼装后的路径:dubbo-demo-spring-boot-provider-3.0.8-SNAPSHOT.jar!/org/apache/dubbo/springboot/demo/provider/DemoServiceImpl.class,可以访问到。
而DemoServiceImpl.class文件在2.3.1版本打包的jar中的路径却是不同,其在BOOT-INF/classes目录下,按Launcher$AppClassLoader拼装后的地址:dubbo-demo-spring-boot-provider-3.0.8-SNAPSHOT.jar!/org/apache/dubbo/springboot/demo/provider/DemoServiceImpl.class 是访问不到;所以在ClassPool.get0()方法中,parent 通过Launcher$AppClassLoader获取不到DemoServiceImpl.class,只能由创建的ClassPool对象通过org.springframework.boot.loader.LaunchedURLClassLoader访问到DemoServiceImpl.class文件,如此,在获取Res.class时,也是由创建的ClassPool对象通过org.springframework.boot.loader.LaunchedURLClassLoader访问,这样就都能获取得到,不会抛javassist.NotFoundException: org.apache.dubbo.springboot.demo.Res 了。
总结
原因有两个:
- dubbo3在Wrapper.makeWrapper()方法中添加一段代码,其中new ClassPool(ClassPool.getDfault())会导致dubbo service实现类中引用二方包中的类时找不到,报大javassist.NotFoundException。
- spring-boot-maven-plugin:1.3.0.RELEASE版本打包生成的jar,ClassPool.getDfault()引用的Launcher$AppClassLoader可以访问到dubbo service实现类,但却访问不了实现类中引用二方包中的类。
其实本质上的原因是在new ClassPool(ClassPool.getDefault())这段代码中,新建的ClassPool对象通过parent : ClassPool.getDefault() 访问到了dubbo service实现类,但却访问不了实现类中依赖的二方包中的class,从而抛javassist.NotFoundException。
以下两种方式可以解决这个问题:
- 使用jdk 9及以上的版本
- 使用2.3.x 版本的spring-boot-maven-plugin打包生成jar