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

时间:2016-09-29 10:45:38

标签: java maven deployment classloader exec-maven-plugin

让我们说我有一个非常简单的maven项目ProjA,它本身没有依赖关系。此项目ProjA的课程XY如下:

class X

package proja;

public class X {

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

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

}

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我有一个班级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>

我使用ProjA安装项目mvn install,然后使用ProjB

编译项目mvn compile

现在,我使用以下方法从类Run运行main方法:
mvn exec:java -Dexec.mainClass="projb.Run"

然后我输入x&lt; ENTER&gt;得到了输出:

X loaded
X hello world

之后我输入y&lt; ENTER&gt;得到了输出:

Y loaded
Y hello world

现在,考虑一下行动的具体顺序:

  1. 开始上课Run(加载课程Run并等待Scanner.nextLine()

  2. 输入x&lt; ENTER&gt; (加载课程X并输出X loaded X hello world

  3. 现在Run正在投放时,请修改课程Y中的内容,例如something()方法的主体:System.out.println("Y hello world new");

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

  5. 返回正在运行的应用并输入y&lt; ENTER&gt;

  6. 现在加载类Y会导致:

  7. 堆栈追踪:

    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
    

    请注意,如果依赖项目中某些尚未卸载的类被更改,部署,然后从依赖项目(已经从依赖项目中加载了至少一个类)的类尝试加载此新更改,则此类加载错误仅可重现类。

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

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

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

    我知道不相容的方式(例如:在方法something(String str)中添加参数)的更改无论如何都会破坏。

    当项目ProjB中的某些内容发生更改和部署时,一种解决方法是重新启动项目ProjA中的所有内容。但是一些流程在启动时具有相对昂贵的初始操作,因此它不是一种选择。

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

    另一个选择是将所有项目合并为一个大项目,但是将不同内容分成不同项目的所有方法都将丢失。

    如何更好地组织我的develop-&gt; build-&gt;运行/重启流程,以便在某个进程启动时以及将来的某个时刻加载类,以便那些加载的类定义等于时间点在此流程启动之前构建的代码库是什么?

    修改

    添加ProjAProjB

    的pom文件

1 个答案:

答案 0 :(得分:1)

出现此问题的原因是exec-maven-plugin uses Maven类路径,即声明的执行Java main的依赖项。

  

在当前VM中执行提供的java类,并将封闭项目的依赖项作为类路径。

这些依赖项在本地Maven存储库.m2中具有物理jar,它们确实可以随时间变化(通过对相关项目的install并行调用)并在{的情况下重写{1}}依赖关系(尊重约定,但您也可以重写已发布的版本,但强烈建议不要这样做。)

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

SNAPSHOT

mvn dependency:build-classpath -Dmdep.outputFile=classpath.txt -DincludeScope=runtime 文件写入classpath.txt运行使用的类路径(请注意exec:java的范围,runtime运行的defaultexec:java文件中的路径将有效指向位于classpath.txt根目录下的jar文件。

因此,重写Maven缓存会影响指向它的类作为类路径,因为Java会加载类at its first reference

更强大且可重现性更好的方法是在发布中生成uber jar并有效地冻结所需的依赖项(程序类路径)并将它们包装到一个提供程序和类路径的jar中。 / p>

因此,没有更多的并行/外部干预可能会影响正在运行的应用程序,同时保持现有的项目分离。

另一种方法是通过versions:lock-snapshots锁定以前生成的m2版本的依赖项目:

  

在pom中搜索所有SNAPSHOT版本,并将其替换为-SNAPSHOT的当前时间戳版本,例如-SNAPSHOT

同样,再次将您的项目与任何并发/外部干预隔离开来。虽然在发布/分发项目时,更推荐使用超级jar方法。

此外,锁定快照只有在本地存储库安装时通过Maven存储库not working可用时才有效:

  

尝试将解锁的快照依赖项版本解析为构建中使用的锁定时间戳版本。例如,像-20090327.172306-4这样的未锁定快照版本可以解析为1.0-SNAPSHOT。如果时间戳快照不可用,则版本将保持不变。如果依赖项仅在本地存储库中可用而不在远程快照存储库中,则会出现这种情况。

因此,在你的情况下,很可能不是一个选项。