重新安装 maven 依赖项项目会在已经 运行 个应用程序中导致 NoClassDefFoundError

Re-installing maven dependency project causes NoClassDefFoundError in already running application

假设我有一个非常简单的 Maven 项目 ProjA,它本身没有依赖项。这个项目 ProjA 有 classes XY 如下:

class X

package proja;

public class X {

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

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

}

class 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 我有一个 class Run 如下:

class 运行

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

现在,我 运行 来自 class Run 的主要方法使用:
mvn exec:java -Dexec.mainClass="projb.Run"

然后我输入 x 并得到输出:

X loaded
X hello world

之后我输入 y 并得到输出:

Y loaded
Y hello world

现在,考虑具体的操作顺序:

  1. 启动classRun(加载classRun并等待Scanner.nextLine()

  2. 键入 x(加载 class X 并输出 X loaded X hello world

  3. 现在 Run 是 运行ning,在 class Y 中编辑一些东西,例如 something() 方法的主体: System.out.println("Y hello world new");

  4. 使用mvn install重新安装项目ProjA(这会导致编译classY打包到目标jar并将打包的jar安装到本地.m2 存储库)

  5. 返回 运行ning 应用并输入 y

  6. 现在加载 class 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.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

请注意,此 class-加载错误只有在更改、部署依赖项目中的某些尚未卸载的 class 时才会重现,然后 class 从依赖项目(已经有至少一个 class 从依赖项目加载)尝试加载这个新更改的 class.

项目和 class 结构只是作为概念从更大的系统中提取出来的,这个系统有更多的 classes 和 main() 方法。其中许多 运行 在不同的 JVM 中并行运行在同一台机器上。

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

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

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

一种解决方法是在项目 ProjA 中的某些内容发生更改和部署时重新启动项目 ProjB 中的所有内容。但有些进程在启动时的初始操作成本相对较高,因此不是一个选择。

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

还有一种选择是将所有项目合并到一个大项目中,但是这样就失去了将不同的东西分成不同项目的所有意义。

我怎样才能更好地组织我的开发->构建->run/restart 流程,以便在某些进程启动时并在将来的某个时间点加载 classes,以便那些加载 classes 定义是否等于在此进程启动之前构建的代码库的时间点?

编辑

添加 ProjAProjB

的 pom 文件

问题的发生是因为 exec-maven-plugin uses Maven class 路径,即声明的依赖项来执行你的 Java main.

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

这些依赖项在本地 Maven 存储库中有它们的物理 jar,.m2,它们确实可以随着时间的推移而改变(通过对相关项目的并行调用 install)并被重写以防万一SNAPSHOT 依赖项(尊重约定,但您也可以重写已发布的版本,尽管强烈不建议)。

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

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

classpath.txt 文件写入 exec:java 运行 使用的 class 路径(注意 runtime、[=34= 的范围] 对于 exec:java 运行)。 classpath.txt 文件中的路径将有效地指向位于 m2 根目录下的 jar 文件。

因此,重写 Maven 缓存会影响 classes 指向它作为 classpath,因为 Java 会加载 class at its first reference .


一种更健壮且对重现性友好的方法是作为发布的一部分生成 uber jar 并有效地冻结所需的依赖项(您的程序 classpath)并将它们包装到一个 jar 中提供程序和 classpath.

因此,不再有 parallel/external 干预会影响 运行ning 应用程序,同时保持现有的项目分离。


另一种方法是通过 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

因此,再次将您的项目与任何 concurrent/external 干预隔离开来。尽管对您的项目 releasing/distribution,更推荐使用 uber 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.

因此,很可能不是您的选择。

清除本地依赖和re-install,你也可以用maven来做:

mvn dependency:purge-local-repository

根据 doc:

The default behaviour is to first resolve the entire dependency tree, then delete the contents from the local repository, and then re-resolve the dependencies from the remote repository.