NO END FOR LEARNING

Writing blog if you feel tired | 学海无涯 苦写博客

为何要用gradle?

| Comments

“Gradle能够取代Maven吗?”

有很多人问这个问题,我没有答案。因为我也想问这个问题。

如果Gradle能够取代Maven,那么“如何取代之?”

取代之后,带来的“好处有哪些呢?”

带着这个3个问题,我们一起好好研究一下这两个项目管理工具。

1.引言

Maven是目前最常用的Java项目管理及自动化构建工具。

与Ant对比时的经典名言:约定由于配置

它包含:

1.一个项目对象模型(Project Object Model)

2.一组标准集合

3.一个项目生命周期(Project Lifecycle)

4.一个依赖管理系统(Dependency Management System)

5.用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑 当你使用Maven的时候,你用一个明确定义的项目对象模型(POM)来描述你的项目。

Gradle是一个基于Ant和Maven概念的项目自动化建构工具。

它能提供的是:

1.一个像ant一样,通用的灵活的构建工具。

2.一种可切换的,像Maven一样的基于约定的构建框架,却又从不锁住你。

3.对多项目构建的强大支持。

4.强大的依赖管理(基于Apache Ivy)。

5.全力支持已有的Maven或者Ivy仓库基础建设。

6.在不需要远程仓库或者pom.xml或者ivy配置文件的前提下,支持传递性依赖管理。

7.Ant的任务(Task)和构建是gradle的一等公民。

8.基于Groovy脚本构建,其build脚本使用groovy语言编写。

9.具有广泛的领域模型支持你的构建。

Gradle使用一种基于Groovy的特定领域语言来声明项目的设置。

2.对比

2.1 语言不同

Gradle和Maven最大的也是最明显的不同就是它们对待管理项目的描述方式不一样,这也是导致它们不同的最本质因素。

Maven采用几乎所有程序员都理解项目描述方式:XML。

Gradle则采用JVM的一个替代语言Groovy作为脚本语言,它的语法与Java的语法相似(这里是指你可以像Java那样去些Groovy程序,但Groovy的一些特性会让Gradle的脚本看的更加简洁,也就是说真正的Gradle脚本看着并不是很像Java)。

Maven通过pom.xml描述一个项目。

Gradle通过build.gradle描述一个项目。

2.2 Maven的核心概念1 - 生命周期

生命周期是Maven的一个核心概念,它是理解Maven工作的重点。

生命周期是什么含义呢?它表示一个项目从构建到发布的所有步骤已经是明确预先定义好了。

对于开发者而言,我们只需要输入几个简单的命令,POM就能确保Maven的执行能够得到我们想要的结果。

Maven有三个内置的生命周期:default,clean,site。

default负责项目部署,clean负责项目的清理,site负责站点和文档生成。

每一个生命周期都是由各种不同的phase(阶段)构成。例如default生命周期就包含下面的这些phase:

validate - validate the project is correct and all necessary information is available

compile - compile the source code of the project

test - test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed

package - take the compiled code and package it in its distributable format, such as a JAR.

integration-test - process and deploy the package if necessary into an environment where integration tests can be run

verify - run any checks to verify the package is valid and meets quality criteria

install - install the package into the local repository, for use as a dependency in other projects locally

deploy - done in an integration or release environment, copies the final package to the remote repository for sharing with other developers and projects.

这些phase会在default生命周期中按照顺序依次执行。

要执行整个default的生命周期。你只需要执行:mvn deploy命令。

这是因为就算你仅仅只是让maven执行deploy这一个phase,它也会把在它之前的所有phase按照顺序全部执行一遍。

例如:mvn integration-test。它会按照顺序从validate开始一直执行到integration-test。

关于mvn clean install命令:它是先执行clean生命周期中的clean(包含pre-clean),然后在执行default生命周期中的install(当然包含install之前的所有phase)。

下面是clean生命周期的所有phase:

pre-clean - executes processes needed prior to the actual project cleaning

clean - remove all files generated by the previous build

post-clean - executes processes needed to finalize the project cleaning

2.3 Maven的核心概念2 - 插件(plugin)及目标(goal)

虽然一个phase代表着Maven生命周期中的一步(阶段),但生命周期(life cycle)和阶段(phase)只是抽象的概念,不涉及具体的功能。

maven去管理和构建一个项目的具体的功能是全都是由goal提供的。

而plugin是goal的容器,plugin和goal之间的关系是包含关系,一个plugin中包含多个goal。

例如:Compiler插件有两个goal: compile和testCompile,分别完成对main和test中源代码的编译。

执行Goal的方式有两种:

绑定到一个或者多个phase上

单独执行

绑定在phase上是比较常做的办法。在Maven执行过程中,所有经历的执行阶段(phase)上绑定的goal都将得到执行。

例如,对于一个jar包应用,当执行mvn package命令时,在执行到compile阶段时,compiler插件的compile goal会被执行,因为这个goal是绑定在compile阶段(phase)上的。

一些插件的goal并不适合绑定到任何阶段(phase)上,或者是,这些goal往往是单独执行,不需要同某个阶段(phase)绑定在一起。

例如jetty插件,它的goal都是将打包或未打包的工程部署到jetty里然后启动jetty容器的,多数情况下,都是独立运行这些goal的,比如:当键入mvn jetty:run后,工程就能完成编译后启动jetty。

生命周期(lifecycle)由多个阶段(phase)组成,每个阶段(phase)会挂接零个到多个goal,如果一个phase没有goal挂载,那么它就不会被执行。

2.4 约定由于配置

这是一个简单的概念。

系统,类库,框架应该假定合理的默认值,而非要求提供不必要的配置。

Maven通过给项目提供明智的默认行为来融合这个概念。

例如:

在没有自定义的情况下,源代码假定是在 ${basedir}/src/main/java

资源文件假定是在 ${basedir}/src/main/resources

测试代码假定是在 ${basedir}/src/test

项目假定会产生一个JAR文件,Maven 假定你想要把编译好的字节码放到 ${basedir}/target/classes,并且在${basedir}/target创建一个可分发的 JAR 文件。

Maven的力量来自它的”武断”,它有一个定义好的生命周期和一组知道如何构建和装配软件的通用插件。如果你遵循这些约定,

Maven只需要几乎为零的工作—仅仅是将你的源代码放到正确的目录,Maven将会帮你处理剩下的事情。

Maven标准目录结构

src/main/java

src/main/resources

src/main/filters

src/main/assembly

src/main/config

src/main/scripts

src/main/webapp

src/test/java

src/test/resources

src/test/filters

src/site

LICENSE.txt

NOTICE.txt

README.txt

关于更多关于Maven的目录结构请参考这里。

POM是Maven工作的基础。它是一个XML文件,包含了关于项目的信息及提供Maven完成构建工作的配置细节。同时它还包含对于大多数项目都一致的默认值。例如:目录结构。

那么问题来了,这些默认值是在哪里设置的呢?答案是Super POM。

Super POM是Maven的默认POM。除非特别指定,所有的POM都继承自这个Super POM,这意味着所有在Super POM中的配置都会在你的工程中继承。

下面摘抄一些Super POM配置。例如:仓库的配置:

pom.xml
1
2
3
4
5
6
7
8
9
10
11
<repositories>
    <repository>
      <id>central</id>
      <name>Maven Repository Switchboard</name>
      <layout>default</layout>
      <url>http://repo1.maven.org/maven2</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
</repositories>

例如,目录的配置:

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<build>
    <directory>target</directory>
    <outputDirectory>target/classes</outputDirectory>
    <finalName>${artifactId}-${version}</finalName>
    <testOutputDirectory>target/test-classes</testOutputDirectory>
    <sourceDirectory>src/main/java</sourceDirectory>
    <scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
    <testSourceDirectory>src/test/java</testSourceDirectory>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory>src/test/resources</directory>
      </testResource>
    </testResources>
</build>

关于Super POM的具体内容请参考这里。

2.5 对Gradle的基本理解

首先,我们不要深入太多细节,从宏观的角度去理解Gradle的工作原理。

Gradle也提出类似于Maven的两个概念:插件(plugin)和任务(task)。

插件与任务之间的关系也是:包含的关系,插件中包含多个任务。

但是与Maven不同的地方是,Gradle没有像Maven那样的内建生命周期,所以并不是像Maven那样,将目标(goals)绑定到生命周期中对应的不同阶段(phase)上然后顺序执行。

Gradle通过定义task和它们之间的依赖关系,并保证它们根据依赖关系依次执行(且每个task只执行一次),task之间形成一种有向无环图,从而产生类似Maven生命周期的行为。

2.6 Gradle的生命周期

Gradle有严格的三个阶段:

初始化(Initialization)配置(Configuration)执行(Execution)

因为Gradle支持单工程和多工程构建。在初始化阶段,Gradle决定哪些工程将参与构建,并为每一个工程创建一个Project的 实例对象。

配置阶段是用于配置在初始化阶段产生的Project的实例对象。工程的构建脚本会在这个阶段被执行。

Gradle将执行在配置阶段被创建和配置的一个task的小集合。这个小集合是由task的名字(task的名字是通过gradle的命令传入)和当前的目录决定。

这里可以看出Gradle的生命周期和Maven的生命周期是完全不同的概念。

2.5 Gradle的Project和Task概念

Project和Task,顾名思义,表示工程和任务。

Project在Gradle中表示某一个待构建的组件,可以是Jar文件,或者web应用,Gradle的构建是有一个或者多个Project组成。

Task在Gradle中表示在构建中的一件独立工作,每个Task都属于某一个Project。

在使用Gradle时,创建Task最常见的方式是:

task
1
2
3
task hello « {
  println hello
}

这里的“«”表示追加的意思,即向hello中加入执行过程。

2.6 进一步深入理解

Gradle提供一套DSL语言来描述构建脚本。这套DSL语言是Groovy的内部DSL语言再添加一些东西,所以是基于Groovy语法的。

上面介绍了一个最常见的创建Task的方法,实际上它是在调用Gradle API提供的一个方法task,该方法属于Project类。方法签名如下:

Task task(String name, Closure configClosure);

创建一个Task对象,给它一个名字,并将它添加到Project对象中。

一个构建脚本build.gradle代表着一个工程。

对于构建中的每一个工程,Gradle都会创建一个Project的实例对象,并把它与构建脚本关联。当构建脚本在执行时,它会配置这个Project的实例对象。

配置方式如下:

任何在你脚本中调用的方法(例如:task方法),如果不是在脚本中定义的方法,就把它代理给Project对象(也就是说它认为是Project对象的一个方法)。

任何你访问的属性(例如:name属性),如果不是在脚本中定义的属性,就把它代理给Project对象(也就是project.name属性了)。

那么Project有哪些方法呢?

task()是一个,你已经看到,用于定义一个任务。

apply()是一个,用于向Project对象添加插件,关于插件的具体内容,后面会讲到。

Project有哪些属性呢?

有name属性,表示工程目录的名字

有project属性,也就它自己,所以:

println name和 println project.name是一样的。

2.7 Gradle的另一个重要概念:Plugin,插件

Gradle的插件的目的是包装起可重用的构建逻辑,这样可以重复使用在许多的工程和构建中。

Gradle的插件可以用任何语言去实现,提供的实现最终都将编译为字节码。

你可以把插件当做是对Gradle的扩展,它会以某种方式帮助你配置工程,典型的是添加一些预配置的task提供你使用。

Gradle作为一种通用的构建工具。核心的功能都用插件来提供,以便构建的使用者可以重用一些模式和实践。

Gradle自带了很多插件,已提供常用的功能。

例如,Java插件,Jetty插件,Maven插件(可以使用Maven的仓库),WAR插件,CheckStyle插件,Jacoco插件(测试覆盖率)等等。

Java插件:

使用的方法是在build.gradle构建脚本中加入一段代码:

task
1
apply plugin: java

在上一篇中已经介绍过,build.gradle的构建脚本在没有明确定义一个函数时,都是代理调用Project对象的方法。这里调用 的是Project对象中的apply方法:

apply(Map<String, ?> option);

java插件向Project中引入了多个Task和Property。java插件比较与众不同的地方,其中之一便是它在项目中引入了构建生命周期的概念,就像Maven一样。但是,和Maven不同的是,Gradle的项目构建生命周期并不是Gradle的内建机制,而是由Plugin自己引入的(这一点前面介绍过)。

当你在命令行中执行“gradle build”命令,可以看到java插件所引入的主要Task:

:compileJava

:processResources

:classes

:jar

:assemble

:compileTestJava

:processTestResources

:testClasses

:test

:check

:build

当然还有一个有用的Task:

clean:Deletes the build directory, removing all built files.

具体的每一个task的含义,我就不一一解释了,在这里可以查得到。

java插件中每个task之间的关系图,他们之间的依赖关系,构成了类似Maven完整的生命周期。

Gradle是一个结合Ant和Maven的项目管理工具。Maven的约定优于配置的思想固然是要在这里体现的。

java插件会假设你的项目结构如下:

src/main/java Production Java source

src/main/resources Production resources

src/test/java Test Java source

src/test/resources Test resources

src/sourceSet/java Java source for the given source set

src/sourceSet/resources Resources for the given source set

Source Set概念(此处摘抄自腾云的博文,待修改)

Gradle在采用了Maven目录结构的同时,还融入了自己的一些概念,即source set。对于上图中的目录结构,Gradle实际上为我们创建了2个source set,一个名为main,一个名为test。

请注意,这里的source set的名字main与上图目录结构中的main文件夹并无必然的联系,只是在默认情况下,Gradle为了source set概念到文件系统目录结构的映射方便,才采用了相同的名字。对于test, 也是如此。我们完全可以在build.gradle文件中重新配置这些source set所对应的目录结构,同时,我们还可以创建新的source set。

从本质上讲,Gradle的每个source set都包含有一个名字,并且包含有一个名为java的Property和一个名为resources的Property,他们分别用于表示该source set所包含的Java源文件集合和资源文件集合。在实际应用时,我们可以将他们设置成任何目录值。比如,我们可以重新设置main的目录结构:

task
1
2
3
4
5
6
7
8
9
10
sourceSets {
  main {
     java {
        srcDir java-sources
     }
     resources {
        srcDir resources
     }
  }
}

2.8 另外一个重点:依赖管理

我们知道依赖管理是项目管理软件中的一个非常重要部分,在Maven的POM文件中,占据最多行配置代码的就是这个部分。

现在我们来看看在Gradle中是如何配置依赖的?

task
1
2
3
4
5
6
7
apply plugin: java
repositories {
      mavenCentral()
}
dependencies {
      testCompile group: junit, name: junit, version: 4.+
}

上面的代码,定义了对junit的依赖,并告诉Gradle到maven的中心仓库去寻找依赖。

在Gradle中,依赖会被分组到不同的configuration对象中。configuration有名字和一些属性,并且他们能够互相扩展。许多Gradle的插件就预定义了一些configuration到工程中。

定义一个configuration的方式如下:

task
1
2
3
configurations {
      compile
}

上面的代码表示定义了一个名字是compile的configuration对象。

实际上他调用的是Project对象的configurations方法:

void configurations(Closure configureClosure)

它是用于配置configuration对象的。

在java插件中已经预先定义了一些configuration,例如:compile,runtime,testCompile,testRuntime。

compile

需要在编译产品代码时使用的依赖

runtime

需要在产品代码的运行时使用的依赖,默认也包含编译时使用的依赖。

testCompile

需要在编译测试代码时使用的依赖。默认也包含编译时和运行时产品代码的依赖。

testRuntime

需要在测试代码运行时的依赖。默认包含产品代码编译时和运行时依赖,以及测试代码编译时依赖。

task
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apply plugin: java

//so that we can use ‘compile’, ‘testCompile’ for dependencies

dependencies {
        //for dependencies found in artifact repositories you can use
         //the group:name:version notation
       compile commons-lang:commons-lang:2.6
       testCompile org.mockito:mockito:1.9.0-rc1
        //map-style notation:
       compile group: com.google.code.guice, name: guice, version: 1.0
        //declaring arbitrary files as dependencies
       compile files(hibernate.jar, libs/spring.jar’)
       //putting all jars from libs onto compile classpath
       compile fileTree(libs)
}

当然还有依赖配置的更高级的功能:

  • 强制某个依赖的版本号以防止冲突

  • 排除某个依赖

  • 避免某个依赖的传递依赖

下面是一个使用了强制,排除和关闭依赖传递性的例子。

task
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apply plugin: java //so that I can declare ‘compile’ dependencies

dependencies {
    compile(org.hibernate:hibernate:3.1) {
        //in case of versions conflict ‘3.1’ version of hibernate wins:
        force = true
        //excluding a particular transitive dependency:
        exclude module: cglib //by artifact name
        exclude group: org.jmock //by group
        exclude group: org.unwanted, module: iAmBuggy //by both name and group
        //disabling all transitive dependencies of this dependency
        transitive = false
    }
}

关于依赖,暂时先讲到这里,后面还有更详细的分析。

2.9 多个项目的构建

多项目,对于Maven而言,称为多模块(Multi-Module),对于Gradle而言,称为多个工程(Multi-Project)。

多模块的好处是你只需在根模块中执行Maven命令,Maven会分别在各个子模块中执行该命令,执行顺序通过Maven的Reactor机制决定。

收集起所有可构建的模块

对模块的构建顺序进行排序

按照正确的顺序进行构建

排序的规则如下:

  • 一个模块的构建依赖于另一个构建中模块

  • 一个模块使用的插件来自于构建中的模块(构建的内容是一个插件)

  • 插件的依赖于另一个模块

  • 一个构建扩展声明来自于另一个构建中的模块

元素中声明的顺序(如果没有其他规则的情况下)

强调一点,在Maven中,由多模块(由上到下)和继承(由下到上)关系并不必同时存在。

在Maven中,定义多模块的方式是在父POM中定义

pom.xml
1
<modules><modules>

而实现继承的方式是在子POM中声明

pom.xml
1
<parent></parent>

多模块的目的是让父模块知道子模块及它们之间的关系。

继承是子模块希望从父模块那里继承属性,减少在子模块中对相同属性的重复定义。

在Gradle中,定义project之间的父子关系,要比Maven更简单。

首先在根project中加入名为settings.gradle的配置文件,然后我们在根project目录下创建两个文件夹来表示子project的目录,分别为sub-project1和sub-project2,

根project在自己的目录下拥有build.gradle文件和settings.gradle文件。而两个子Project拥有他们自己的 build.gradle文件。

目录结构如下:

root-project/

sub-project1/

build.gradle

sub-project2/

build.gradle

build.gradle

settings.gradle

最后,需要在settings.gradle中加入一段脚本:

task
1
include sub-project1, sub-project2

在前面,我们讲过Gradle的声明周期,初始化,配置,执行。

其中初始化的过程就是用来确定多个project的过程。

而配置阶段就是根据构建脚本内容对project对象的配置过程。

Gradle提出一种按需配置的模式。意思是说,根据需要,并不是所有的project都需要配置,根据task的需求而定。

关于这一点的具体内容,后续在继续聊。

定义所有project的共同内容

在父project的build.gradle中定义:

task
1
2
3
allprojects {
      task hello « { task -> println Im $task.project.name }
}

它会调用project对象的allprojects方法:

void allprojects(Closure configureClosure)

在这个里面的配置是提供给所有的project,这意味着父project和所有子project。

定义给子project的共同内容

task
1
2
3
subprojects {
      hello « {println - I depend on water}
}

它会调用project对象的subprojects方法:

void subprojects(Closure configureClosure)

这里面的配置是提供给所有的子project使用。

还可以在父project的build.gradle中写一句话来指定某一个子project做一件事情。

task
1
2
3
project(:bluewhale).hello « {
    println - Im the largest animal that has ever lived on this planet.
}

传入子project的名字(就是它的目录名),并定义一个task。

Comments