定制Gradle插件用于统一静态代码分析
#教程 #java #codequality #gradle

Static code analysis是一种令人难以置信的技术,它使您的代码库更易于维护。但是,如果您在不同的存储库中有多个服务(可能是由单独的团队开发的),那么如何使每个人都遵循既定的代码样式?一个好的方法是将所有规则封装在单个插件中,该插件将自动执行每个项目构建所需的验证。

Meme cover

所以,在本文中,我向您展示:

  1. 如何使用自定义PMD和CheckStyle规则创建Gradle插件。
  2. 如何将其发布到plugins.gradle.org
  3. 如何使用github动作自动化发布过程。

您可以在this repository中查看代码示例。

PMD,CheckStyle和Polyrepos的困难

PMDCheckstyle是静态分析工具,可在每个项目构建上检查您的代码。 Gradle允许轻松应用它们。

plugins {
    id 'java'
    id 'pmd'
    id 'checkstyle'
}

现在您可以按照想要的方式调整每个插件。

checkstyle {
    toolVersion = '10.5.0'
    ignoreFailures = false
    maxWarnings = 0
    configFile = file(pathToCheckstyleConfig)
}

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

如果您的整个项目(甚至公司)是monorepository,那么此设置绝对可以。您只需要将这些配置放入root build.gradle文件中即可为每个现有模块应用这些插件。但是,如果您的选择是polyrepository怎么办?如果您想在开发人员正在开发的公司中的所有项目中共享相同的代码样式(以及程序员将来都会创建的所有代码)怎么办?好吧,您可以告诉他们简单地进行插件配置。无论如何,这是一种容易出错的方法。总是有可能有人进行错误配置。

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

自定义Gradle插件

构建配置

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

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

现在,让S逐步解构配置,从plugins块开始。查看下面的代码段。

plugins {
    id 'java-gradle-plugin'
    id 'com.gradle.plugin-publish' version '1.1.0'
}

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

我最近向您展示了整个发布过程。

然后是基本项目配置。

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

repositories {
    mavenCentral()
}

group定义了groupId,支持Apache Maven naming conventionssourceCompatibility是目标Java二进制文件的版本。尽管Java 8现在已经过时了,但我建议您使用开发人员在公司中使用的最早JDK版本来构建Gradle插件。否则,您将阻止他们遵循您的代码样式准则。

然后是dependencies范围。

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'])
        }
    }
}

websitevcsUrl应使用插件的源代码指出公共git存储库。 plugins块定义了项目中Plugin接口的每个实现。最后,tags只是在注册表中搜索插件的标签。

当您将Gradle插件发布到https://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配置。

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

插件代码

我想向您展示整个插件代码。当我打算向您解释每个细节时。查看下面的代码段。

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接口。

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并覆盖我想要的属性。查看下面的代码块。

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应用于他们的项目中时会收到奇异的错误消息。 There are some workarounds,但最简单的方法是将内容复制到临时文件。

查看下面的PMD配置。这与checkstyle One类似。

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

runStaticAnalysis任务触发所有检查方案和PMD任务,以顺序运行。当您想在创建拉动请求之前验证整个项目时,它会派上用场。如果您直接在build.gradle中添加runStaticAnalysis任务,则看起来像这样:

task runStaticAnalysis {
    dependsOn checkstyleMain, checkstyleTest, pmdMain, pmdTest
}

测试

测试呢?最好在构建过程中跟踪错误,但不要在开发人员已经在项目中应用您的插件时。尽管Gradle提供了用于功能测试的Gradle TestKit,但我向您展示的情况很简单,单位测试就足够了。

再次,我会立即显示整个代码作品,当我指出重要的细节时。

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项目实例化。查看下面的代码段。

import org.gradle.testfixtures.ProjectBuilder;
import org.gradle.api.Project;

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

Gradle提供了一些用于单元测试的固定装置。 ProjectBuilder创建了Project接口的API兼容实现。因此,您可以安全地将其传递到YourPluginClass.apply方法。

在调用业务逻辑之前,我们还手动应用了java插件。我们的插件针对Java应用程序。因此,通过Java配置的Project实现很自然。

然后,我们简单地调用自定义插件方法并传递配置的Project实现。

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

之后,主张。我们需要确保runStaticAnalysis任务已成功注册。

final Task task = project.getTasks().getByName("runStaticAnalysis");
assertNotNull(task, "runStaticAnalysis task should be registered");

如果存在,我们根据现有的CheckStyle和PMD任务来验证任务。

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

这是我们在将插件推入https://plugins.gradle.org/之前应该测试的最小情况。

用github操作释放插件

当您在https://plugins.gradle.org/上注册一个新帐户时,请转到您的页面并打开API Keys选项卡。您应该生成新键。将有两个。

gradle.publish.key=...
gradle.publish.secret=...

之后,打开存储库的Settings,然后转到Secrets and Variables -> Actions项目。您必须将获得的密钥存储为您的存储库秘密。

最后是GitHub动作构建配置。

我将我的我放在.github/workflow/build.yml文件中。

查看下面的整个设置。然后我告诉您特定块的含义。

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

顶部文件声明说明管道触发规则。

name: Java CI with Gradle

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

管道在每个拉的请求上运行到master分支和master分支本身的每一个建筑物。

构建由两个工作组成。第一个是微不足道的。它只是运行Gradle build任务。查看下面的配置。

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插件不能以快照的形式出版。

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 Worker仅在建立master分支时运行管道。因此,github动作会在拉动请求构建过程中触发publish过程。

现在,我们需要发布包装的插件本身。查看下面的代码段。

      - 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操作通过秘密和新项目版本作为环境变量传递gradle.publish.keygradle.publish.secret属性。

结论

您可以看到,自动化代码样式规则检查在Gradle中并不是那么复杂。顺便说一句,您可以应用项目中描述的插件,包括id 'io.github.simonharmonicminor.code.style' version '0.1.0'

如果您有任何疑问或建议,请在下面放下评论。感谢您的阅读!

资源

  1. My article "Why You Need Static Code Analysis"
  2. plugins.gradle.org
  3. The repository with source code
  4. PMD
  5. Checkstyle
  6. Monorepository
  7. Polyrepository
  8. Copy and paste programming
  9. Apache Maven naming conventions
  10. Reading a resource file from within jar
  11. Gradle TestKit