国外的疫情越来越严重,不排除有卷土重来的可能,今年突如其来的疫情影响着每一个人,就业形势也不容乐观,不管是企业还是个人都要共同面对这次生存考验,今年的毕业生也是非常的不容易,不管外界如何变化,持续强化自己的专业技能永远是立身之本,提升自己的核心竞争力永远是生存之道。最近学了一些自定义gradle插件相关的知识,开发了自动上传fir并发送钉钉消息的gradle插件,提升了个人和团队的工作效率,在此记录并分享。
由于公司当前的开发流程尚未用到Jenkins这类在线的自动化构建平台,开发流程仍然是开发者本地打包再上传fir托管平台并发送上传完成的消息到钉钉群里,再@相关的测试人员提醒可进行测试,这一系列流程化的操作显得过于繁琐。作为Android开发掌握groovy这门语言也是有必要的,毕竟这是Android studio的构建脚本语言且语法与java相似,完全可以利用构建脚本实现打包后自动上传到fir托管平台,再利用自定义钉钉机器人的官方api发送钉钉消息给相关的测试人员,解放双手,自动化这一系列的流程操作来提升工作效率,减少加班的可能,打包/上传/发消息这段时间喝杯咖啡放松一下不是更好吗?我决定把这个插件打包上传到JitPack分享给团队和网络上的其他人,因为帮更多的人提升工作效率确实是一件很cool的事情
实现自定义Gradle插件的三种方式
app的gradle文件中定义
- 直接修改app的gradle文件,继承gradle的Plugin类,定义一个自定义插件的实现类,重写apply方法完成插件逻辑优点:方便调试
1
2
3
4
5
6
7apply plugin: PluginImpl
class PluginImpl implements Plugin<Project>{
void apply(Project project){
println("hello Plugin")
}
}
缺点:跟app工程代码混在一起,无法给其它项目使用buildSrc目录定义
在工程目录下,新建buildSrc目录,在该目录下开发
新建文件夹src/main/groovy/xxx/xxx/xxx,其中xxx/xxx/xxx是包名,在这个目录下自定义gradle插件类PluginImpl
新建文件夹resource/META-INF/gradle-plugins,在这个目录下新建文件xxx.xxx.xxx.properties,声明插件实现类implementation-class=xxx.xxx.xxx.PluginImpl
优点:方便调试,与app工程分离
缺点:无法给其它项目使用
新增插件lib定义
新建一个gradle插件的lib库,删除其它的所有文件,保持项目结构与buildSrc目录一样,在这个lib库下开发
优点:与app工程分离,可打包上传maven,在其它项目中引用并分享给其他人
缺点:调试稍微麻烦,需发布到本地仓库
以上三种方式最好的是第三种,与app工程分离,可发布到本地maven仓库下调试,也可以打包插件上传到私有的maven仓库或者公有maven仓库如JitPack分享给其他人
定义插件参数
定义fir配置参数
1 | package com.demo.plugin |
定义钉钉配置参数
1 | package com.demo.plugin |
定义插件参数
- 结合上面两类参数配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package com.demo.plugin
import org.gradle.api.Action
import org.gradle.api.model.ObjectFactory
class UploadApkPluginExtension {
FirExtension firExtension
DingTalkExtension dingTalkExtension
public UploadApkPluginExtension(ObjectFactory objectFactory) {
firExtension = objectFactory.newInstance(FirExtension.class)
dingTalkExtension = objectFactory.newInstance(DingTalkExtension.class)
}
public void fir(Action<FirExtension> action) {
action.execute(firExtension)
}
public void dingTalk(Action<DingTalkExtension> action){
action.execute(dingTalkExtension)
}
}插件逻辑
groovy中我们仍然可以使用okhttp,Gson这类Android中常用的开源框架来实现网络请求,Json序列化/反序列化相关的逻辑,封装网络请求工具类,最后在自定义Plugin中调用这个工具类完成自动化引入开源库
修改插件lib中的gradle文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15apply plugin: 'java-library'
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()//gradle sdk
implementation localGroovy()//groovy sdk
implementation 'com.squareup.okhttp3:okhttp:4.7.2'
implementation 'com.google.code.gson:gson:2.8.6'
}
repositories {
mavenCentral()
jcenter()
}网络请求工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95package com.demo.plugin
import com.google.gson.Gson
import okhttp3.*
import java.util.concurrent.TimeUnit
public class OkHttpUtil{
OkHttpClient okHttpClient
Gson gson
DingTalk dingTalk // 序列化钉钉消息工具类
public OkHttpUtil(){
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS).build()
gson = new Gson()
dingTalk = new DingTalk()
}
// 获取fir上传证书
BundleApp getCert(String appPackage, String apiTokenFir){
FormBody.Builder build = new FormBody.Builder()
build.add("bundle_id", appPackage)
build.add("api_token", apiTokenFir)
build.add("type", "android")
Request request = new Request.Builder().url("http://api.bq04.com/apps").post(build.build()).build()
Response response = okHttpClient.newCall(request).execute()
String result = response.body.string()
return gson.fromJson(result, BundleApp.class)
}
// 上传apk到fir
String uploadApk(String apkPath,String key,String token,String appName,String appVersion,String appBuild,String fileName,String upload_url) {
RequestBody fileBody = RequestBody.create(MediaType.parse("application/octet-stream"), new File(apkPath))
MultipartBody body = new MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
.addFormDataPart("key", key)
.addFormDataPart("token", token)
.addFormDataPart("x:name", appName)
.addFormDataPart("x:version", appVersion)
.addFormDataPart("x:build", appBuild)
.addFormDataPart("file", fileName, fileBody)
.build()
Request requestApk = new Request.Builder().url(upload_url).post(body).build()
Response responseApk = okHttpClient.newCall(requestApk).execute()
return responseApk.body.string()
}
// 上传icon到fir
String uploadIcon(String apkIconPath,String keyIcon,String tokenIcon,String upload_urlIcon){
RequestBody fileBodyIcon = RequestBody.create(MediaType.parse("application/octet-stream"),new File(apkIconPath))
MultipartBody bodyIcon = new MultipartBody.Builder()
.setType(MediaType.parse("multipart/form-data"))
.addFormDataPart("key", keyIcon)
.addFormDataPart("token", tokenIcon)
.addFormDataPart("file", "icon.png", fileBodyIcon)
.build()
Request requestIcon = new Request.Builder().url(upload_urlIcon).post(bodyIcon).build()
Response responseIcon = okHttpClient.newCall(requestIcon).execute()
return responseIcon.body.string()
}
ApkInfo getApkUrl(String appPackage, String apiTokenFir) {
// 获取成功连接
String queryurl =
"http://api.bq04.com/apps/latest/$appPackage?api_token=$apiTokenFir&type=android"
Request requestUrl = new Request.Builder().url(queryurl).get().build()
Response responseUrl = okHttpClient.newCall(requestUrl).execute()
String result = responseUrl.body.string()
return gson.fromJson(result,ApkInfo.class)
}
// 发送钉钉链接消息
void sendDingTalkLink(String text,String title,String url,String webHook){
RequestBody linkBody = FormBody.create(MediaType.parse("application/json; charset=utf-8")
, dingTalk.createLinkMsg(text,title,url))
Request linkDingTalk = new Request.Builder().url(webHook)
.post(linkBody).build()
Response responseLink = okHttpClient.newCall(linkDingTalk).execute()
String result = responseLink.body.string()
println("已发送钉钉链接:$result")
}
// 发送钉钉文本消息
void sendDingTalkMsg(String text,String webHook,boolean isAtAll,List<String> atMobiles){
RequestBody textBody = FormBody.create(MediaType.parse("application/json; charset=utf-8")
, dingTalk.createTextMsg(text,atMobiles,isAtAll))
Request textDingTalk = new Request.Builder().url(webHook)
.post(textBody).build()
Response responseText = okHttpClient.newCall(textDingTalk).execute()
String result = responseText.body.string()
println("已发送钉钉消息:$result")
}
}序列化工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92package com.demo.plugin
import com.google.gson.Gson
public class DingTalk {
Gson gson
public DingTalk(){
gson = new Gson()
}
// 定义钉钉链接消息的bean类
def class LinkMsg{
String msgtype
Link link
public LinkMsg(String msgtype,Link link){
this.msgtype = msgtype
this.link = link
}
}
def class Link{
String title
String text
String messageUrl
public Link(String title,String text,String messageUrl){
this.title = title
this.text = text
this.messageUrl = messageUrl
}
}
// 定义钉钉文本消息的bean类
def class TextMsg{
String msgtype
Text text
At at
public TextMsg(String msgtype,Text text,At at){
this.msgtype = msgtype
this.text = text
this.at = at
}
}
def class At{
List<String> atMobiles = new ArrayList<>()
boolean isAtAll = true
public At(List<String> atMobiles,boolean isAtAll){
this.atMobiles = atMobiles
if (!atMobiles.isEmpty()){
this.isAtAll = false
}
else{
this.isAtAll = isAtAll
}
}
}
def class Text{
String content
public Text(String content){
this.content = content
}
}
/**
* 构建一个钉钉链接消息
* @param text String
* @param title String
* @param url String
* @return String
*/
String createLinkMsg(String text ,String title,String url) {
Link link = new Link(title,text,url)
LinkMsg linkMsg = new LinkMsg("link",link)
return gson.toJson(linkMsg)
}
/**
* 构建一个钉钉文本消息
* @param msgtype String
* @param content String
* @param text String
* @return String
*/
String createTextMsg(String content,List<String> atMobiles,boolean isAtAll){
Text text = new Text(content)
At at = new At(atMobiles,isAtAll)
TextMsg textMsg = new TextMsg("text", text, at )
return gson.toJson(textMsg)
}
}
反序列化bean类定义
1 | package com.demo.plugin; |
1 | package com.demo.plugin; |
自定义Plugin
- 继承gradle的Plugin类,重写apply方法中实现插件逻辑
- 在assemble任务后执行插件相关的逻辑
- 兼容Debug/Release两种打包方式和各个flavor下的打包任务,在这些任务打包完成后执行插件逻辑,采用”assemble” + variant.name.capitalize() +”Fir”的打包命令灵活配置
- 如渠道名为googlePlay下打debug/release包则打包任务名为assembleGooglePlayDebug/assembleGooglePlayRelease
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94package com.demo.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
class PluginImpl implements Plugin<Project> {
UploadApkPluginExtension extension
void apply(Project project) {
extension = project.extensions.create('uploadApk', UploadApkPluginExtension.class, project.getObjects())
if (project.android.hasProperty("applicationVariants")) {
project.android.applicationVariants.all { variant ->
// 定义gradle任务名称
Task uploadFir = project.task("assemble${variant.name.capitalize()}Fir").doLast {
println("开始上传Fir")
def (String appPackage, String apiTokenFir, String apkPath, String fileName, String appName, String appVersion, String appBuild, String apkIconPath) = getParams(project, variant)
OkHttpUtil okHttpUtil = new OkHttpUtil()
BundleApp bundleApp = okHttpUtil.getCert(appPackage,apiTokenFir)
println("获取凭证信息成功")
BundleApp.CertBean certBean = bundleApp.getCert()
// 上传apk
println("上传apk中...")
String key = certBean.getBinary().getKey()
String token = certBean.getBinary().getToken()
String upload_url = certBean.getBinary().getUpload_url()
String jsonApk = okHttpUtil.uploadApk(apkPath,key,token,appName,appVersion,appBuild,fileName,upload_url)
println("上传apk文件返回结果:$jsonApk")
// 上传icon
println("上传Icon中...")
String keyIcon = certBean.getIcon().getKey()
String tokenIcon = certBean.getIcon().getToken()
String upload_urlIcon = certBean.getIcon().getUpload_url()
String jsonIcon = okHttpUtil.uploadIcon(apkIconPath,keyIcon,tokenIcon,upload_urlIcon)
println("上传Icon返回结果:$jsonIcon")
ApkInfo apkInfo = okHttpUtil.getApkUrl(appPackage,apiTokenFir)
println("下载链接:${apkInfo.installUrl}")
def (String content, String title, String webHook, boolean isAtAll,List<String> atMobiles) = getDingTalkParams()
String dingTalkMsg = "点击跳转gilos下载链接(版本号:$appBuild 版本名称:$appVersion)"
if (content.length() > 0){
dingTalkMsg = "${dingTalkMsg},此次更新:$content"
}
/**
* 发送钉钉消息
*/
okHttpUtil.sendDingTalkLink(dingTalkMsg,title,apkInfo.installUrl,webHook)
okHttpUtil.sendDingTalkMsg(content,webHook,isAtAll,atMobiles)
}
// 在assembleDebug执行后执行
uploadFir.dependsOn project.tasks["assemble${variant.name.capitalize()}"]
}
}
}
// 获取钉钉消息配置相关的参数并返回
private List getDingTalkParams() {
String webHook = extension.getDingTalkExtension().getWebHook()
String title = extension.getDingTalkExtension().getTitle()
String content = extension.getDingTalkExtension().getContent()
String isAtAll = extension.getDingTalkExtension().getIsAtAll()
List<String> atMobiles = extension.getDingTalkExtension().getAtMobiles()
[content, title, webHook, isAtAll, atMobiles]
}
// 获取相关gradle配置文件和fir配置相关的参数并返回
private List getParams(Project project, variant) {
String appName = extension.getFirExtension().getAppName()
String appPackage = project.android.defaultConfig.applicationId
String appVersion = project.android.defaultConfig.versionName
String appBuild = project.android.defaultConfig.versionCode
String apkPath = variant.outputs.first().outputFile
String fileName = apkPath.substring(apkPath.lastIndexOf("\\") + 1, apkPath.length())
String apkIconPath = project.android.applicationVariants.first().outputs.first().outputFile.parent.split("build")[0] + extension.getFirExtension().getIconPath()
String apiTokenFir = extension.getFirExtension().getToken()
// 获取上传凭证
// println("appName:$appName")
// println("appPackage:$appPackage")
// println("appVersion:${appVersion}")
// println("appBuild:${appBuild}")
// println("apiTokenFir:${apiTokenFir}")
// println("apkIconPath:${apkIconPath}")
println("文件路径:$apkPath")
println("文件名称:$fileName")
[appPackage, apiTokenFir, apkPath, fileName, appName, appVersion, appBuild, apkIconPath]
}
}声明插件
在lib的配置文件xxx.xxx.xxx.properties(xxx.xxx.xxx是插件定义类所在的包名)中声明插件1
com.demo.plugin.PluginImpl
获取所有打包任务下的插件执行命令
variant.name.capitalize()这个参数是打包任务名称首字母大写,如不确定当前的打包任务名称,可以增加app的gradle代码,打印所有的打包任务名称来获取插件的执行命令,同步一下即可输出1
2
3project.android.applicationVariants.all { variant ->
println("assemble${variant.name.capitalize()}Fir")
}插件上传
上传到maven仓库
上传本地仓库实际上就是生成maven工程的本地文件夹再引用,方便调试
增加插件lib的gradle文件代码执行gradle uploadArchives命令上传1
2
3
4
5
6
7
8
9
10
11
12
13
14uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../repo')) // 上传到本地仓库调试
// 上传到远程仓库
// repository(url: "xxx.xxx.xxx:xxxx/repo私有仓库maven地址") {
// authentication(userName: "用户名", password: "密码")
// }
pom.groupId = 'com.demo.plugin'//插件lib包名
pom.artifactId = 'firPlugin'
pom.version = '0.4' //插件版本号
}
}
}上传到远程仓库(私有maven/公有maven)
使用插件
- 修改根目录的gradle文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
// maven { url './repo' } //本地Maven仓库地址
// maven { url 'xxx.xxx.xxx:xxxx/repository/release'} // 私有仓库引用
maven { url 'https://jitpack.io' } //Jitpack仓库引用
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.jessieeeee:upload-apk-fir-plugin:0.7' //Jitpack插件引用
// classpath 'com.demo.plugin:firPlugin:0.4' // 本地/私有仓库插件引用
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
} - 修改app的gradle文件,引入插件并进行相关配置github完整项目传送门,欢迎star,issue~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16apply plugin: 'com.demo.plugin'
uploadApk {
fir {
appName = "你的app名称"
iconPath = "src/main/res/mipmap-xxxhdpi/ic_launcher.png"
token = "fir平台的token"
}
dingTalk{
webHook = "钉钉机器人的webhook"
title = "Android:xxx打包完成"
content = "带关键字的消息内容" //这个关键字跟自定义钉钉机器人的安全设置有关
isAtAll = false // 是否at所有人
atMobiles = ["手机号1","手机号2"] //at某些人
}
}