java – 重新安装maven依赖项目会导致已经运行的应用程序中出现NoClassDefFoundError

假设我有一个非常简单的maven项目ProjA,它本身没有依赖项.这个项目ProjA的类X和Y如下:

X级

package proja;

public class X {

    static {
        System.out.println("X loaded");
    }

    public void something() {
        System.out.println("X hello world");
    }

}

Y级

package proja;

public class Y {

    static {
        System.out.println("Y loaded");
    }

    public void something() {
        System.out.println("Y hello world");
    }

}

ProjA .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.tomac</groupId>
    <artifactId>ProjA</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
</project>

接下来我有第二个maven项目ProjB,它将项目ProjA作为依赖项.

我项目ProjB我有一个类Run如下:

类运行

package projb;

import proja.X;
import proja.Y;
import java.util.Scanner;

public class Run {

    public void run() {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            String msg = scanner.nextLine();
            switch (msg) {
                case "x":
                    new X().something();
                    break;
                case "y":
                    new Y().something();
                    break;
            }
        }
    }

    public static void main(String[] args) {
        new Run().run();
    }
}

ProjB .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.tomac</groupId>
    <artifactId>ProjB</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>ProjA</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
</project>

我使用mvn install安装项目ProjA,然后使用mvn compile编译项目ProjB

现在,我从类Run运行main方法:
mvn exec:java -Dexec.mainClass =“projb.Run”

然后我输入x< ENTER>得到输出:

X loaded
X hello world

之后我输入y< ENTER>得到输出:

Y loaded
Y hello world

现在,考虑具体的行动顺序:

>启动类Run(加载类Run并等待Scanner.nextLine())
>输入x< ENTER> (加载类X和outputsX加载X hello world)
>现在,当Run运行时,在Y类中编辑一些东西,例如body()方法体:System.out.println(“Y hello world new”);
>使用mvn install重新安装项目ProjA(这会导致将Y类打包编译到目标jar并将打包的jar安装到本地.m2存储库中)
>返回正在运行的应用程序并输入y< ENTER>
>现在加载Y类会导致:

堆栈跟踪:

java.lang.reflect.InvocationTargetException
    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.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NoClassDefFoundError: proja/Y
    at projb.Run.run(Run.java:18)
    at projb.Run.main(Run.java:25)
    ... 6 more
Caused by: java.lang.ClassNotFoundException: proja.Y
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 8 more

请注意,如果依赖项目中的某个尚未卸载的类被更改,部署,然后从依赖项目(已经从依赖项目中加载了至少一个类)的类尝试加载此新更改的类,则此类加载错误仅可重现.

项目和类结构只是从更大的系统中提取出来的概念,它有更多的类与main()方法.其中许多都在不同的JVM中并行运行在同一台机器上.

问题:如何防止这种情况发生?

注意,我不需要在运行时进行任何类型的动态类重新加载.

我知道以不兼容的方式进行更改(例如:在方法中添加一个参数(String str))无论如何都会破坏.

一个解决方法是在项目ProjA中的某些内容更改和部署时重新启动项目ProjB中的所有内容.但是一些进程在启动时具有相对昂贵的初始操作,因此它不是一种选择.

另一种解决方法是以某种方式强制(使用例如Reflections Library)来自项目ProjA的所有类的类加载,从项目ProjB启动每个进程.但这对我来说太过分了,可能导致很多不必要的类加载,并且可能导致OutOfMemoryException.

另一个选择是将所有项目合并为一个大项目,但是将不同的东西分成不同项目的所有方面都将丢失.

如何更好地组织我的develop-> build->运行/重启流程,以便在某个进程启动时以及将来的某个时刻加载类,以便那些加载的类定义等于代码库构建的时间点在这个过程的启动时间之前?

编辑

添加ProjA和ProjB的pom文件

最佳答案 出现此问题是因为exec-maven-plugin
uses是Maven类路径,即声明的执行Java main的依赖项.

Executes the supplied java class in the current VM with the enclosing project’s dependencies as classpath.

这些依赖项在本地Maven存储库中有物理jar,.m2,它确实可以随着时间的推移而改变(通过在相关项目上安装的并行调用),并在SNAPSHOT依赖项的情况下重写(为了遵守约定,但你可以也重写发布的版本,虽然强烈不建议).

您可以通过运行dependency:build-classpath来检查.

mvn dependency:build-classpath -Dmdep.outputFile=classpath.txt -DincludeScope=runtime

将classpath.txt文件写入exec:java run使用的类路径(注意运行时的范围,exec:java run的default). classpath.txt文件中的路径将有效地指向位于m2根目录下的jar文件.

因此,重写到Maven缓存会影响指向它的类作为类路径,因为Java将加载类at its first reference.

一种更健壮且可重现性更好的方法是生成uber jar版本的一部分,并有效地冻结所需的依赖项(程序类路径)并将它们包装到一个提供程序和类路径的jar中.

因此,在保持项目的现有分离的同时,不再有并行/外部干预可能影响正在运行的应用程序.

另一种方法是通过versions:lock-snapshots锁定先前生成的依赖项目的SNAPSHOT版本:

searches the pom for all -SNAPSHOT versions and replaces them with the current timestamp version of that -SNAPSHOT, e.g. -20090327.172306-4

因此,再次将您的项目与任何并发/外部干预隔离开来.虽然在发布/分发项目时,更推荐使用超级jar方法.

此外,锁定快照仅在通过本地存储库安装的Maven存储库not working可用时才有效:

Attempts to resolve unlocked snapshot dependency versions to the locked timestamp versions used in the build. For example, an unlocked snapshot version like 1.0-SNAPSHOT could be resolved to 1.0-20090128.202731-1. If a timestamped snapshot is not available, then the version will remained unchanged. This would be the case if the dependency is only available in the local repository and not in a remote snapshot repository.

因此,在您的情况下,很可能不是一个选项.

点赞