Java集成GradleToolingAPI编译Gradle项目

前情提要

在之前的文章中Java 集成Maven Embedder 编译 Maven 项目 已经学习了如何使用MavenEmbedder来编译Maven项目,接下来学习下如何使用GradleToolingAPI来编译Gradle项目。

解决方案

使用Java代码控制Gradle编译与Maven Embedder编译有所不同,需要额外安装Gradle工具到本地,MavenEmbedder则是直接通过集成jar的方式实现。所以在编译Gradle项目之前,需要先在本地安装对应版本的Gradle,无需配置环境变量。

安装Gradle

下载Gradle:https://gradle.org/releases/ 选择需要的版本安装即可 image.png

  • binary-only: 只有二进制文件
  • complete: 在二进制包基础上增加了 文档 , 源码

建议选择binary-only版本,我们只需要实现代码编译即可,无需引入其他内容。

引入依赖

<!-- gradle版本查询:https://mvnrepository.com/artifact/org.netbeans.external/gradle-tooling-api -->
<dependency>
    <groupId>org.netbeans.external</groupId>
    <artifactId>gradle-tooling-api</artifactId>
    <version>${gradle-tooling-api.version}</version>
</dependency>

笔者使用的版本为:RELEASE170

代码实现

GradleCommand

/**
 * @author: wick
 * @date: 2024/6/19 21:12
 * @description: gradle编译相关命令
 */

public class GradleCommand {
    /**
     * gradle命令
     */
    public static final String GRADLE = "gradle";

    /**
     * 清理构建产物
     */
    public static final String CLEAN = "clean";

    /**
     * 执行测试
     */
    public static final String TEST = "test";

    /**
     * 排除某个任务
     */
    public static final String EXCLUDE = "-x";

    /**
     * 编译 class
     */
    public static final String CLASSES = "classes";

    public static final List<String> COMMAND = new ArrayList<>() {{
        add(CLEAN);
        add(CLASSES);
    }};
}

Constant

public static final String JAVA_HOME = "java.home";
public static final File DEFAULT_GRADLE_USER_HOME = new File("~/.gradle");

GradleManager

/**
 * @author: wick
 * @date: 2024/6/19 21:06
 * @description: gradle编译接口
 */

public interface GradleBuildManager {
    /**
     * 代码编译
     *
     * @param dto dto
     */
    void compiler(CompileDTO dto);

    /**
     * 获取模块列表
     *
     * @param jdkPath jdk安装路径
     * @param gradlePath gradle 安装路径
     * @param codePath   代码路径
     * @return 模块列表
     */
    List<String> modules(String jdkPath,String gradlePath, String codePath);
}

GradleBuildManagerImpl

@Component
@Slf4j
public class GradleBuildManagerImpl implements GradleBuildManager {

    @Override
    public void compiler(CompileDTO dto) {
        log.info("开始编译Gradle项目,编译工具路径: {},代码路径: {}, 编译参数: {}", dto.getBuildToolPath(), dto.getCodePath(), dto.getCommands());
        long startTime = System.currentTimeMillis();
        // 重定向标准错误输出流
        ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
        PrintStream originalErrStream = System.err;
        // 重定向标准输出流
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PrintStream originalOutStream = System.out;
        try (ProjectConnection connection = GradleConnector.newConnector()
                .forProjectDirectory(new File(dto.getCodePath()))
                .useGradleUserHomeDir(CoverageConstant.DEFAULT_GRADLE_USER_HOME)
                .useInstallation(new File(dto.getBuildToolPath()))
                .connect()) {
            BuildLauncher build = connection.newBuild();
            build.setJavaHome(new File(dto.getJdkPath()));
            System.setErr(new PrintStream(errorStream));
            System.setOut(new PrintStream(outputStream));
            build.forTasks(dto.getCommands().toArray(new String[0]))
                    .setStandardOutput(System.out)
                    .setStandardError(System.err)
                    // 跳过单测,多线程编译
                    .withArguments(GradleCommand.EXCLUDE, GradleCommand.TEST)
                    // 不使用彩色日志,否则会导致日志中的颜色代码被打印出来,导致日志不易阅读
                    .setColorOutput(false)
                    // 限制 gradle 内存,防止编译过程中内存溢出,具体配置视服务器内存而定
                    .setJvmArguments("-Xmx512m");
            build.run();
            log.info("编译日志:\n {}", outputStream);
        } catch (Exception e) {
            log.error("代码: {} 编译失败, 异常详情: {}", dto.getCodePath(), ExceptionUtils.getRootCauseMessage(e));
            log.error("编译异常日志:\n {}", errorStream);
            throw new ServiceException("编译失败");
        } finally {
            System.setErr(originalErrStream);
            System.setOut(originalOutStream);
        }
        log.info("结束编译Gradle项目,编译耗时: {} s", (System.currentTimeMillis() - startTime) / 1000);
    }

    @Override
    public List<String> modules(String jdkPath, String gradlePath, String codePath) {
        String originJavaHome = System.getProperty(SystemPropertiesConstant.JAVA_HOME);
        System.setProperty(SystemPropertiesConstant.JAVA_HOME, jdkPath);
        log.info("开始扫描Gradle项目模块,编译工具路径: {},代码路径: {}", gradlePath, codePath);
        try (ProjectConnection connection = GradleConnector.newConnector()
                .forProjectDirectory(new File(codePath))
                .useInstallation(new File(gradlePath))
                .connect()) {
            GradleProject model = connection.getModel(GradleProject.class);
            return model.getChildren().stream().map(GradleProject::getName).collect(Collectors.toList());
        } catch (Exception e) {
            log.error("代码: {} 模块扫描失败, 异常详情: {}", codePath, ExceptionUtils.getRootCauseMessage(e));
            throw new ServiceException("模块扫描失败");
        } finally {
            System.setProperty(SystemPropertiesConstant.JAVA_HOME, originJavaHome);
        }
    }
}