Maven Surefire/Failsafe 中的类加载和分叉

本页讨论 Maven Surefire/Failsafe 下的类加载和分叉,着眼于解决问题。

执行摘要

如果遇到问题,您可能需要修改这三个设置:forkMode、useSystemClassLoader 和 useManifestOnlyJar。

Surefire 解决了什么问题?

最初,问题似乎很简单。只需使用类路径启动 Java,如下所示:

java -classpath foo.jar:bar.jar MyApp

但是这里有一个问题:在某些操作系统(Windows)上,您可以制作命令行的时间有限制,因此您可以制作类路径的时间也有限制。不同版本的 Windows 限制不同;在某些版本中,只允许几百个字符,而在其他版本中则为几千个,但无论哪种情况,限制都非常严格。

人们一般如何解决这个问题?

您可以使用两个“技巧”来解决此问题;在某些情况下,它们都可能导致其他问题。

1.隔离类加载器:一种解决方法是使用隔离类加载器。除了直接启动 MyApp 之外,我们还可以启动其他一些具有更短类路径的应用程序(“引导程序”)。然后,我们可以创建一个新的 java.lang.ClassLoader(通常是 java.net.URLClassLoader)并配置好您的类路径。然后引导程序可以从类加载器加载 MyApp;当 MyApp 引用其他类时,它们会自动从我们隔离的类加载器中加载。

使用隔离类加载器的问题在于您的类路径并不正确,并且某些应用程序可以检测到这个和对象。例如,系统属性“java.class.path”不会包含您的 jar;如果您的应用注意到这一点,则可能会导致问题。

使用隔离的类加载器还有另一个类似的问题:任何类都可以调用静态方法 ClassLoader.getSystemClassLoader() 并尝试从该类加载器中加载类,而不是使用默认的类加载器。如果类需要创建自己的类加载器,它们通常会这样做......不幸的是,基于 Java 的 Web 应用程序服务器(如 Jetty、Tomcat、BEA WebLogic 和 IBM WebSphere)很可能会尝试摆脱孤立的类加载器的限制。

2. Manifest-Only Jar:另一种解决方法是使用“manifest-only jar”。在这种情况下,您将创建一个几乎完全为空的临时 jar,但 META-INF/MANIFEST.MF 文件除外。Java 清单可以包含 Java VM 将作为指令尊重的属性;例如,您可以有一个“Class-Path”属性,其中包含要添加到类路径的其他 jar 列表。那么你可以像这样运行你的代码:

java -classpath booter.jar MyApp

这更现实一点,因为在这种情况下系统类加载器、线程上下文类加载器和默认类加载器都是相同的;不可能“逃避”类加载器。但这仍然是对“正常”类路径的奇怪模拟,应用程序仍然有可能注意到这一点。同样,java.class.path 可能不是您所期望的(“为什么它只包含一个 jar?”)。此外,可以查询系统类加载器以从中获取 jar 列表;如果您的应用程序在那里只找到我们的 booter.jar,它可能会感到困惑!

每种解决方案的优点/缺点

如果您的应用程序尝试查询其自己的类加载器以获取 jar 列表,则它在隔离类加载器下可能比在仅清单 jar 下工作得更好。但是,如果您的应用程序试图逃避其默认的类加载器,它可能根本无法在隔离的类加载器下运行。

使用隔离类加载器的一个优点是它是使用隔离类加载器的唯一方法,无需派生一个单独的进程,在与 Maven 本身相同的进程中运行所有测试。但这本身就有很大的风险,尤其是当 Maven 嵌入在您的 IDE 中运行时!

最后,当然,您可以尝试连接一个普通的旧 Java 类路径,并希望它足够短。最坏的情况是您的类路径可能在某些机器上运行,而在其他机器上运行不正常。Windows 机器的行为与 Linux 机器不同;短用户名的用户可能比长用户名的用户更成功,等等。出于这个原因,我们选择不将基本类路径设为默认值,尽管我们确实提供了它作为一个选项(主要是作为最后的手段)。

Surefire 是做什么的?

Surefire 提供了一种使用多种策略的机制。确定这一点的主要参数称为“useSystemClassLoader”。如果 useSystemClassLoader 为 true,那么我们使用 manifest-only jar;否则,我们使用隔离的类加载器。如果你想使用一个基本的普通 Java 类路径,你可以设置 useManifestOnlyJar=false,它只在 useSystemClassLoader=true 时有效。

(useSystemClassLoader 的默认值在 Surefire 2.3 和 Surefire 2.4 之间发生了变化,这是一个非常显着的变化。在 Surefire 2.3 中,useSystemClassLoader 默认为 false,我们使用了隔离的类加载器。在 Surefire 2.4 中,useSystemClassLoader 默认为 true。没有值适用于所有人,但我们认为这个默认值是一种改进;当我们使用 SystemClassLoader=true 时,一堆难以诊断的错误会变得更好。)

不幸的是,如果你的应用程序的 useSystemClassLoader 设置不正确,你就会遇到一个很难诊断的问题。您甚至可能被迫阅读像这样的长文档页面。;-)

如果您在加载类时遇到问题,请尝试设置 useSystemClassLoader=false 以查看是否有帮助。您可以使用下面的 POM 片段或通过设置“-Dsurefire.useSystemClassLoader=false”来做到这一点。如果这不起作用,请尝试将 useSystemClassLoader 设置回 true 并将 useManifestOnlyJar 设置为 false。

<project>
  [...]
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.5</version>
        <configuration>
          <useSystemClassLoader>false</useSystemClassLoader>
        </configuration>
        <executions>
          <execution>
            <id>integration-test</id>
            <goals>
              <goal>integration-test</goal>
            </goals>
          </execution>
          <execution>
            <id>verify</id>
            <goals>
              <goal>verify</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  [...]
</project>

调试类路径问题

如果您已经读到这里,您可能已经完全具备诊断类加载过程中可能出现的问题的能力。以下是一些尝试的一般提示:

  • 使用 --debug (aka -X) 运行 mvn 以获得更详细的输出
  • 检查你的 forkMode。如果 forkMode=never,则无法使用系统类加载器或普通的旧 Java 类路径;我们必须使用一个隔离的类加载器。
  • 如果您使用默认值,useSystemClassLoader=true 和 useManifestOnlyJar=false。在这种情况下,请查看生成的仅限清单的 surefire 引导程序 jar。打开它(它只是一个 zip)并阅读它的清单。
  • 使用 -Dmaven.failsafe.debug 运行 mvn,并使用调试器附加到正在运行的进程。