Gradle系列4--多模块构建

可以作为官方指南(以下简称"指南")Creating Multi-project Builds的一个翻译版本,与指南不同和参考的地方会做声明。

首先说下这里的“工程”,有两个含义: 根工程(root project)和子工程(subproject)。根工程用于全局的管理,子工程作为根工程的一个模块(module). 为了简明, 不引起混淆情况下,文中"项目"指根工程, "模块"指子工程。

Gradle中统一使用Project 来实例化对应的构建脚本(build.gradle). 可以简单的将build.gradle所在的目录称为一个project, 如果这个project又是用于管理其他project的就称为root project, 而其余的都是subproject. 模块化是gradle构建的一个重要功能, "模块"也是集成开发环境中提供的称呼, 比如 IntelliJ IDEA里面就是"New Module". 实在理不清就直接用英文root/sub project来代替,不需要翻译了, 撸起袖子敲上一个demo可能就明白了.

多模块的构建有助于项目的模块化, 可以使你专注于大项目中的某个区域,而Gradle负责项目的依赖。这个多模块demo同时作为gradle命令的一个练习.

环境

列举我的开发环境, 以本机环境自行微调,不细表. jdk1.8, gradle-4.6-all, Linux系统. 另外markdown的原因无法提供指南中groovy和kotlin两个版本, 这里用groovy(应该是目前流行的方式), 所以kotlin环境也没必要列出

创建项目

首先为新项目创建一个文件夹,并将Gradle Wrapper添加到项目中(对比指南)。

$ mkdir creating-multi-project-builds
$ cd creating-multi-project-builds/
$ gradle wrapper --gradle-version 4.6 --distribution-type all #指定已有版本,免得下载
$ ./gradlew init #初始化项目

执行后项目结构:

creating-multi-project-builds
├── build.gradle                            #当前项目的配置脚本, 顶级构建脚本
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar              #Gradle Wrapper执行的jar
│       └── gradle-wrapper.properties        #Gradle Wrapper配置属性
├── gradlew                                #Unix系统 Gradle Wrapper执行脚本
├── gradlew.bat                              #Windows系统 Gradle Wrapper执行脚本
└── settings.gradle                         #Gradle构建设置

可以删除 build.gradle和settings.gradle中的注释.

配置

build.gradle

allprojects {
    repositories {
        jcenter() 
    }
}

allprojects块用于添加根工程和所有子工程的配置, 类似的subprojects块用于添加所有子工程(模块)的配置. 可以在根工程中任意的使用这两个块. 现在通过subproject来配置所有模块的版本, 在顶级build.gradle中加入:

subprojects {
    version = '1.0'
}

添加一个Groovy库模块

$ mkdir greeting-library
$ cd greeting-library
$ ../gradlew init --type groovy-library

保留build.gradle和src目录, 其余文件(夹)全部删除, 添加include 'greeting-library'到项目顶级settings.gradle中, 删掉不必要的注释. 这样不需要手动创建约定的源码目录.

修改 greeting-library/build.gradle

plugins {
    id 'groovy'
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.13'

    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4',{
        exclude module:'groovy-all'
    }
}

repositories {
    jcenter()
}

settings.gradle

include 'greeting-library'

最后, 在 greeting-library 中创建包目录 greeter

$ mkdir -p src/main/groovy/greeter
$ mkdir -p src/test/groovy/greeter

添加一个 GreetingFormatter 类到src/main/groovy/greeter.

greeting-library/src/main/groovy/greeter/GreetingFormatter.groovy

package greeter

import groovy.transform.CompileStatic

@CompileStatic
class GreetingFormatter {
    static String greeting(final String name) {
        "Hello, ${name.capitalize()}"
    }
}

添加Spock 框架测试类GreetingFormatterSpecsrc/test/groovy/greeter.

创建greeting-library/src/test/groovy/greeter/GreetingFormatterSpec.groovy

package greeter

import spock.lang.Specification

class GreetingFormatterSpec extends Specification {

    def 'Creating a greeting'() {

        expect: 'The greeting to be correctly capitalized'
        GreetingFormatter.greeting('gradlephant') == 'Hello, Gradlephant'

    }
}

从根工程目录中执行 ./gradlew build . 单个模块并不能真正构建多模块下面添加一个将使用这个库的模块.

添加Java应用模块

$ mkdir greeter
$ cd greeter
$ ../gradlew init --type java-application
$ rm -r gradle* sett* src/*/*/*.java #删除多余的文件(夹)
$ mkdir -p src/main/java/greeter src/test/java/greeter 

添加include 'greeter'到顶级settings.gradle中(另起一行)

创建java文件greeter/src/main/java/greeter/Greeter.java

package greeter;

public class Greeter {
    public static void main(String[] args) {
        final String output = GreetingFormatter.greeting(args[0]);
        System.out.println(output);
    }
}

目标是java应用,需要告诉Gradle那个类作为入口点. 修改当前模块build.gradle,将一个拥有main方法的java类名设置给mainClassName:

mainClassName = 'greeter.Greeter'

项目根目录执行 ./gradlew build:

/xxx/creating-multi-project-builds/greeter/src/main/java/greeter/Greeter.java:5: 错误: 找不到符号
        final String output = GreetingFormatter.greeting(args[0]);
                              ^
  符号:   变量 GreetingFormatter
  位置: 类 Greeter
1 个错误

FAILURE: Build failed with an exception.

构建失败这是因为greeter模块不知道去哪里找greeting-library。创建模块不会自动在其他模块中可用——这样会使得项目变得脆弱(任何改动都会波及整个项目)。Gradle具有特定语法将一个模块链接到另一个模块的依赖项。编辑greeter/build.gradle添加:

dependencies {
    compile project(':greeting-library') 
}

执行 ./gradlew build --console verbose 将会看到以下输出:

$ ./gradlew build --console verbose
Picked up _JAVA_OPTIONS:   -Dawt.useSystemAAFontSettings=gasp
> Task :greeting-library:compileJava NO-SOURCE
> Task :greeting-library:compileGroovy 
Picked up _JAVA_OPTIONS:   -Dawt.useSystemAAFontSettings=gasp
> Task :greeting-library:processResources NO-SOURCE
> Task :greeting-library:classes 
> Task :greeting-library:jar 
> Task :greeter:compileJava 
> Task :greeter:processResources NO-SOURCE
> Task :greeter:classes 
> Task :greeter:jar 
> Task :greeter:startScripts 
> Task :greeter:distTar 
> Task :greeter:distZip 
> Task :greeter:assemble 
> Task :greeter:compileTestJava NO-SOURCE
> Task :greeter:processTestResources NO-SOURCE
> Task :greeter:testClasses UP-TO-DATE
> Task :greeter:test NO-SOURCE
> Task :greeter:check UP-TO-DATE
> Task :greeter:build 
> Task :greeting-library:assemble 
> Task :greeting-library:compileTestJava NO-SOURCE
> Task :greeting-library:compileTestGroovy 
> Task :greeting-library:processTestResources NO-SOURCE
> Task :greeting-library:testClasses 
> Task :greeting-library:test 
Picked up _JAVA_OPTIONS:   -Dawt.useSystemAAFontSettings=gasp
> Task :greeting-library:check 
> Task :greeting-library:build 

我用的Gradle版本执行./gradlew build输出没有指南的详细, -h查看帮助中的--console如下描述

--console Specifies which type of console output to generate. Values are 'plain', 'auto' (default), 'rich' or 'verbose'.

因此采用--console verbose版本输出来展示详细的构建执行时序

请注意每个模块如何在输出中作为前缀,以便您知道正在执行哪个模块的任务。另外Gradle在移动到另一个模块之前不会处理所有任务。

添加测试以确保应用程序本身的代码有效。由于Spock 框架也是测试Java代码的流行方法,因此首先将Groovy插件添加到greeter模块的构建脚本中来创建测试。这需要groovy插件, 并且已经包含了java插件,因此您可以将java替换成groovy:

greeter/build.gradle

plugins {
    id 'groovy' 
}

dependencies {
   //将junit替换成下面
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4', {
        exclude module: 'groovy-all'
    }
}

创建测试类greeter/src/test/groovy/greeter/GreeterSpec.groovy

package greeter

import spock.lang.Specification

class GreeterSpec extends Specification {

    def 'Calling the entry point'() {

        setup: 'Re-route standard out'
        def buf = new ByteArrayOutputStream(1024)
        System.out = new PrintStream(buf)

        when: 'The entrypoint is executed'
        Greeter.main('gradlephant')

        then: 'The correct greeting is output'
        buf.toString() == "Hello, Gradlephant\n".denormalize()
    }
}

切换到项目根目录下执行greetertest任务:./gradlew :greeter:test

添加文档

这部分直接参看指南Add documentation, 不想看也可以跳过

重构公共的构建脚本

此时您可能已经注意到在模块greeting-librarygreeter构建脚本中都有通用脚本代码。Gradle的一个关键特性是能够在根项目中放置这样的公共构建脚本代码。编辑根目录下的build.gradle增加:

configure(subprojects.findAll { it.name == 'greeter' || it.name == 'greeting-library' }) {
    apply plugin: 'groovy'
    dependencies {
        testCompile 'org.spockframework:spock-core:1.0-groovy-2.4', {
            exclude module: 'groovy-all'
        }
    }
}

删除greeting-library下已在顶级build.gradle中的所有配置:

plugins {
    id 'groovy'
}

dependencies {
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4',{
        exclude module:'groovy-all'
    }
}

repositories {
    jcenter()
}

此时greeting-library/build.gradle只剩下

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.13'
}

对于greeter类似的删除重复代码, 在根目录重新运行./gradlew clean build以确保它仍然有效。此外, 项目还可以在IntelliJ IDEA等支持gradle构建的IDE中打开来进行开发。

总结

按照上面的步骤进行操作, 您已经掌握了

  • 通过组合多个模块来创建模块化的项目。
  • 让一个模块消费另一个模块的产品。
  • 轻松使用多语言项目(Java + Groovy + ...)。
  • 在所有模块中运行名字相似的任务。
  • 在特定模块中运行任务而无需切换到该模块目录下。
  • 将通用的模块设置重构到根项目中。
  • 从根项目中有选择地配置模块。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容