Gradle入门系列(3):依赖管理

转载:
本文由 伯乐在线 - JustinWu 翻译。未经许可,禁止转载!
英文出处:Petri Kainulainen。欢迎加入翻译组

在现实生活中,要创造一个没有任何外部依赖的应用程序并非不可能,但也是极具挑战的。这也是为什么依赖管理对于每个软件项目都是至关重要的一部分。

这篇教程主要讲述如何使用Gradle管理我们项目的依赖,我们会学习配置应用仓库以及所需的依赖,我们也会理论联系实际,实现一个简单的演示程序。

让我们开始吧。

仓库管理简介

本质上说,仓库是一种存放依赖的容器,每一个项目都具备一个或多个仓库。

Gradle支持以下仓库格式:

我们来看一下,对于每一种仓库类型,我们在构建中应该如何配置。

在构建中加入Ivy仓库

我们可以通过URL地址或本地文件系统地址,将Ivy仓库加入到我们的构建中。

如果想通过URL地址添加一个Ivy仓库,我们可以将以下代码片段加入到build.gradle文件中:

repositories {
    ivy {
        url "http://ivy.petrikainulainen.net/repo"
    }
}

如果想通过本地文件系统地址添加一个Ivy仓库,我们可以将以下代码片段加入到build.gradle文件中:

repositories {
    ivy {       
        url "../ivy-repo"
    }
}

小贴士:如果你想要获得更多关于Ivy仓库配置的信息,你可以参考以下资源:

我们继续,下面是如何在构建中加入Maven仓库。

在构建中加入Maven仓库

与Ivy仓库很类似,我们可以通过URL地址或本地文件系统地址,将Maven仓库加入到我们的构建中。

如果想通过URL地址添加一个Maven仓库,我们可以将以下代码片段加入到build.gradle文件中:

repositories {
    maven {
        url "http://maven.petrikainulainen.net/repo"
    }
}

如果想通过本地文件系统地址添加一个Maven仓库,我们可以将以下代码片段加入到build.gradle文件中:

repositories {
    maven {       
        url "../maven-repo"
    }
}

在加入Maven仓库时,Gradle提供了三种“别名”供我们使用,它们分别是:

  • mavenCentral()别名,表示依赖是从Central Maven 2 仓库中获取的。
  • jcenter()别名,表示依赖是从Bintary’s JCenter Maven 仓库中获取的。
  • mavenLocal()别名,表示依赖是从本地的Maven仓库中获取的。

如果我们想要将Central Maven 2 仓库加入到构建中,我们必须在build.gradle文件中加入以下代码片段:

repositories {
    mavenCentral()
}

小贴士:如果你想要获取更多关于Maven仓库配置的信息,你可以参考这篇文章:
section 50.6.4 Maven Repositories of the Gradle User Guide

我们继续,下面是如何在构建中加入Flat Directory仓库。

在构建中加入Flat Directory仓库

如果我们想要使用Flat Directory仓库,我们需要将以下代码片段加入到build.gradle文件中:

repositories {
    flatDir {
        dirs 'lib'
    }
}

这意味着系统将在lib目录下搜索依赖,同样的,如果你愿意的话可以加入多个目录,代码片段如下:

repositories {
    flatDir {
        dirs 'libA', 'libB'
    }
}

小贴士:如果你想要获得更多关于Flat Directory仓库配置的信息,你可以参考以下资源:

我们继续,下面要讲的是,如何使用Gradle管理项目中的依赖。

依赖管理简介

在配置完项目仓库后,我们可以声明其中的依赖,如果我们想要声明一个新的依赖,可以采用如下步骤:

  1. 指定依赖的配置。
  2. 声明所需的依赖。

让我们看一下详细步骤:

配置中的依赖分类

在Gradle中,依赖是按照指定名称进行分类的,这些分类被称为配置项,我们可以使用配置项声明项目的外部依赖。

Java插件指定了若干依赖配置项,其描述如下:

  • 当项目的源代码被编译时,compile配置项中的依赖是必须的。
  • runtime配置项中包含的依赖在运行时是必须的。
  • testCompile配置项中包含的依赖在编译项目的测试代码时是必须的。
  • testRuntime配置项中包含的依赖在运行测试代码时是必须的。
  • archives配置项中包含项目生成的文件(如Jar文件)。
  • default配置项中包含运行时必须的依赖。

我们继续,下面是如何在项目中声明依赖。

声明项目依赖

最普遍的依赖称为外部依赖,这些依赖存放在外部仓库中。一个外部依赖可以由以下属性指定:

  • group属性指定依赖的分组(在Maven中,就是groupId)。
  • name属性指定依赖的名称(在Maven中,就是artifactId)。
  • version属性指定外部依赖的版本(在Maven中,就是version)。

小贴士:这些属性在Maven仓库中是必须的,如果你使用其他仓库,一些属性可能是可选的。打个比方,如果你使用Flat directory仓库,你可能只需要指定名称和版本

我们假设我们需要指定以下依赖:

  • 依赖的分组是foo。
  • 依赖的名称是foo。
  • 依赖的版本是0.1。
  • 在项目编译时需要这些依赖。

我们可以将以下代码片段加入到build.gradle中,进行依赖声明:

dependencies {
    compile group: 'foo', name: 'foo', version: '0.1'
}

我们也可以采用一种快捷方式声明依赖:[group]:[name]:[version]。如果我们想用这种方式,我们可以将以下代码段加入到build.gradle中:

dependencies {
    compile 'foo:foo:0.1'
}

我们也可以在同一个配置项中加入多个依赖,传统的方式如下:

dependencies {
    compile (
        [group: 'foo', name: 'foo', version: '0.1'],
        [group: 'bar', name: 'bar', version: '0.1']
    )
}

如果采用快捷方式,那可以是这样:

dependencies {
    compile 'foo:foo:0.1', 'bar:bar:0.1'
}

自然地,声明属于不同配置项的依赖也是可以的。比如说,如果我们想要声明属于compile和testCompile配置项的依赖,可以这么做:

dependencies {
    compile group: 'foo', name: 'foo', version: '0.1'
    testCompile group: 'test', name: 'test', version: '0.1'
}

同样的,给力的快捷方式又来了( ̄︶ ̄)

dependencies {
    compile 'foo:foo:0.1'
    testCompile 'test:test:0.1'
}

小贴士:你可以从这篇文章中获得更多关于依赖声明的信息。

我们已经学习了依赖管理的基础知识,下面我们来实现一个演示程序。

创建演示程序

演示程序的需求是这样的:

  • 演示程序的构建脚本必须使用Maven central仓库。
  • 演示程序必须使用Log4j写入日志。
  • 演示程序必须包含包含单元测试,保证正确的信息返回,单元测试必须使用JUnit编写。
  • 演示程序必须创建一个可执行的Jar文件。

我们来看一下怎样实现这些需求。

配置仓库

我们的演示程序的一个需求是构建脚本必须使用Maven central仓库,在我们使用Maven central仓库配置构建脚本后,源代码如下:

apply plugin: 'java'
 
repositories {
    mavenCentral()
}
 
jar {
    manifest {
        attributes 'Main-Class': 'net.petrikainulainen.gradle.HelloWorld'
    }
}

我们再来看一下如何对我们的演示程序进行依赖声明。

依赖声明

在build.gradle文件中,我们声明了两个依赖:

在我们声明了这些依赖后,build.gradle文件是这样的:

apply plugin: 'java'
 
repositories {
    mavenCentral()
}
 
dependencies {
    compile 'log4j:log4j:1.2.17'
    testCompile 'junit:junit:4.11'
}
 
jar {
    manifest {
        attributes 'Main-Class': 'net.petrikainulainen.gradle.HelloWorld'
    }
}

我们继续,稍微加入一些代码。

编写代码

为了实现我们演示程序的需求,“我们不得不过度工程化一下”,我们会按照下列步骤创建程序:

  1. 创建一个MessageService类,当其中的getMessage()方法被调用时,返回字符串“Hello World!”。
  2. 创建一个MessageServiceTest类,确保MessageService类中的getMessage()方法返回字符串“Hello World!”。
  3. 创建程序的主类,从MessageService对象获取信息,并使用Log4j写入日志。
  4. 配置Log4j。

我们按部就班的操作一下。

首先,在src/main/java/net/petrikainulainen/gradle目录下新建一个MessageService类并加以实现,代码如下:

package net.petrikainulainen.gradle;
 
public class MessageService {
 
    public String getMessage() {
        return "Hello World!";
    }
}

其次,在src/main/test/net/petrikainulainen/gradle目录下新建一个MessageServiceTest类,编写单元测试,代码如下:

package net.petrikainulainen.gradle;
 
import org.junit.Before;
import org.junit.Test;
 
import static org.junit.Assert.assertEquals;
 
public class MessageServiceTest {
 
    private MessageService messageService;
 
    @Before
    public void setUp() {
        messageService = new MessageService();
    }
 
    @Test
    public void getMessage_ShouldReturnMessage() {
        assertEquals("Hello World!", messageService.getMessage());
    }
}

第三,在src/main/java/net/petrikainulainen/gradle目录下新建一个HelloWorld类,这是程序的主类,从MessageService对象获取信息,并使用Log4j写入日志,代码如下:

package net.petrikainulainen.gradle;
 
import org.apache.log4j.Logger;
 
public class HelloWorld {
 
    private static final Logger LOGGER = Logger.getLogger(HelloWorld.class);
 
    public static void main(String[] args) {
        MessageService messageService = new MessageService();
 
        String message = messageService.getMessage();
        LOGGER.info("Received message: " + message);
    }
}

第四,在src/main/resources目录中,使用log4j.properties配置log4j,log4j.properties文件如下:

log4j.appender.Stdout=org.apache.log4j.ConsoleAppender
log4j.appender.Stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.Stdout.layout.conversionPattern=%-5p - %-26.26c{1} - %m\n
 
log4j.rootLogger=DEBUG,Stdout

这样就好了,我们看看如何执行测试。

执行测试

我们可以通过以下命令执行测试。

gradle test

当测试通过时,我们能看到如下输出:

> gradle test
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources 
:testClasses
:test
 
BUILD SUCCESSFUL
 
Total time: 4.678 secs

然而,如果测试失败,我们将看到如下输出:

> gradle test
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources
:testClasses
:test
 
net.petrikainulainen.gradle.MessageServiceTest > getMessage_ShouldReturnMessageFAILED
    org.junit.ComparisonFailure at MessageServiceTest.java:22
 
1 test completed, 1 failed
:test FAILED
 
FAILURE: Build failed with an exception.
 
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/loke/Projects/Java/Blog/gradle-examples/dependency-management/build/reports/tests/index.html
 
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
 
BUILD FAILED
 
Total time: 4.461 secs

正如我们所看到的,如果单元测试失败了,输出信息中将描述以下信息:

  • 哪一个测试失败了。
  • 执行了几个测试,其中几个失败了。
  • 测试报告的位置,测试报告提供了失败(或成功)的测试的额外信息。

当我们执行单元测试时,Gradle会在相应目录创建测试报告:

  • build/test-results目录包含每次测试执行的原始数据。
  • build/reports/tests目录包含一个HTML报告,描述了测试的结果。

HTML测试报告是一个非常有用的工具,因为它描述了测试失败的原因。比如说,如果我们的单元测试认为MessageService类中的getMessage()方法返回字符串“Hello Worl1d!”,那么HTML报告看上去就像下图一样:


image

我们继续,下面是如何打包和运行我们的演示程序。

打包和运行程序

我们能够可以使用以下任意一种命令打包程序:gradle assembly或gradle build,这两个命令都会在build/libs目录中创建dependency-management.jar文件。

当我们使用java -jar dependency-management.jar命令运行演示程序时,我们可以看到如下输出:

> java -jar dependency-management.jar
 
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/log4j/Logger
    at net.petrikainulainen.gradle.HelloWorld.<clinit>(HelloWorld.java:10)
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Logger
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

抛出异常的原因是,当我们运行程序时,Log4j的依赖在classpath中没有找到。

解决这个问题最简单的方式是创建一个所谓的“胖”Jar文件,即把所有程序运行所需的依赖都打包到Jar文件中去。

通过查阅Gradle Cookbook中的教程,可以修改构建脚本,如下:

apply plugin: 'java'
 
repositories {
    mavenCentral()
}
 
dependencies {
    compile 'log4j:log4j:1.2.17'
    testCompile 'junit:junit:4.11'
}
 
jar {
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
    manifest {
        attributes 'Main-Class': 'net.petrikainulainen.gradle.HelloWorld'
    }
}

现在,我们可以运行演示程序了(打包后),一切正常:

> java -jar dependency-management.jar 
INFO  - HelloWorld                 - Received message: Hello World!

这些就是今天的内容了,我们总结一下学到了什么。

总结

这篇教程教会了我们四个方面的内容:

  • 我们学会了配置构建所需的仓库。
  • 我们学会了如何声明所需的依赖以及依赖的分类(分组)。
  • 我们了解了Gradle会在测试执行后创建一个HTML测试报告。
  • 我们学会了创建一个所谓的“胖”Jar文件。

如果你想要玩一玩这篇教程所涉及的演示程序,你可以从GitHub那获取。

关于作者:JustinWu

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,745评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,469评论 25 707
  • 转载注明出处:http://www.jianshu.com/p/5255b100930e 0. 前言 完全由个人翻...
    王三的猫阿德阅读 2,501评论 0 4
  • 1.介绍 如果你正在查阅build.gradle文件的所有可选项,请点击这里进行查阅:DSL参考 1.1新构建系统...
    Chuckiefan阅读 12,115评论 8 72