靜態代碼分析是一項了不起的技術, 它能讓代碼庫更易于維護. 但是, 如果你在不同的版本庫中擁有多個服務(可能由不同的團隊開發), 如何才能讓每個人都遵循既定的代碼風格呢? 一個好辦法是將所有規則封裝在一個插件中, 該插件會在每個項目構建時自動執行所需的驗證.
因此, 在本文中我將向你展示:
- 如何創建帶有自定義 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 = falsemaxWarnings = 0configFile = file(pathToCheckstyleConfig)
}pmd {consoleOutput = truetoolVersion = '6.52.0'ignoreFailures = falseruleSetFiles = 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> {@Overridepublic 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> {@Overridepublic 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 {@Testvoid 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 Gradleon:push:branches: [ "master" ]pull_request:branches: [ "master" ]permissions:contents: readjobs:build:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Set up JDK 8uses: actions/setup-java@v3with:java-version: '8'distribution: 'temurin'- name: Build with Gradleuses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1with:arguments: buildpublish:needs:- buildif: github.ref == 'refs/heads/master'runs-on: ubuntu-lateststeps:- name: Auto Increment Semver Actionuses: MCKanpolat/auto-semver-action@1.0.5id: versioningwith:releaseType: minorincrementPerCommit: falsegithub_token: ${{ secrets.GITHUB_TOKEN }}- name: Next Release Numberrun: echo ${{ steps.versioning.outputs.version }}- uses: actions/checkout@v3- name: Set up JDK 8uses: actions/setup-java@v3with:java-version: '8'distribution: 'temurin'- name: Publish Gradle pluginuses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1with: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 Gradleon:push:branches: [ "master" ]pull_request:branches: [ "master" ]
管道會在每次向master
分支提出拉取請求和每次構建master
分支時運行.
構建由兩項工作組成. 第一個工作很簡單. 它只是運行 Gradle build
任務. 請看下面的配置.
jobs:build:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Set up JDK 8uses: actions/setup-java@v3with:java-version: '8'distribution: 'temurin'- name: Build with Gradleuses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1with:arguments: build
然后是發布的任務本身. 它也包含幾個步驟. 第一個步驟是自動增加版本并保存到環境變量中. 這很方便, 因為 Gradle 插件不能以快照的形式發布.
publish:needs:- buildif: github.ref == 'refs/heads/master'runs-on: ubuntu-lateststeps:- name: Auto Increment Semver Actionuses: MCKanpolat/auto-semver-action@1.0.5id: versioningwith:releaseType: minorincrementPerCommit: falsegithub_token: ${{ secrets.GITHUB_TOKEN }}- name: Next Release Numberrun: 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 8uses: actions/setup-java@v3with:java-version: '8'distribution: 'temurin'
- name: Publish Gradle pluginuses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1with: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進階資料
敲代碼不易,關注一下吧。?( ′・?・` )