当前位置: 首页 > news >正文

Android SDK 上传 Maven 喂奶级教程

最近领导给安排了个任务,让我把我们现有的一个 SDK 上传到 Maven 上去,方便客户直接用 gradle 依赖,不再需要拷贝 jar 和 so 了,此前我也看过一些相关的文章我想问题也不大,觉得工作量也就一两天的事情,主要的难点在于如何隐藏源码上传 maven(因为是商业 SDK),万万没想到问题这么多,网上有用的文章也很少,加上 gradle 的版本捣捣乱让我整整一周焦头烂额,一言难尽,略过,直接进入正题!

前期准备

AndroidStudio 版本(不同版本默认的 gradle 版本不同)

./upload-maven/image-20220423102431037

Gradle 版本

classpath 'com.android.tools.build:gradle:4.1.3'
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

总体流程

  1. 注册 sonatype 账号并申请 groupid
  2. 压缩 jar、so、其它资源到 aar
  3. 编写 gradle 脚本
  4. 解决源码混淆问题(非商用 SDK 可跳过)
  5. 解决文档 javadoc 问题(非商用 SDK 可跳过)
  6. GPG 签名
  7. 上传

注册 sonatype 并申请 group id

buildscript {repositories {jcenter()google()mavenCentral()}dependencies {...}
}

我们在项目的 gradle.build 中一般都有这么一段代码,其中 repositories 下的每一行代码代表一种远程代码仓库,其中 jcenter 已于 2021 年 3 月 31 日被设定为只读代码库,从 AndroidStudio 中也可以看到相关的提醒;

./upload-maven/image-20220423113702766

google 代表的是 google 提供的代码仓库,mavenCentral 则代表着我们想要上传的仓库,但这个 mavenCentral 它不能直接上传,它需要通过它所支持的第三方仓库来同步更新。也就是说你想要上传一个包,需要上传到它指定的一个第三方代码仓库中,第三方仓库会定时与 mavenCentral 同步,在同步后你就可以在 mavenCentral 中找到它了,其中 sonatype 是一个比较好的代码仓库,上传和同步都比较及时。

注册 sonatype 第一步,不要去 google 搜索 sonatype,那会让你不知所措。搜到的大概率是这个东西:

./upload-maven/image-20220423121058261

Sontype 管理代码库的申请用的是 jira,所以你要注册它的账号需要登录这个网站:

https://issues.sonatype.org/secure/Signup!default.jspa

./upload-maven/image-20220423121843328

这里的密码有点烦,它不会一下子告诉你该有什么要求,每次都是填完密码、验证码后点 sign up 然后告诉你本次的密码哪里不合格,错了好多遍之后我知道了所有的密码要求:

  1. 必须大于等于 8 位
  2. 必须有英文和数字和特殊符号三种,缺一不可
  3. 必须同时包含大写和小写

登录之后你想申请一个 group id 需要创建一个 issue,label 选 Community Support - Open Source Project Repository Hosting (OSSRH),问题类型选 new project。

./upload-maven/image-20220423125736170 ./upload-maven/image-20220423125925379

Group id 这里要认真填写,它对应的是下图中红框圈出的部分,如果你拥有一个域名,可以填写 com.你的域名,不要填写你无法影响的域名,后面会让你在 DNS 解析上加记录来验证域名是你所有的。这里也可以填写你的 github 地址,例如我的为 io.github.shaolongfei。

./upload-maven/image-20220423130136150

Project url 这里填写你的项目地址,一般这里为 git 地址,例如我的 https://github.com/ShaoLongFei/AndroidOpenGL。

scm url 这里写你项目的下载方式,一般这里也可以填 git 地址,例如我的 https://github.com/ShaoLongFei/AndroidOpenGL.git。

username 是你希望发布包时的用户名,非必填项,没必要写,也不会审核这个,后面想写可以直接配置在 gradle 脚本上。

alread synced to central(是否已经同步到 mavenCentral 代码库),保持为 no 就可以,我觉得看这篇文章的人应该没有 yes 的吧。

提交之后这个 issue 会自动分配人员来对你提交的信息进行审核,由于他们的审核人员在国外所以会有一些时差,白天提交的话过一晚上就能收到回复了,晚上 10 点提交的话过一会就能收到回复。下面是我的回复:

./upload-maven/image-20220423132142968

我提交了一个 com.liuyue 的 group id ,它让我在 DNS 上加一条记录,但由于我并不拥有这个域名所以我选了下面的一个操作,更改 group id 为 io.github.shaolongfei ,在我的 github 上建一个他指定名称的库来验证 github 账号确实是我所有的。

在我做好这个操作后,更改这个 issue 的状态为 open(可以 comment 一下状态就会改变了) ,这样审核人员就会再次处理了。

./upload-maven/image-20220423133224293

收到这样的回复就代表你已经通过审核了。group id 的申请也就完成了。

压缩 jar、so、其它资源到 aar

以前客户使用我们的 sdk 都是分别拷贝 jar、so、资源文件到客户的项目中进行依赖,这样的优势是灵活,由于我们的 so 是分模块的,如果客户不想用某些功能可以根据自己的需求进行裁剪,也可以根据自己想要的 so 的架构进行依赖,因为几种架构的 so 体积加起来还挺大的;而上 maven 就不能散着了,需要打成一个 aar 把 jar 、so 、资源文件都放进去,这样的优势是客户使用起来很方便。

我们的项目是有两个 moudle 的,每个 moudle 会打出来一个 jar ,平时打 jar 包的流程是各自打 aar 包,分别解压出来 jar,将两个 jar 合并到一起。那打 aar 我原先的思路是在此前打 jar 的基础上往里面放入各个架构的 so 和指定路径的资源文件,最后搞了搞发现完全不需要这么麻烦,还得自己写脚本。

我找到了一个神器 https://github.com/kezong/fat-aar-android

这个用起来非常方便,非常简单,几行代码就可以解决打 aar 的各种烦恼,什么依赖库冲突,什么多 moudle ,混淆规则合并,AndroidManifest 合并,R.class 合并等等问题它都能搞定。工欲善其事,必先利其器啊。

Apply classpath

第一步,在项目的 build.gradle 中加入 mavenCentral 和 classpath

buildscript {repositories {mavenCentral()}dependencies {classpath 'com.github.kezong:fat-aar:1.3.8'}
}

Add plugin

第二步,在项目主 library 中的 build.gradle 添加此插件

apply plugin: 'com.kezong.fat-aar'

Embed dependencies

第三步,embed 你所需要的工程, 用法类似 implementation。

embed 的工程会被压缩到 aar 中,implementation 的项目只参与编译的过程,不会参与打包的过程。

dependencies {implementation 'com.qiniu:qiniu-android-sdk:8.3.2'embed project(':MediaLibrary')
}

执行 assemble 命令

此时配置已经完成了,在 AndroidStudio 的右侧 Gradle 任务栏里可以找到 assemble 任务、assembleDebug 任务和 assembleRelease 任务。

./upload-maven/image-20220423141427692./upload-maven/image-20220423141511496

执行 assemble 任务可以打出来 library-debug.aar 和 library-release.aar;执行 assembleDebug 任务可以打出来 library-debug.aar;执行 assembleRelease 任务可以打出来 library-release.aar。

打出来的 aar 都在 build/outputs/aar 路径下。

./upload-maven/image-20220423142105170

**注意:**如果你在 AndroidStudio 右侧的 gradle 任务列表里找不到这些任务,那你需要在 AndroidStudio 的设置中取消下图勾画的这一项设置,这个设置是新版的 AndroidStudio 默认勾画的,取消它!!!

./upload-maven/image-20220423142413216

如果你习惯了命令行的话也可以直接在项目根目录下敲这些命令:

# 产物:library-debug.aar 和 library-release.aar
./gradlew assemble
# 产物:library-debug.aar
./gradlew assembleDebug
# 产物:library-release.aar
./gradlew assembleRelease

编写 gradle 脚本

先上代码,解释都在注释里

plugins {id 'signing'id 'maven-publish'
}task sourcesJar(type: Jar) {from android.sourceSets.main.java.srcDirsclassifier = 'sources'
}task javadoc(type: Javadoc) {source = android.sourceSets.main.java.srcDirsclasspath += project.files(android.getBootClasspath().join(File.pathSeparator))
}task javadocJar(type: Jar, dependsOn: javadoc) {classifier = 'javadoc'from javadoc.destinationDir
}// 发布任务
publishing {publications {maven(MavenPublication) {artifact "build/outputs/aar/library-release.aar" // 产物artifact sourcesJarartifact javadocJargroupId = 'io.github.shaolongfei' // 此前在 sonatype 上申请的artifactId = 'OpenGLESUtils' // 项目的名称,依赖的时候要用到的version = '0.0.18' // 项目版本pom {name = 'OpenGLESUtils' /// 项目名称packaging = 'aar' // 发布的形式url = 'https://github.com/ShaoLongFei/AndroidOpenGL' // 项目地址description = 'OpenGLES 常用的工具类' // 项目描述scm {connection = 'scm:git:git://ShaoLongFei/AndroidOpenGL.git'developerConnection = 'scm:git:ssh://ShaoLongFei/AndroidOpenGL.git'url = 'https://github.com/ShaoLongFei/AndroidOpenGL'}//开源协议licenses {license {name = 'The Apache License, Version 2.0'url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'}}// sdk 发布者信息developers {developer {name = 'liuyue' // 发布者名称email = 'liuyueshaolongfei@foxmail.com'  // 联系邮箱}}}}}repositories {mavenLocal() // 发布到本地,目录在 username/.m2/repository/ 下maven {name 'sonatypeRepository'url 'https://s01.oss.sonatype.org/content/repositories/releases/'credentials {username = ''  // 之前在 sonatype 注册的账户名password = '' // 对应的密码}}}
}// 不可打乱顺序
signing {sign configurations.archives
}

这是我查了好多文章总结出来的写法,网上很多文章讲的都是老版本的 maven 插件,现在它已经被弃用了,需要使用新版本的 maven-publish 插件才可以,新老版本差异还是挺大的,比如老版本的上传任务是 uploadArchives ,新版本的是 publishing。

publishing 下面套 publications 再套 maven(MavenPublication),这个配置可以满足绝大多数需求,非必要别改动。

artifact 声明的是要上传的东西,可以声明多行,每行一个,它自己会把它们集合到一起,也可以用大括号的形式声明一个列表

artifact{......
}

sourcesJar 和 javadocJar 这两个 task 可以直接照搬,后面会再讲。

artifactId 是构件ID,这个名称一般都为小写字母,没有其他的特殊字符,我这里写的有问题,大写不会报错,也不会影响打包、推送,就是不太符合规范,推荐使用“实际项目名称-模块名称”的方式定义,例如:spirng-mvn、spring-core等。

如果 gradle 报错出现 main 找不到的情况,可以加入下列代码,显示的声明路径:

sourceSets {main {java { srcDirs = ["src/java"] }resources { srcDir "src/resources" }}
}

licenses 开源协议不必要写,如果你的项目不开源的话可以不写。

repositories 下写的是要发布到哪里,建议先添加一个 mavenLocal() 发布到本地试试,它会发布到 username/.m2/repository/ 目录下,确定好没问题再上传 sonatype 上。

https://s01.oss.sonatype.org/content/repositories/releases/

这个地址是发布 release 的地址,如果你还要发布其它版本的话可以看看下列的地址:

https://s01.oss.sonatype.org/content/groups/public/

https://s01.oss.sonatype.org/content/groups/staging/

https://s01.oss.sonatype.org/content/repositories/snapshots/

这个地址千万不能填错,因为 sonatype 更换了新的服务器,但是网上的文章都是旧的,用的都是旧地址,会导致上传有问题。

最后的 signing 一定要写在 publishing 之后,不然会出现语法错误。

解决源码混淆问题(非商用 SDK 可跳过)

事先声明,此部分我未解决,留下我的解决经验,以便后来者可以借鉴,减少重复劳动或者减少试错成本。

上传 maven 一般来说需要三样产物,release.jar 是打包好的 sdk,source.jar 是便于使用者查看源码的 jar,javadoc.jar 是方便使用者看文档和代码上的注释的 jar。

由于我们是商用的 SDK ,所以上传时不能上传源码,我就想,那上传的时候选择性的混淆一下吧,外层接口保存原装,内部代码混淆一下,于是就开始研究怎么过滤这个代码。

task sourcesJar(type: Jar) {from android.sourceSets.main.java.srcDirsclassifier = 'sources'
}

思路一:写一个 gradle plugin

通过一个 gradle plugin 添加一个任务,在打 sourceJar 的时候通过一些规则混淆源码。

在项目下新建一个 moudle ,他通过使用 gradle 和 javaparser 来处理代码。加入如下依赖:

dependencies {// gradle sdkimplementation gradleApi()// groovy sdkimplementation localGroovy()implementation 'com.android.tools.build:gradle:7.1.3'implementation 'com.github.javaparser:javaparser-core:3.24.2'
}

这个插件完成之后还要发布到本地,然后另一个项目来引用并 apply。

plugins{id 'groovy'id 'maven-publish'
}
afterEvaluate {publishing {publications {maven(MavenPublication) {artifact "build/libs/hidesourceplugin.jar"groupId = 'com.liuyue.plugin'artifactId = 'HideSourcePlugin'version = '0.0.2'}}repositories{mavenLocal()}}
}

最简略的发布逻辑,接下来处理代码。

import org.gradle.api.Plugin
import org.gradle.api.Projectclass HideSourcePlugin implements Plugin<Project> {@Overridevoid apply(Project project) {final android = project.extensions.androidHideSourceTask sourcesTask = project.tasks.findByName("hideSourceJar") as HideSourceTaskif (sourcesTask == null) {sourcesTask = project.tasks.create("hideSourceJar", HideSourceTask)}sourcesTask.from(android.sourceSets.main.java.srcDirs)}
}

首先它要实现 plugin 的接口,重写 applay 的方法,当这个插件被 apply 的时候生效。

检查当前任务列表里有没有一个 hideSourceJar 的任务,没有的话就创建添加一个,然后让它来处理我们源码路径下的所有文件。

import com.github.javaparser.JavaParser
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.body.ConstructorDeclaration
import com.github.javaparser.ast.body.MethodDeclaration
import com.github.javaparser.ast.expr.FieldAccessExpr
import com.github.javaparser.ast.expr.MethodCallExpr
import com.github.javaparser.ast.expr.NameExpr
import com.github.javaparser.ast.expr.StringLiteralExpr
import com.github.javaparser.ast.stmt.BlockStmt
import com.github.javaparser.ast.visitor.VoidVisitorAdapter
import org.gradle.jvm.tasks.Jar/*** @author moonshoter*/
public class HideSourceTask extends Jar {HideSourceTask() {super()group = 'artifacts'classifier = "sources-hide"filter(CodeFilterReader.class)}static class CodeFilterReader extends FilterReader {CodeFilterReader(Reader reader) {super(reader)CompilationUnit compilationUnit = JavaParser.parse(reader)compilationUnit.accept(new MethodVisitor(), null)String codeAfterHide = compilationUnit.toString()this.in = new StringReader(codeAfterHide)reader.close()}private static class MethodVisitor extends VoidVisitorAdapter<Void> {@Overridevoid visit(MethodDeclaration n, Void arg) {// 清除原数据n.removeBody()// 修改BlockStmt block = new BlockStmt()n.setBody(block)NameExpr clazz = new NameExpr("System")FieldAccessExpr field = new FieldAccessExpr(clazz, "out")MethodCallExpr call = new MethodCallExpr(field, "println")call.addArgument(new StringLiteralExpr("Some Unspoken Thing~~"))block.addStatement(call)}@Overridevoid visit(ConstructorDeclaration n, Void arg) {if (n.body != null) {n.body.statements.clear()}// 修改BlockStmt block = new BlockStmt()n.body = blockNameExpr clazz = new NameExpr("System")FieldAccessExpr field = new FieldAccessExpr(clazz, "out")MethodCallExpr call = new MethodCallExpr(field, "println")call.addArgument(new StringLiteralExpr("Some Unspoken Thing~~"))block.addStatement(call)}}}
}

这是一个替换方法中的代码为 System.out.println("Some Unspoken Thing~~") 的逻辑,想要替换哪些方法可以自己写一个逻辑来控制。

好,现在代码写好了,publishing 到本地检查本地,确实有此插件,jar 也正确。

./upload-maven/image-20220423162713383

到了应用的时候了,先在项目的 build.gradle 中加入 mavenLocal() 和 classpath

buildscript {repositories {mavenLocal()}dependencies {classpath 'com.liuyue.plugin:HideSourcePlugin:0.0.2'}
}

**注意:**如果你使用的 gradle 是 7.x 的,那么你项目的 build.gradle 会变成这个样子

plugins {id 'com.android.application' version '7.2.0-alpha07' apply falseid 'com.android.library' version '7.2.0-alpha07' apply false
}task clean(type: Delete) {delete rootProject.buildDir
}

看到这个让我感觉有点不知所措,之前这东西不在这的呀,然后我发现 setting.gradle 也变了,以前只有 include ‘xxx’ 来着,现在多了一堆东西。

pluginManagement {repositories {gradlePluginPortal()google()mavenCentral()}
}
dependencyResolutionManagement {repositories {google()mavenCentral()}
}
rootProject.name = "OpenGLESUtils"
include ':app'
include ':library'
include ':hidesourceplugin'

仔细对比可以发现,原来是原来项目的一些配置移动到了 setting.gradle 中去了,那我就在 setting.gradle 中写吧,咔咔咔写上去,发现它居然报错了!!!

Cannot resolve external dependency com.liuyue.plugin:HideSourcePlugin:0.0.2 because no repositories are defined.

我只好再 build.gradle 中写了,emmm,成功了。

好,现在,applay 它,运行,叮~,出错了。

Build was configured to prefer settings repositories over project repositories but repository 'Gradle Libs' was added by unknown code

它觉得你添加的 gradle libs 是一个未知的来源,它不敢用。

真怂,它不敢,我敢,删除 setting.gradle 中的 repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) ,报错解决,搞定。

现在问题拮据了,下一步,publishing,emmm,没有用。

Game over~

那我换一种思路吧,生成 sourceJar 的时候 from 指定了要生成 jar 包的代码的路径,这块是可以写多个的,它会自己合并,这里还有一个方法叫 filter,我能不能用这个过滤,做一些操作呢,我理想的写法:

task sourcesJar(type: Jar) {from android.sourceSets.main.java.srcDirsfilter(HideSourceReader.class)classifier = 'sources'
}

但这样会报错找不到 HideSourceReader.class ,那就解决它!

先 import 试试,因为 gradle 也是用 java 实现的,所以语法差不多,试试。

Emmm,不行!

把这个类打成 jar ,然后在 gradle 中依赖它,这样应该能找到了吧,试试。

Emmm,不行!

Game,over~

最后仔细想一想,source.jar 它并不是必传的东西,所以如果不想暴露源代码可以不传,这是我最后的解决方案了。

解决文档 javadoc 问题(非商用 SDK 可跳过)

事先声明,此部分我未解决,留下我的解决经验,以便后来者可以借鉴,减少重复劳动或者减少试错成本。

task javadocJar(type: Jar, dependsOn: Javadoc) {source = android.sourceSets.main.java.srcDirsclasspath += project.files(android.getBootClasspath().join(File.pathSeparator))classifier = 'javadoc'from javadoc.destinationDir
}

这个 task 打出来的 javadoc.jar 正常情况下是这样的:

./upload-maven/image-20220423204932980

如果没有特殊要求的话那么直接用这个就可以了,但我们是商业的 SDK ,这种打 jar 的方式会把内部的代码暴露出来(已测试)。那我们想要的是一个什么样式的呢?外部 API 的代码注释使用者可以正常查看,内部的代码隐藏,或者内部的代码混淆,注释清除。

第一点,我先注意到了 android.jar 中的 @hide 注解,只要是被这个修饰的类或者代码段我们平常开发者就无法查看(另外提一嘴,如果想查看 android 源码的话可以去网上找已经去除 @hide 的 android.jar,替换本地的就可以了,不过某些版本的 AndroidStudio 会检测 jar 的签名,会报错 mock jar,这个怎么解决自己去找一下吧),那我们能不能用 @hide 来修饰我们内部的代码呢?

怀着激动的心情,在我的 demo 工程上试了一下,不行!

这个东西不是你在注解包里找不到的问题,它是普通开发者就无法使用!

Game over~

那有没有办法去除掉一些类再打包 javadoc.jar 呢?

有!exclude 可以指定去除某些类。类似:

exclude('com/liuyue/library/haha/**')

但是,这样写之后会导致 javadoc 执行失败,因为依然存在的类可能引用着被去除掉的类,这样就会报错。

Game over~

查看 AndroidStudio 自带的打 javadoc 的程序执行的参数,试图在打包的时候带上一些参数阻止因为错误导致的打包停止,发现没什么特别的,暂时无法解决。

Game over~

在这里我还遇到一个坑,因为我为了测试打包上传这些功能自己写了一个 demo 工程,它写的比较简单,然后我写了 4 个类,有外部的类有内部的类,有混淆的有不混淆的,有引用其它类的有单独自己的,但是经过混淆后发现就剩两个类了,这就让我没法测试内部注释是否可以展示这个问题了,左思右想我意识到是混淆的问题,因为这里会对代码做优化,把它觉得不必要的类,没有用的类合并或者删掉,所以导致我最后缺了俩类。

一开始我觉得是混淆开启的压缩导致的,因为我配置的压缩等级是 5,这也是推荐的压缩等级。

-optimizationpasses 5

我把它改为 0 ,发现并不是这样子的,然后我查如何关系混淆代码优化,发现是这句代码,写上这句话可以关闭混淆优化:

-dontshrink

这个问题会出现在 AndroidStudio 3.4 版本以后,因为此版本后都默认开启了 R8 代码缩减。

最后仔细想一想,javadoc.jar 它并不是必传的东西,所以如果不想暴露源代码可以不传,这是我最后的解决方案了。

PGP 签名

在 gradle 的最后一段代码里有这么句

signing {sign configurations.archives
}

这句就是签名用的。网上关于 PGP 签名的文章还是很多的,从下载到最后生成我都没遇到困难,但是到了打包那一步可是坑死我了,因为一个签名算法的问题我被迫从头再来。

第一步,下载 GPG

Mac 用户可以直接使用 homebrew 下载

brew install gpg

Winodws 用户可以在 https://www.gpg4win.org/ 这里下载

第二步,生成密钥

gpg --full-gen-key

这里会让你选择密钥算法,密钥长度,密钥有效期

./upload-maven/image-20220423215320258

这里一定要选 4 RSA 仅用于签名(我也吃了这个的亏),不然操作到最后你会发现一个错误:

unknown public key algorithm encountered

gradle 会无法理解里的加密算法

./upload-maven/image-20220423215622396

随后它会让你写一个名字,电子邮件地址,信息,都确认无误后,它会让你输入一个密码。

这个密码一定要记住了,以后要用到!

操作完后,你的密钥就已经被生成了。你可以使用下列命令来查看你已经创建的密钥:

gpg --list-keys --keyid-format short

–keyid-format shot 可以让你的密钥以短 ID 的形式展示,这个后面会用到。

./upload-maven/image-20220424114114355

红框内是你的密钥ID,它的左上角是你的短ID。这里注意一下左上角有两组数字,一个是 ed25519,如果这串数字是 ed 开头的,那么恭喜你,选错密钥加密算法了,这种加密算法为 EDDSA ,非 RSA ,趁早赶紧重来吧。

如果错了可以选择删除这个密钥,如果不删除的话后续再生成同样用户名、邮箱的密钥会比较乱,很容易分不清。

而且如果你已经进行了下一步,那你就再也无法从网络上删掉这个密钥了。

因为现在仍然在工作的绝大多数密钥服务器都是使用的sks密钥服务器(组),其有以下几个特性:

  1. 分布式,提交的密钥提交至任何一个在sks服务器池的服务都会很快与其他位于sks池的程序同步。通过分布式提高了sks池整体的可用性可靠性和稳定性。
  2. 不可删除,即使你控制着一个sks密钥服务器,删除了一个公钥,很快就会通过sks的公钥算法同步,而想要命令所有的sks池同时删除一个指定的公钥几乎是不可能的。这样可以阻止恶意第三方恶意删除公钥,但是也阻止了正常的公钥删除流程。

所以一旦上传至 sks 池,将不可能从sks公钥服务器删除公钥 。顶多只能在公钥上面加上一段"我从此以后不再信任/使用该证书"的声明(又称 吊销密钥) 。所以这一行为也可以作为攻击 sks 服务器的一种手段,讲远了。

如果你想吊销一对密钥需要先生成一个吊销证书,而如果你只是不想使用了之前的密钥对,你可以先将该密钥的信息修改成垃圾信息,然后清空所有有效 uid 和所有 subkey ,并将截止时间修改为第二天,然后上传到公钥服务器。第二天的时候额外上传吊销证书,这样可以既保证密钥服务器的信息不乱,也可以吊销这个密钥(更方便是从来也不上传到密钥服务器)。

gpg --gen-revoke 你的密钥 > gpg-revoke.asc(吊销证书保存地址)

然后它会问你为什么要吊销的原因,你可以选 3 不再使用,然后一路确定就可以了。

然后将撤销证书导入本地 GPG 库中,撤销本地公钥

gpg --import gpg-revoke.asc(吊销证书的地址)

最后把这个密钥上传到公网上去就好了

gpg --send-keys 你的密钥ID

过一会你可以使用 search-keys 来搜索你刚才吊销的密钥,会发现它的状态已经改变了,会在最后有一个(revoked)的标识。

gpg --search-keys 你的密钥ID或者当时设置的name

在本地删除公钥和私钥,删除公钥和私钥分别有一条命令,不过我建议你全部删除,执行以下命令:

gpg --delete-secret-and-public-key 你的密钥ID

这样就完成掉密钥的吊销和删除了

第三步 上传公钥

为了让大家都知道你的公钥以做验证,你需要上传的你的密钥,只需要下面的一行命令,它会自己上传到一个地方,然后这个地方的公钥会很快与其它各处地方进行同步,很快你的公钥大家就都知道了,这个就叫做钥匙环。

gpg --send-keys 你的密钥ID
./upload-maven/image-20220424113249727

第四步 导出私钥

在 gradle 的最后一步会对上传的包进行签名,这里会用到私钥,需要配置一个私钥的地址,它需要配置在 gradle.properties 文件中(也许可以配置在 gradle 脚本中,但是我没找到具体的办法)。

signing.keyId=你的密钥ID
signing.password=密钥密码
signing.secretKeyRingFile=私钥所在地址

keyId 这里需要填写要签名密钥的ID,这里要填短ID,在第一步的时候讲过如何查看短ID(其实就是密钥ID的后8位)

gpg --list-keys --keyid-format short

password 要填你密钥的密码,之前提醒过你要记住,记不住的话我也没办法,重来吧。

secretKeyRingFile 这里要填私钥的地址,一定要是私钥(小坑),一定要是二进制格式的(巨坑)。

可以使用下面的命令导出私钥到文件

gpg -o /Users/shaolongfei/Downloads/secring.gpg(这个替换成你的地址) --export-secret-key 你的密钥ID

这里一定不要加 -a 或者 -armor 等等的,那会使导出的私钥文件是 ascii 编码,gradle 不认,它会报错(使用公钥也会报这个错):

It may not be a PGP secret key ring

配置完后就可以进行最后一步上传了~

上传

上传最简单了,但我建议你先 publish 到本地看一下,添加 mavenLocal() 后它会发布到 /username/.m2/repository 文件夹,一个正常的发布文件是这样的:

./upload-maven/image-20220424151220226

写好 gradle 脚本后在 AndroidStudio 的右侧 gradle task 列表里你可以找到 publish 任务:

./upload-maven/image-20220424151456098

双击执行就可以了,或者命令行:

./gradlew publish

执行过后它就会推送到 sonatype 的 nexus 上去了,你可以登录 nexus 查看一下,账号密码和你注册 sonatype jira 时一样。

https://s01.oss.sonatype.org/

因为网上很多都是老的文章,所以它的地址是老的地址,但你依然可以登录,界面和新的也完全一样,但是登录后它会告诉你账号密码不对或者你没有这个权限,就很让人摸不到头脑。

登录上去,网上的文章很多都说去 staging Repositories 选项下去找你刚才发布的包,它会为 staging 的状态,close 后会经过检查,最后点击 release 才能真正进行发布。但我发现并不是这样的,一开始我也很纳闷为什么我的找不到,到底是哪里出问题导致推送失败了,也没有报错,后来我发现在 nexus 的代码库中是可以搜索到我发布的包的,那说明就已经成功了,不用担心:

./upload-maven/image-20220424153532744

过一会你就可以在本地通过 gradle 引用你发布的包了~

mavenCentral 的同步通常是 30 分钟,最多可能需要 4 个小时。

相关文章:

Android SDK 上传 Maven 喂奶级教程

最近领导给安排了个任务&#xff0c;让我把我们现有的一个 SDK 上传到 Maven 上去&#xff0c;方便客户直接用 gradle 依赖&#xff0c;不再需要拷贝 jar 和 so 了&#xff0c;此前我也看过一些相关的文章我想问题也不大&#xff0c;觉得工作量也就一两天的事情&#xff0c;主要…...

R语言绘图教程 | 双侧条形图绘制教程

写在前面 双侧条形图在我们的文章中也是比较常见的,那么这样的图形是如何绘制的呢? 以及它使用的数据类型是什么呢? 这些都是我们在绘制图形前需要掌握的,至少我们知道绘图的数据集如何准备,这样才踏出第一步。 今天的教程,我们会从数据的准备,以及数据如何整理,以及…...

ubuntu篇---ubuntu安装python3.9

ubuntu篇—ubuntu安装python3.9 在ubuntu上安装Python有两种方法:在线安装和源码编译安装。 方法1&#xff1a;使用apt在线安装 1.更新软件包列表并安装必备组件&#xff1a; $ sudo apt update $ sudo apt install software-properties-common2.将Deadsnakes PPA添加到系统…...

git初始化一个远程空仓库

目录 1. 仅做简单初始化2. 推送现有的非仓库文件夹3. 推送现有的仓库 git初始化一个远程空仓库主要有以下三种途径&#xff1a; 仅做简单初始化&#xff0c;例如添加 README.md 和 .gitignore。将现有的文件夹&#xff08;非仓库&#xff09;推送到远程仓库。将现有的仓库推送…...

装箱问题+宠物小精灵之收服+数字组合——01背包

一、装箱问题 (裸题) 有一个箱子容量为 V&#xff0c;同时有 n 个物品&#xff0c;每个物品有一个体积&#xff08;正整数&#xff09;。 要求 n 个物品中&#xff0c;任取若干个装入箱内&#xff0c;使箱子的剩余空间为最小。 输入 第一行是一个整数 V (0 < V ≤ 20000)&…...

记一次页面接口502问题:“502 Bad Gateway”

接收别人的项目进行迭代&#xff0c;项目部署到服务器上之后&#xff0c;有一个接口数据刷不出来&#xff0c;一直502 后来联想到网关的问题&#xff0c;想通过设置白名单的方式解决&#xff0c;设置之后依旧不行。 查看nginx日志发现报错&#xff1a; *169 connect() failed …...

Oracle systemstate、gdb、dbx介绍

当数据库出现严重的性能问题或者hang了的时候&#xff0c; 可能最常用的办法就是重启数据库&#xff0c;简单有效解决问题&#xff1b;但是重启后如何追踪问题的根本原因成了难题&#xff0c;很多信息随着重启也消失不见了&#xff0c;让追查问题变的十分棘手&#xff0c;这时就…...

Stable Diffusion 模型下载:RealCartoon-Anime - V10

文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十下载地址模型介绍 这个检查点是从 RealCartoon3D 检查点分支出来的。它的目标是产生更多的“动漫”风格,因为我喜欢动漫。:)我知道有很多人做得很好(...

课时22:内置变量_字符串相关

2.4.2 字符串相关 学习目标 这一节&#xff0c;我们从 基础知识、简单实践、小结 三个方面来学习 基础知识 字符串相关的变量解析 字符串计数${#file} 获取字符串的长度字符串截取 - 语法为${var:pos:length} 表示对变量var从pos开始截取length个字符&#xff0c;pos为…...

软件应用实例分享,电玩计时计费怎么算,佳易王PS5游戏计时器系统程序教程

软件应用实例分享&#xff0c;电玩计时计费怎么算&#xff0c;佳易王PS5游戏计时器系统程序教程 一、前言 以下软件教程以 佳易王电玩计时计费管理系统软件V17.9为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 点击开始计时后&#xff0c;图片…...

架设游戏服务器租用价格?腾讯云和阿里云价格对比

游戏服务器租用多少钱一年&#xff1f;1个月游戏服务器费用多少&#xff1f;阿里云游戏服务器26元1个月、腾讯云游戏服务器32元&#xff0c;游戏服务器配置从4核16G、4核32G、8核32G、16核64G等配置可选&#xff0c;可以选择轻量应用服务器和云服务器&#xff0c;阿腾云atengyu…...

ag-Grid:对数据变化的单元格进行高亮显示

对单元格高亮 问:ag-grid 当 rowData 数据变化,如何对数据变化的党员个进行高亮? 解析: 在ag-Grid中,想要对数据变化的单元格进行高亮显示,你可以使用以下步骤来实现: 监听数据变化:首先,你需要监听rowData的变化。这可以通过在你的组件中观察rowData属性的变化来实…...

Oracle 几种行转列的方式 sum+decode sum+case when pivot

目录 原始数据&#xff1a; 方式一&#xff1a; 方式二&#xff1a; 方式三&#xff1a; unpivot的使用&#xff1a; 原始数据&#xff1a; 方式一&#xff1a; select t_name,sum(decode(t_item, item1, t_num, 0)) item1,sum(decode(t_item, item2, t_num, 0)) item2,s…...

[AIGC] Tomcat:一个简单 and 高效的 Java Web 服务器

Tomcat&#xff08;Tomcat Server&#xff09;是 Apache 基金会下的一个开源项目&#xff0c;它是一个简单 and 高效的 Java Web 服务器&#xff0c;支持 Servlet 2.5、JSP 2.2 和 EL 2.2 规范。Tomcat 是当今最受欢迎的 Java Web 服务器之一&#xff0c;它在 Java 世界中被广泛…...

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之Toggle组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之Toggle组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、Toggle组件 组件提供勾选框样式、状态按钮样式及开关样式。 子组件 仅当Toggl…...

使用耳机壳UV树脂制作私模定制耳塞有哪些选择呢?

私模定制耳塞人士的选择可以从以下几个方面考虑&#xff1a; 专业经验&#xff1a;选择有丰富经验的私模定制耳塞人士&#xff0c;能够更好地理解用户需求&#xff0c;提供更专业的建议和服务。可以通过查看其作品和客户评价来了解其经验和口碑。材料质量&#xff1a;选择使用…...

什么是集群服务器

近一段时间来&#xff0c;集群服务器被广大站长热议&#xff0c;所谓集群服务器就是指很多台服务器把它们集中在一起来进行同一种服务。集群服务器也可以由很多个的计算机并行去计算&#xff0c;这样可以获得非常高的计算速度&#xff0c;提升服务器整体的工作效. 其实我们都知…...

HCIA-HarmonyOS设备开发认证V2.0-3.轻量系统内核基础

目录 一、前言二、LiteOS-M系统概述三、内核框架3.1、CMSIS 和 POSIX 整体架构3.2、LiteOS-M内核启动流程 四、内核基础4.1、任务管理4.2、时间管理(待续)4.3、中断管理(待续)4.4、软件定时器(待续) 五、内存管理5.1、静态内存(待续)5.2、动态内存(待续) 六、内核通信机制6.1、…...

【JavaWeb】头条新闻项目实现 基本增删改查 分页查询 登录注册校验 业务功能实现 第二期

文章目录 一、为什么使用token口令二、登录注册功能2.1 登录表单提交后端代码&#xff1a; 2.2 根据token获取完整用户信息代码实现&#xff1a; 2.3 注册时用户名占用校验代码实现&#xff1a; 2.4 注册表单提交代码实现&#xff1a; 三、头条首页功能3.1 查询所有头条分类3.2…...

HiveQL——不借助任何外表,产生连续数值

注&#xff1a;参考文章&#xff1a; HiveSql一天一个小技巧&#xff1a;如何不借助其他任何外表&#xff0c;产生连续数值_hive生成连续数字-CSDN博客文章浏览阅读1.3k次。0 需求描述输出结果如下所示&#xff1a;12345...1001 问题分析方法一&#xff1a;起始值&#xff08;…...

Docker容器监控-CIG

目录 一、CIG说明 1. CAdvisor 2. InfluxDB 3. Grafana 二、环境搭建 1. 创建目录 2. 编写 docker-compose.yml 3. 检查并运行容器 三、进行测试 1. 查看 influxdb 存储服务 是否能正常访问 2. 查看 cAdvisor 收集服务能否正常访问 3. 查看 grafana 展现服务&#…...

python调用golang中函数方法

一、原因说明&#xff1a;由于simhash方法有多种实现方式&#xff0c;现python中simhash方法与golang中的不一样&#xff0c;需要两者代码生成结果保持一致&#xff0c;故采用python中的代码调用golang编译的so文件来实现。 环境配置&#xff1a;①Windows10系统要有gcc环境&a…...

Ps:颜色取样器工具

颜色取样器工具 Color Sampler Tool允许用户从图像中设置特定的颜色取样点&#xff0c;然后利用“信息”面板查看该点在不同颜色模式下&#xff08;包括&#xff1a;RGB、CMYK、Lab、灰度等&#xff09;的颜色值&#xff0c;因此可为色彩分析、颜色校正、色彩匹配等提供精确的数…...

Rust引用、借用和所有权详解

在Rust中&#xff0c;引用、借用和所有权是语言的核心概念&#xff0c;它们共同构成了内存管理的基石。本篇博客将介绍Rust中的这些概念&#xff0c;并通过具体的例子深入探讨它们的用法和优势。 所有权 Rust中的所有权是一种独特的内存管理机制&#xff0c;它规定了在特定作…...

C# 字体大小的相关问题

设置字体大小无法这么写&#xff0c; button1.Font.Size 20&#xff1b; 这个是只读属性&#xff1b; 把字体大小改为16&#xff0c; button2.Font new Font(button2.Font.Name, 16); 程序运行的时候先看一下窗体和控件的默认字体尺寸&#xff0c;都是9&#xff1b;然后点b…...

使用 Express.js 和 MySQL 构建 Web 应用程序

Express.js 是一个流行的 Node.js Web 框架&#xff0c;而 MySQL 是一种广泛使用的关系型数据库。本文将介绍如何结合使用 Express.js 和 MySQL 构建功能强大的 Web 应用程序。 一、安装和设置 首先&#xff0c;确保您已经安装了 Node.js 和 MySQL。然后&#xff0c;通过以下…...

1306. 跳跃游戏 III

经过测试&#xff0c;两种写法耗时差距10倍&#xff0c;我也不知道原因是啥 用访问次数的是更快的 class Solution { public:int n;bool dfs(vector<int>& arr, int start, vector<int>& visited){if(start<0||start>n || visited[start]1) return …...

List与数组相互转换

文章目录 将 List 转 数组List.toArra()Stream().toArray() 将 数组 转 ListLists.newArrayList()Stream.of() 将 List 转 数组 List.toArra() List<Integer> list Lists.newArrayList(1, 2, 3);// 无需转换类型 --> [1,2,3]Integer[] intArray list.toArray(new I…...

Linux openKylin(开放麒麟)系统SSH服务安装配置与公网远程连接

文章目录 前言1. 安装SSH服务2. 本地SSH连接测试3. openKylin安装Cpolar4. 配置 SSH公网地址5. 公网远程SSH连接6. 固定SSH公网地址7. SSH固定地址连接8. 结语 前言 openKylin是中国首个基于Linux 的桌面操作系统开发者平台&#xff0c;通过开放操作系统源代码的方式&#xff…...

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之AlphabetIndexer组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之AlphabetIndexer组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、AlphabetIndexer组件 可以与容器组件联动用于按逻辑结构快速定位容器显…...

java学习07---综合练习

飞机票 1.需求: 机票价格按照淡季旺季、头等舱和经济舱收费、输入机票原价、月份和头等舱或经济舱。 按照如下规则计算机票价格&#xff1a;旺季&#xff08;5-10月&#xff09;头等舱9折&#xff0c;经济舱8.5折&#xff0c;淡季&#xff08;11月到来年4月&#xff09;头等舱7…...

【RL】Bellman Equation (贝尔曼等式)

Lecture2: Bellman Equation State value 考虑grid-world的单步过程&#xff1a; S t → A t R t 1 , S t 1 S_t \xrightarrow[]{A_t} R_{t 1}, S_{t 1} St​At​ ​Rt1​,St1​ t t t, t 1 t 1 t1&#xff1a;时间戳 S t S_t St​&#xff1a;时间 t t t时所处的sta…...

PyTorch 2.2大更新!集成FlashAttention-2,性能提升2倍

【新智元导读】新的一年&#xff0c;PyTorch也迎来了重大更新&#xff0c;PyTorch 2.2集成了FlashAttention-2和AOTInductor等新特性&#xff0c;计算性能翻倍。 新的一年&#xff0c;PyTorch也迎来了重大更新&#xff01; 继去年十月份的PyTorch大会发布了2.1版本之后&#…...

2.9日学习打卡----初学RabbitMQ(四)

2.9日学习打卡 一.RabbitMQ 死信队列 在MQ中&#xff0c;当消息成为死信&#xff08;Dead message&#xff09;后&#xff0c;消息中间件可以将其从当前队列发送到另一个队列中&#xff0c;这个队列就是死信队列。而在RabbitMQ中&#xff0c;由于有交换机的概念&#xff0c;实…...

大数据Flume--入门

文章目录 FlumeFlume 定义Flume 基础架构AgentSourceSinkChannelEvent Flume 安装部署安装地址安装部署 Flume 入门案例监控端口数据官方案例实时监控单个追加文件实时监控目录下多个新文件实时监控目录下的多个追加文件 Flume Flume 定义 Flume 是 Cloudera 提供的一个高可用…...

【SQL高频基础题】550.游戏玩法分析IⅣ

这个SQL花了很久。但是有挺多启发的。 如果我们做不出来&#xff0c;就去看答案。 但是看完答案之后&#xff0c;不要着急就去看下一道题&#xff0c;先把这道题吃透&#xff0c;后面的题目就会更有思路。 题目&#xff1a; Table: Activity ----------------------- | Co…...

sheng的学习笔记-部署-目录

标题传送门 sheng的学习笔记-docker部署&#xff0c;原理图&#xff0c;命令&#xff0c;用idea设置docker sheng的学习笔记-docker部署&#xff0c;原理图&#xff0c;命令&#xff0c;用idea设置docker sheng的学习笔记-docker部署springboot sheng的学习笔记-docker部署spri…...

【Java】悲观锁和乐观锁有什么区别?

Java中的悲观锁和乐观锁的主要区别体现在以下几个方面&#xff1a; 加锁策略&#xff1a;悲观锁在操作数据时&#xff0c;总是假设最坏的情况&#xff0c;即认为其他线程会修改数据&#xff0c;因此在读取或操作数据时&#xff0c;会先对数据进行加锁&#xff0c;以保证数据的…...

Elasticsearch:使用查询规则(query rules)进行搜索

在之前的文章 “Elasticsearch 8.10 中引入查询规则 - query rules”&#xff0c;我们详述了如何使用 query rules 来进行搜索。这个交互式笔记本将向你介绍如何使用官方 Elasticsearch Python 客户端来使用查询规则。 你将使用 query rules API 将查询规则存储在 Elasticsearc…...

Java核心设计模式:代理设计模式

一、生活中常见的代理案例 房地产中介&#xff1a;客户手里没有房源信息&#xff0c;找一个中介帮忙商品代购&#xff1a;代理者一般有好的资源渠道&#xff0c;降低购物成本&#xff08;如海外代购&#xff0c;自己不用为了买东西出国&#xff09; 二、为什么要使用代理 对…...

JSP编程

JSP编程 您需要理解在JSP API的类和接口中定义的用于创建JSP应用程序的各种方法的用法。此外,还要了解各种JSP组件,如在前一部分中学习的JSP动作、JSP指令及JSP脚本。JSP API中定义的类提供了可借助隐式对象通过JSP页面访问的方法。 1. JSP API的类 JSP API是一个可用于创建…...

【Flink入门修炼】1-1 为什么要学习 Flink?

流处理和批处理是什么&#xff1f; 什么是 Flink&#xff1f; 为什么要学习 Flink&#xff1f; Flink 有什么特点&#xff0c;能做什么&#xff1f; 本文将为你解答以上问题。 一、批处理和流处理 早些年&#xff0c;大数据处理还主要为批处理&#xff0c;一般按天或小时定时处…...

刘谦龙年春晚魔术模拟

守岁共此时 代码 直接贴代码了&#xff0c;异常处理有点问题&#xff0c;正常流程能跑通 package com.yuhan.snginx.util.chunwan;import java.util.*;/*** author yuhan* since 2024/02/10*/ public class CWMS {static String[] num {"A", "2", &quo…...

re:从0开始的CSS学习之路 9. 盒子水平布局

0. 写在前面 过年也不能停止学习&#xff0c;一停下就难以为继&#xff0c;实属不应 1. 盒子的水平宽度 当一个盒子出现在另一个盒子的内容区时&#xff0c;该盒子的水平宽度“必须”等于父元素内容区的宽度 盒子水平宽度&#xff1a; margin-left border-left padding-lef…...

【MySQL基础】:深入探索DQL数据库查询语言的精髓(上)

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; MySQL从入门到进阶 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. DQL1.1 基本语法1.2 基础查询1.3 条件查询1.3 聚合函数 &#x1f324;️ 全篇…...

JavaScript实现轮播图方法

效果图 先来看下效果图&#xff0c;嫌麻烦就不用具体图片来实现了&#xff0c;主要是理清思路。&#xff08;自动轮播&#xff0c;左右按钮切换图片&#xff0c;小圆点切换图片&#xff0c;鼠标移入暂停轮播&#xff0c;鼠标移出继续轮播&#xff09; HTML 首先是html内容&am…...

Web课程学习笔记--jsonp的原理与简单实现

jsonp的原理与简单实现 原理 由于同源策略的限制&#xff0c;XmlHttpRequest只允许请求当前源&#xff08;域名、协议、端口&#xff09;的资源&#xff0c;为了实现跨域请求&#xff0c;可以通过script标签实现跨域请求&#xff0c;然后在服务端输出JSON数据并执行回调函数&…...

第78讲 修改密码

系统管理实现 修改密码实现 前端 modifyPassword.vue&#xff1a; <template><el-card><el-formref"formRef":model"form":rules"rules"label-width"150px"><el-form-item label"用户名&#xff1a;&quo…...

Docker 容器网络:C++ 客户端 — 服务器应用程序。

一、说明 在下面的文章中&#xff0c; 将向您概述 docker 容器之间的通信。docker 通信的验证将通过运行 C 客户端-服务器应用程序和标准“ping”命令来执行。将构建并运行两个单独的 Docker 映像。 由于我会关注 docker 网络方面&#xff0c;因此不会提供 C 详细信息。…...

Android 识别车牌信息

打开我们心爱的Android Studio 导入需要的资源 gradle //开源车牌识别安卓SDK库implementation("com.github.HyperInspire:hyperlpr3-android-sdk:1.0.3")button.setOnClickListener(v -> {Log.d("Test", "");try (InputStream file getAs…...