静态代码分析是一项了不起的技术, 它能让代码库更易于维护. 但是, 如果你在不同的版本库中拥有多个服务(可能由不同的团队开发), 如何才能让每个人都遵循既定的代码风格呢? 一个好办法是将所有规则封装在一个插件中, 该插件会在每个项目构建时自动执行所需的验证.
因此, 在本文中我将向你展示:
- 如何创建带有自定义 PMD 和 Checkstyle 规则的 Gradle 插件.
- 如何发布到 plugins.gradle.org.
- 如何使用 GitHub Actions 自动执行发布流程.
你可以查看本仓库中的代码示例.
PMD, Checkstyle 和多仓库的难点
PMD和Checkstyle 是静态分析工具, 可在每次项目构建时检查代码. 通过 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'])
}
}
}
website
和vcsUrl
应指向包含插件源代码的公共 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 禁止
plugin
和gradle
作为标签值. 在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.key
和gradle.publish.secret
属性, 并将新项目版本作为环境变量.
总结一下
正如你所看到的, 在 Gradle 中自动检查代码样式规则并不复杂. 顺便说一句, 你可以通过包含 id 'io.github.simonharmonicminor.code.style' version '0.1.0'
来应用项目中描述的插件.
Happy Coding & Stay GOLDEN!