自定义一个gradle插件动态修改jar包Class文件

动态修改jar包中的class文件,预埋占位符字符串,在编译代码时动态植入要修改的值。记录一下整个过程及踩过的坑。

Github 地址:ClassPlaceholder

  1. 创建一个Android项目,再创建一个Android library,删掉里面所有代码。添加groovy支持。如:
apply plugin: 'groovy'

sourceCompatibility = 1.8
targetCompatibility = 1.8

buildscript {
    repositories {
        mavenCentral()
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation localGroovy()
    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:3.1.4'
    implementation 'com.android.tools.build:gradle-api:3.1.4'
    implementation 'org.javassist:javassist:3.20.0-GA'
}
  1. 创建自定义的插件的配置文件: resource/META_INF/gradle-plugins,该目录为固定目录,下面创建自定义的插件配置文件。并将替换原来src/main/java替换为src/main/groovy;在配置文件中添加外部引用的插件名:
implementation-class=me.xp.gradle.placeholder.PlaceholderPlugin
  1. 创建完成后整个目录结构为:
image
  1. 准备工作完成,开始创建代码。先定义要扩展的内容格式:
class PlaceholderExtension {
/**
* 要替换的文件
*/
    String classFile = ''
    /**
     * 要替换的模板及值,
     * 如:${template}* map->
     *{"template","value"}*/
    Map<String, String> values = [:]

    /**
     * 是否修改项目下的java源文件
     */
    boolean isModifyJava = false
}

  1. 再将创建的扩展注册到transform中:
class PlaceholderPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        //build.gradle中使用的扩展
        project.extensions.create('placeholders', Placeholders, project)

        def android = project.extensions.getByType(AppExtension)
        def transform = new ClassPlaceholderTransform(project)
        android.registerTransform(transform)
    }
}
  1. 在自定义的Transform中遍历项目下的jar包及所有文件,找到要替换的文件及预埋的占位符的字符串,关键代码:
@Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
        def outputProvider = transformInvocation.getOutputProvider()
        def inputs = transformInvocation.inputs

        def placeholder = project.extensions.getByType(Placeholders)
        println "placeholders = ${placeholder.placeholders.size()}"
        inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput dirInput ->
                // 获取output目录
                def dest = outputProvider.getContentLocation(dirInput.name,
                        dirInput.contentTypes, dirInput.scopes,
                        Format.DIRECTORY)

                File dir = dirInput.file
                if (dir) {
                    HashMap<String, File> modifyMap = new HashMap<>()
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
                        File classFile ->
                            def isNeedModify = Utils.isNeedModify(classFile.absolutePath)
                            if (isNeedModify) {
                                println " need modify class ${classFile.path}"
                                File modified = InjectUtils.modifyClassFile(dir, classFile, transformInvocation.context.getTemporaryDir())
                                if (modified != null) {
                                    //key为相对路径
                                    modifyMap.put(classFile.absolutePath.replace(dir.absolutePath, ""), modified)
                                }
                            }
                    }

                    modifyMap.entrySet().each {
                        Map.Entry<String, File> entry ->
                            File target = new File(dest.absolutePath + entry.getKey())
                            println "entry --> ${entry.key} target = $target"
                            if (target.exists()) {
                                target.delete()
                            }
                            FileUtils.copyFile(entry.getValue(), target)
                            println "dir = ${dir.absolutePath} "

                            saveModifiedJarForCheck(entry.getValue(), new File(dir.absolutePath + entry.getKey()))
                            entry.getValue().delete()
                    }
                }
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(dirInput.file, dest)
            }

            input.jarInputs.each { JarInput jarInput ->

                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                //生成输出路径
                def dest = outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                def modifyJarFile = InjectUtils.replaceInJar(transformInvocation.context, jarInput.file)
                if (modifyJarFile == null) {
                    modifyJarFile = jarInput.file
//                    println "modifyJarFile = ${modifyJarFile.absolutePath}"
                } else {
                    //文件修改过
                    println "++++ jar modified  >> ${modifyJarFile.absolutePath}"
                    saveModifiedJarForCheck(modifyJarFile, jarInput.file)
                }

                //将输入内容复制到输出
                FileUtils.copyFile(modifyJarFile, dest)
            }
        }

  1. 在遍历找到要替换的字符串后,直接替换即可:
@Override
FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
    println('* visitField *' + " , " + access + " , " + name + " , " + desc + " , " + signature + " , " + value)
    if (!isOnlyVisit) {
        modifyMap.each { k, v ->
            def matchValue = "\${$k}"
            println "matchValue = $matchValue , value = $value --> ${matchValue == value}"
            if (matchValue == value) {
                value = v
            }
        }
    }
    return super.visitField(access, name, desc, signature, value)
}

踩过的坑:

  1. 对于要内嵌的扩展,需要动态的添加。如这里在使用时的格式为:
placeholders {
    addholder {
        //is modify source java file
        isModifyJava = true
        //modify file name
        classFile = "me/xp/gradle/classplaceholder/AppConfig.java"
        //replace name and value
        values = ['public' : 'AppConfigPubic',
                  'private': 'AppConfigPrivate',
                  'field'  : 'AppConfigField']
    }
    addholder {
        isModifyJava = false
        classFile = "me/xp/gradle/jarlibrary/JarConfig.class"
        values = ['config': 'JarConfigPubic']
    }
}

由于addholder扩展内嵌在placeholders扩展中,就需要将addholder动态添加扩展,而最外层的placeholders则需要在自定义的PlaceholderPlugin类中静态添加:

project.extensions.create('placeholders', Placeholders, project)

在自定义的placeholders类中动态添加addholder扩展,将闭包作为当作参数传入,这样才能自动将build.gradle中定义的lambda值转成对应的extension对象:

/**
* 添加一个扩展对象
* @param closure
*/
void addholder(Closure closure) {
    def extension = new PlaceholderExtension(project)
    project.configure(extension, closure)
    println " -- > $extension"
    placeholders.add(extension)
}
  1. 若要修改在java源文件的值,则只需要在generateBuildConfig任务添加一个任务执行即可。创建一个任务并依赖在 generateBuildConfigTask后:
//执行修改java源代码的任务
        android.applicationVariants.all { variant ->

            def holders = project.placeholders
            if (holders == null || holders.placeholders == null) {
                println "not add place holder extension!!!"
                return
            }
            ExtensionManager.instance().cacheExtensions(holders.placeholders)
//            println "holders = ${holders.toString()} --> ${holders.placeholders}"

            //获取到scope,作用域
            def variantData = variant.variantData
            def scope = variantData.scope

            //创建一个task
            def createTaskName = scope.getTaskName("modify", "PlaceholderPlugin")
            println "createTaskName = $createTaskName"
            def createTask = project.task(createTaskName)
            //设置task要执行的任务
            createTask.doLast {
                modifySourceFile(project, holders.placeholders)
            }
            //设置task依赖于生成BuildConfig的task,在其之后生成我们的类
            String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
            def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
            if (generateBuildConfigTask) {
                createTask.dependsOn generateBuildConfigTask
                generateBuildConfigTask.finalizedBy createTask
            }
        }
  1. 要修改java源代码,这里相当于直接修改文件。使用gradle自带的ant工具便非常适用。如:
ant.replace(
        file: filPath,
        token: matchKey,
        value: v
) {
    fileset(dir: dir, includes: className)
}

ant还提供常用的正则匹配替换的函数ant.replaceregexp,但由于这里使用占位符,使用$关键字,在java中会自动当作正则的一部分使用,故这里直接使用ant.repace方法,修改完成后直接调用fileset函数即可修改源文件。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 提示:本连载内的非试读单篇文章可以免费赠送给十位亲友阅读,欢迎在朋友圈和微信群转发。 《金瓶梅》号称“天下第一奇书...
    占芳阅读 7,072评论 23 43
  • 你寂寞的像雨一样,来的时候,每个人都躲着你。
    秭萸阅读 246评论 0 0
  • 最近上映不少动漫类型的电影,可是,唯有《你的名字》评分最高。 前天朋友去看了,看完在朋友圈里一通感慨:你和我不在一...
    呆丫阅读 16,172评论 5 0
  • 1. 提升效率,效果显著的是番茄工作法和意志力延伸法。 2. 番茄工作法,不仅可以屏蔽即时的干扰,还能培养内心抗干...
    晴晴爱颖颖阅读 206评论 0 1