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.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;
}
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设计的解决方案。有两个选项:MavenEmbedder和MavenInvoker。
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构建。实现这一点的最佳方式是使用特定的库,如MavenEmbedder或MavenInvoker。同时,还有几种更底层的方法可以得到类似的结果。
像往常一样,本教程的所有代码都可以在GitHub上找到。