Skip to the content.

基于Gradle的maven 21分钟入门


本文是一篇关于maven的不正经介绍,可以解决你在普通Gradle项目中可能遇到的绝大部分问题。如果你希望学会复杂的mvn命令、用pom文件管理项目、最终成为maven的专家,你可能需要前往Apache Maven 的官网

1. 基础概念

1.1 maven的用途

maven就是发布和管理组件二进制的仓库。将这句话展开,可以这样来理解:

  1. 可以分门别类的发布和管理众多组件,组件之间通过maven坐标相区别;
  2. 组件以二进制的形式发布到仓库,如jar、aar、zip等;
  3. maven仓库并不管理代码(那是git仓库的事情),但能以sources.jarjavadoc.jar等形式为组件关联源代码和文档;

简单的说,可以把maven仓库想象成一个组件市场,你可以通过http协议上传和下载组件二进制。

你可能会觉得maven看起来没啥好处呀,还不如直接拷贝jar、aar文件来的更快更直接。你可能是对的,但比较优劣并不是本文的目标。

1.2 maven坐标

maven通过坐标来管理各个组件,每个组件都拥有各不相同且唯一的坐标。例如org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21就是一个maven坐标。

maven坐标可以完整的表示为:groupId:artifactId:version:packaging@classifier,如com.company.project:component:1.0.0:jar@official。坐标中的packageclassifier在绝大多数情况下可以省略,只保留groupId:artifactId:version,那么上例就简化为com.company.project:component:1.0.0

关于maven坐标的更多介绍可以参考Apache的指引 Maven Coordinates

1.3 maven本地仓库和目录结构

maven公共仓库通常是一个http站点,但在个人电脑上的一个本地目录也可以当成是一个仅供自己使用的本地仓库,可以方便离线开发。

maven默认会吧~/.m2/repository目录作为一个本地仓库,即mavenLocal。在Gradle中可以通过mavenLocal()来引用此仓库。

// build.gradle
buildscript {
    repositories {
        mavenLocal()
    }
}

除了mavenLocal以外,任意一个可读写的本地目录都可以直接作为一个临时maven仓库。这一点在你想调试脚本的时候会很有用。例如有目录/tmp/repo/,那么可以通过file:///tmp/repo来引用此目录。

// build.gradle
buildscript {
    repositories {
        maven { url 'file:///tmp/repo' }
    }
}

在绝大多数使用场景中,你并不需要关心组件实际以什么形式存储在哪个地方,但你可能有那么一两次需要知道一些细节。maven坐标和组件在仓库中的路径存在一个直观的映射关系。如组件com.company.project:component:1.0.0:jarmavenLocal中的存储路径通常为~/.m2/repository/com/company/project/component/1.0.0/component-1.0.0.jar。如果换成maven服务器http://example.com/repositories/sample-repo/,那么文件路径通常为http://example.com/repositories/sample-repo/com/company/project/component/1.0.0/component-1.0.0.jar

1.4 组件版本

maven的一大好处就是可以管理组件的版本。

maven官方建议使用形如1.0.0的格式作为版本号,其间可以穿插字母,字母原则上按照字符顺序比较,除了以下关键字:”alpha” < “beta” < “milestone” < “rc” = “cr” < “snapshot” < “” = “final” = “ga” < “sp“。

表面上任意字符串都可以作为一个版本,但实际上maven在实现的时候会对版本中的字符串做一些特殊处理,比如可能会认为”1-a1” = “1-alpha-1“,之类的。maven对版本号的处理在maven寻找最新版本或局部最高版本的时候会很关键,过于复杂的命名可能会导致Comparison method violates its general contract的异常。

关于版本命名的详细说明可参考Apache官方文档 Dependency Version Requirement Specification

上述形式的版本,统称release版本,即发布版本,是发布了就不会/不应再改变的版本,已上传的版本不能/不应被覆盖(其实可以覆盖,但可能导致各编译机下载的组件不一致)。除了release版本外,还有snapshot版本。snapshot版本形如1.0.0-SNAPSHOT。通常认为snapshot版本是开发版本,是不保证稳定的。snapshot版本可以反复上传,同一个版本新上传的文件可以覆盖之前上传的文件(实际maven服务器会保存所有上传的文件,但maven协议默认通过上传时间戳或序号排序后返回最新的文件)。由于release版本和snapshot版本的特性,在开发和调试中可以使用snapshot版本,在集成的时候则应该使用release版本。

release版本和snapshot版本可能会存储在不同的maven仓库上,如https://example.com/repository/maven/public保管release版本,https://example.com/repository/maven/public-snapshots保管snapshot版本。

2. 基本用法

系统的说明可以阅读 Apache官网 POM ReferenceGradle官网 Dependency management in Gradle。这里只介绍一些容易接触到的问题。

2.1 在Gradle配置对maven的依赖

配置maven依赖,涉及到三个方面:

分别用以下示例脚本来说明:

buildscript {
    // 1. 这里配置maven仓库。脚本会按顺序在这些仓库查找需要的组件。
    repositories {
        mavenLocal() // 引用了mavenLocal仓库
        maven { url 'file:///some/local/directory' } // 把一个本地目录视为一个仓库
        maven { url 'https://example.com/repository/maven-public' } // 一个不需要账号校验的远程仓库(公共仓库)
        maven {
            url 'https://example.com/repository/maven-private'
            credentials { // 一个需要账号校验的远程仓库(私有仓库)
                username 'somebody'
                password 'password'
            }
        }
    }
    // 2. 这里配置脚本运行时的依赖
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0' // 依赖android编译插件,俗称AGP
        classpath 'com.example:something:1.0.0' // 自定义Gradle插件也要写到这里
    }
    // 这一段不是必要配置,但如果发现gradle总是使用本地缓存而不到maven服务器检查更新,可以加上这一段
    configurations.all {
        resolutionStrategy.cacheChangingModulesFor 1, 'seconds'
        resolutionStrategy.cacheDynamicVersionsFor 1, 'seconds'
    }
}
// 3. 这里配置编译的依赖
dependencies {
    implementation "com.android.support:support-fragment:26.0.1" // 支持compile,provided,comileOnly,api等类别(具体取决于脚本引入了什么插件),也支持自定义类别
}

可以创建自定义依赖类别:

project.configurations.create('someName')

depencencies {
    someName 'com.example:anything:1.0.0'
}

在Gradle上配置组件依赖,可以让Gradle自动查找满足特定条件的最高版本,如满足5.4.x的最高版本:

dependencies {
	compile 'some.group:some-module:5.4.+' // 注意末尾的+
}

Gradle会将+视为查找最高版本的通配符。如maven仓库中有5.4.15.4.25.4.3,那么最终会使用5.4.3。虽然Gradle有这项能力,但这项能力并不一定是好的,它是一把双刃剑,可能导致跟版本稳定性问题和不可回溯问题。在DevOps等推荐的最佳实践中都要求使用固定版本,禁止在主线或集成中通配符或snashot版本。

2.2 maven的依赖树

在Gradle工程中运行./gradlew dependencies,会看到类似下面的内容:

armDebugCompileClasspath - Resolved configuration for compilation for variant: armDebug
+--- com.android.support:multidex:1.0.2
+--- com.android.support:support-annotations:25.0.1 -> 26.0.1
+--- com.example:component1:1.0.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21
|    |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.21
|    |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.21
|    |         \--- org.jetbrains:annotations:13.0
|    +--- com.example:component2:1.4.24
|    |    +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21 (*)
|    |    +--- com.android.support:recyclerview-v7:26.0.1
|    |    |    +--- com.android.support:support-annotations:26.0.1
|    |    |    +--- com.android.support:support-compat:26.0.1
|    |    |    |    \--- com.android.support:support-annotations:26.0.1
|    |    |    \--- com.android.support:support-core-ui:26.0.1
|    |    |         +--- com.android.support:support-annotations:26.0.1
|    |    |         \--- com.android.support:support-compat:26.0.1 (*)
*    *    *                            *
*    *    *                            *
*    *    *                            *

这就是工程的依赖树,树的每个节点都是通过maven引入的依赖(其实这个说法并不准确)。我们在Gradle脚本中可能只写了几行依赖配置,那么这种庞大的树形依赖关系是怎么冒出来的呢?答案在maven的pom.xml里面。

一个组件在maven上会存储这些文件:

Name                           Last modified      Size
../
component1-1.0.0-sources.jar   20-Nov-2019 15:01  144.68 KB
component1-1.0.0.aar           20-Nov-2019 15:01  394.09 KB
component1-1.0.0.pom           20-Nov-2019 15:01  2.75 KB

在其中有一个扩展名为.pom的文件,其内容类似下面的样子:

<project ...>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>component1</artifactId>
    <version>1.0.0</version>
    <packaging>aar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk7</artifactId>
            <version>1.3.21</version>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>component2</artifactId>
            <version>1.4.24</version>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>yet-another-component</artifactId>
            <version>2.1.0</version>
            <exclusions>...</exclusions>
        </dependency>
    </dependencies>
</project>

pom的<dependencies>标签下记录的内容就是当前组件对其他组件的直接依赖。Gradle递归的汇总这些信息后,就能构建出依赖树。

如果你要发布的组件依赖了其他组件,那么最好将依赖都写入pom。写入pom的方法可参考下文描述。关于pom的详细说明还可参考Apache官网 Pom Relationships

2.3 Gradle如何处理maven依赖的冲突的

在复杂或庞大的项目中,难免出现两个不同的组件依赖了一个相同的组件,而且依赖的版本不一致,这就是版本冲突的问题。版本冲突可能导致代码不稳定,轻则编译失败,重则线上出随机问题。版本冲突理论上应该由开发者自行检查和排除,因为只有开发者能在复杂的条件下判断如何处理冲突。但如果依赖的组件具有较高的兼容性,依赖了低版本组件的代码可以同样在高版本组件上使用,那么可以充分利用Gradle自带的冲突处理能力。

Gradle在发现有版本冲突时,会默认使用版本较高的那一个。例如在下面的依赖树中,rxjava的版本就被Gradle的默认行为定为了2.1.5了。

com.example:component:1.4.24
+--- android.arch.persistence.room:rxjava2:1.1.1
|    +--- io.reactivex.rxjava2:rxjava:2.0.6 -> 2.1.5
|    |    \--- org.reactivestreams:reactive-streams:1.0.1
|    +--- android.arch.persistence.room:common:1.1.1
|    +--- android.arch.persistence.room:runtime:1.1.1 (*)
|    \--- android.arch.core:runtime:1.1.1 (*)
+--- io.reactivex.rxjava2:rxandroid:2.0.1
|    \--- io.reactivex.rxjava2:rxjava:2.0.1 -> 2.1.5 (*)
\--- io.reactivex.rxjava2:rxjava:2.1.5 (*)

然而并不是所有时候都是高版本就好,有些时候可能希望使用某个较低的稳定版本。这时可以修改Gradle的解析策略:

allprojects {
    configurations.all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == 'com.android.support' && !details.requested.name.contains('multidex') ) {
                details.useVersion "26.0.1"
            }
        }
    }
}

于是得到下面的结果:

project :sample
+--- com.github.whataa:pandora2:2.0.+ -> 2.0.9
     +--- com.android.support:appcompat-v7:26.1.0 -> 26.0.1
     |    +--- com.android.support:support-annotations:26.0.1
     |    +--- com.android.support:support-v4:26.0.1 (*)
     |    +--- com.android.support:support-vector-drawable:26.0.1
     |    |    +--- com.android.support:support-annotations:26.0.1
     |    |    \--- com.android.support:support-compat:26.0.1 (*)
     |    \--- com.android.support:animated-vector-drawable:26.0.1
     |         +--- com.android.support:support-vector-drawable:26.0.1 (*)
     |         \--- com.android.support:support-core-ui:26.0.1 (*)
     +--- com.android.support:recyclerview-v7:26.1.0 -> 26.0.1 (*)
     \--- com.android.support:design:26.1.0 -> 26.0.1
          +--- com.android.support:support-v4:26.0.1 (*)
          +--- com.android.support:appcompat-v7:26.0.1 (*)
          +--- com.android.support:recyclerview-v7:26.0.1 (*)
          \--- com.android.support:transition:26.0.1
               +--- com.android.support:support-annotations:26.0.1
               \--- com.android.support:support-v4:26.0.1 (*)

在上面的依赖树中,所有的com.android.support都被固定到了26.0.1版本。

上述方案还有个变化形式:

configurations.all {
   resolutionStrategy {
       force 'some.group:some-module:1.3'
   }
}

2.4 依赖排除

当你依赖了组件A,且组件A依赖了组件B,那么组件B可能会因为A的关系被引入到你的项目来。有一天你不希望组件B参与构建,希望把组件B从依赖树中排出掉,怎么办呢?你可以告诉Gradle不要把组件A的某些依赖树传递到本项目中。

假设在项目中配置如下:

dependencies {
    api 'some.group:module-a:1.0'
    api 'some.group:module-c:2.0'
    api 'some.group:module-e:3.0'
}

得到如下依赖树:

+--- some.group:module-a:1.0
|    \--- some.group:module-b:22.0.0
+--- some.group:module-c:2.0
|    \--- some.group:module-d:0.2
\--- some.group:module-e:3.0
     \--- some.group:module-f:1.1

按照如下方式配置transitive = false,可以剪断依赖树:

configurations.all {
    transitive = false
}
+--- some.group:module-a:1.0
+--- some.group:module-c:2.0
\--- some.group:module-e:3.0

除了配置全局transitive以外,还能以单条依赖配置的粒度设置transitive

dependencies {
    api 'some.group:module-a:1.0'
    api('some.group:module-c:2.0') {
    	transitive = false
    }
    api 'some.group:module-e:3.0', {
        transitive = false
    }
}

transitive的粒度可能仍然太粗了,如果希望更精确的控制依赖树,可以使用exclude

假设依赖配置如下:

dependencies {
    androidTestCompile('com.android.support.test:runner:0.2')
    androidTestCompile('com.android.support.test:rules:0.2')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.1')
}

得到如下依赖树:

+--- com.android.support.test:runner:0.2
|    +--- junit:junit-dep:4.10
|    +--- com.android.support.test:exposed-instrumentation-api-publish:0.2
|    \--- com.android.support:support-annotations:22.0.0
+--- com.android.support.test:rules:0.2
|    \--- com.android.support.test:runner:0.2 (*)
\--- com.android.support.test.espresso:espresso-core:2.1
     +--- com.android.support.test:rules:0.2 (*)
     +--- com.squareup:javawriter:2.1.1
     +--- org.hamcrest:hamcrest-integration:1.1
     +--- com.android.support.test.espresso:espresso-idling-resource:2.1
     +--- org.hamcrest:hamcrest-library:1.1
     +--- javax.inject:javax.inject:1
     +--- com.google.code.findbugs:jsr305:2.0.1
     +--- com.android.support.test:runner:0.2 (*)
     \--- javax.annotation:javax.annotation-api:1.2

可以在全局配置排除规则:

configurations {
    all*.exclude group: 'org.hamcrest', module: 'hamcrest-core'
    all*.exclude group: 'com.android.support.test' // 可以单独配置group或module
}

也可以单独对某个依赖配置排除规则:

dependencies {
    androidTestCompile('com.android.support.test:runner:0.2')
    androidTestCompile('com.android.support.test:rules:0.2')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.1') {
        exclude group: 'org.hamcrest', module: 'hamcrest-core'
    }
}

关于依赖排除在Apache官网 Exclusions中也有讨论。

3. 稍微复杂点儿的操作

上一章的内容,能覆盖到日常开发容易遇到的大多数场景。接下来的内容可能稍微有点复杂,但你有一天可能会用到。

3.1 上传文件到maven

在Gradle中,可以使用'maven'插件或'maven-publish'插件来上传文件,二选一即可。关于Gradle插件的通用话题,不在本文范围内。两个插件孰优孰劣,请读者自行判断。

使用'maven'插件

首先是上传单个文件:

// build.gradle
apply plugin: 'maven'

uploadArchives {
    repositories.mavenDeployer {
        repository(url: "http://some.site.com/repository/some/release")
        snapshotRepository(url: "http://some.site.com/repository/some/snapshots")
        pom {
            groupId = 'com.example'
            version = '1.0-SNAPSHOT'
            artifactId = 'module-name'
        }
    }
}

对一个Java项目、Groovy项目、或Android Library项目来说,在执行了./gradlew uploadArchives之后,就能把编译出来的jar或aar上传到指定的maven仓库。

上面的脚本配置了两个maven仓库,一个是release仓库,一个是snapshots仓库,'maven'插件会根据version字段来决定最终上传到哪个仓库。如version-SNAPSHOT结尾,就会上传到snapshots仓库,否则上传到release仓库。

对于Android Library项目,’maven’插件默认是上传release编译(即assembleRelease)生成的aar,且会自动先执行编译,等编译成功了才会执行上传。

这里支持用一个本地目录作为临时maven仓库(如离线开发、脚本调试、等)。配置如下:

// build.gradle
apply plugin: 'maven'

uploadArchives {
    repositories.mavenDeployer {
        repository(url: "file:///some/path/on/computer/release")
        snapshotRepository(url: "file:///some/path/on/computer/snapshots")
        //......
    }
}

如果你的maven仓库需要做账号校验,可以做如下配置:

uploadArchives {
    repositories.mavenDeployer {
        repository(url: "http://example.com/repository/private/release") {
            authentication(userName: 'your-name', password: 'your-password')
        }
        //......
    }
}

如果你不希望上传默认的那个文件,想自己指定一个不同的文件,可以做如下配置:

uploadArchives {
    artifacts {
        // name 要跟下面的 artifacctId 保持一致
        archives file: '/path/to/another/file.jar', name: 'module-name'
    }
    repositories.mavenDeployer {
        repository(url: "http://example.com/repository/public/release")
        snapshotRepository(url: "http://example.com/repository/public/snapshots")
        pom {
            groupId = 'com.example'
            version = '1.0-SNAPSHOT'
            artifactId = 'module-name'
        }
    }
}

接下来再提升一下难度。假设你需要同时上传多个文件,你可以做这样的配置:

// the task that generates sources.jar
task generateSourcesJar(type: Jar) {
    from sourceSets.main.allSource
}

// the task that generates javadoc.jar
task generateJavadoc(type: Jar, dependsOn: javadoc) {
    from javadoc.destinationDir
}

artifacts {
    archives('/path/to/file-1.jar') {
        name 'module-name-1'
        builtBy jar
    }
    archives('/path/to/file-1-sources.jar') {
        name 'module-name-1' // <- 跟上面保持一致,会关联起来
        builtBy generateSourcesJar // <- 这样可以建立Task依赖关系
        classifier 'sources' // <- 这个很重要
    }
    archives('/path/to/file-1-javadoc.jar') {
        name 'module-name-1' // <- 跟上面保持一致,会关联起来
        builtBy generateJavadoc // <- 这样可以建立Task依赖关系
        classifier 'javadoc' // <- 这个很重要
    }
    archives('/path/to/file-2.jar') {
        name 'module-name-2' // 这是另一个组件的jar,可以跟着上面的组件一起上传
        builtBy jar
    }
}

uploadArchives {
    repositories.mavenDeployer {
        ......; // <- repositories 配置

        // 注意名称跟上面配置的对应关系
        addFilter('module-name-1') { artifact, file ->
            artifact.name == 'module-name-1'
        }
        pom('module-name-1') {
            groupId = 'com.example'
            version = '1.0.0'
            artifactId = 'module-name-a'
        }

        // 第二个jar的配置
        addFilter('module-name-2') { artifact, file ->
            artifact.name == 'module-name-2'
        }
        pom('module-name-2') {
            groupId = 'com.example'
            version = '1.0.0'
            artifactId = 'module-name-b'
        }
    }
}

当你执行./gradlew uploadArchives之后,将会得到两个组件:com.example:module-name-a:1.0.0com.example:module-name-b:1.0.0,其中module-name-a还带有sources.jarjavadoc.jar

sources.jarjavadoc.jar有啥用?当你写代码的时候,看到module-name-a的类或方法,可以在AndroidStudio中F1查看javadoc(笔者假设大家都知道javadoc)。当你调试代码单步进入module-name-a的类或方法时,你看到的不是AndroidStudio根据class文件反编译出来的代码,而是Java源代码。诶,好像很方便耶。

使用'maven-publish'插件

'maven-publish'插件的效果跟'maven'插件差不多,最直观的差别是语法不同。先来上传单个文件:

//这里设置了工程的属性
group 'com.example'
version '1.0.0'

apply plugin: 'maven-publish'

publishing {
    publications {
        moduleName(MavenPublication) {
            artifact jar // jar 是Java项目生成.jar文件的默认任务
        }
    }
    repositories {
        mavenLocal()
    }
}

假设工程名称为module-nameproject.name == 'module-name'),那么在执行了./gradlew publish之后,将会在mavenLocal生成组件com.example:module-name:1.0.0'maven-publish'插件默认会从工程的属性中读取groupversion字段,并取用工程的name属性作为artifactId。后面会看到,设置的工程的groupname、和version属性在上传maven这件事情上,并不会让你捡到什么便宜,但在Gradle混合构建(貌似是原文是“Composite Build”)的时候,会提供不少便利,有兴趣可以自行查阅相关资料。

如果想更灵活的指定要上传的文件,可做如下配置:

publishing {
    publications {
        moduleName(MavenPublication) {
            artifact '/path/to/file.jar'
        }
    }
    //......
}

如果想更精细的控制组件的坐标,可做如下配置:

publishing {
    publications {
        moduleName(MavenPublication) {
            artifact '/path/to/file.jar'
            // 这里的设置比工程的属性更优先
            groupId 'com.example.another'
            artifactId 'another-name'
            version '3.2.1'
        }
    }
    //......
}

如果要上传到一个http仓库,可做如下配置:

publishing {
    //......
    repositories {
	    maven {
    	    url "https://example.com/repository/public"
        }
    }
}

如果需要账号验证,可做如下配置:

publishing {
    //......
    repositories {
        maven {
            url "https://example.com/repository/private"
            credentials {
                username "your-name"
                password "your-password"
            }
        }
    }
}

有没有发现这里的写法跟在Gradle配置对maven的依赖一节配置maven仓库的写法完全一样。

这里带来一个问题:'maven'插件可以同时配置一个release仓库和一个snapshot仓库,然后根据version字段是否以-SNAPSHOT结尾来选择上传到哪个仓库,但'maven-publish'插件只能配置一个仓库,想上传release版本就配置一个release仓库,想上传snapshot版本需要修改仓库配置。一种解决方案是,添加一段脚本根据version字段内容用if-else语句做差异化控制。

如果你需要同时上传多个组件,可以做如下配置:

publishing {
    publications {
        module1(MavenPublication) {
            groupId 'com.example'
            artifactId 'module-1'
            version '1.0'
            artifact '/path/to/module-1.jar'
        }
        module2(MavenPublication) {
            groupId 'com.example'
            artifactId 'module-2'
            version '1.0'
            artifact '/path/to/module-2.jar'
        }
    }
    repositories {
        mavenLocal()
    }
}

诶,好像比'maven'插件简单的多诶。

在Android项目中执行./gradlew publish时Gradle默认不会先执行assembleRelease(与'maven'插件不同),可能会导致publish执行失败。如果你遇到这种情况,可以尝试加上下面一段脚本:

afterEvaluate {
    project.tasks.grep({ it.group == 'publishing' }).each { it.dependsOn 'assembleRelease' }
}

往pom写入依赖信息

在前面maven的依赖树一节中提到pom.xml中会记录组件的依赖。'maven'插件在uploadArchives的时候会根据当前工程(即Gradle的project)的dependencies {...}配置自动生成pom,但如果项目中存在较多子工程(project),'maven'插件对依赖的处理有些不够完美(比如依赖版本被设定为unspecified)。下面介绍通过脚本写入pom的方法。

先看'maven'插件的写法。假设你的组件有如下依赖:

dependencies {
    compile 'com.example.a:dep-module-1:1.0'
    compile 'com.example.b:dep-module-2:2.0'
}

可以这样写pom:

uploadArchives {
    repositories.mavenDeployer {
        repository(url: "http://example.com/repository/maven/release")
        pom {
            groupId = 'com.example'
            version = '1.0-SNAPSHOT'
            artifactId = 'module-name'

            withXml {
                def root = asNode()
                def depsNode = root['dependencies'][0] ?: root.appendNode('dependencies')
                depsNode.appendNode('dependency').with {
                    appendNode('groupId', 'com.example.a')
                    appendNode('artifactId', 'dep-module-1')
                    appendNode('version', '1.0')
                }
                depsNode.appendNode('dependency').with {
                    appendNode('groupId', 'com.example.b')
                    appendNode('artifactId', 'dep-module-2')
                    appendNode('version', '2.0')
                    // 这里可以做很多事情,比如添加exclusions节点。这里本质就是操作xml
                    appendNode('exclusions').with {
                        //......
                    }
                }
            }
        }
    }
}

接下来看下'maven-publish'插件的写法:

publishing {
    publications {
        moduleA(MavenPublication) {
            //......
            pom.withXml {
                def root = asNode()
                def dependencies = root.appendNode('dependencies')
                //......跟'maven'插件一样......;
            }
        }
    }
    //......
}

除了依赖信息,pom里面还能写不少信息,如开发者信息、仓库信息、构建信息、等。关于pom.xml的更多说明,可以参考 Apache官网 POM Relations

3.2 从maven下载文件的方法

上面说了怎么上传文件到maven仓库,下面说说怎么把maven上的文件下载到本地呢。通常Gradle会负责文件的下载和缓存,但如果项目有特殊需求,可以尝试下面的方法:

def dep = project.dependencies.create('com.example:module-name:2.0')
def conf = project.configurations.detachedConfiguration(dep)
copy {
    from conf
    into '/some/path'
}
downloadedFilePath = '/some/path/module-name-2.0.jar'

3.3 解析maven依赖

Gradle在执行构建的过程中,会主动解析所有的maven依赖,通常并不需要开发人员编写额外的解析脚本。当你遇到了一些特殊需求的时候,如你需要遍历、分析、处理依赖树,或需要对一些依赖做更细致的操作,你可以参考本节的内容。

利用已经被AGP(Android Gradle Plugin)解析过的依赖信息

AGP会帮你解析构建所需的所有依赖信息。下面介绍一些读取并利用这些信息的方法。

先看一段脚本(特别说明:以下示例基于AGP3.0.0,涉及一些非公开API,这些API的包名、类名可能会随着AGP版本变化而变化,请注意AGP版本适配问题):

dependencies {
    implementation 'com.android.support:support-fragment:26.+'
}

afterEvaluate {
    project.android.applicationVariants.each { variant ->
        new ArtifactDependencyGraph().createDependencies(variant.variantData.scope, false, new Consumer<SyncIssue>() {
            @Override
            void accept(SyncIssue syncIssue) {
                println("Error: ${syncIssue}")
            }
        }).with({ it.libraries + it.javaLibraries }).collect({ it.resolvedCoordinates }).each {
            println "${variant.name}: ${it.groupId}:${it.artifactId}:${it.version}"
        }
    }
}

以上脚本运行结果如下:

debug: com.android.support:support-fragment:26.1.0
debug: com.android.support:support-core-ui:26.1.0
debug: com.android.support:support-core-utils:26.1.0
debug: com.android.support:support-compat:26.1.0
debug: android.arch.lifecycle:runtime:1.0.0
debug: com.android.support:support-annotations:26.1.0
debug: android.arch.lifecycle:common:1.0.0
debug: android.arch.core:common:1.0.0
release: com.android.support:support-fragment:26.1.0
release: com.android.support:support-core-ui:26.1.0
release: com.android.support:support-core-utils:26.1.0
release: com.android.support:support-compat:26.1.0
release: android.arch.lifecycle:runtime:1.0.0
release: com.android.support:support-annotations:26.1.0
release: android.arch.lifecycle:common:1.0.0
release: android.arch.core:common:1.0.0

这里在脚本读到了'com.android.support:support-fragment:26.+'的解析结果:

我们还可以进一步从AGP拿到更多信息,比如组件下载后的文件路径、aar的解压路径、aar里面包含的libs、assets、jni、等各种信息。下面来看个稍复杂一点的脚本:

def getArtifactDependencyGraphDependencies(variant) {
    return new ArtifactDependencyGraph().createDependencies(variant.variantData.scope, false, new Consumer<SyncIssue>() {
        @Override
        void accept(SyncIssue syncIssue) {
            println("Error: ${syncIssue}")
        }
    })
}

def coordOf(lib) {
    lib.resolvedCoordinates.with({ "${it.groupId}:${it.artifactId}:${it.version}".toString() })
}

def printDependency(parent, lead, tail, aarLibs, jarLibs) {
    println lead + '+- ' + parent.name + ' -> ' + (aarLibs[parent.name]?.folder ?: jarLibs[parent.name]?.jarFile)
    def children = parent.children.collect() as List
    lead += (tail ? ' ' : '|') + '  '
    if (parent.name in aarLibs) {
        def aar = aarLibs[parent.name]
        aar.localJars.each {
            println lead + '+- libs/' + it.name
        }
    }
    for (def i = 0; i < children.size(); ++i) {
        def child = children.get(i)
        printDependency(child, lead, i == children.size() - 1, aarLibs, jarLibs)
    }
}

afterEvaluate {
    project.android.applicationVariants.each { variant ->
        def depImpl = getArtifactDependencyGraphDependencies(variant)
        def aarLibs = depImpl.libraries.collectEntries({ [(coordOf(it)): it] })
        def jarLibs = depImpl.javaLibraries.collectEntries({ [(coordOf(it)): it] })

        def conf = project.configurations.findByName("${variant.name}CompileClasspath")
        println "\nconfiguration '${conf.name}'"
        conf.resolvedConfiguration.firstLevelModuleDependencies.each {
            printDependency(it, '', false, aarLibs, jarLibs)
        }
    }
}

上述脚本运行后会得到形如下面的输出:

configuration 'debugCompileClasspath'
+- com.example:module1:2019.11.28.16.28.14 -> ~/.gradle/caches/transforms-1/files-1.1/module1-2019.11.28.16.28.14.aar/39149d9c0b802aabe789df37c757bc5a
|  +- com.example:module2:2019.11.26.13.37.50 -> ~/.gradle/caches/transforms-1/files-1.1/module2-2019.11.26.13.37.50.aar/00756fec39d4dfcd60c4ac51728dcca7
|  +- com.example:module3:1.4.24 -> ~/.gradle/caches/transforms-1/files-1.1/module3-1.4.24.aar/c75a5e78fe58718b76fc4c0724f2dcc8
|  |  +- libs/comic.jar
|  |  +- libs/sonic_sdk_v3.0.0.jar
|  |  +- com.example:utils:1.0.40 -> ~/.gradle/caches/transforms-1/files-1.1/utils-1.0.40.aar/46128aa84f3c7ed1e1f28d430644b636
|  |  |  +- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.21 -> ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.3.21/d207ce2c9bcf17dc8e51bab4dbfdac4d013e7138/kotlin-stdlib-jdk7-1.3.21.jar
|  |  |     +- org.jetbrains.kotlin:kotlin-stdlib:1.3.21 -> ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.21/4bcc2012b84840e19e1e28074284cac908be0295/kotlin-stdlib-1.3.21.jar
|  |  |        +- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.21 -> ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.21/f30e4a9897913e53d778f564110bafa1fef46643/kotlin-stdlib-common-1.3.21.jar
|  |  |        +- org.jetbrains:annotations:13.0 -> ~/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar
.		.		.		.		.		.		.
.		.		.		.		.		.		.
.		.		.		.		.		.		.
.		.		.		.		.		.		.

关于AGP解析的依赖信息就点到为止。基于这套方法还可以搞很多事情,有兴趣的同学可联系笔者交流。

解析一个maven坐标

上面介绍了如何获取AGP解析过的依赖信息,下面通过实例介绍一下脱离于AGP解析依赖的方法。脚本如下:

repositories {
    maven { url 'https://example.com/repository/maven/public' }
}

def dep = project.dependencies.create('org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.+')
def conf = project.configurations.detachedConfiguration(dep)
println "dependencies = ${conf.dependencies.collect({ "${it.group}:${it.name}:${it.version}" })}"

println "resolvedConfigurations = ${conf.resolvedConfiguration.lenientConfiguration.allModuleDependencies.collect({ it.name })}"

conf.resolve().each {
    println "resolved and downloaded: ${it}"
}

运行脚本后会得到如下信息:

dependencies = [org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.+]
resolvedConfigurations = [org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61, org.jetbrains.kotlin:kotlin-stdlib:1.3.61, org.jetbrains.kotlin:kotlin-stdlib-common:1.3.61, org.jetbrains:annotations:13.0]
Download https://example.com/repository/maven/public/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.3.61/kotlin-stdlib-jdk7-1.3.61.jar
Download https://example.com/repository/maven/public/org/jetbrains/kotlin/kotlin-stdlib/1.3.61/kotlin-stdlib-1.3.61.jar
Download https://example.com/repository/maven/public/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.61/kotlin-stdlib-common-1.3.61.jar
resolved and downloaded: ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-jdk7/1.3.61/70dffc5f8ac5ea7c34f30deac5b9d8b1d48af066/kotlin-stdlib-jdk7-1.3.61.jar
resolved and downloaded: ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.3.61/4702105e97f7396ae41b113fdbdc180ec1eb1e36/kotlin-stdlib-1.3.61.jar
resolved and downloaded: ~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.3.61/65abb71d5afb850b68be03987b08e2c864ca3110/kotlin-stdlib-common-1.3.61.jar
resolved and downloaded: ~/.gradle/caches/modules-2/files-2.1/org.jetbrains/annotations/13.0/919f0dfe192fb4e063e7dacadee7f8bb9a2672a9/annotations-13.0.jar

基于这套脚本,还可以拿到其他解析结果,可以做一些特别的事情。这里点到为止,不再详细展开。欢迎有兴趣的同学可以联系笔者交流。