简体   繁体   中英

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. This project ProjA has classes X and Y as follows:

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>

Next I have a second maven project ProjB which has project ProjA as dependency.

I project ProjB I have a class Run as follows:

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

I install project ProjA using mvn install and then compile project ProjB using mvn compile

Now, I run main method from class Run using:
mvn exec:java -Dexec.mainClass="projb.Run"

Then I type x <ENTER> and got output:

X loaded
X hello world

After that I type y <ENTER> and got output:

Y loaded
Y hello world

Now, consider specific ordering of actions:

  1. Start class Run (loads class Run and waits on Scanner.nextLine() )

  2. Type x <ENTER> (loads class X and outputs 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");

  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)

  5. Go back to running app and type y <ENTER>

  6. Now loading of class Y causes:

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.

Project and class structure is just extracted as concept from bigger system which has many more classes with main() methods. And many of them run on same machines in parallel in separate JVMs.

Question : How can I prevent this from happening?

Note, I don't need any kind of dynamic class reloading at runtime.

I know that changes in incompatible ways (example: add a parameter in method something(String str) ) would break no matter what.

One workaround would be to restart everything in project ProjB when something in project ProjA is changed and deployed. 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 . But this is overkill for me and could cause a lot of unnecessary class loads and potentialy lead to 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?

Edit

Add pom files of ProjA and ProjB

The issue occurs because the exec-maven-plugin uses the Maven classpath, that is, the declared dependencies to execute your Java main.

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

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

You can check that by running 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). Paths in the classpath.txt file would effectively point to the jar files located under the m2 root.

Hence, rewrite to the Maven cache would impact classes pointing to it as classpath, because Java would load the class at its first reference .


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.

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 :

searches the pom for all -SNAPSHOT versions and replaces them with the current timestamp version of that -SNAPSHOT , eg -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.

Also, locking snapshots would only work if available via a Maven repository, not working on local repository installations:

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.

Hence, most probably not an option in your case.

To purge local dependencies and re-install, you can also do with 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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