大型移动应用解决之道 - 依赖管理
读者如果阅读过”插件化”与”组件化”这两篇文章的话,可能多少对下面这张图应该会有印象
image上图我用红色着重标记出来的Maven仓库,它的作用是什么?为什么会引入这样一台服务器?
如果我们足够细化架构,那么必然会有通用的组件或模块被提取出来,通常每个通用组件或模块都有专门的团队来负责开发维护,既然是通用的,那么其他功能模块的研发团队都需要依赖他们来做事情,而依赖的方式大概有以下两种:
1. 代码依赖
image就像上图,组件团队提交代码与依赖库到SCM,模块团队将组件工程从SCM上checkout下来,将代码copy到工程内或将组件工程作为lib依赖,这两种做法非常容易引起依赖冲突与无用的依赖,而且这种依赖是直接的代码依赖,具有很大的安全性性问题。
2. 二进制包依赖
二进制包的依赖应该说是比较好的依赖方式(没有安全性等问题),但是面临的问题是以什么样的方式去获取依赖包?
2.1 copy依赖包
组件团队将组件打包成AAR/JAR后,将组件包与依赖的第三方包都上传到约定的服务器上,各模块团队都到指定服务器上去取。 这种方式虽然避免了模块团队直接依赖代码,但是由于需要手动从服务器下载组件包与依赖包,出错概率很高,很有可能出现在手动copy过程中出现依赖版本copy错误的问题,而且要想更新组件所依赖的第三方库的版本也是非常繁琐的事情。
2.2 通过maven
image在看上图,组件团队将组件打包成AAR/JAR(同时生成POM文件,文件中声明依赖),并通过Maven插件上传到指定的私有Maven服务器上,各模块团队通过Maven插件去引用组件(没有手动过程),而依靠Maven的依赖传递性将自动把各组件所依赖的所有第三方库及第三方库依赖的包进行下载并引入。
很明显2.2部分,使用maven进行依赖的管理是最佳的方案,那么Maven是什么?
Maven是一个项目的管理工具,它能够贯穿整个研发流程,从开发,测试,打包,发布等都能派上用场,而Maven最突出的能力就是依赖管理,也就是我们今天所说的内容。 使用了Maven的依赖管理,从此告别手动copy依赖包,人工维护依赖包版本,依赖冲突与重复依赖等问题,节省了很多时间,同时避免了很多错误。
在Android中,我们基本上都是通过Gradle来操作maven(Gradle继承了maven的依赖管理操作,并将依赖的引用变的更加简单),使用nexus来搭建Maven服务器(nexus实现了maven的依赖管理)。
在iOS中,则通过cocoapods来进行依赖管理,我们后面会在iOS系列文章中讲解cocoapods的使用。
Maven是如何进行依赖管理的,在Gradle中如何使用?
定位依赖包
我们要使用依赖包,首先需要定位到这个依赖包,而在maven中,要定义一个依赖包,需要了解包的定位坐标groupid,artifactId,version,基于三者的信息就可以在本地或远程服务器进行定位。
例如:commons-collections的定位坐标,看下图XML部分定义
imageGradle中定义一个依赖包
commons-collections:commons-collections:3.2.2
依赖版本
通常我们可能会有多个组件引用了commons-collections包,如果需要统一升级commons-collections版本,我们只能逐个到项目中去修改,这样就增加了出错的概率。 而maven中支持我们通过引用变量的方式,在POM文件中以${变量名}来进行引用。那么我们通过修改这个变量名对应的值就可以做到批量修改对应的版本号。
变量名=值(commonscollections.version = 3.2.2)
Maven配置
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commonscollections.version}</version>
</dependency>
Gradle引入
commons-collections:commons-collections:${commonsnet.version}
在Gradle中,我们通常将版本配置在.gradle中,或配置在.properties文件中。
依赖范围
根据依赖范围的配置,我们可以控制所依赖的包,是否被打包进我们的产品中。
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commonscollections.version}</version>
<scope>provided</scope>
</dependency>
依赖范围 是否被打包
compile true
runtime true
provided false
test false
依赖分类
通常可以通过groupid(组ID),artifactId(依赖包名称),version(依赖包版本)来定位一个依赖包。 但是有时,我们还需要依赖包的其他部分:比如,源码(src),资源(sources),文档(javadoc)等,这些包他们的groupid,artifactid,version是相同的,只是类型不同,也就是classifier不同。 我们看下dbutils组件都做了哪些分类,从下图XML部分,可以看到当前classifier是src,也就表明,这是一个源代码的包。
image从下图XML中,可以看到当前classifier是sources,表明这是一个资源的包。
image我们也可以将同一个jar包打包成两个不同classifier的jar包来进行区分,使用者根据classifier来进行引用。
通过classifier,使我们可供依赖的资源变的更加丰富和灵活。
通过Gradle来引入classifier的依赖包
Gradle引入
commons-dbutils:commons-dbutils:1.6:sources
依赖传递
假设我们有模块A , B, C , D, A->B(A直接依赖B),B->C,C->D。那么A间接依赖B,C,D模块(A->B->C->D),这就是依赖传递。
禁止传递
Maven配置,排除所有依赖
<dependency>
<groupId>xx.xx</groupId>
<artifactId>YY</artifactId>
<version>oo</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
gradle禁止依赖传递
compile('xx:YY:xx') {
transitive = false
}
依赖排除
按照上面例子,A间接依赖B,C,D模块,如果A想排除对D的依赖,可以在pom文件中添加如下配置。
<dependency>
<groupId>xx.xx</groupId>
<artifactId>YY</artifactId>
<version>oo</version>
<exclusions>
<exclusion>
<groupId>xx.xx</groupId>
<artifactId>YY</artifactId>
</exclusion>
</exclusions>
</dependency>
在gradle中,排除依赖
dependencies {
compile("xx.xx.xx:YY:oo") {
exclude group: 'xx.xx', module: 'YY'
}
}
依赖冲突
A->B->collections3.1, B->C->D->collections3.2.2
从上面依赖关系可以看出,B依赖collections3.1,而按照B的依赖路径,同时也会依赖collections3.2.2,细心的读者可能发现了问题。
maven会选择最短的路径进行依赖,也就是v3.1,而我们本意是要使用最新的版本做为依赖,这样可能会导致崩溃或功能性错误。我们可以缩短高版本的依赖路径,修改POM文件为A->B->collections3.2.2,而B->C->D->collections3.1
而Gradle默认是按照高版本进行依赖的, 即便上面的依赖路径,Gradle也会去依赖v3.2.2的版本。而有时,我们可能也需要强制的依赖某个版本,就像下面:
compile('xxx.xxx:YY:oo') {
force = true
}
全局配置强制依赖
configurations.all {
resolutionStrategy {
force 'xx.xx:YY:oo'
}
}
通过上面的讲述,相信读者基本已经了解maven的依赖管理的优势。 那么如何搭建自己的Maven服务器呢? 上面也提到了使用Nexus,关于Nexus的搭建教程推荐大家这篇文章
http://blog.csdn.net/wang379275614/article/details/43940259
通过这篇文章,相信读者也基本了解了Nexus相关概念和搭建技巧。
而如何去操作nexus,通常有两种方法:
1. 手动上传
在nexus的web ui上进行管理
2. 通过gradle的maven-plugin
通过gradle命令进行管理
gradle如何配置和使用命令
1. 配置
Nexus相关配置 build-nexus-config.gradle
gradle.ext {
NEXUS_SERVER_GROUP='http://ip:port/nexus/content/groups/public/'
NEXUS_SERVER_RELEASE='http://ip:port/nexus/content/repositories/release/'
NEXUS_SERVER_SNAPSHOT='http://ip:port/nexus/content/repositories/snapshot/'
NEXUS_SERVER_UNAME='admin'
NEXUS_SERVER_UPWD='123456'
}
版本相关配置build-artifact-config.gradle
gradle.ext {
ARTIFACT_SNAPSHOT_VERSION='1.0.0.5-SNAPSHOT'
ARTIFACT_RELEASE_VERSION='1.0.0.5'
ARTIFACT_ID=’common’
ARTIFACT_GROUP_ID='com.xx.xx'
}
在build.gradle中配置仓库地址
allprojects {
repositories {
maven{ url gradle.NEXUS_SERVER_GROUP }
jcenter()
}
}
2. 上传组件
在上传时需要区分当前要上传的是snapshot仓库,还是relaese仓库。
可以通过参数来进行区分(由自己定义),默认情况下,发布的为快照版本,如果添加参数名为'repositoryRelease'则表示为正式版本。
Gradle发布snapshot命令:
gradle uploadArchives
Gradle发布release命令:
gradle uploadArchives -P repositoryRelease
apply plugin: 'maven'
apply plugin: "maven-publish"
def isSnapshot = true
if(project.hasProperty('repositoryRelease')){
isSnapshot = false
}
gradle.ext.isSnapshotVersion = isSnapshot;
uploadArchives {
repositories {
mavenDeployer {
repository(url: isSnapshot ? gradle.NEXUS_SERVER_SNAPSHOT : gradle.NEXUS_SERVER_RELEASE) {
authentication(userName: gradle.NEXUS_SERVER_UNAME, password: gradle.NEXUS_SERVER_UPWD)
}
pom.version = isSnapshot ? gradle.ARTIFACT_SNAPSHOT_VERSION :gradle.ARTIFACT_RELEASE_VERSION
pom.artifactId = gradle.ARTIFACT_ID
pom.groupId = gradle.ARTIFACT_GROUP_ID
pom.withXml {
def dependenciesNode = asNode().appendNode('dependencies')
configurations.compile.allDependencies.each {
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
}
}
}
}
}
通过以上,我们可以总结出下图:
image从上图可以看出,所有通用组件(业务/技术/SDK等)全部都存储在Nexus服务器上,Nexus上由不同的仓库(snapshot/release/proxy/group等)组成。组件团队与业务团队均通过配置Maven插件达到对指定的仓库进行上传和下载。
另外一个Maven重要的功能,既是代理远程的仓库,见下图:
image通过代理远端仓库,达到加快本地快速访问的目的。
写到这里,不知道读者是不是已经对依赖管理有了一些了解。如果读者在搭建过程中有任何问题,我们可以一起讨论。