Dubbo-11191 用Maven插件应急修复Dubbo3.1.5以前的空指针异常

问题
java.lang.NullPointerException: null
    at org.apache.dubbo.registry.client.metadata.MetadataServiceDelegation.getAllUnmodifiableSubscribedURLs(MetadataServiceDelegation.java:117)
    at org.apache.dubbo.registry.client.metadata.MetadataServiceDelegation.getSubscribedURLs(MetadataServiceDelegation.java:88)

推测Dubbo 3.1.2~3.1.4 之间均有这个问题。

原因

查看源码发现 getAllUnmodifiableSubscribedURLs 就是单纯没有进行空判断。

这个现象应该是Dubbo-Admin调用Dubbo的应用触发的这个bug。

现状

截至发文,2023年1月18日,Dubbo-3.1.5已经修复这个问题。

方法
  • 直接升级
    直接升级可能有未知风险,就像从2.x升级到3.1.2遇到这个问题一样。

  • 重新编译
    没必要重新编译发布到私服,这种办法一般是针对 BUG没有人发现、没人修复、迭代缓慢的项目。

  • 重写+构建
    通过SPI注册自己的实现类,修正他的行为,再结合maven打包插件来达成目的。
    这种方法不算简单,有点舍近求远,但是不依赖其他环境,不依赖自建maven仓库,契合原来的构建系统。

利用的是OOP编程的继承多态思想。

分析

Dubbo 使用了扩展点加载机制,也就是类SPI,本质上是一样的。

找到扩展点

/**
 * Listen for Dubbo application deployment events
 */
@SPI(scope = ExtensionScope.APPLICATION)
public interface ApplicationDeployListener extends DeployListener<ApplicationModel> {
    default void onModuleStarted(ApplicationModel applicationModel) {

    }
}

找到实现类

ExporterDeployListener
编写子类
点击查看代码
public class SlankkaHackedExporterDeployListener extends ExporterDeployListener {
    private static final Logger logger = LoggerFactory.getLogger(SlankkaHackedExporterDeployListener.class);

    //提前注入自己的MetadataServiceDelegation实例
    @Override
    public synchronized void onModuleStarted(ApplicationModel applicationModel) {
        logger.info("{} injected", SlankkaHackedExporterDeployListener.class.getName());
        applicationModel.getBeanFactory().registerBean(null, new SlankkaMetadataServiceDelegation(applicationModel));
        super.onModuleStarted(applicationModel);
    }

    static class URLComparator implements Comparator<URL> {
        //etc
    }
    //重写有问题的类,重写getSubscribedURLs方法(可以直接复制原来的)
    public static class SlankkaMetadataServiceDelegation extends MetadataServiceDelegation {

        public SlankkaMetadataServiceDelegation(ApplicationModel applicationModel) {
            super(applicationModel);
        }

        @Override
        public SortedSet<String> getSubscribedURLs() {
            SortedSet<URL> bizURLs = new TreeSet<>(URLComparator.INSTANCE);
            List<MetadataInfo> metadataInfos = getMetadataInfos();
            for (MetadataInfo metadataInfo: metadataInfos) {
                Map<String, SortedSet<URL>> serviceURLs = metadataInfo.getSubscribedServiceURLs();
                if (serviceURLs == null) {
                    continue;
                }
                for (Map.Entry<String, SortedSet<URL>> entry : serviceURLs.entrySet()) {
                    SortedSet<URL> urls = entry.getValue();
                    if (urls != null) {
                        for (URL url : urls) {
                            if (!MetadataService.class.getName().equals(url.getServiceInterface())) {
                                bizURLs.add(url);
                            }
                        }
                    }
                }
            }
            return MetadataService.toSortedStrings(bizURLs);
        }
    }
}

构建

新建一个模块(假设叫做 dubbo-hack,后面会提到),pom.xml中的关键信息:

点击查看代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.5.0</version>
    <executions>
        <execution>
            <id>unpack-dependencies</id>
            <phase>validate</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>

其中 artifactItems 中应该包含两类信息:

  1. 要解压的jar:指定 dubbo 的GAV, 要排除 ExporterDeployListener 的SPI文件
  2. 要解压的jar的传递依赖: fastjson,fastjson2,com.alibaba.spring:spring-context-support等。

  1. dubbo
<artifactItem>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>${dubbo.version}</version>
    <type>jar</type>
    <overWrite>false</overWrite>
    <outputDirectory>${project.build.directory}/repackDubbo</outputDirectory>
    <destFileName>slankka-repacked.jar</destFileName>
    <includes>**/*</includes>
    <excludes>META-INF/dubbo/internal/org.apache.dubbo.common.deploy.ApplicationDeployListener</excludes>
</artifactItem>
  1. 这部分省略,和上面的类似。

到此 如果直接编译会将dubbo.jar自身的文件解压到outputDirectory文件夹内。

重新打包

这个时候可以用maven-assembly-plugin。

maven-assembly-plugin 的关键配置

<configuration>
    <!-- Name of new archive -->
    <finalName>dubbo-${dubbo.version}-slankka-solution</finalName>
    <!-- We don't want the id of the assembly descriptor in the filename -->
    <appendAssemblyId>false</appendAssemblyId>
    <descriptors>
        <!-- Descriptor of the contents of the new archive -->
        <descriptor>src/assembly/repack.xml</descriptor>
    </descriptors>
</configuration>

repack.xml的关键配置 fileSets

<fileSets>
    <fileSet>
        <!-- Files that are extracted from the original archive -->
        <directory>target/repackDubbo</directory>
        <outputDirectory/>
    </fileSet>
</fileSets>

到此 执行打包 会将解压后的文件重新生成一个jar。

最后一步

根据应用的不同,使用的最终打包工具不一样。

  • maven-shade-plugin
  • maven-jar-plugin
  • maven-war-plugin

这里项目用到的是war,但一行pom都不用改,只要war所在的pom,依赖了dubbo-hack这个模块,打包的程序会自动将生成的target下的jar 打进 war内。

SPI

由于实现类是放在 war所在的模块,写一份SPI文件来替换Dubbo自己的:
文件所在位置 META-INF/dubbo/internal/org.apache.dubbo.common.deploy.ApplicationDeployListener

exporter=com.slankka.dubbo.hack.SlankkaHackedExporterDeployListener

这里写的是子类

进一步优化

由于新建的专门打包dubbo用的模块 dubbo-hack,里面没有任何代码,不需要生成jar,因此可以关闭编译过程。
编译默认使用的maven-jar-plugin
因此可以通过配置maven-jar-plugin,来关闭这个过程。

声明一个 maven-jar-plugin,填写配置

<configuration>
    <skipIfEmpty>true</skipIfEmpty>
</configuration>

这样dubbo-hack生成的 target内只有一个dubbo的jar。
神奇的是,会被 war打包的过程中当成 dubbo-hack.jar,无论叫什么名字。

验证

服务启动以后,会出现一行日志

SlankkaHackedExporterDeployListener injected

QQ交流群

Java 框架技术
作者自己的 QQ交流群:202588131

posted @ 2023-01-18 15:21  一杯半盏  阅读(275)  评论(0编辑  收藏  举报