Skip to content

docker-compose.yml

依赖管理

统一使用 gradle 推荐的 toml 管理方式

toml
[versions]
java = "25"
springBoot = "3.5.10"
springBootManagement = "1.1.7"
spotless = "8.2.1"
sonarqube = "7.2.2.6593"
allurePlugin = "3.1.0"
errorprone = "5.1.0"
allure = "2.34.1"
smartDoc = "3.1.4"


sqliteJdbc = "3.51.2.0"
h2Jdbc = "2.4.240"
duckdbJdbc = "1.5.1.0"
flywayDuckdb = "10.17.0"
mapstruct = "1.6.3"
poi = "5.5.1"
netty = "4.2.10.Final"
commonsLang3 = "3.20.0"
fastjson2 = "2.0.57"
oshi = "6.9.3"
jcasbin = "1.99.0"
casbinJdbcAdapter = "2.4.0"
javaJwt = "4.4.0"
jbcrypt = "0.4"
mockito = "5.23.0"
restAssured = "5.4.0"
hamcrest = "3.0"
allureRest = "2.33.0"
comenDebug = "2.0.4-SNAPSHOT"
jasypt = "4.0.4"
[libraries]
# 调试
comen-debug = { group = "com.comen", name = "comen-basehub-ui", version.ref = "comenDebug" }
# spring
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
spring-boot-starter-data-jpa = { group = "org.springframework.boot", name = "spring-boot-starter-data-jpa" }
spring-boot-starter-validation = { group = "org.springframework.boot", name = "spring-boot-starter-validation" }
spring-boot-starter-cache = { group = "org.springframework.boot", name = "spring-boot-starter-cache" }
spring-boot-starter-actuator = { group = "org.springframework.boot", name = "spring-boot-starter-actuator" }
spring-boot-starter-test = { group = "org.springframework.boot", name = "spring-boot-starter-test" }

jasypt = { group = "com.github.ulisesbocchio", name = "jasypt-spring-boot-starter", version.ref = "jasypt" }
h2-jdbc = { group = "com.h2database", name = "h2", version.ref = "h2Jdbc" }
sqlite-jdbc = { group = "org.xerial", name = "sqlite-jdbc", version.ref = "sqliteJdbc" }
duckdb-jdbc = { group = "org.duckdb", name = "duckdb_jdbc", version.ref = "duckdbJdbc" }
flyway-core = { group = "org.flywaydb", name = "flyway-core" }
flyway-duckdb = { group = "org.flywaydb", name = "flyway-database-duckdb", version.ref = "flywayDuckdb" }
netty-all = { group = "io.netty", name = "netty-all", version.ref = "netty" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commonsLang3" }
fastjson2 = { group = "com.alibaba.fastjson2", name = "fastjson2", version.ref = "fastjson2" }
oshi-core = { group = "com.github.oshi", name = "oshi-core", version.ref = "oshi" }
jcasbin = { group = "org.casbin", name = "jcasbin", version.ref = "jcasbin" }
casbin-jdbc-adapter = { group = "org.casbin", name = "jdbc-adapter", version.ref = "casbinJdbcAdapter" }
java-jwt = { group = "com.auth0", name = "java-jwt", version.ref = "javaJwt" }
jbcrypt = { group = "org.mindrot", name = "jbcrypt", version.ref = "jbcrypt" }
hibernate-community-dialects = { group = "org.hibernate.orm", name = "hibernate-community-dialects" }
caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine" }

# 监控
micrometer-registry-prometheus = { group = "io.micrometer", name = "micrometer-registry-prometheus" }

# 办公 & 转换
poi-excelant = { group = "org.apache.poi", name = "poi-excelant", version.ref = "poi" }
mapstruct = { group = "org.mapstruct", name = "mapstruct", version.ref = "mapstruct" }
jackson-dataformat-yaml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-yaml" }
jackson-dataformat-csv = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-csv" }


# 注解处理器
mapstruct-processor = { group = "org.mapstruct", name = "mapstruct-processor", version.ref = "mapstruct" }
lombok = { group = "org.projectlombok", name = "lombok" } # 版本由 Spring Boot 管理
jakarta-persistence-api = { group = "jakarta.persistence", name = "jakarta.persistence-api" }
jakarta-annotation-api = { group = "jakarta.annotation", name = "jakarta.annotation-api" }
jakarta-validation-api = { group = "jakarta.validation", name = "jakarta.validation-api" }




# 测试相关
junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" }
mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
# Rest-Assured & Testing Support
rest-assured = { group = "io.rest-assured", name = "rest-assured", version.ref = "restAssured" }
json-path = { group = "io.rest-assured", name = "json-path", version.ref = "restAssured" }
hamcrest = { group = "org.hamcrest", name = "hamcrest", version.ref = "hamcrest" }
allure-rest-assured = { group = "io.qameta.allure", name = "allure-rest-assured", version.ref = "allureRest" }
allure-junit5 = { group = "io.qameta.allure", name = "allure-junit5", version.ref = "allureRest" }


[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "springBoot" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
allure = { id = "io.qameta.allure", version.ref = "allurePlugin" }
errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone" }
spring-dependency = { id = "io.spring.dependency-management", version.ref = "springBootManagement" }
smart-doc = { id = "com.ly.smart-doc", version.ref = "smartDoc" }
kts
dependencies {
    implementation(libs.spring.boot.starter.web)
    implementation(libs.spring.boot.starter.data.jpa)
}

代码风格

gradle spotless插件, 每个项目使用相同的 CodeStyle配置文件, build 之后自动代码风格整理,确保项目所有代码风格一致

可以使用idea默认配置导出一个 eclipse CodeStyle.xml 方便使用idea

kts
subprojects {
    val libs = rootProject.extensions.getByType<VersionCatalogsExtension>().named("libs")
    apply(plugin = "io.spring.dependency-management")
    apply(plugin = "com.diffplug.spotless")
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(libs.findVersion("java").get().requiredVersion)
        }
    }
    configure<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension> {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }
    tasks.withType<JavaCompile> {
        dependsOn(tasks.matching { it.name == "spotlessApply" })
        options.generatedSourceOutputDirectory.set(file("src/main/generated"))
    }
    spotless {
        java {
            target("src/main/java/**/*.java", "src/test/java/**/*.java")
            targetExclude("**/build/generated/**/*.java")
            eclipse().configFile("${project.rootDir}/codeStyle.xml")
            removeUnusedImports()
            trimTrailingWhitespace()
            endWithNewline()
            formatAnnotations()
        }
    }
}

静态扫描

使用Sonar Idea插件

sonar lint plugin

配合 Jacoco 单测覆盖率, 下文示例仅处理 service 单测覆盖率

kts
plugins {
    java
    jacoco
    alias(libs.plugins.spring.boot) apply false
    alias(libs.plugins.spring.dependency) apply false
    alias(libs.plugins.sonarqube)
}

sonar {
    properties {
        property("sonar.host.url", "http://xxxx")
        property("sonar.token", "token")
        property("sonar.projectKey", "x")
        property("sonar.projectName", "x")
        property("sonar.java.binaries", "${layout.buildDirectory.get()}/classes/java/main")
        property("sonar.sourceEncoding", "UTF-8")
    }
}
subprojects {
    val libs = rootProject.extensions.getByType<VersionCatalogsExtension>().named("libs")
    apply(plugin = "java")
    apply(plugin = "jacoco")
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(libs.findVersion("java").get().requiredVersion)
        }
    }
    configure<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension> {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }
    tasks.named<Delete>("clean") {
        delete("src/main/generated")
    }
    tasks.test {
        useJUnitPlatform()
        reports {
            junitXml.required.set(true)
        }
        filter {
            includeTestsMatching("com.comen.unit.**")
        }
        failOnNoDiscoveredTests = false
        description = "运行单元测试并统计 Service 覆盖率  "
    }
    tasks.jacocoTestReport {
        dependsOn(tasks.test)
        reports {
            xml.required.set(true)
            xml.outputLocation.set(layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml"))
            html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco/ut-service-report"))
        }
        classDirectories.setFrom(
            sourceSets.main.get().output.asFileTree.matching {
                include("com/comen/**/service/**")
            }
        )
    }
    sonar {
        properties {
            property(
                "sonar.exclusions",
                "**/*.js,**/*.ts,**/*.py,**/*.go,**/*.xml,**/*.properties,**/*.html,**/*.css,**/*.sql,**/*.kts"
            )
            val reportPaths = subprojects.mapNotNull { subproject ->
                val reportFile =
                    subproject.layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml").get().asFile
                if (reportFile.exists()) reportFile.absolutePath else null
            }
            val rootReport = layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml").get().asFile
            val allPaths = if (rootReport.exists()) reportPaths + rootReport.absolutePath else reportPaths
            property("sonar.coverage.jacoco.xmlReportPaths", allPaths.joinToString(","))
            val allTestResults = subprojects.map {
                it.layout.buildDirectory.dir("test-results/test").get().asFile.absolutePath
            }
            property("sonar.junit.reportPaths", allTestResults.joinToString(","))
            property(
                "sonar.coverage.exclusions", listOf(
                    "**/App**",
                    "**/api/**",
                    "**/entity/**",
                    "**/utils/**",
                    "**/util/**",
                    "**/cache/**",
                    "**/annotation/**",
                    "**/constant/**",
                    "**/constants/**",
                    "**/exception/**",
                    "**/enums/**",
                    "**/listener/**",
                    "**/repo/**",
                    "**/net/**",
                    "**/runner/**",
                    "**/vo/**",
                    "**/config/**",
                    "**/Q*.*"
                ).joinToString(",")
            )
        }
    }
}

[docker-compose.yml](docker-compose.yml)

动态编译

使用 errorprone

我们习惯了从编译器中获取帮助,但它除了静态类型检查之外几乎不做其他事情。 使用 Error Prone 来增强编译器的类型分析,你可以在它们耗费你时间或最终成为生产中的错误之前捕获更多的错误。 我们在 Google 的 Java 构建系统中使用 Error Prone 来消除严重错误类别的错误进入我们的代码,并且我们已经将其开源,你也可以这样做!

kts
import net.ltgt.gradle.errorprone.errorprone

plugins {
    java
    jacoco
    alias(libs.plugins.spring.boot) apply false
    alias(libs.plugins.spring.dependency) apply false
    alias(libs.plugins.errorprone)
}
subprojects {
    val libs = rootProject.extensions.getByType<VersionCatalogsExtension>().named("libs")
    apply(plugin = "java")
    apply(plugin = "io.spring.dependency-management")
    apply(plugin = "net.ltgt.errorprone")
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(libs.findVersion("java").get().requiredVersion)
        }
    }
    dependencies {
        errorprone("com.google.errorprone:error_prone_core:2.49.0")
    }
    tasks.withType<JavaCompile>().configureEach {
        options.errorprone.disableWarningsInGeneratedCode = true
        options.errorprone {
            enabled.set(true)
            disable("MissingSummary")
            disable("EmptyBlockTag", "StatementSwitchToExpressionSwitch", "FutureReturnValueIgnored")
            disable("InvalidBlockTag")    
            disable("StringSplitter")
        }
    }
}

GIT规范

使用Gradle 自定义插件实现 , 在约定式提交(Conventional Commits)规范中,这些前缀(Type)是为了让开发者通过 Git Log 就能一眼看出代码变动的本质

fix(core){英文冒号}{空格}提交内容


  • feat (feature): 新功能。

    • 场景:你在 app-icu 模块增加了呼吸机波形的实时渲染逻辑。
    • 示例feat(icu): 增加波形数据的高频采样接口
  • fix (bug fix): 修复 Bug。

    • 场景:修复了 SQLite 在多线程写入时偶尔出现的 Database is locked 异常。
    • 示例fix(db): 解决高并发下 SQLite 写入冲突
  • refactor (refactor): 重构。

    • 代码改了,但功能没变(不是加功能也不是修 Bug)。
    • 场景:你把一段复杂的 if-else 逻辑改成了策略模式,或者优化了类结构。
    • 示例refactor(core): 提取设备协议解析的通用父类
  • perf (performance): 性能优化。

    • 场景:你发现之前的随机数生成导致了 CPU 波动,改用了 ThreadLocalRandom;或者优化了 Netty 的内存分配。
    • 示例perf(netty): 减少波形数据传输时的内存拷贝
  • test (test): 测试相关。

    • 场景:你为 base-common 编写了 JUnit 5 单元测试,或者配置了 Allure 的测试步骤。
    • 示例test(common): 增加协议转换器的边界值测试
  • docs (documentation): 文档变动。

    • 场景:修改了 README.md,或者更新了 API 接口文档(Smart-Doc)。
    • 示例docs(api): 更新 api doc
  • style (style): 格式调整。

    • 不影响代码逻辑的改动(如空格、缩进、分号等)。
    • 场景:运行了 spotlessApply 统一了全项目的缩进。
    • 示例style(core): 统一代码缩进规范
  • chore (chore): 日常杂务。

    • 场景:修改 .gitignore、更新 libs.versions.toml 里的依赖版本、配置 Gradle 脚本。
    • 示例chore(deps): 升级 Spring Boot 到 3.5.10
  • ci (continuous integration): 持续集成配置。

    • 场景:修改了 GitLab CI 文件、SonarQube 扫描配置或代码发布脚本。
    • 示例ci: 增加自动发布 Docker 镜像的任务

错误的提交会被阻止

kts
val installGitHooks by tasks.registering {
    group = "verification"
    description = "自动安装 Git 提交规范校验脚本"

    doLast {
        val hooksDir = File(rootProject.rootDir, ".git/hooks")
        if (!hooksDir.exists()) {
            println("⚠️ 未检测到 .git 目录,跳过 Hook 安装")
            return@doLast
        }

        val commitMsgHook = File(hooksDir, "commit-msg")
        commitMsgHook.writeText(
            """
            #!/bin/sh
            # Git 提交规范校验 Hook
            
            # 关键:检查是否是合并提交(必须放在最前面)
            # 方法1:检查 .git/MERGE_MSG 文件是否存在
            if [ -f ".git/MERGE_MSG" ]; then
                # 正在进行合并,使用合并信息作为提交信息,跳过校验
                exit 0
            fi
            
            # 方法2:检查提交信息是否以 Merge 开头
            commit_msg=$(cat ${'$'}1)
            if echo "${'$'}commit_msg" | grep -qE "^(Merge branch|Merge pull request|Merge remote-tracking|merge )"; then
                exit 0
            fi
            
            # 方法3:检查是否存在 MERGE_HEAD(正在进行合并)
            if [ -f ".git/MERGE_HEAD" ]; then
                exit 0
            fi
            
            # 定义约定式提交的正规则
            pattern="^(feat|fix|docs|style|refactor|perf|test|chore|ci|revert)(\\(.*\\))?: .+"

            if ! [[ ${'$'}commit_msg =~ ${'$'}pattern ]]; then
                echo ""
                echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                echo -e "\033[31m❌ 提交日志不符合规范!\033[0m"
                echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
                echo ""
                echo "正确格式: <type>(范围): 描述"
                echo "示例: feat(icu): 优化高并发下随机数生成性能"
                echo "支持类型: feat, fix, docs, style, refactor, perf, test, chore, ci"
                echo ""
                echo "合并提交会自动跳过校验"
                echo "如需强制跳过: git commit --no-verify"
                echo ""
                exit 1
            fi
            """.trimIndent()
        )

        commitMsgHook.setExecutable(true)
        println("✅ Git commit-msg hook 已成功安装/更新")
    }
}

// 2. 将该任务挂载到 Gradle 初始化阶段
// 这样每次你点击 IDEA 的 Gradle 刷新按钮,或者执行编译时,它都会自动检查/更新 Hook
tasks.named("prepareKotlinBuildScriptModel") {
    dependsOn(installGitHooks)
}

// 如果是纯 Java 项目,可以挂载到这个任务上
tasks.matching { it.name == "processResources" }.all {
    dependsOn(installGitHooks)
}