'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:
Start class
Run
(loads classRun
and waits onScanner.nextLine()
)Type
x
<ENTER> (loads classX
and outputsX loaded
X hello world
)Now while
Run
is running, edit something in classY
, for example body ofsomething()
method to:System.out.println("Y hello world new");
Re-install project
ProjA
usingmvn install
(which causes compilation of classY
packaging into target jar and installing packaged jar into local .m2 repository)Go back to running app and type
y
<ENTER>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 e.g. 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
Solution 1:[1]
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
, e.g.-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 to1.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.
Solution 2:[2]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Community |
Solution 2 | syl-oh |