1. 概述

Maven 对大多数Java项目来说是一个不可或缺的工具。它提供了一种方便的方式来运行和配置构建。然而,在某些情况下,我们可能需要对这个过程有更多的控制权。 从Java直接运行Maven构建使得它更具可配置性,因为我们可以根据运行时做出许多决策。

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

2. 学习平台

让我们通过一个具体的例子来更好地理解直接从Java与Maven交互的目标和实用性:想象一个面向初学者的Java学习平台,学生可以从各种主题中选择并完成作业。

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

为了从头创建项目,我们将使用Apache的maven-model库:

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

我们的构建器将采取简单的步骤,创建一个包含初始信息的Maven坐标文件:

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"程序之前处理依赖管理和配置可能会让初学者感到吃力。

此外,我们还希望引入一个可以与Maven从Java交互的包装器:

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);
}

目前,这个包装器只会编译项目。但是,我们可以扩展它以添加更多操作。

3. 统一执行器

首先,让我们检查一个可以运行简单脚本的工具。因此,解决方案并不局限于Maven,但我们可以运行mvn命令。我们有两个选项:Runtime.execProcessBuilder。它们非常相似,以至于我们可以使用一个额外的抽象类来处理异常:

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;
}

3.1. Runtime 执行器

让我们看看如何使用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需要运行的任何脚本和命令,这是一种相当直接的方法。

3.2. ProcessBuilder

另一个选项是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可以使用类似流式处理的管道。这样,我们就可以运行构建并触发额外的处理。

4. Maven API

现在,让我们考虑专门为Maven设计的解决方案。有两个选项:MavenEmbedderMavenInvoker

4.1. MavenEmbedder

虽然之前的解决方案不需要额外依赖,但这个解决方案需要使用以下包(https://mvnrepository.com/artifact/org.apache.maven/maven-embedder):

<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);
    }
}

4.2. MavenInvoker

另一个类似于MavenEmbedder的工具是MavenInvoker。要使用它,我们也需要导入一个库(https://mvnrepository.com/artifact/org.apache.maven.shared/maven-invoker):

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

它也提供了与Maven交互的漂亮高级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);
        }
    }
}

5. 测试

现在,我们可以确保创建和编译项目:

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进程可能对某些项目有益。

6. 总结

Maven根据Maven坐标文件配置和构建项目。然而,XML配置在处理动态参数和条件逻辑时并不理想。

我们可以通过直接从代码中运行它来设置Maven构建。实现这一点的最佳方式是使用特定的库,如MavenEmbedderMavenInvoker。同时,还有几种更底层的方法可以得到类似的结果。

像往常一样,本教程的所有代码都可以在GitHub上找到。