17

用 Gradle 来构建 Java 企业级微服务应用的正确姿势(继承、公有模块、依赖管理、多环...

 4 years ago
source link: https://www.skypyb.com/2020/06/jishu/1560/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

虽说现在连 SpringBoot 都抛弃 Maven 开始使用 Gradle 来构建了,可关于Gradle这个工具呢在用的人还是挺少的,在网上搜的资料也大多还都是基于安卓项目的配置。

当然 Java 的也会有, 不过基本上都是些很基础的单个项目的构建法,复杂的很少。而一个真实的企业级应用中项目构建会涉及到的点还是比较多的,网上搜的Gradle的配置基本上都不全。

我用Gradle也有好一段时间了, 第一篇关于 Gradle 的博客还是2018年写的–> 使用Gradle部署java项目_远比XML阅读性更高的体验 这篇博客讲的东西很简单,营养不多。

到现在两年过去了,关于 Gradle 呢我也更熟悉了一些,那么我就在这分享一下 用 Gradle 来构建复杂项目的正确姿势

既然是复杂项目, 那么就用微服务的架构来举例子。

最后面会将微服务架构下使用Gradle构建的根项目的 build.gradle 文件放上

父子继承关系

关于微服务项目的构建,肯定有一部分东西是 全局通用 的,一般的构建工具都会用继承模式来设计此功能。

首先需要知道一点 Gradle 的基本知识, Gradle 构建里有两个核心文件,一个是 build.gradle (包含项目构建所使用的脚本)另一个是 settings.gradle (包含必要的设置,比如任务/项目之间的依赖关系)

想要实现父子继承的关系, 那么当然要先创建一个根项目(父项目)。

在根项目的 build.gradle 中进行设置。 主要是设置 allprojectssubprojects

allprojects是对所有project的配置,包括Root Project。而subprojects是对所有Child Project的配置

根节点的 build.gradle 的结构大概会长成这样:

allprojects {
//全局配置文件加载
//版本定义
//全局插件
}
 
subprojects {
//构建脚本
//子模块插件
//依赖仓库
//子模块依赖
//打包方式
}

然后可以创建子项目了。直接在根项目下新建,建立好了子项目后在根项目的setting.gradle 中按文件夹层次导入进来就行,这种事IDE都会自动帮你干。

子项目比较简单的话,那么它的build.gradle 只要写个 dependencies 来表示有哪些依赖即可。

dependencies {
    compile 'xxx'
    compile 'xxx'
    compile 'xxx'
}

如果子项目是一个复杂一点的项目, 需要单独依赖一些插件、定义版本之类的。 那么它可能会是这样的结构:

version = 'v1.0.0'
 
apply plugin: 'xxx'
 
dependencies {
    compile project(':xxx-core:common-utils')
    compile 'xxx'
    compile 'xxx'
}
 
dependencyManagement {
    imports {
        mavenBom 'xxx'
        mavenBom 'xxx'
    }
}

其实在子模块定义之前, 应该先建立 容器模块 ,这么一个复杂服务如果全部都 直接继承父类 ,那么会导致整体的臃肿。

所以应该根据项目的职责来进行模块划分。 比如 公有模块、API模块、服务模块等, 服务模块还能以业务逻辑继续拆下去。尽量将粒度控制在一个合理的范围。

这样子的一个项目结构的目录层级将会是 根项目 > 容器项目 > 具体子项目

容器模块只是单纯为了划分具体的项目, 它本身不应参与到构建之中, 免得耽误时间,所以在 Root 项目的build.gradle中应该把容器模块给排除掉。

如果直接使用 subprojects {} 方式则默认会配置到 Root Project 下所有的 Child Project 之上,这显然是不行的。

所以我们需要使用另一种方式 configure , 这个配置更自由, 我们将 subprojects 传进去, 就可以根据条件来决定配置的细节, 就像这样:

//容器模块名字
def holderProjects = Arrays.asList('xxx-core', 'xxx-service')
 
configure(subprojects) { subp ->
    //过滤容器模块中的插件和依赖配置
    if (!holderProjects.contains(subp.name)) {
        // 具体配置...
    }
}

用这种写法就可以对所有的容器项目进行过滤, 使得依赖和插件等配置不会应用到其上。

公有模块

一个微服务项目中必定会有一些公有的模块, 比如系统内部进行RPC通信时需要暴露出对应的API。或者项目本身有着对于认证、鉴权之类的封装。 还有一些通用的工具类等等…

公有模块在 Gradle 的配置中是很简单的,直接在dependencies 之中按照目录的名字来引用即可, 上边关于继承的配置中已经有了。这里再贴一下:

dependencies {
    compile project(':xxx-core:common-utils')
    compile 'xxx'
    compile 'xxx'
}

只要使用 compile project 方法, 即可引入指定的项目,  比如这里在当前项目中引入了 xxx-core 这个容器模块下的 common-utils 项目。

而这个具体引用的名字呢,  在根项目的 setting.gradle 中的项目定义中也可以找到 ( include )

rootProject.name = 'xxx-project'
 
if (!JavaVersion.current().java11Compatible) {
    throw new GradleException("Gradle must be run with Java 11")
}
 
include ':xxx-core:common-base'
include ':xxx-core:common-utils'
include ':xxx-core:common-model'
include ':xxx-core:common-web'
include ':xxx-service:service-system'
include ':xxx-service:service-gateway'
include ':xxx-service:service-tc'
 
/**
 * 递归检查build.gradle文件是否根据模块名生成
 * @param dirs
 * @return
 */
def assertProjectBuildFile(Set<ProjectDescriptor> dirs) {
    if (dirs.size() == 0) {
        return
    }
 
    dirs.each { project ->
        project.buildFileName = "${project.name}.gradle"
        assert project.projectDir.isDirectory()
        assert project.buildFile.exists()
        assert project.buildFile.isFile()
    }
    if (dirs.children.size() > 0) {
        dirs.children.each { project ->
            assertProjectBuildFile(project)
        }
    }
}
 
assertProjectBuildFile(rootProject.children)

既然这里将我的 setting.gradle 贴出来了,那么应该会注意到我这有个 assertProjectBuildFile 方法, 并且在最下面调用了此方法, 传入了所有的子项目。

那就在说一个注意点。 那就是子项目他们的 build.gradle 文件名。

像是 Gradle 他默认的构建文件名就叫做 build.gradle, 可以等同于 Maven 的 pom.xml。 所有的项目的配置都叫这个名字, 着实不好区分。

所以我这里就以 项目名 来作为构建配置文件的文件名, 然后在根节点的setting.gradle中对每个子项目都作出限制, 使其构建名必须符合语义。比如 service-system 项目, 他的配置文件就会叫做 service-system.gradle 。 这样子在修改、查看配置文件时, 会比清一色的 build.gradle 好看很多。

依赖管理

依赖统一管理是重中之重, 最优雅的方式当然是将对应的依赖的版本放到另一个文件中单独维护啦。

这也是 Gradle 我很喜欢的一个地方, 随意引入外部的配置。

我们对于多个项目的依赖可以建立一个 dependency.gradle 来专门维护,这是我一个项目的配置文件,我剪掉了一大半放上来,不然太长了,可以将这个文件作为参考

ext {
    ver = [
            utils : [
                    guava              : "28.2-jre",
                    lombok             : "1.18.8",
                    junit              : "4.12",
                    groovy_all         : "2.4.13",
                    hibernate_validator: "6.1.0.Final",
                    hibernate_jpa      : "1.0.0.Final",
                    javax_jpa          : "2.2",
                    jaxb_api           : "2.3.1",
                    commons_pool       : "2.8.0",
 
            ],
            spring: [
                    boot         : "2.2.5.RELEASE",
                    cloud        : "Hoxton.SR2",
                    security     : "5.3.0.RELEASE",
                    cloud_alibaba: "2.2.0.RELEASE",
                    management   : "1.0.8.RELEASE",
 
            ],
    ]
    libs = [
            //UTILS
            "lombok"                                : "org.projectlombok:lombok:$ver.utils.lombok",
            "guava"                                 : "com.google.guava:guava:$ver.utils.guava",
            "groovy-all"                            : "org.codehaus.groovy:groovy-all:$ver.utils.groovy_all",
            "hibernate-validator"                   : "org.hibernate:hibernate-validator:$ver.utils.hibernate_validator",
            "hibernate-jpa"                         : "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:$ver.utils.hibernate_jpa",
            "javax.persistence-api"                 : "javax.persistence:javax.persistence-api:$ver.utils.javax_jpa",
            "jaxb-api"                              : "javax.xml.bind:jaxb-api:$ver.utils.jaxb_api",
            "common-pool"                           : "org.apache.commons:commons-pool2:$ver.utils.commons_pool",
    ]
}

我在这个配置文件中创建了一个数组来定义对应的依赖(libs),再另起一个数组专门维护依赖的版本(ver)

这样就将依赖和版本解耦了, 接着在最顶层根项目的 build.gradle 中将该配置文件导入进来, 就可以在所有子项目里访问到这些变量,从而以变量名导入依赖。

allprojects {
    group = 'com.xxx'
    version = 'xxx'
    apply from: "${rootDir}/dependency.gradle"
    apply plugin: 'maven'
    ext.gradleScriptDir = "${rootProject.projectDir}/tasks"
    apply from: "${gradleScriptDir}/task.gradle"
}

其他子项目的导入依赖方式举例:

dependencies {
    compile project(':roacg-core:common-utils')
    compile libs['spring-cloud-gateway']
    compile libs['spring-cloud-zookeeper-discovery']
    compile libs['common-pool']
    compile libs['spring-boot-redis-reactive']
    compile libs['spring-security-config']
    compile libs['spring-security-oauth2-client']
    compile libs['spring-security-oauth2-resource-server']
}

多环境打包

接触过企业级开发的肯定都知道,  一个项目是可以分为 开发环境、测试环境、预发布环境、正式环境 等等杂七杂八的环境的。

哪怕你是微服务项目,用了配置中心将配置文件外部化了。也难免会有一小部分写在项目中。所以多环境打包还是很有必要的。

多环境打包的话首先还是要定义项目有哪些环境, 所以得在根项目的 gradle.properties 属性文件中将环境写好。

# 开启线程守护,第一次编译时开线程,之后就不会再开了
org.gradle.daemon=true
# 配置编译时的虚拟机大小
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# 开启并行编译
org.gradle.parallel=true
# 启用新的孵化模式
org.gradle.configureondemand=true
 
# 环境文件夹和当前环境
envdirs=dev,pro
env=dev

然后在需要采取多环境配置的项目的 resources 目录下创建对应的环境目录 (dev、pro) 。

这样子前期的工作就做好了。

关于 Gradle 多环境打包我写了段代码。在打包时根据当前环境直接将对应环境文件夹下的所有文件打包到resources根目录下。

这样就可以将 不变的配置单独拿出来在 resources 目录下 维护 ,  会改变的配置则放在不同的环境目录下单独维护。

只需要将下面这段代码放在 subprojects 配置之中即可

//指定编译的目录
sourceSets {
    main {
        //直接将环境包目录下的文件打包到resources根目录下
        resources {
 
            //所有的环境文件夹
            String[] envDirs = envdirs.split(",")
 
            srcDir "src/main/resources/${env}"
            println "Current environment [${env}] by ${project.name}. "
 
            sourceSets.main.resources.srcDirs.each {
                it.listFiles().each {
                    if (it.isDirectory() && envDirs.contains(it.name)) {
                        exclude "${it.name}"
                    }
                }
            }
        }
    }
}

结语

上边说了这么多配置(继承、公有模块、依赖管理、多环境打包)  都是分开说的,考虑到可能看着不太直观。

而我手上正好有个微服务项目就是用 Gradle 搭的。

所以我就把其根项目的 build.gradle 脱个敏后拿出来,  权当做个参考。有需要的直接复制后改改就差不多了。

allprojects {
    group = 'com.xxx'
    version = 'xxx'
    apply from: "${rootDir}/dependency.gradle"
    apply plugin: 'maven'
    ext.gradleScriptDir = "${rootProject.projectDir}/tasks"
    apply from: "${gradleScriptDir}/task.gradle"
}
 
//容器模块名字
def holderProjects = Arrays.asList('xxx-core', 'xxx-service')
 
//依赖仓库
def repository = {
    mavenLocal()
    maven { url = 'https://maven.aliyun.com/repository/jcenter' }
    maven { url = 'https://oss.sonatype.org/content/repositories/snapshots/' }
    maven { url = "https://plugins.gradle.org/m2/" }
    jcenter()
    mavenCentral()
}
 
configure(subprojects) { subp ->
 
    //此处主要为了过滤容器模块中的插件配置,容器模块的主要用来管理下属部分的模块,无需添加依赖和插件
    if (holderProjects.contains(subp.name)) return
 
    buildscript {
        repositories repository
        dependencies {
            classpath libs["spring-boot-gradle-plugin"]
        }
    }
 
    repositories repository
    
    apply plugin: 'java'
 
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
 
    [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8'
 
    apply from: "${rootDir}/dependency.gradle"
 
    dependencies {
        //lombok
        annotationProcessor libs['lombok']
        compileOnly libs['lombok']
        testAnnotationProcessor libs['lombok']
        testCompileOnly libs['lombok']
        //test
        testImplementation libs["spring-boot-test"]
    }
 
 
    //指定编译的目录
    sourceSets {
        main {
            //直接将环境包目录下的文件打包到resources根目录下
            resources {
 
                //所有的环境文件夹
                String[] envDirs = envdirs.split(",")
 
                srcDir "src/main/resources/${env}"
                println "Current environment [${env}] by ${project.name}. "
 
                sourceSets.main.resources.srcDirs.each {
                    it.listFiles().each {
                        if (it.isDirectory() && envDirs.contains(it.name)) {
                            exclude "${it.name}"
                        }
                    }
                }
            }
 
        }
    }
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK