PAYATHON 2026

Android 集成支付宝支付时 Gradle 误用字节仓库

支付小周

结论

mavenCentral() 排在第一位,不代表 Gradle 无论何时都会先访问 Maven Central,只有失败后才访问其他仓库。

问题主要出在依赖使用了动态版本:

com.alipay.sdk:alipaysdk-android:+@aar

+ 会让 Gradle 查询可用版本,再选择符合条件的最新版本。这个过程可能需要访问多个仓库并读取版本元数据。Gradle 一旦将某个仓库认定为该组件的来源,通常还会继续从这个仓库获取后续元数据和 AAR 文件。如果字节仓库声明或代理了这个坐标,却没有完整的 POM、Gradle Module Metadata 或 AAR,就可能出现这种情况:虽然 mavenCentral() 配在最前面,Gradle 最终仍向字节仓库下载文件并报错。

建议这样处理:

  1. 不使用 +,改为明确的 SDK 版本。
  2. 为仓库设置内容过滤,只允许字节仓库解析它负责的 group。
  3. 必要时使用 exclusiveContent,指定某些依赖只能从特定仓库解析。
  4. 确认仓库配置位于实际参与依赖解析的位置,而不是仅配置在 buildscript 或 pluginManagement 中。

为什么仓库顺序没有产生预期效果

仓库顺序会影响解析,但 Gradle 并不是简单地在第一个仓库下载失败后,再尝试第二个仓库。

使用固定版本时,例如:

implementation("com.alipay.sdk:alipaysdk-android:具体版本")

Gradle 可以按照仓库顺序查找该版本。但版本写成 + 后,Gradle 必须先确定有哪些版本可用:

implementation("com.alipay.sdk:alipaysdk-android:+@aar")

为此,Gradle 可能读取多个仓库中的 maven-metadata.xml。如果排在后面的字节仓库返回了相关版本信息,Gradle 可能选中该仓库提供的候选版本,随后继续向同一仓库请求 AAR。

Gradle 还会绑定组件与其来源仓库。组件一旦从某个仓库解析,相关元数据和构件通常不会随意与其他仓库中的文件混用。这样可以避免 POM 来自一个仓库、AAR 来自另一个仓库,造成不可重复的依赖解析结果。

以下情况都可能导致错误最终指向字节仓库:

  • 使用了 + 等动态版本。
  • 字节仓库是代理仓库,并缓存或暴露了 com.alipay.sdk 的部分元数据。
  • 字节仓库中有该坐标的 POM 或版本列表,却缺少对应的 AAR。
  • Gradle 缓存了之前的动态版本解析结果。
  • 实际生效的仓库配置不是当前看到的这一段。
  • 项目、插件或第三方脚本添加了其他仓库。

首选方案:固定支付宝 SDK 版本

可发布项目不要使用 +:

dependencies {
    implementation("com.alipay.sdk:alipaysdk-android:具体版本")
}

如果确实只需要获取 AAR,可以保留构件类型声明:

dependencies {
    implementation("com.alipay.sdk:alipaysdk-android:具体版本@aar")
}

不过,@aar 会明确指定构件类型,也可能影响基于 POM 的传递依赖处理。除非 SDK 接入说明明确要求,否则一般应优先使用普通依赖写法:

dependencies {
    implementation("com.alipay.sdk:alipaysdk-android:具体版本")
}

其中的“具体版本”应以支付宝当前接入文档或实际仓库中确认存在的版本为准,不要凭经验填写版本号。

固定版本有两个直接好处:

  • 构建结果可以重复,不会因为仓库发布了新版本而突然改变。
  • Gradle 不必为了计算“最新版本”而频繁查询多个仓库的版本元数据。

使用 content 限制字节仓库

如果字节仓库只用于获取特定 group 的组件,应为它添加内容过滤。下面的 group 只是示例,必须替换为项目实际使用的字节依赖 group:

repositories {
    mavenCentral()

    maven {
        name = "ByteDance"
        url = uri("字节仓库地址")

        content {
            includeGroup("实际的字节依赖Group")
            includeGroupByRegex("实际的字节Group前缀\\..*")
        }
    }
}

添加过滤后,com.alipay.sdk 不符合字节仓库的内容规则,Gradle 不会再向该仓库查询支付宝 SDK。

如果字节相关依赖分布在多个 group 中,可以逐一声明:

repositories {
    mavenCentral()

    maven {
        name = "ByteDance"
        url = uri("字节仓库地址")

        content {
            includeGroup("com.example.bytedance")
            includeGroup("com.example.bytedance.sdk")
        }
    }
}

示例中的 group 和仓库地址只用于展示配置结构,不能直接作为真实坐标使用。

使用 exclusiveContent 明确仓库归属

如果字节依赖必须且只能从字节仓库下载,可以使用独占内容规则:

repositories {
    mavenCentral()

    exclusiveContent {
        forRepository {
            maven {
                name = "ByteDance"
                url = uri("字节仓库地址")
            }
        }

        filter {
            includeGroup("实际的字节依赖Group")
            includeGroupByRegex("实际的字节Group前缀\\..*")
        }
    }
}

这种配置比单纯调整仓库顺序更可靠,因为它明确规定了组件与仓库的归属关系:

  • 符合过滤条件的组件由字节仓库解析。
  • 其他组件不使用字节仓库。
  • com.alipay.sdk 仍由 Maven Central 等通用仓库处理。

也可以反过来规定支付宝 SDK 只能来自 Maven Central:

repositories {
    exclusiveContent {
        forRepository {
            mavenCentral()
        }

        filter {
            includeGroup("com.alipay.sdk")
        }
    }

    maven {
        name = "ByteDance"
        url = uri("字节仓库地址")
    }
}

实际项目中,一般更适合限制私有仓库或厂商仓库的范围,而不是逐个限制公共依赖。

在 settings.gradle 中集中管理仓库

较新的 Android Gradle 项目通常会在 settings.gradle 或 settings.gradle.kts 中统一配置依赖仓库。

Groovy DSL 示例:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

    repositories {
        mavenCentral()
        google()

        maven {
            name = "ByteDance"
            url = uri("字节仓库地址")

            content {
                includeGroup("实际的字节依赖Group")
                includeGroupByRegex("实际的字节Group前缀\\..*")
            }
        }
    }
}

Kotlin DSL 示例:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

    repositories {
        mavenCentral()
        google()

        maven {
            name = "ByteDance"
            url = uri("字节仓库地址")

            content {
                includeGroup("实际的字节依赖Group")
                includeGroupByRegex("实际的字节Group前缀\\..*")
            }
        }
    }
}

FAIL_ON_PROJECT_REPOS 可以防止模块的 build.gradle 或其他脚本自行追加仓库,仓库来源也会更容易审计。启用后,如果子项目仍然声明了 repositories,构建会直接报错,需要将这些仓库迁移到统一配置中。

下面三个仓库配置区域的用途并不相同:

  • pluginManagement.repositories:解析 Gradle 插件。
  • buildscript.repositories:解析传统构建脚本依赖。
  • dependencyResolutionManagement.repositories 或项目的 repositories:解析应用和库使用的业务依赖。

支付宝 SDK 是业务依赖,只修改 pluginManagement.repositories 不会改变它的解析方式。

检查实际解析结果

可以通过 Gradle 的依赖报告确认最终选择的版本:

./gradlew :app:dependencyInsight \
  --dependency alipaysdk-android \
  --configuration debugRuntimeClasspath

如果项目中没有 debugRuntimeClasspath,需要替换为实际存在的 configuration。

也可以输出完整依赖树:

./gradlew :app:dependencies \
  --configuration debugRuntimeClasspath

如果需要查看更详细的仓库请求信息,可以提高日志级别:

./gradlew :app:assembleDebug --info

不要只根据最后一条下载失败的 URL 判断仓库优先级。还要检查之前的元数据请求、版本选择结果,以及解析过程是否命中了缓存。

修改配置后仍访问旧仓库怎么办

Gradle 可能已经缓存了动态版本和仓库解析结果。固定版本并添加仓库过滤后,可以执行:

./gradlew :app:dependencies --refresh-dependencies

也可以在正常构建时刷新:

./gradlew :app:assembleDebug --refresh-dependencies

--refresh-dependencies 会重新校验依赖,不适合用作每次构建的常规参数。它只能帮助排查问题或刷新缓存,不能代替正确的仓库隔离配置。

注意事项

  • 不要只依靠仓库排列顺序来隔离公共仓库和厂商仓库,应优先使用 content 或 exclusiveContent。
  • 不要在生产构建中使用 +、latest.release 等动态版本。
  • 不要随意添加来源不明的仓库镜像,否则可能导致依赖投毒或构建结果无法重复。
  • 即使厂商仓库是 Maven Central 的代理,也应限制它可以解析的 group 范围。
  • 仓库过滤针对的是 Maven 坐标中的 group,不是包名、模块名或 AAR 文件名。
  • 如果支付宝 SDK 的官方接入方式发生变化,应以当前官方文档提供的仓库和依赖坐标为准。

这并不是 mavenCentral() 的排序失效,而是动态版本解析、组件来源绑定以及仓库内容没有隔离共同造成的结果。固定 SDK 版本,并限制字节仓库可以解析的 group,才能消除这种不确定性。

备注:内容仅供参考。