简体   繁体   English

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

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

Let's say I have a very simple maven project ProjA which has no dependencies itself.假设我有一个非常简单的 maven 项目ProjA ,它本身没有依赖项。 This project ProjA has classes X and Y as follows:这个项目ProjA有类XY如下:

class X class ×

package proja;

public class X {

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

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

}

class Y class 是

package proja;

public class Y {

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

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

}

ProjA.pom 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>

Next I have a second maven project ProjB which has project ProjA as dependency.接下来我有第二个 maven 项目ProjB ,它有项目ProjA作为依赖项。

I project ProjB I have a class Run as follows:我项目ProjB我有一个 class Run如下:

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 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>

I install project ProjA using mvn install and then compile project ProjB using mvn compile我使用mvn install安装项目ProjA ,然后使用mvn compile编译项目ProjB

Now, I run main method from class Run using:现在,我从 class 运行主要方法Run使用:
mvn exec:java -Dexec.mainClass="projb.Run"

Then I type x <ENTER> and got output:然后我键入x <ENTER> 并得到 output:

X loaded
X hello world

After that I type y <ENTER> and got output:之后我输入y <ENTER> 并得到 output:

Y loaded
Y hello world

Now, consider specific ordering of actions:现在,考虑特定的操作顺序:

  1. Start class Run (loads class Run and waits on Scanner.nextLine() )开始 class Run (加载 class Run并等待Scanner.nextLine()

  2. Type x <ENTER> (loads class X and outputs X loaded X hello world )键入x <ENTER>(加载 class X并输出X loaded X hello world

  3. Now while Run is running, edit something in class Y , for example body of something() method to: System.out.println("Y hello world new");现在,当Run运行时,在 class Y中编辑一些东西,例如something()方法的主体: System.out.println("Y hello world new");

  4. Re-install project ProjA using mvn install (which causes compilation of class Y packaging into target jar and installing packaged jar into local.m2 repository)使用mvn install重新安装项目ProjA (这会导致将 class Y打包编译到目标 jar 并将打包的 jar 安装到 local.m2 存储库中)

  5. Go back to running app and type y <ENTER> Go 回到正在运行的应用程序并键入y <ENTER>

  6. Now loading of class Y causes:现在加载 class Y会导致:

Stack trace:堆栈跟踪:

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

Note that this class-loading error is only reproducible if some yet unloaded class in dependency project is changed, deployed and then class from dependant project (which already has at least one class loaded from dependency project) tries to load this newly changed class.请注意,此类加载错误只有在依赖项目中一些尚未卸载的 class 被更改、部署,然后来自依赖项目的 class(已经至少有一个 class 从依赖项目加载)尝试加载这个新更改的 class 时才会重现。

Project and class structure is just extracted as concept from bigger system which has many more classes with main() methods.项目和 class 结构只是作为概念从更大的系统中提取出来的,这个系统有更多的类和main()方法。 And many of them run on same machines in parallel in separate JVMs.其中许多在不同的 JVM 中并行运行在同一台机器上。

Question : How can I prevent this from happening?问题:如何防止这种情况发生?

Note, I don't need any kind of dynamic class reloading at runtime.请注意,我不需要在运行时重新加载任何类型的动态 class 。

I know that changes in incompatible ways (example: add a parameter in method something(String str) ) would break no matter what.我知道以不兼容的方式进行更改(例如:在方法something(String str)中添加参数)无论如何都会中断。

One workaround would be to restart everything in project ProjB when something in project ProjA is changed and deployed.一种解决方法是在更改和部署项目ProjA中的某些内容时重新启动项目ProjB中的所有内容。 But some of processes have relatively costly initial operations on startup so it's not an option.但是有些进程在启动时的初始操作成本相对较高,因此这不是一种选择。

Another workaround would be to somehow force (using eg Reflections Library ) class loading of all classes from project ProjA on startup of each process from project ProjB .另一种解决方法是以某种方式强制(使用例如Reflections Library )class 在项目ProjB的每个进程启动时从项目ProjA加载所有类。 But this is overkill for me and could cause a lot of unnecessary class loads and potentialy lead to OutOfMemoryException .但这对我来说太过分了,可能会导致很多不必要的 class 负载并可能导致OutOfMemoryException

Yet another option wold be to merge all projects into one big project, but then all point of separating different stuff into different projects would be lost.另一种选择是将所有项目合并到一个大项目中,但是这样就失去了将不同的东西分成不同项目的所有意义。

How can I better organize my develop->build->run/restart flow so that when some process is started and in some point in future it loads classes, so that those loaded classes definitions are equal to point in time of codebase builded before time of this process's startup?我怎样才能更好地组织我的 develop->build->run/restart 流程,以便在启动某些进程时以及在将来的某个时间加载类,以便那些加载的类定义等于在时间之前构建的代码库的时间点这个进程的启动?

Edit编辑

Add pom files of ProjA and ProjB添加ProjAProjB的pom文件

The issue occurs because the exec-maven-plugin uses the Maven classpath, that is, the declared dependencies to execute your Java main. 出现此问题是因为exec-maven-plugin 使用 Maven类路径,即声明的依赖项来执行Java main。

Executes the supplied java class in the current VM with the enclosing project's dependencies as classpath. 在当前VM中执行提供的java类,并将封闭项目的依赖项作为类路径。

These dependencies have their physical jars in the local Maven repository, .m2 , which indeed can change over time (by parallel invocations of install on concerned projects) and be re-written in case of SNAPSHOT dependencies (to respect the conventions, but you could also rewrite released versions, although strongly not advised). 这些依赖项在本地Maven存储库中有物理jar, .m2 ,它确实可以随着时间的推移而改变(通过在相关项目上install的并行调用),并在SNAPSHOT依赖项的情况下重写(为了遵守约定,但你可以也重写发布的版本,虽然强烈不建议)。

You can check that by running dependency:build-classpath . 您可以通过运行dependency:build-classpath来检查它。

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

Would write to the classpath.txt file the classpath used by the exec:java run (note the scope to runtime , default for the exec:java run). classpath.txt文件写入exec:java run使用的类路径(注意该范围为runtime默认exec:java run)。 Paths in the classpath.txt file would effectively point to the jar files located under the m2 root. classpath.txt文件中的classpath.txt将有效地指向位于m2根目录下的jar文件。

Hence, rewrite to the Maven cache would impact classes pointing to it as classpath, because Java would load the class at its first reference . 因此,重写到Maven缓存会影响指向它的类作为类路径,因为Java会在第一次引用时加载该类。


A more robust and reproducibility-friendly approach would be to generate as part of the release an uber jar and effectively freezing the required dependencies (your program classpath) and wrapping them into one jar providing both program and classpath. 一种更强大且可重现性更友好的方法是在发布中生成一个超级jar并有效地冻结所需的依赖项(您的程序类路径)并将它们包装到一个提供程序和类路径的jar中。

As such, no more parallel/external interventions could affect the running application, while keeping the existing separation of projects. 因此,在保持项目的现有分离的同时,不再有并行/外部干预可能影响正在运行的应用程序。


Another approach would be to lock the previously generated SNAPSHOT versions of dependent projects, via the versions:lock-snapshots : 另一种方法是通过以下versions:lock-snapshots以前生成的依赖项目的SNAPSHOT versions:lock-snapshots

searches the pom for all -SNAPSHOT versions and replaces them with the current timestamp version of that -SNAPSHOT , eg -20090327.172306-4 搜索POM所有-SNAPSHOT版本,并与当前的时间戳版本替换它们-SNAPSHOT ,如-20090327.172306-4

and as such, again, isolate your project from any concurrent/external interventions. 因此,再次将您的项目与任何并发/外部干预隔离开来。 Although towards releasing/distribution of your project, the uber jar approach is more recommended. 虽然在发布/分发项目时,更推荐使用超级jar方法。

Also, locking snapshots would only work if available via a Maven repository, not working on local repository installations: 此外,锁定快照仅在通过Maven存储库可用时才起作用, 而不是在本地存储库安装上工作:

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 . 例如, 1.0-SNAPSHOT等未锁定的快照版本可以解析为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. 如果依赖项仅在本地存储库中可用而在远程快照存储库中不可用,则会出现这种情况。

Hence, most probably not an option in your case. 因此,在您的情况下,很可能不是一个选项。

To purge local dependencies and re-install, you can also do with maven:要清除本地依赖项并重新安装,您还可以使用 maven:

mvn dependency:purge-local-repository

As per the 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.默认行为是先解析整个依赖树,然后从本地仓库中删除内容,然后从远程仓库中重新解析依赖。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM