[英]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
有类X
和Y
如下:
package proja;
public class X {
static {
System.out.println("X loaded");
}
public void something() {
System.out.println("X hello world");
}
}
package proja;
public class Y {
static {
System.out.println("Y loaded");
}
public void something() {
System.out.println("Y hello world");
}
}
<?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
如下:
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();
}
}
<?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:现在,考虑特定的操作顺序:
Start class Run
(loads class Run
and waits on Scanner.nextLine()
)开始 class
Run
(加载 class Run
并等待Scanner.nextLine()
)
Type x
<ENTER> (loads class X
and outputs X loaded
X hello world
)键入
x
<ENTER>(加载 class X
并输出X loaded
X hello world
)
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");
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 存储库中)
Go back to running app and type y
<ENTER> Go 回到正在运行的应用程序并键入
y
<ENTER>
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 流程,以便在启动某些进程时以及在将来的某个时间加载类,以便那些加载的类定义等于在时间之前构建的代码库的时间点这个进程的启动?
Add pom files of ProjA
and ProjB
添加
ProjA
和ProjB
的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 to1.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
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.