在Java代码中运行Maven

Maven是大多数 Java 项目不可或缺的工具。它提供了一种运行和配置构建的便捷方法。然而,在某些情况下,我们需要对流程进行更多控制。从 Java 运行 Maven 构建使其更加可配置,因为我们可以在运行时做出许多决定。

在本教程中,我们将学习如何与 Maven 交互并直接从代码运行构建。

让我们考虑以下示例,以便更好地理解直接从 Java 使用 Maven 的目标和用处:想象一个 Java 学习平台,学生可以在其中选择各种主题并完成作业。

由于我们的平台主要针对初学者,因此我们希望尽可能简化整个体验。因此,学生可以选择 他们想要的任何主题,甚至可以将它们组合起来。我们在服务器上生成项目,学生在线完成。

要从头开始生成项目,我们将使用Apache 的maven-model库:

<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-model</artifactId>
    <version>3.9.6</version>
</dependency>

我们的构建器将采取简单的步骤来创建包含初始信息的POM文件:

public class ProjectBuilder {
    // constants
    public ProjectBuilder addDependency(String groupId, String artifactId, String version) {
        Dependency dependency = new Dependency();
        dependency.setGroupId(groupId);
        dependency.setArtifactId(artifactId);
        dependency.setVersion(version);
        dependencies.add(dependency);
        return this;
    }
    public ProjectBuilder setJavaVersion(JavaVersion version) {
        this.javaVersion = version;
        return this;
    }
    public void build(String userName, Path projectPath, String packageName) throws IOException {
        Model model = new Model();
        configureModel(userName, model);
        dependencies.forEach(model::addDependency);
        Build build = configureJavaVersion();
        model.setBuild(build);
        MavenXpp3Writer writer = new MavenXpp3Writer();
        writer.write(new FileWriter(projectPath.resolve(POM_XML).toFile()), model);
        generateFolders(projectPath, SRC_TEST);
        Path generatedPackage = generateFolders(projectPath,
          SRC_MAIN_JAVA +
            packageName.replace(PACKAGE_DELIMITER, FileSystems.getDefault().getSeparator()));
        String generatedClass = generateMainClass(PACKAGE + packageName);
        Files.writeString(generatedPackage.resolve(MAIN_JAVA), generatedClass);
   }
   
// utility methods
}

首先,我们确保所有学生都有正确的环境。其次,我们减少了他们需要采取的步骤,从获得作业到开始编码。设置环境可能很简单,但在编写第一个“Hello World”程序之前处理依赖关系管理和配置对于初学者来说可能有点困难。

另外,我们想引入一个可以从 Java 与 Maven 交互的包装器:

public interface Maven {
    String POM_XML = "pom.xml";
    String COMPILE_GOAL =
"compile";
    String USE_CUSTOM_POM =
"-f";
    int OK = 0;
    String MVN =
"mvn";
    void compile(Path projectFolder);
}

目前,这个包装器只会编译该项目。但是,我们可以通过其他操作来扩展它。

通用执行器
首先,让我们检查一下可以运行简单脚本的工具。因此,该解决方案并非特定于 Maven,但我们可以运行mvn命令。我们有两个选项:Runtime.exec 和ProcessBuilder。 它们非常相似,我们可以使用一个额外的抽象类来处理异常:

public abstract class MavenExecutorAdapter implements Maven {
    @Override
    public void compile(Path projectFolder) {
        int exitCode;
        try {
            exitCode = execute(projectFolder, COMPILE_GOAL);
        } catch (InterruptedException e) {
            throw new MavenCompilationException("Interrupted during compilation", e);
        } catch (IOException e) {
            throw new MavenCompilationException(
"Incorrect execution", e);
        }
        if (exitCode != OK) {
            throw new MavenCompilationException(
"Failure during compilation: " + exitCode);
        }
    }
    protected abstract int execute(Path projectFolder, String compileGoal)
      throws InterruptedException, IOException;
}

1.运行时执行器
让我们检查一下如何使用Runtime.exec(String[])运行一个简单的命令:

public class MavenRuntimeExec extends MavenExecutorAdapter {
    @Override
    protected int execute(Path projectFolder, String compileGoal) throws InterruptedException, IOException {
        String[] arguments = {MVN, USE_CUSTOM_POM, projectFolder.resolve(POM_XML).toString(), COMPILE_GOAL};
        Process process = Runtime.getRuntime().exec(arguments);
        return process.waitFor();
    }
}

对于我们需要从 Java 运行的任何脚本和命令来说,这是一种非常简单的方法。

2.流程生成器
另一个选择是ProcessBuilder。它与之前的解决方案类似,但提供了稍微更好的 API:

public class MavenProcessBuilder extends MavenExecutorAdapter {
    private static final ProcessBuilder PROCESS_BUILDER = new ProcessBuilder();
    protected int execute(Path projectFolder, String compileGoal) throws IOException, InterruptedException {
        Process process = PROCESS_BUILDER
          .command(MVN, USE_CUSTOM_POM, projectFolder.resolve(POM_XML).toString(), compileGoal)
          .start();
        return process.waitFor();
    }
}

从Java 9开始,ProcessBuilder可以使用类似于Streams 的管道。这样,我们就可以运行构建并触发额外的处理。

Maven API
现在,让我们考虑一下为 Maven 量身定制的解决方案。有两个选项:MavenEmbedder和MavenInvoker。

1. Maven嵌入器
虽然以前的解决方案不需要任何额外的依赖项,但对于这个解决方案,我们需要使用以下包:

<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-embedder</artifactId>
    <version>3.9.6</version>
</dependency>

这个库为我们提供了高级API并简化了与Maven的交互:

public class MavenEmbedder implements Maven {
    public static final String MVN_HOME = "maven.multiModuleProjectDirectory";
    @Override
    public void compile(Path projectFolder) {
        MavenCli cli = new MavenCli();
        System.setProperty(MVN_HOME, projectFolder.toString());
        cli.doMain(new String[]{COMPILE_GOAL}, projectFolder.toString(), null, null);
    }
}

2. Maven调用者
另一个与MavenEmbedder类似的工具是MavenInvoker。要使用它,我们还需要导入一个库:

<dependency>
    <groupId>org.apache.maven.shared</groupId>
    <artifactId>maven-invoker</artifactId>
    <version>3.2.0</version>
</dependency>

它还提供了一个很好的高级 API 进行交互:

public class MavenInvoker implements Maven {
    @Override
    public void compile(Path projectFolder) {
        InvocationRequest request = new DefaultInvocationRequest();
        request.setPomFile(projectFolder.resolve(POM_XML).toFile());
        request.setGoals(Collections.singletonList(Maven.COMPILE_GOAL));
        Invoker invoker = new DefaultInvoker();
        try {
            InvocationResult result = invoker.execute(request);
            if (result.getExitCode() != 0) {
                throw new MavenCompilationException("Build failed", result.getExecutionException());
            }
        } catch (MavenInvocationException e) {
            throw new MavenCompilationException(
"Exception during Maven invocation", e);
        }
    }
}

测试
现在,我们可以确保创建并编译一个项目:

class MavenRuntimeExecUnitTest {
    private static final String PACKAGE_NAME = "com.baeldung.generatedcode";
    private static final String USER_NAME =
"john_doe";
    @TempDir
    private Path tempDir;
    @BeforeEach
    public void setUp() throws IOException {
        ProjectBuilder projectBuilder = new ProjectBuilder();
        projectBuilder.build(USER_NAME, tempDir, PACKAGE_NAME);
    }
    @ParameterizedTest
    @MethodSource
    void givenMavenInterface_whenCompileMavenProject_thenCreateTargetDirectory(Maven maven) {
        maven.compile(tempDir);
        assertTrue(Files.exists(tempDir));
    }
    static Stream<Maven> givenMavenInterface_whenCompileMavenProject_thenCreateTargetDirectory() {
        return Stream.of(
          new MavenRuntimeExec(),
          new MavenProcessBuilder(),
          new MavenEmbedder(),
          new MavenInvoker());
    }
}

我们从头开始生成一个对象,并直接从 Java 代码编译它。虽然我们不会每天遇到这样的需求,但自动化 Maven 流程可能会让某些项目受益。

结论
Maven 根据 POM 文件配置并构建项目。但是,XML 配置不能很好地处理动态参数和条件逻辑。

我们可以利用 Java 代码直接从代码运行来设置 Maven 构建。实现此目的的最佳方法是使用特定的库,例如MavenEmbedder或MavenInvoker。同时,还有几种更底层的方法可以获得类似的结果。