dubbo3 使用低版本spring-boot-maven-plugin打包springboot项目报javassist.NotFoundException

现象

项目使用的框架和插件信息:

框架/插件版本
dubbo3.0.5
spring-boot1.4.1.RELEASE
spring-boot-maven-plugin1.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&timestamp=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的结构介绍下

classmoduledescribe
org.apache.dubbo.springboot.demo.Resdubbo-demo-spring-boot-interfacepojo类
org.apache.dubbo.springboot.demo.DemoServicedubbo-demo-spring-boot-interfacedubbo service 接口
org.apache.dubbo.springboot.demo.provider.DemoServiceImpldubbo-demo-spring-boot-providerdubbo service provider实现类
org.apache.dubbo.springboot.demo.provider.ProviderApplicationdubbo-demo-spring-boot-providerspringboot启动类

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()那段新增代码的流程。

  1. 先获取的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

  2. 获取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目录结构

item1.3.02.3.1
org/apache/dubbo/springboot/demo/providerorg.apache.dubbo.springboot.demo.provider包直接在根目录下(该路径下包含DemoServiceImpl.class文件)org.apache.dubbo.springboot.demo.provider包在BOOT-INF/classes目录下(该路径下包含DemoServiceImpl.class文件)
liblib目录直接在根目录下,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 了。

总结

原因有两个:

  1. dubbo3在Wrapper.makeWrapper()方法中添加一段代码,其中new ClassPool(ClassPool.getDfault())会导致dubbo service实现类中引用二方包中的类时找不到,报大javassist.NotFoundException。
  2. 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。

以下两种方式可以解决这个问题:

  1. 使用jdk 9及以上的版本
  2. 使用2.3.x 版本的spring-boot-maven-plugin打包生成jar
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值