静态代码分析是一项了不起的技术, 它能让代码库更易于维护. 但是, 如果你在不同的版本库中拥有多个服务(可能由不同的团队开发), 如何才能让每个人都遵循既定的代码风格呢? 一个好办法是将所有规则封装在一个插件中, 该插件会在每个项目构建时自动执行所需的验证.
因此, 在本文中我将向你展示:
- 如何创建带有自定义 PMD 和 Checkstyle 规则的 Gradle 插件.
- 如何发布到 [plugins.gradle.org]
- 如何使用 GitHub Actions 自动执行发布流程.
你可以查看[本仓库]中的代码示例.
PMD, Checkstyle 和多仓库的难点
[PMD] 是静态分析工具, 可在每次项目构建时检查代码. 通过 [Gradle]“https://medium.com/javarevisited/why-java-developer-should-learn-maven-or-gradle-aefe7ea20a83”), 可以轻松应用它们.
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)
}
如果你的整个项目(甚至是公司)都是[单仓库, 那么这样的设置绝对没问题. 你只需将这些配置放入根build.gradle
文件中, 就能将这些插件应用到现有的每个模块中. 但如果你选择的是[多仓库]呢?
如果你想在公司内开发人员正在开发的所有项目(以及程序员将来创建的所有项目)中共享相同的代码风格, 该怎么办?
那么, 你可以告诉他们只需[复制并粘贴]插件的配置即可. 无论如何, 这种方法容易出错. 总有人可能会配置错误.
事实上, 我们需要在每个可行的项目中以某种方式重复使用已定义的代码样式配置. 答案很简单. 我们需要一个定制的 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()
}
现在让我们从 plugins
块开始, 一步步解构配置. 请看下面的代码片段.
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.
我最近正在向你展示整个发布过程.
然后是基本的项目配置.
group = 'io.github.simonharmonicminor.code.style'
sourceCompatibility = '8'
repositories {
mavenCentral()
}
group
定义了groupId
, 以符合[Apache Maven 命名规范] sourceCompatibility
是目标 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'
}
这里没什么特别的. 接下来是发布配置.
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
任务执行过程中, 这样的构建会失败.
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 时就会得到奇怪的错误信息. 有一些变通方法, 但最简单的方法是将内容复制到临时文件中.
看看下面的 PMD 配置. 与 Checkstyle 类似.
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
任务会触发所有 Checkstyle 和 PMD 任务按顺序运行. 当你想在创建拉取请求前验证整个项目时, 它就派上用场了. 如果直接在build.gradle
中添加runStaticAnalysis
任务, 它将看起来像这样:
task runStaticAnalysis {
dependsOn checkstyleMain, checkstyleTest, pmdMain, pmdTest
}
同样, 我将一次性展示整段代码, 然后指出重要的细节.
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
创建了一个与 API 兼容的Project
接口实现. 因此, 你可以放心地将它传递给 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()
)
);
这是我们在将插件推送到 [plugins.gradle.org/]之前应该测试的最基本情况.
使用 GitHub Actions 发布插件
当你在 [plugins.gradle.org/]上注册一个新账户时, 进入你的页面并打开 API Keys
选项卡. 你应该生成新的密钥. 会有两个.
gradle.publish.key=...
gradle.publish.secret=...
然后, 打开版本库的Settings
, 转到Secrets and Variables -> Actions
项. 你必须把获得的密钥存储为版本库秘密.
最后是 GitHub Actions 的构建配置.
我把自己的文件放在了
.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 只有在master
分支在构建的时候才能运行管道线中的任务. 因此, 在拉取请求构建过程中, GitHub Actions 不会触发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 Actions 通过secrets
传递了gradle.publish.key
和gradle.publish.secret
属性, 并将新项目版本作为环境变量.
总结一下
正如你所看到的, 在 Gradle 中自动检查代码样式规则并不复杂. 顺便说一句, 你可以通过包含 id 'io.github.simonharmonicminor.code.style' version '0.1.0'
来应用项目中描述的插件.
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )