赞
踩
如今,项目开发已经不再是单兵作战的时代,而往往是多团队、多组件协同开发。此时,我们会发布组件 & 管理组件的技巧;
在这篇文章里,我将带你理解组件的基本概念,以及组件发布 & 快照预览 & 依赖切换的实战应用经验。如果能帮上忙,请务必点赞加关注,这对我真的非常重要;
本文相关代码可以从 DemoHall·MavenPulish 下载查看。
POM(Project Object Model)指项目对象模型,用于描述项目构件的基本信息。一个有效的 POM 节点中主要包含一下信息:
配置 | 描述 | 举例('com.github.bumptech.glide:glide:4.11.0') |
---|---|---|
groupId | 组织 / 公司的名称 | com.github.bumptech.glide |
artifactId([ˈɑːtɪfækt]) | 组件的名称 | glide |
version | 组件的版本 | 4.11.0 |
packaging | 打包的格式 | aar |
在项目中,我们会需要依赖各种各样的二方库或三方库,这些依赖一定会存放在某个位置(Place),这个 “位置” 就叫做仓库。使用仓库可以帮助我们管理项目构件,例如 jar、aar 等等。
主流的构建工具都有三个层次的仓库概念:
构建时搜索依赖的顺序如下:
repositories
中声明的顺序依次查找。如果找到,则下载依赖文件到本地仓库,否则执行步骤 3;如何在项目中声明仓库:
Winodws 下 Gradle 默认的本地仓库目录:C:/Users/Administrator/.gradle/caches/modules-2/files-2.1
而 Mac OS 下是/User/用户名/.gradle/caches/modules-2/files-2.1
。Gradle 不会默认执行远程仓库和中央仓库,需要在项目级或模块级 build.gradle 文件中声明。例如: 0.
项目级别 build.gradle
- buildscript {
- repositories {
- [Gradle 插件的仓库]
- }
- }
- allprojects {
- repositories {
- [项目中所有模块依赖的仓库]
- }
- }
模块级别 build.gradle
- repositories{
- [当前模块依赖的仓库]
- }
Gradle 支持多种类型的仓库,例如 Maven、ivy、flatDir。其中 flatDir 一般用于指定本地 aar 文件的地址。更多分析在 第 4.2 节。
- repositories{
- maven { url '...' }
- ivy { url '...' }
- flatDir { dirs '...' }
- }
Gradle 内置了一些常用中央仓库的路径,可以直接通过函数获取。例如:
- google() // https://dl.google.com/dl/android/maven2/
- mavenCentral() // https://repo.maven.apache.org/maven2/
- jCenter() // https://jcenter.bintray.com/
有时候,直接访问中央仓库的速度太慢,此时可以尝试替换为国内大厂的中央仓库镜像。例如:
- maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
- maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
- maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
-
「Gradle 插件」和「Gradle」这两个概念是比较容易混淆的。Gradle 是构建工具,相当于一个构建环境;而 Gradle 插件本质上就是具体的构建任务,我们将一个构建任务模块化抽离出来,提供给其他项目复用,就是一个 Gradle 插件。例如:
快照是一种特殊的版本,与常规版本最大的不同是:快照版本每次构建时都会在远程仓库中检查最新的快照。
为什么会有这种设计呢(牺牲编译时间)?因为在大型软件项目中,往往是多个团队(或多个同学)协同开发不同模块,例如 A 模块依赖 B 模块,两个模块并行开发。如果模块 B 不使用快照版本(例如版本为 1.0.0),那么当 B 模块在开发阶段需要更新,A 模块就无法接收到更新。因为 A 模块本地仓库中已经下载了 B 模块的 1.0.0 版本,所以构建时不会重复去下载远程仓库中更新的版本。
直接的解决办法可以清除 A 模块的本地仓库缓存,或者每次 B 模块更新都升级版本,很显然两个办法都不灵活,频繁升级版本也是对版本号的滥用,不利于版本管理。而如果模块 B 使用快照版本(1.0.0-SNAPSHOT),A 模块每次构建都会去检查远程仓库是否有 B 模块的新快照,就可以保证一直依赖 B 模块的最新版本。
总的来说,SNAPSHOT 适合快节奏协同开发阶段,代表着不稳定 & 开发中的版本。常规版本适合于正式发布版本,如果正式版本使用 SNAPSHOT,会导致重复构建正式版本不稳定。
我们需要使用 Maven 插件来发布类库,简单理解下 Maven 构建的生命周期,主要分为以下个步骤:
任务 | 阶段 | 描述 |
---|---|---|
compile | 编译 | 编译源代码 |
test | 测试 | 执行单元测试 |
package | 打包 | 创建发布组件,如 jar、aar |
install | 安装 | 安装组件包到本地仓库 |
deploy / upload | 部署 | 上传组件包到远程仓库 |
—— 图片引用自网络
在 Gradle 中发布组件,可以使用以下两个 Maven 插件:
我们需要使用 Maven 插件的uploadArchives
任务,并且需要指定组件的信息。例如:
模块级 build.gradle
- plugins {
- id 'com.android.library'
- id 'kotlin-android'
- id 'maven'
- }
- ...
- uploadArchives {
- repositories {
- mavenDeployer {
- // 发布地址:直接发布到项目本地路径
- repository(url: uri('../repository'))
- // 组件信息:com.pengxr.demo:maven:v1.0.0
- pom.groupId = "com.pengxr.demo"
- pom.artifactId = "maven"
- pom.version = "v1.0.0"
- }
- }
- }
执行 Gradle Sync 之后,就可以在 Gradle 窗口该模块的 Tasks 列表中找到名为uploadArchives
的任务。执行任务,完成后项目中会新增一个repository
目录,里面就是新发布的组件。
注意事项:
2、无法发布应用模块
- plugins {
- id 'com.android.application' // 无法发布应用模块
- id 'kotlin-android'
- id 'maven'
- }
发布组件到本地仓库只能单机使用,在实际工作中,我们往往需要将组件发布给其他团队成员使用。此时,可以将组件发布到 局域网私有仓库。最常见的私有仓库管理工具是 Nexus [ˈneksəs]。按照以下步骤搭建环境:
1、下载 Nexus 安装包: 这里以 Mac 环境为例:下载地址;
2、启动 Nexus 服务进程: 进入安装路径/nexus-3.30.1-01/bin
,在终端运行命令:
- ./nexus start
- ./nexus status
-
- 输出:nexus is running. 表示启动成功
-
- 需要停止服务时,可以执行命令:
- ./nexus stop
这个列表包含了所有的 Nexus 仓库,点击 “Copy” 按钮,可以复制仓库的 URL 地址。其中两个仓库比较常用:
maven-release:策略为 Release 的宿主类型仓库,用于部署内部组件的发布版本;
maven-snapshots:策略为 Shapshot 的宿主类型仓库,用于部署内部组件的快照版本。
类型(Type):group(仓库组)、hosted(宿主)、proxy(代理)和 virtual(虚拟); 格式(Format):maven1、maven2、nuget
模块级 build.gradle
- apply plugin: 'maven' // Maven 插件
- ...
- uploadArchives {
- repositories {
- mavenDeployer {
- // url:仓库路径
- // userName:账号名
- // password:密码
- repository(url: "http://127.0.0.1:8081/repository/maven-releases/"){
- authentication(userName: "admin", password: "pengxurui123")
- }
-
- pom.groupId = "com.pengxr.demo"
- pom.artifactId = "maven"
- pom.version = "v1.0.0"
- }
- }
- }
执行任务,发布成功后可以在 nexus 管理平台上看到新发布的类库:
项目级 build.gradle
- allprojects {
- repositories {
- google()
- mavenCentral()
- maven { url "http://127.0.0.1:8081/repository/maven-releases/" }
- }
- }
模块级 build.gradle
- dependencies {
- ...
- implementation 'com.github.pengxurui:MavenPuhlish:v1.0.4'
- }
提示: 当然了,实际项目中 nexus 不可能配置在本机上,而是会配置在局域网服务器中。
如果你需要将开源,那么就需要发布到公共仓库,这一节介绍发布到 Github 的步骤:
项目级 build.gradle
- dependencies {
- ...
- classpath "com.github.dcendents:android-maven-gradle-plugin:1.5" // // GitHub Maven 插件
- }
模块级 build.gradle
apply plugin: 'com.github.dcendents.android-maven' // GitHub Maven 插件
模块级 build.gradle
- apply plugin: 'com.github.dcendents.android-maven' // GitHub Maven 插件
- group = 'com.github.pengxurui' // github 的用户名
4、将项目 push 到 Github
5、在 Github 上创建 Release Tag(在本地创建 Tag 再推到 Gtihub 也一样):
项目级 build.gradle
- allprojects {
- repositories {
- google()
- mavenCentral()
- maven { url "https://jitpack.io" }
- }
- }
模块级 build.gradle
- dependencies {
- ...
- implementation 'com.github.pengxurui:MavenPuhlish:v1.0.4'
- }
踩坑记录:
Exception is: java.lang.IllegalAccessError: tried to access method org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier.<init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V from class org.gradle.api.plugins.AndroidMavenPlugin$8
使用新版 Maven 插件,可以直接以指定二进制文件的方式发布组件。例如:
- apply plugin: 'maven-publish'
-
- publishing {
- publications {
- [任务名](MavenPublication) {
- groupId MAVEN_GROUP_ID
- artifactId MAVEN_ARTIFACTID
- version MAVEN_VERSION
- artifact([文件路径])
- }
- }
- repositories {
- maven {
- // 发布仓库路径
- url MAVEN_RELEASE_URL
- }
- }
- }
-
随着项目组件化程度加深,越来越多组件需要发布到 Maven 仓库,此时就很有必要将 Maven 发布能力封装为一个通用脚本,步骤如下:
maven.gradle
- apply plugin: 'maven'
-
- uploadArchives {
- repositories {
- mavenDeployer {
- // 是否快照版本
- def isSnapShot = Boolean.valueOf(MAVEN_IS_SNAPSHOT)
- def versionName = MAVEN_VERSION
- if (isSnapShot) {
- versionName += "-SNAPSHOT"
- }
- // 组件信息
- pom.groupId = MAVEN_GROUP_ID
- pom.artifactId = MAVEN_ARTIFACTID
- pom.version = versionName
-
- // 快照仓库路径
- snapshotRepository(url: uri(MAVEN_SNAPSHOT_URL)) {
- authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME)
- }
- // 发布仓库路径
- repository(url: uri(MAVEN_RELEASE_URL)) {
- authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME)
- }
-
- println("###################################"
- + "\nuploadArchives = " + pom.groupId + ":" + pom.artifactId + ":" + pom.version + "." + pom.packaging
- + "\nrepository =" + (isSnapshot ? MAVEN_SNAPSHOT_URL : MAVEN_RELEASE_URL)
- + "\n###################################"
- )
- }
- }
- }
这段脚本会读取 MAVEN_IS_SNAPSHOT 配置参数,如果为 true,会在版本号后追加 -SNAPSHOT 后缀,表示快照版本。随后声明了两个仓库:repository(...) 声明的是 Release 仓库地址,而 snapshotRepository(...) 声明的是快照仓库地址。Maven 会自动将版本号带 -SNAPSHOT 后缀的组件发布到 snapshotRepository(...) 仓库中,这样就 自动将正式版本和快照版本分发的不同仓库中。
当然了,不用 snapshotRepository(...) 也有办法实现:
- def url = isSnapShot ? MAVEN_SNAPSHOT_URL : MAVEN_RELEASE_URL
- repository(url: url) {
- authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME)
- }
项目级 gradle.properties
- MAVEN_SNAPSHOT_URL = /Users/pengxurui/workspace/public/DemoHall/snapshotRepository
- MAVEN_RELEASE_URL = /Users/pengxurui/workspace/public/DemoHall/releaseRepository
- MAVEN_USERNAME =
- MAVEN_PASSWORD =
- MAVEN_IS_SNAPSHOT = true
- MAVEN_GROUP_ID = com.pengxr.demo
- ...
参数 | 描述 |
---|---|
MAVEN_SNAPSHOT_URL | 快照仓库地址 |
MAVEN_RELEASE_URL | 发布仓库地址 |
MAVEN_USERNAME | 仓库账号 |
MAVEN_PASSWORD | 仓库密码 |
MAVEN_IS_SNAPSHOT | 是否快照版本 |
MAVEN_GROUP_ID | 组织 / 公司的名称 |
MAVEN_ARTIFACTID | 组件的名称(在发布模块配置) |
MAVEN_VERSION | 组件的版本(在发布模块配置) |
模块级 build.gradle
- apply from: '../maven.gradle'
- ...
模块级 gradle.properties
- MAVEN_ARTIFACTID = maven
- MAVEN_VERSION = v1.0.0
- MAVEN_IS_SNAPSHOT = true
- ...
完成以上步骤并 Sync 后,就可以在 Gradle 窗口中该模块下找到 uploadArchives 任务,执行发布:
输出: Executing tasks: [uploadArchives] in project /Users/pengxurui/workspace/public/DemoHall/MavenPublish/lib > Configure project :lib ################################### uploadArchives = com.pengxr.demo:maven:v1.0.0-SNAPSHOT.jar repository =/Users/pengxurui/workspace/public/DemoHall/snapshotRepository ################################### > Task :lib:preBuild UP-TO-DATE ...
项目级 build.gradle
- allprojects {
- repositories {
- maven { url MAVEN_RELEASE_URL }
- maven { url MAVEN_SNAPSHOT_URL }
- ...
- }
- }
模块级 build.gradle
- dependencies {
- implementation "com.pengxr.demo:maven:v1.0.0+"
- }
其中,版本号 v1.0.0+ 中的 “+” 号表示依赖最大的版本号,优先正式版本。比如远程仓库中存在 v1.0.0,v1.0.0.1,v1.0.0.1-SNAPSHOT 三个类库,那么 v1.0.0+ 依赖的是其 v1.0.0.1。
+ 号和 -SNAPSHOT 的区别?
+ 号影响类库版本的选择,而 -SNAPSHOT 影响是否向远程仓库更新最新版本。
完整代码和演示工程你可以直接下载查看: MavenPublish 下载路径。Demo 里配置的仓库都为本地仓库,在实际项目中,你需要替换为你公司内的私有仓库。
有时候,我们直接依赖第三方或第二方提供的 aar 文件。例如:
- - aarlib
- \ libs
- - lib-debug-aar
- - build.gradle // api(name: 'lib-debug', ext: 'aar')
-
- 输出:Unable to resolve dependency for ':aarlib@debugUnitTest/compileClasspath': Could not resolve :lib-debug.
但是,这样并不能成功依赖。你需要 build.gradle 文件中声明 aar 的 Flat Directory 仓库地址。你可以放在 android{} 节点内,或者直接放在根节点,效果是一样的。例如:
aarlib 模块 build.gradle
- dependencies {
- ...
- api(name: 'lib-debug', ext: 'aar')
- }
-
- repositories {
- flatDir {
- dirs "libs"
- }
- }
现在你就可以成功依赖了。但如果存在另一个依赖 aarlib 的模块,而这个模块又需要依赖 lib-debug.aar,还是会出依赖不到的问题:
- - app
- - build.gradle // implementation project(':aarlib')
- |
- - aarlib
- \ libs
- - lib-debug-aar
- - build.gradle // api(name: 'lib-debug', ext: 'aar')
此时,你同样需要在 app 模块里声明 aar 的 Flat Directory 仓库地址。
app 模块 build.gradle
- dependencies {
- ...
- implementation project(':aarlib')
- }
-
- repositories {
- flatDir {
- dirs project(':aarlib').file('libs')
- }
- }
如果项目组件结构比较简单,第 4.2 节的方法就足够应对本地引用 aar 的问题。否则还是会遇到一些麻烦的,你需要在每个模块的 build.gradle 中都声明 repositories.flatDir{},有办法优化吗?
方法 1:直接依赖改为间接依赖: 新建模块封装 aar,对外部提供外观 API
方法 2:统一将 aar 文件放置在一个文件夹,并在项目级 build.gradle 中声明仓库地址:
项目级 build.gradle
- allprojects {
- repositories {
- google()
- mavenCentral()
- flatDir { dirs project(':aarlib').file('libs') } // 文件夹要放在某个 module 内
- }
- }
模块级 build.gradle
- api(name: 'lib-debug', ext: 'aar') // 允许间接依赖 aar
- implementation(name: 'lib-debug', ext: 'aar') // 不允许间接依赖 aar
方法 3:二次打包 aar: 以上方法在单工程项目下表现良好,但在如果你们的项目包括多个工程,那还是有点麻烦的,有办法优化吗?你可以对 aar 文件二次打包,并发布到 Maven 仓库,这样你就不需要声明 Flat 本地仓库。
- - aarpacker
- \ libs
- - lib.aar
- - lib2.aar
- - build.gradle
aarpacker build.gradle
- apply plugin: 'maven-publish'
-
- def libPath = project.getProjectDir().getAbsolutePath()
-
- publishing {
- publications {
- lib1(MavenPublication) {
- groupId MAVEN_GROUP_ID
- artifactId "lib"
- version "v1.0.0"
- artifact(libPath + "/libs/lib.aar")
- }
-
- lib2(MavenPublication) {
- groupId MAVEN_GROUP_ID
- artifactId "lib2"
- version "v1.0.0"
- artifact(libPath + "/libs/lib2.aar")
- }
- }
- repositories {
- maven {
- // 发布仓库路径
- url MAVEN_RELEASE_URL
-
- // 本地仓库地址不适用账号密码
- // > Failed to publish publication 'maven' to repository 'maven'
- // > Authentication scheme 'all'(Authentication) is not supported by protocol 'file'
- // credentials(PasswordCredentials) {
- // username = MAVEN_USERNAME
- // password = MAVEN_PASSWORD
- // }
- }
- }
- }
当 Library module 编译完成后,最终会生成 aar 文件,但其中并不包含 compile/implementation 引用的其他 Library module 的代码或资源。然而在组件化开发中,有的时候我们希望发布的 aar 文件中需要包含 Library module 的内容,需要怎么做呢?有两种方法:
增加开关字段:
- dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
- if (useLocalLib.toBoolean())
- implementation project(":lib")
- else
- implementation 'com.pengxr.demo:maven:v1.0.0+'
- }
推荐阅读
Gradle 构建工具完整目录如下(2023/07/12 更新):
作者:彭旭锐
链接:https://juejin.cn/post/6963633839860088846
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。