自定义 Gradle 插件进行统一的静态代码分析

静态代码分析是一项了不起的技术, 它能让代码库更易于维护. 但是, 如果你在不同的版本库中拥有多个服务(可能由不同的团队开发), 如何才能让每个人都遵循既定的代码风格呢? 一个好办法是将所有规则封装在一个插件中, 该插件会在每个项目构建时自动执行所需的验证.

因此, 在本文中我将向你展示:

  1. 如何创建带有自定义 PMD 和 Checkstyle 规则的 Gradle 插件.
  2. 如何发布到 plugins.gradle.org.
  3. 如何使用 GitHub Actions 自动执行发布流程.

你可以查看本仓库中的代码示例.

PMD, Checkstyle 和多仓库的难点

PMDCheckstyle 是静态分析工具, 可在每次项目构建时检查代码. 通过 Gradle, 可以轻松应用它们.

bash 复制代码
plugins {
    id 'java'
    id 'pmd'
    id 'checkstyle'
}

现在, 你可以按照自己的方式调整每个插件.

ini 复制代码
checkstyle {
    toolVersion = '10.5.0'
    ignoreFailures = false
    maxWarnings = 0
    configFile = file(pathToCheckstyleConfig)
}

pmd {
    consoleOutput = true
    toolVersion = '6.52.0'
    ignoreFailures = false
    ruleSetFiles = file(pathToPmdConfig)
}

如果你的整个项目(甚至是公司)都是单仓库, 那么这样的设置绝对没问题. 你只需将这些配置放入根build.gradle文件中, 就能将这些插件应用到现有的每个模块中. 但如果你选择的是多仓库呢?

如果你想在公司内开发人员正在开发的所有项目(以及程序员将来创建的所有项目)中共享相同的代码风格, 该怎么办?

那么, 你可以告诉他们只需复制并粘贴插件的配置即可. 无论如何, 这种方法容易出错. 总有人可能会配置错误.

事实上, 我们需要在每个可行的项目中以某种方式重复使用已定义的代码样式配置. 答案很简单. 我们需要一个定制的 Gradle 插件来封装 PMD 和 Checkstyle 规则.

自定义 Gradle 插件

构建配置

请看下面的 build.gradle 声明. 这是 Gradle 插件项目的基本设置.

bash 复制代码
plugins {
    id 'java-gradle-plugin'
    id 'com.gradle.plugin-publish' version '1.1.0'
}

group = 'io.github.simonharmonicminor.code.style'
sourceCompatibility = '8'

repositories {
    mavenCentral()
}

ext {
    set('lombokVersion', '1.18.24')
}

dependencies {
    compileOnly "org.projectlombok:lombok:${lombokVersion}"
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
}

gradlePlugin {
    website = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    vcsUrl = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    plugins {
        gradleCodeStylePluginExample {
            id = 'io.github.simonharmonicminor.code.style'
            displayName = 'Gradle Plugin Code Style Example'
            description = 'Predefined Checkstyle and PMD rules'
            implementationClass = 'io.github.simonharmonicminor.code.style.CodingRulesGradlePluginPlugin'
            tags.set(['codestyle', 'checkstyle', 'pmd'])
        }
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

现在让我们从 plugins 块开始, 一步步解构配置. 请看下面的代码片段.

bash 复制代码
plugins {
    id 'java-gradle-plugin'
    id 'com.gradle.plugin-publish' version '1.1.0'
}

java-gradle-plugin命令会启用常规 Gradle 插件项目的任务. com.gradle.plugin-publish命令允许打包插件并发布到plugins.gradle.org.

我最近正在向你展示整个发布过程.

然后是基本的项目配置.

ini 复制代码
group = 'io.github.simonharmonicminor.code.style'
sourceCompatibility = '8'

repositories {
    mavenCentral()
}

group定义了groupId, 以符合Apache Maven 命名规范. sourceCompatibility是目标 Java 二进制文件的版本. 虽然 Java 8 现在已经过时, 但我还是建议你使用公司开发人员使用的最早 JDK 版本构建 Gradle 插件. 否则, 你会阻碍他们遵循你的代码风格指南.

然后是 dependencies 范围.

bash 复制代码
ext {
    set('lombokVersion', '1.18.24')
}

dependencies {
    compileOnly "org.projectlombok:lombok:${lombokVersion}"
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
}

这里没什么特别的. 接下来是发布配置.

ini 复制代码
website = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    vcsUrl = 'https://github.com/SimonHarmonicMinor/gradle-code-style-plugin-example'
    plugins {
        gradleCodeStylePluginExample {
            id = 'io.github.simonharmonicminor.code.style'
            displayName = 'Gradle Plugin Code Style Example'
            description = 'Predefined Checkstyle and PMD rules'
            implementationClass = 'io.github.simonharmonicminor.code.style.CodingRulesGradlePluginPlugin'
            tags.set(['codestyle', 'checkstyle', 'pmd'])
        }
    }
}

websitevcsUrl应指向包含插件源代码的公共 Git 仓库. plugins块定义了项目中Plugin接口的每个实现. 最后,tags只是在注册表中搜索插件的hash标签.

当你将 Gradle 插件发布到 plugins.gradle.org 时, 包的名称至关重要. 你的插件代码应该可以在 GitHub 上找到. 如果不是开源的, 发布时可能会遇到问题. 那么, 你可以将软件包名称声明为io.github.your_github_login.any.package.you.like.

但是, 如果你想使用其他名称, 如com.mycompany.my.plugin, 请确保域名mycompany.com. 否则, Gradle 工程师可能会拒绝发布.

注意 Gradle 禁止plugingradle作为标签值. 在gradle publishPlugins任务执行过程中, 这样的构建会失败.

最后是JUnit 5的配置.

javascript 复制代码
tasks.named('test') {
    useJUnitPlatform()
}

插件代码

我想向大家展示整个插件的代码. 然后我将向你解释每个细节. 请看下面的代码片段.

arduino 复制代码
public class CodingRulesGradlePluginPlugin implements Plugin<Project> {

  @Override
  public void apply(Project project) {
    project.getPluginManager().apply("checkstyle");
    project.getExtensions().configure(CheckstyleExtension.class, checkstyleExtension -> {
      checkstyleExtension.setToolVersion("10.5.0");
      checkstyleExtension.setIgnoreFailures(false);
      checkstyleExtension.setMaxWarnings(0);
      checkstyleExtension.setConfigFile(
          FileUtil.copyContentToTempFile("style/checkstyle.xml", ".checkstyle.xml")
      );
    });

    project.getPluginManager().apply("pmd");
    project.getExtensions().configure(PmdExtension.class, pmdExtension -> {
      pmdExtension.setConsoleOutput(true);
      pmdExtension.setToolVersion("6.52.0");
      pmdExtension.setIgnoreFailures(false);
      pmdExtension.setRuleSets(emptyList());
      pmdExtension.setRuleSetFiles(project.files(
          FileUtil.copyContentToTempFile("style/pmd.xml", ".pmd.xml")
      ));
    });

    final SortedSet<String> checkstyleTaskNames = project.getTasks()
        .withType(Checkstyle.class)
        .getNames();

    final SortedSet<String> pmdTaskNames = project.getTasks()
        .withType(Pmd.class)
        .getNames();

    project.task(
        "runStaticAnalysis",
        task -> task.setDependsOn(
            Stream.concat(
                checkstyleTaskNames.stream(),
                pmdTaskNames.stream()
            ).collect(Collectors.toList())
        )
    );
  }
}

最明显也是最重要的细节是, 每个插件任务都必须实现 Gradle Plugin 接口.

typescript 复制代码
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class CodingRulesGradlePluginPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) { ... }
}

然后我在配置 Checkstyle 任务. 我只需应用 checkstyle 插件, 获取 CheckstyleConfiguration 并覆盖我想要的属性. 请看下面的代码块.

arduino 复制代码
project.getPluginManager().apply("checkstyle");
project.getExtensions().configure(CheckstyleExtension.class, checkstyleExtension -> {
  checkstyleExtension.setToolVersion("10.5.0");
  checkstyleExtension.setIgnoreFailures(false);
  checkstyleExtension.setMaxWarnings(0);
  checkstyleExtension.setConfigFile(
      FileUtil.copyContentToTempFile("style/checkstyle.xml", ".checkstyle.xml")
  );
});

FileUtil.copyContentToTempFile函数需要解释一下. 我把 Checkstyle 配置放到了 src/main/resources/style/checkstyle.xml 文件中. 但是, 如果你直接指向它, 那么人们在他们的项目中应用你的 Gradle 时就会得到奇怪的错误信息. 有一些变通方法, 但最简单的方法是将内容复制到临时文件中.

看看下面的 PMD 配置. 与 Checkstyle 类似.

arduino 复制代码
project.getPluginManager().apply("pmd");
project.getExtensions().configure(PmdExtension.class, pmdExtension -> {
  pmdExtension.setConsoleOutput(true);
  pmdExtension.setToolVersion("6.52.0");
  pmdExtension.setIgnoreFailures(false);
  pmdExtension.setRuleSets(emptyList());
  pmdExtension.setRuleSetFiles(project.files(
      FileUtil.copyContentToTempFile("style/pmd.xml", ".pmd.xml")
  ));
});

现在我们准备就绪. 我们可以将其应用到实际项目中. 虽然也有一点改进. 请看下面的代码片段.

scss 复制代码
final SortedSet<String> checkstyleTaskNames = project.getTasks()
    .withType(Checkstyle.class)
    .getNames();

final SortedSet<String> pmdTaskNames = project.getTasks()
    .withType(Pmd.class)
    .getNames();

project.task(
    "runStaticAnalysis",
    task -> task.setDependsOn(
        Stream.concat(
            checkstyleTaskNames.stream(),
            pmdTaskNames.stream()
        ).collect(Collectors.toList())
    )
);

runStaticAnalysis任务会触发所有 Checkstyle 和 PMD 任务按顺序运行. 当你想在创建拉取请求前验证整个项目时, 它就派上用场了. 如果直接在build.gradle中添加runStaticAnalysis任务, 它将看起来像这样:

markdown 复制代码
task runStaticAnalysis {
    dependsOn checkstyleMain, checkstyleTest, pmdMain, pmdTest
}

测试

那么测试呢? 最好是在构建过程中跟踪错误, 而不是在开发人员已经将插件应用到他们的项目中时. 虽然 Gradle 提供了用于功能测试的 Gradle TestKit, 但我展示的情况比较简单, 单元测试就足够了.

同样, 我将一次性展示整段代码, 然后指出重要的细节.

scss 复制代码
class CodingRulesGradlePluginPluginTest {

  @Test
  void shouldApplyPluginSuccessfully() {
    final Project project = ProjectBuilder.builder().build();
    project.getPluginManager().apply("java");

    assertDoesNotThrow(
        () -> new CodingRulesGradlePluginPlugin().apply(project)
    );

    final Task task = project.getTasks().getByName("runStaticAnalysis");
    assertNotNull(task, "runStaticAnalysis task should be registered");
    final Set<String> codeStyleTasks =
        Stream.of("checkstyleMain", "checkstyleTest", "pmdTest", "pmdMain").collect(toSet());
    assertTrue(
        task.getDependsOn().containsAll(codeStyleTasks),
        format(
            "Task runStaticAnalysis should contain '%s' tasks, but actually: %s",
            codeStyleTasks,
            task.getDependsOn()
        )
    );
  }
}

首先是 Gradle 项目实例化测试. 请看下面的代码片段.

ini 复制代码
import org.gradle.testfixtures.ProjectBuilder;
import org.gradle.api.Project;

final Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply("java");

Gradle 为单元测试提供了一些固定装置. ProjectBuilder创建了一个与 API 兼容的Project接口实现. 因此, 你可以放心地将它传递给 YourPluginClass.apply 方法.

在调用业务逻辑之前, 我们还要手动应用 java 插件. 我们的插件针对 Java 应用程序. 因此, 传递 Java 配置的 Project 实现是很自然的.

然后, 我们只需调用自定义插件方法并传递配置的 Project 实现.

scss 复制代码
assertDoesNotThrow(
    () -> new CodingRulesGradlePluginPlugin().apply(project)
);

之后是断言. 我们需要确保 runStaticAnalysis 任务注册成功.

ini 复制代码
final Task task = project.getTasks().getByName("runStaticAnalysis");
assertNotNull(task, "runStaticAnalysis task should be registered");

如果存在, 我们将根据现有的 Checkstyle 和 PMD 任务验证该任务.

less 复制代码
final Set<String> codeStyleTasks =
    Stream.of("checkstyleMain", "checkstyleTest", "pmdTest", "pmdMain").collect(toSet());
assertTrue(
    task.getDependsOn().containsAll(codeStyleTasks),
    format(
        "Task runStaticAnalysis should contain '%s' tasks, but actually: %s",
        codeStyleTasks,
        task.getDependsOn()
    )
);

这是我们在将插件推送到 plugins.gradle.org/ 之前应该测试的最基本情况.

使用 GitHub Actions 发布插件

当你在 plugins.gradle.org/ 上注册一个新账户时, 进入你的页面并打开 API Keys 选项卡. 你应该生成新的密钥. 会有两个.

ini 复制代码
gradle.publish.key=...
gradle.publish.secret=...

然后, 打开版本库的Settings, 转到Secrets and Variables -> Actions项. 你必须把获得的密钥存储为版本库秘密.

最后是 GitHub Actions 的构建配置.

我把自己的文件放在了.github/workflow/build.yml.

请看下面的整个设置. 然后, 我将告诉你特定区块的含义.

yaml 复制代码
name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build
  publish:
    needs:
      - build
    if: github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - name: Auto Increment Semver Action
        uses: MCKanpolat/auto-semver-action@1.0.5
        id: versioning
        with:
          releaseType: minor
          incrementPerCommit: false
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Next Release Number
        run: echo ${{ steps.versioning.outputs.version }}
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Publish Gradle plugin
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} -Pversion=${{ steps.versioning.outputs.version }}

文件顶部的声明说明了管道触发的规则.

yaml 复制代码
name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

管道会在每次向master分支提出拉取请求和每次构建master分支时运行.

构建由两项工作组成. 第一个工作很简单. 它只是运行 Gradle build 任务. 请看下面的配置.

yaml 复制代码
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 8
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'temurin'
      - name: Build with Gradle
        uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
        with:
          arguments: build

然后是发布的任务本身. 它也包含几个步骤. 第一个步骤是自动增加版本并保存到环境变量中. 这很方便, 因为 Gradle 插件不能以快照的形式发布.

yaml 复制代码
publish:
    needs:
      - build
    if: github.ref == 'refs/heads/master'
    runs-on: ubuntu-latest
    steps:
      - name: Auto Increment Semver Action
        uses: MCKanpolat/auto-semver-action@1.0.5
        id: versioning
        with:
          releaseType: minor
          incrementPerCommit: false
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Next Release Number
        run: echo ${{ steps.versioning.outputs.version }}

if: github.ref == 'refs/heads/master'告知 GitHub Actions 只有在master分支在构建的时候才能运行管道线中的任务. 因此, 在拉取请求构建过程中, GitHub Actions 不会触发publish进程.

现在, 我们需要发布打包的插件本身. 请看下面的代码片段.

yaml 复制代码
- uses: actions/checkout@v3
- name: Set up JDK 8
  uses: actions/setup-java@v3
  with:
      java-version: '8'
      distribution: 'temurin'
- name: Publish Gradle plugin
  uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
  with:
      arguments: build publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} -Pversion=${{ steps.versioning.outputs.version }}

如你所见, GitHub Actions 通过secrets传递了gradle.publish.keygradle.publish.secret属性, 并将新项目版本作为环境变量.

总结一下

正如你所看到的, 在 Gradle 中自动检查代码样式规则并不复杂. 顺便说一句, 你可以通过包含 id 'io.github.simonharmonicminor.code.style' version '0.1.0' 来应用项目中描述的插件.

Happy Coding & Stay GOLDEN!

相关推荐
单丽尔2 小时前
Gemini for China 大更新,现已上架 Android APP!
android
JerryHe3 小时前
Android Camera API发展历程
android·数码相机·camera·camera api
Synaric4 小时前
Android与Java后端联调RSA加密的注意事项
android·java·开发语言
程序员老刘·5 小时前
如何评价Flutter?
android·flutter·ios
JoyceMill7 小时前
Android 图像效果的奥秘
android
想要打 Acm 的小周同学呀8 小时前
ThreadLocal学习
android·java·学习
天下是个小趴菜8 小时前
蚁剑编码器编写——中篇
android
命运之手8 小时前
【Android】自定义换肤框架05之Skinner框架集成
android·skinner·换肤框架·不重启换肤·无侵入换肤
DS小龙哥8 小时前
QT+OpenCV在Android上实现人脸实时检测与目标检测
android·人工智能·qt·opencv·目标检测
SwBack9 小时前
【pearcmd】通过pearcmd.php 进行GetShell
android·开发语言·php