自定義一個gradle插件動態(tài)修改jar包Class文件

動態(tài)修改jar包中的class文件,預埋占位符字符串辕宏,在編譯代碼時動態(tài)植入要修改的值爷肝。記錄一下整個過程及踩過的坑。

Github 地址:ClassPlaceholder

  1. 創(chuàng)建一個Android項目泉沾,再創(chuàng)建一個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. 創(chuàng)建自定義的插件的配置文件: resource/META_INF/gradle-plugins,該目錄為固定目錄敲霍,下面創(chuàng)建自定義的插件配置文件俊马。并將替換原來src/main/java替換為src/main/groovy丁存;在配置文件中添加外部引用的插件名:
implementation-class=me.xp.gradle.placeholder.PlaceholderPlugin
  1. 創(chuàng)建完成后整個目錄結(jié)構(gòu)為:
image
  1. 準備工作完成,開始創(chuàng)建代碼柴我。先定義要擴展的內(nèi)容格式:
class PlaceholderExtension {
/**
* 要替換的文件
*/
    String classFile = ''
    /**
     * 要替換的模板及值柱嫌,
     * 如:${template}* map->
     *{"template","value"}*/
    Map<String, String> values = [:]

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

  1. 再將創(chuàng)建的擴展注冊到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)
                }

                //將輸入內(nèi)容復制到輸出
                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. 對于要內(nèi)嵌的擴展,需要動態(tài)的添加晕窑。如這里在使用時的格式為:
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擴展內(nèi)嵌在placeholders擴展中抑片,就需要將addholder動態(tài)添加擴展,而最外層的placeholders則需要在自定義的PlaceholderPlugin類中靜態(tài)添加:

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

在自定義的placeholders類中動態(tài)添加addholder擴展杨赤,將閉包作為當作參數(shù)傳入敞斋,這樣才能自動將build.gradle中定義的lambda值轉(zhuǎn)成對應的extension對象:

/**
* 添加一個擴展對象
* @param closure
*/
void addholder(Closure closure) {
    def extension = new PlaceholderExtension(project)
    project.configure(extension, closure)
    println " -- > $extension"
    placeholders.add(extension)
}
  1. 若要修改在java源文件的值,則只需要在generateBuildConfig任務添加一個任務執(zhí)行即可疾牲。創(chuàng)建一個任務并依賴在 generateBuildConfigTask后:
//執(zhí)行修改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

            //創(chuàng)建一個task
            def createTaskName = scope.getTaskName("modify", "PlaceholderPlugin")
            println "createTaskName = $createTaskName"
            def createTask = project.task(createTaskName)
            //設置task要執(zhí)行的任務
            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還提供常用的正則匹配替換的函數(shù)ant.replaceregexp,但由于這里使用占位符舌剂,使用$關鍵字济锄,在java中會自動當作正則的一部分使用,故這里直接使用ant.repace方法霍转,修改完成后直接調(diào)用fileset函數(shù)即可修改源文件荐绝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市避消,隨后出現(xiàn)的幾起案子低滩,更是在濱河造成了極大的恐慌,老刑警劉巖沾谓,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件委造,死亡現(xiàn)場離奇詭異,居然都是意外死亡均驶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門枫虏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妇穴,“玉大人爬虱,你說我怎么就攤上這事√谒” “怎么了跑筝?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瞒滴。 經(jīng)常有香客問我曲梗,道長,這世上最難降的妖魔是什么妓忍? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任虏两,我火速辦了婚禮,結(jié)果婚禮上世剖,老公的妹妹穿的比我還像新娘捆昏。我一直安慰自己讳推,他們只是感情好,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著俭嘁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪互纯。 梳的紋絲不亂的頭發(fā)上红柱,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音宁仔,去河邊找鬼售滤。 笑死,一個胖子當著我的面吹牛台诗,可吹牛的內(nèi)容都是我干的完箩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼拉队,長吁一口氣:“原來是場噩夢啊……” “哼弊知!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起粱快,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤秩彤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后事哭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漫雷,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年鳍咱,在試婚紗的時候發(fā)現(xiàn)自己被綠了降盹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡谤辜,死狀恐怖蓄坏,靈堂內(nèi)的尸體忽然破棺而出价捧,到底是詐尸還是另有隱情,我是刑警寧澤涡戳,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布结蟋,位于F島的核電站,受9級特大地震影響渔彰,放射性物質(zhì)發(fā)生泄漏嵌屎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一恍涂、第九天 我趴在偏房一處隱蔽的房頂上張望宝惰。 院中可真熱鬧,春花似錦乳丰、人聲如沸掌测。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汞斧。三九已至,卻和暖如春什燕,著一層夾襖步出監(jiān)牢的瞬間粘勒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工屎即, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留庙睡,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓技俐,卻偏偏與公主長得像乘陪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子雕擂,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理啡邑,服務發(fā)現(xiàn),斷路器井赌,智...
    卡卡羅2017閱讀 134,707評論 18 139
  • 提示:本連載內(nèi)的非試讀單篇文章可以免費贈送給十位親友閱讀谤逼,歡迎在朋友圈和微信群轉(zhuǎn)發(fā)。 《金瓶梅》號稱“天下第一奇書...
    占芳閱讀 7,094評論 23 43
  • 你寂寞的像雨一樣仇穗,來的時候流部,每個人都躲著你。
    秭萸閱讀 252評論 0 0
  • 最近上映不少動漫類型的電影,可是,唯有《你的名字》評分最高宾茂。 前天朋友去看了瓷马,看完在朋友圈里一通感慨:你和我不在一...
    呆丫閱讀 16,204評論 5 0
  • 1. 提升效率拴还,效果顯著的是番茄工作法和意志力延伸法跨晴。 2. 番茄工作法,不僅可以屏蔽即時的干擾片林,還能培養(yǎng)內(nèi)心抗干...
    晴晴愛穎穎閱讀 214評論 0 1