手把手实践NoSuchMethodError、NoClassDefFoundError依赖冲突

前序

在实际项目开发中,经常会遇到程序在没有做任何修改的情况下无法启动了。前两天正在电脑前发呆,突然隔壁的程序媛小王火急火燎的说,涛哥帮忙看一个问题吧?这个问题已经阻塞了一中午了。看到异常信息后,第一反应是这种问题99.9999%的原因是maven依赖包有不兼容的问题。不过用idea的Dependency Analyzer排查时发现根本就不存在有冲突的包。这下发现遇到真问题了。虽然嘴上跟人家说是因为版本冲突导致的,但是心里面还是开始犯嘀咕了。不过最后还是把问题给发现了,同时也证明了当时自己的判断是正确的。

问题重现与解决

为了重现当时的问题,在本文中创建了一个mini版本的项目来说明问题的表象,出现启动异常的原因以及解决方法。

演示项目的整体结构如下图

img

程序启动时的异常堆栈信息

img

针对以上异常,做一个简单的分析,其中collision为一个Parent,在该pom中引入的spring boot版本为2.1.3.RELEASE,同时该maven工程中又设置了属性,其中spring-boot的版本为2.0.5.RELEASE。进行初步的debug后发现org.springframework.core.KotlinDetector类里面的isKotlinReflectPresent()方法确实不存在。

img

这时再通过spring的doc发现,isKotlinReflectPresent()方法是从spring 5.1版本才引入的。通过依赖分析可以发现在项目中引入的spring-core版本是5.0.9。

img

这时,我们可以通过引入的spring的版本来反推出spring boot的版本。因为spring boot 2.0.5.RELEASE是基于spring 5.0.9.RELEASE来开发的,spring boot 2.1.3.RELEASE是基于spring 5.1.5.RELEASE来开发的。

有了上面的初步分析,将上面的spring boot版本进行统一处理后,上面的问题自然就解决了。

透过现象看本质

在上一章节中,已经对产生的原因做了初步的分析并把问题解决掉了。针对上面的问题,我们是否可以在做一些深入的分析和总结呢?答案是肯定了。在分析的过程中,笔者还学到了maven的依赖加载相关的知识点,在这里也一并与大家进行分享和实例演示。与大家一起学习和探讨。

在实际的项目开发中除了会遇到NoSuchMethodError,还会遇到例如ClassNotFoundException,NoClassDefFoundError等问题。一般都会把这些异常归结为依赖版本的冲突或者说版本兼容性问题。

NoSuchMethodError

根据java api文档里面的描述,当类的定义发生了不兼容的改变,程序运行时便会抛出该异常。为了简单化,笔者做了三个更简化的演示程序。其中incompatible项目依赖于third-party-dependency-a项目,third-party-dependency-a项目依赖于third-party-dependency-b的1.1-SNAPSHOT版本。同时在本地的私服里面还存在third-party-dependency-b的1.0-SNAPSHOT版本。

third-party-dependency-b 1.0-SNAPSHOT版本的接口定义与实现

package com.mc.third.party.dependency.service;

/**
 * @author M.C
 * @description SayFeedBackService
 * @date 2019-02-28 14:44
 **/
public interface SayFeedBackService {
    /**
     * sayFeedBackForChinese
     */
    public String sayFeedBackForChinese();

}

接口的具体实现

package com.mc.third.party.dependency.service.impl;

import com.mc.third.party.dependency.service.SayFeedBackService;
import lombok.extern.slf4j.Slf4j;

/**
 * @author M.C
 * @description SayFeedBackServiceImpl
 * @date 2019-02-28 14:45
 **/
@Slf4j
public class SayFeedBackServiceImpl implements SayFeedBackService {

    /**
     * sayFeedBackForChinese
     */
    @Override
    public String sayFeedBackForChinese() {
         log.info("你好!我来自中国。");
         return "你好!我来自中国。";
    }
    
}

third-party-dependency-b 1.1-SNAPSHOT版本的接口定义与实现

package com.mc.third.party.dependency.service;

/**
 * @author M.C
 * @description SayFeedBackService
 * @date 2019-02-28 14:44
 **/
public interface SayFeedBackService {
    /**
     * sayFeedBackForChinese
     */
    public String sayFeedBackForChinese();

    /**
     * sayFeedBackForAmerican
     */
    public String sayFeedBackForAmerican();
}

具体实现

package com.mc.third.party.dependency.service.impl;

import com.mc.third.party.dependency.service.SayFeedBackService;
import lombok.extern.slf4j.Slf4j;

/**
 * @author M.C
 * @description SayFeedBackServiceImpl
 * @date 2019-02-28 14:45
 **/
@Slf4j
public class SayFeedBackServiceImpl implements SayFeedBackService {

    /**
     * sayFeedBackForChinese
     */
    @Override
    public String sayFeedBackForChinese() {
         log.info("你好!我来自中国。");
         return "你好!我来自中国。";
    }

    /**
     * sayFeedBackForAmerican
     */
    @Override
    public String sayFeedBackForAmerican() {
        log.info("Hello! I am come from the USA.");
        return "Hello! I am come from the USA.";
    }
}

third-party-dependency-a的接口定义与实现

package com.mc.third.party.dependency.service;

/**
 * @author M.C
 * @description SayHelloService
 * @date 2019-02-28 14:31
 **/
public interface SayHelloService {
    /**
     *
     */
    public String sayHello(String country);
}

具体的实现类

package com.mc.third.party.dependency.service.impl;

import com.mc.third.party.dependency.service.SayFeedBackService;
import com.mc.third.party.dependency.service.SayHelloService;
import lombok.extern.slf4j.Slf4j;

/**
 * @author M.C
 * @description SayHelloServiceImpl
 * @date 2019-02-28 14:32
 **/
@Slf4j
public class SayHelloServiceImpl implements SayHelloService {

    SayFeedBackService sayFeedBackService  =  new SayFeedBackServiceImpl();
    /**
     * sayHello
     * @param country
     */
    @Override
    public String  sayHello(String country) {
        String result="";
            switch (country){
                case "China":
                    result = sayFeedBackService.sayFeedBackForChinese();break;
                case "US":
                    result = sayFeedBackService.sayFeedBackForAmerican();break;
                    default: log.info("功能还未实现");break;
            }
            return result;
    }
}

incompatible工程为一个调用的客户端

package com.mc.incompatible;

import com.mc.third.party.dependency.service.SayHelloService;
import com.mc.third.party.dependency.service.impl.SayHelloServiceImpl;
import lombok.extern.slf4j.Slf4j;

/**
 * @author M.C
 * @description Incompatible
 * @date 2019-02-28 16:23
 **/
@Slf4j
public class Incompatible {
    /**
     * main
     * @param args
     */
    public static void main(String[] args) {
        SayHelloService sayHelloService = new SayHelloServiceImpl();
        log.info(sayHelloService.sayHello("China"));
        log.info(sayHelloService.sayHello("US"));
    }
}

正常情况下maven的依赖配置

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.26</version>
        </dependency>
        <dependency>
            <groupId>com.mc</groupId>
            <artifactId>third-party-dependency-a</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

正常时程序执行结果

img

把上面的pom文件做一个修改,比如我们错误的引入了third-party-dependency-b的1.0版本。

        <dependency>
            <groupId>com.mc</groupId>
            <artifactId>third-party-dependency-a</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.mc</groupId>
            <artifactId>third-party-dependency-b</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

再次执行上面的程序,就会发现出现异常了。

img

通过日志可以发现,出现的异常信息与之前遇到的问题是如此的惊人相似。因为他们的根本原因都是一样的。客户端通过maven依赖引入了a,然后a通过依赖传递引入了b的1.1版本。同时客户端也直接引入了b的旧版本1.0。但是为什么maven不选择一个较高的依赖版本,反而用的是旧的版本呢。这时候就需要简单了解一下maven的仲裁机制。

  • 优先按照依赖管理**<dependencyManagement>**元素中指定的版本声明进行仲裁,此时下面的两个原则都无效了
  • 若无版本声明,则按照“短路径优先”的原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本
  • 若路径长度一致,则按照“第一声明优先”的原则进行仲裁,即选择POM中最先声明的版本

有了上面的仲裁机制,就能知道为什么在客户端运行的时候,会出现异常了。首先在演示项目中没有dependencyManagement,所以进行短路径优先的原则。针对依赖b的路径,一条是app->a-b 另外一个是app->b 。

ClassNotFoundException/NoClassDefFoundError

在什么情况下,会出现ClassNotFoundExcetion/NoClassDefFoundError 呢?针对这个问题,先把之前的演示项目做一个改造。

        <dependency>
            <groupId>com.mc</groupId>
            <artifactId>third-party-dependency-a</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.mc</groupId>
                    <artifactId>third-party-dependency-b</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

执行客户端程序结果

Exception in thread "main" java.lang.NoClassDefFoundError: com/mc/third/party/dependency/service/SayFeedBackService
	at com.mc.incompatible.Incompatible.main(Incompatible.java:19)
Caused by: java.lang.ClassNotFoundException: com.mc.third.party.dependency.service.SayFeedBackService
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 1 more

因为在程序运行时需要SayFeedBackService,但是在POM中把该类包含的依赖做了排除处理。导致在运行时没有找到SayFeedBackService。这类典型异常通常是由于,没有在依赖管理中声明版本。还有一个原因是maven仲裁的时候选取了错误版本,而这个版本缺少我们需要的某个class而导致该错误。

NoClassDefFoundError和ClassNotFoundException都是由于在CLASSPATH下找不到对应的类而引起的,通常是缺少对应的jar包,JVM认为:(1)当应用运行时没有找到对应的引用,则会抛出java.lang.NoClassDefFoundError;(2)当你在代码中显式加载类(使用Class.forName())时没有找到对应的类,则会抛出java.lang.ClassNotFoundException。开发者经常遇到的情况是:ClassNotFoundException异常引起了ClassNoDefFoundError。

最佳实践

对于NoSuchMethodError、NoClassDefFoundError或ClassNotFoundException包冲突问题,通常的做法是用**<excludes>排除不需要的版本,但这种做法带来的问题是每次引入带有传递性依赖的Jar包时,都需要一一进行排除,非常麻烦。maven为此提供了集中管理依赖信息的机制,即依赖管理元素<dependencyManagement>**,对依赖Jar包进行统一版本管理,一劳永逸。通常的做法是,在parent模块的pom文件中尽可能地声明所有相关依赖Jar包的版本,并在子pom中简单引用该构件即可。

重新回头看一开始的那个问题,根据最佳实践,最终修改后的pom文件如下。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mc</groupId>
    <artifactId>collision</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>collision-service</module>
        <module>collision-domain</module>
        <module>collision-dao</module>
        <module>collision-controller</module>
        <module>collision-common</module>
    </modules>

    <properties>
        <spring.boot.version>2.1.3.RELEASE</spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

子模块的pom定义

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>collision</artifactId>
        <groupId>com.mc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>collision-controller</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mc</groupId>
            <artifactId>collision-service</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

后序

在实际的开发中,一个小小的异常就可以引起这么多的知识点,也是在开始写这篇文字是没有想到的。感觉有时候问题别看着小,但是就怕深入的挖掘。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值