動態(tài)修改jar包中的class文件,預埋占位符字符串辕宏,在編譯代碼時動態(tài)植入要修改的值爷肝。記錄一下整個過程及踩過的坑。
Github 地址:ClassPlaceholder
- 創(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'
}
- 創(chuàng)建自定義的插件的配置文件: resource/META_INF/gradle-plugins,該目錄為固定目錄敲霍,下面創(chuàng)建自定義的插件配置文件俊马。并將替換原來src/main/java替換為src/main/groovy丁存;在配置文件中添加外部引用的插件名:
implementation-class=me.xp.gradle.placeholder.PlaceholderPlugin
- 創(chuàng)建完成后整個目錄結(jié)構(gòu)為:
image
- 準備工作完成,開始創(chuàng)建代碼柴我。先定義要擴展的內(nèi)容格式:
class PlaceholderExtension {
/**
* 要替換的文件
*/
String classFile = ''
/**
* 要替換的模板及值柱嫌,
* 如:${template}* map->
*{"template","value"}*/
Map<String, String> values = [:]
/**
* 是否修改項目下的java源文件
*/
boolean isModifyJava = false
}
- 再將創(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)
}
}
- 在自定義的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)
}
}
- 在遍歷找到要替換的字符串后嘉抓,直接替換即可:
@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)
}
踩過的坑:
- 對于要內(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)
}
- 若要修改在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
}
}
- 要修改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ù)即可修改源文件荐绝。