开篇
在前面几篇博文中,我记录了Android AOP编程的一些基础知识,包括Gradle插件的开发、TransformAPI的使用,以及一些操作字节码的工具如AspectJ,Javassist和ASM:
- Android AOP编程(一)——AspectJ基础知识
- Android AOP编程(二)——AspectJ语法&实战
- Android AOP编程(三)——Javassist基础
- Android Gradle插件开发基础
- Android Transform API的使用
- Android AOP编程(四)——ASM基础
本篇将要记录的是将这些知识点串联起来的实战开发,要完成如下几个功能:
- 在Activity的onCreate方法中插入新代码(本篇主要是在每个Activity的onCreate中插入一个Toast)
- 处理点击事件重复触发问题(使用自定义注解处理快速点击时事件重复触发问题)
- 统计某个方法的执行时长(使用自定义注解统计某个方法的执行时长,类似JakeWharton开发的hugo)
- 修复第三方jar包中的错误代码(比如修复某个jar包中的bug,直接修改字节码而不是修改源码后重新打包成jar)
下面请跟着我的步骤一步步完成上面几个功能吧!
开始
在实现上面说的4个功能之前,我们有一些通用的步骤:
- 创建新的Android项目AopDemo
- 在根目录下创建buildSrc目录并编译项目,buildSrc目录下会自动创建一些文件,我们的插件项目会基于buildSrc目录编写
- 在buildSrc目录下创建build.gradle文件并添加如下配置:
apply plugin: 'java-library' apply plugin: 'groovy' apply plugin: 'maven' repositories { google() mavenCentral() mavenLocal() } dependencies { implementation gradleApi() implementation localGroovy() implementation 'com.android.tools.build:gradle:4.2.2' // javassist.jar文件可以在文末的源码中找到 implementation files('libs/javassist.jar') } sourceSets { main { java { srcDir 'src/main/java' } resources { srcDir 'src/main/resources' } } } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 }
- 在buildSrc目录下创建
src/main/java
和src/main/resources
目录 - 在
src/main/java
目录下创建包com.test.plugin
,在src/main/resources
目录下创建META-INF/gradle-plugins
目录,并在该目录中添加文件,名为com.test.plugin.properties
,文件内容为implementation-class=com.test.plugin.MyPlugin
- 在
com.test.plugin
包中加入如下两个类//MyPlugin.java package com.test.plugin; import com.android.build.gradle.BaseExtension; import org.gradle.api.Plugin; import org.gradle.api.Project; public class MyPlugin implements Plugin<Project> { @Override public void apply(Project target) { // 在Plugin中注册自定义的Transform BaseExtension baseExtension = target.getExtensions().findByType(BaseExtension.class); if (baseExtension != null) { baseExtension.registerTransform(new MyTransform()); } } }
//MyTransform.java package com.test.plugin; import com.android.build.api.transform.DirectoryInput; import com.android.build.api.transform.Format; import com.android.build.api.transform.JarInput; import com.android.build.api.transform.QualifiedContent; import com.android.build.api.transform.Transform; import com.android.build.api.transform.TransformException; import com.android.build.api.transform.TransformInput; import com.android.build.api.transform.TransformInvocation; import com.android.build.gradle.internal.pipeline.TransformManager; import com.android.utils.FileUtils; import com.test.plugin.utils.InjectUtil; import org.apache.commons.io.IOUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Enumeration; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; public class MyTransform extends Transform { @Override public String getName() { return "MyCustomTransform"; } @Override public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; } @Override public Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; } @Override public boolean isIncremental() { return false; } @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation); if (!transformInvocation.isIncremental()) { // 非增量编译,则删除之前的所有输出 transformInvocation.getOutputProvider().deleteAll(); } // 拿到所有输入 Collection<TransformInput> inputs = transformInvocation.getInputs(); if (!inputs.isEmpty()) { for (TransformInput input : inputs) { // directoryInputs保存的是存放class文件的所有目录,可以通过方法内打印的log查看具体的目录 Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs(); handleDirInputs(transformInvocation, directoryInputs); // jarInputs保存的是所有依赖的jar包的地址,可以通过方法内打印的log查看具体的jar包路径 Collection<JarInput> jarInputs = input.getJarInputs(); handleJarInputs(transformInvocation, jarInputs); } } } // 处理输入的目录 private void handleDirInputs(TransformInvocation transformInvocation, Collection<DirectoryInput> directoryInputs) { for (DirectoryInput directoryInput : directoryInputs) { String absolutePath = directoryInput.getFile().getAbsolutePath(); // System.out.println(">>>> directory input file path: " + absolutePath); // 处理class文件 InjectUtil.inject(absolutePath); // 获取目标地址 File contentLocation = transformInvocation.getOutputProvider().getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY); // 拷贝目录 try { FileUtils.copyDirectory(directoryInput.getFile(), contentLocation); } catch (IOException e) { e.printStackTrace(); } } } // 处理输入的Jar包 private void handleJarInputs(TransformInvocation transformInvocation, Collection<JarInput> jarInputs) { for (JarInput jarInput : jarInputs) { String absolutePath = jarInput.getFile().getAbsolutePath(); // System.out.println(">>>> jar input file path: " + absolutePath); File contentLocation = transformInvocation.getOutputProvider().getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR); try { // 匹配要修复的jar包 if (absolutePath.endsWith("calculator-0.0.1.jar")) { // 原始的jar包 JarFile jarFile = new JarFile(absolutePath); // 处理后的jar包路径 String tmpJarFilePath = jarInput.getFile().getParent() + File.separator + jarInput.getFile().getName() + "_tmp.jar"; File tmpJarFile = new File(tmpJarFilePath); JarOutputStream jos = new JarOutputStream(new FileOutputStream(tmpJarFile)); // System.out.println("origin jar file path: " + jarInput.getFile().getAbsolutePath()); // System.out.println("tmp jar file path: " + tmpJarFilePath); Enumeration<JarEntry> entries = jarFile.entries(); // 遍历jar包中的文件,找到需要修改的class文件 while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); String name = jarEntry.getName(); jos.putNextEntry(new ZipEntry(name)); InputStream is = jarFile.getInputStream(jarEntry); // 匹配到有问题的class文件 if ("com/bug/calculator/BugCalculator.class".equals(name)) { // 处理有问题的class文件并将新的数据写入到新jar包中 jos.write(InjectUtil.fixJarBug(absolutePath)); } else { // 没有问题的直接写入到新的jar包中 jos.write(IOUtils.toByteArray(is)); } jos.closeEntry(); } // 关闭IO流 jos.close(); jarFile.close(); // 拷贝新的Jar文件 // System.out.println(">>>>>>>>copy to dest: " + contentLocation.getAbsolutePath()); FileUtils.copyFile(tmpJarFile, contentLocation); // 删除临时文件 // System.out.println(">>>>>>>>tmpJarFile: " + tmpJarFile.getAbsolutePath()); tmpJarFile.delete(); } else { FileUtils.copyFile(jarInput.getFile(), contentLocation); } } catch (Exception e) { e.printStackTrace(); } } } }
以上代码主要是创建了一个gradle插件并在其中添加了一个自定义的Transform,这样Android项目在编译过程中,会自动执行我们定义的MyTransform类中的transform方法。
上面的代码中最主要的是MyTransform
类中的transform
方法,其编写格式基本固定如上面的代码所示,通过TransformInvocation
对象拿到所有的class输入,其中又包括目录和jar包,对目录和jar包要单独处理,分别对应上面代码中的handleDirInputs()
handleJarInputs()
方法。
handleDirInputs()
方法主要是通过InjectUtil.inject()
方法完成对class文件的处理。
handleJarInputs()
方法主要是匹配需要处理的jar包,然后通过Java提供的一些操作Jar包的API来读取Jar包内需要修改的class文件,再将jar包拷贝到指定的目录下。
InjectUtil
类在com.test.plugin.utils
包下,它主要完成对class文件的一些操作。
在开篇中我们需要完成的四个功能,主要逻辑都集中在InjectUtil
类中,下面就看看每个功能是如何实现的吧!
在方法中插入新代码
在本例子中,我们将通过一个gradle插件,在代码编译期,向所有Activity的onCreate方法中插入一句Toast代码,弹出当前Activity的名称。下面直接上源码:
/**
* 使用Javassist操作Class字节码,在所有Activity的onCreate方法中插入Toast
* @param filePath class文件路径
*/
private static void addToast(String filePath) {
try {
CtClass ctClass = classPool.getCtClass(getFullClassName(filePath));
if (ctClass.isFrozen()) {
ctClass.defrost();
}
// 获取Activity中的onCreate方法
CtMethod onCreate = ctClass.getDeclaredMethod("onCreate");
// 要插入的代码
// getSimpleClassName()方法返回的是类名如"MainActivity"
String insertCode = String.format(Locale.getDefault(),
"Toast.makeText(this, \"%s\", Toast.LENGTH_SHORT).show();",
getSimpleClassName(filePath));
// 在onCreate方法开始处插入上面的Toast代码
onCreate.insertBefore(insertCode);
// 写回原来的目录下,覆盖原来的class文件
// mainModuleClassPath是Android项目主module存放class的路径,一般是"<Android项目根目录>/app/build/intermediates/javac/debug/classes/"
ctClass.writeFile(mainModuleClassPath);
ctClass.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
在Activity的onCreate方法中插入Toast的处理代码非常简短,以上代码仅仅调用了CtMethod的insertBefore方法即可在某个方法开始处插入新代码。
要验证以上代码是否正常工作也非常简单,直接打开编译后的class文件即可,或者运行项目到手机或模拟器,可以看到每个Activity在打开时都会弹出toast提示当前的Activity名称。
处理点击事件重复触发
在Android AOP编程(二)——AspectJ语法&实战这篇文章中,我记录过使用AspectJ处理点击事件重复触发的问题,也是使用自定义注解,结合AspectJ匹配注解来完成的。本篇将使用Javassist库完成同样的功能。
首先需要创建一个java-library,这里命名为annotation,在annotation的根目录下编辑build.gradle文件,内容如下:
apply plugin: 'java-library'
apply plugin: 'maven'
repositories {
mavenLocal()
}
dependencies {
implementation gradleApi()
}
//publish to local directory
group "com.example.annotation"
version "1.0.0"
uploadArchives{ //当前项目可以发布到本地文件夹中
repositories {
mavenDeployer {
repository(url: uri('./repo')) //定义本地maven仓库的地址
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
之所以上面要配置uploadArchives,是因为在buildSrc中无法直接引用annotation库,需要将annotation库上传到本地maven,再在buildSrc中依赖它。
annotation库中的代码很简单,在src/main/java
目录下创建一个com.example.annotation
包并在其中添加ClickOnce
这个注解,代码如下:
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ClickOnce {
}
然后我们将annotation这个library上传到本地maven,上传的方法为打开AndroidStudio右侧的gradle视图,在其中找到uploadArchives
菜单双击即可,如下图所示:
执行uploadArchives
成功后,会在annotation目录下生成repo目录,在其中可以看到上传的library。
为了让buildSrc插件项目可以引用我们创建的annotation,需要在buildSrc目录的build.gradle文件中,加入如下配置:
repositories {
...
maven {
// 这个地址填
url '/Users/xxx/IdeaProjects/AopDemo/annotation/repo/'
}
}
dependencies {
...
implementation 'com.example.annotation:annotation:1.0.0'
}
我们需要实现的功能是,使用@ClickOnce
注解的方法,在600ms内只执行一次,这样就能保证在快速点击时,不重复触发点击事件,在MainActivity中我们加入测试代码如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
textView.setOnClickListener(this);
}
@Override
@ClickOnce
public void onClick(View v) {
startActivity(new Intent(this, OtherActivity.class));
}
}
然后我们使用InjectUtil
工具类来操作class文件,找到被@ClickOnce
注解的方法,主要逻辑如下:
/**
* 处理被@ClickOnce注解的方法,确保这个方法在600ms内只执行一次
* @param filePath
*/
private static void ensureClickOnce(String filePath) {
try {
CtClass ctClass = classPool.getCtClass(getFullClassName(filePath));
if (ctClass.isFrozen()) {
ctClass.defrost();
}
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
// 类中是否有被@ClickOnce注解的方法
boolean clzHasClickAnnotation = false;
for (CtMethod m : declaredMethods) {
if (m.hasAnnotation(ClickOnce.class)) {
clzHasClickAnnotation = true;
break;
}
}
// 如果类中有被@ClickOnce注解的方法,则创建新方法,并在所有被@ClickOnce注解的方法开始处插入检查代码
if (clzHasClickAnnotation) {
// 创建新方法并添加到类中
createClickOnceMethod(ctClass);
// 重新读取并加载class,因为上一步中写入了新的方法
ctClass = classPool.get(getFullClassName(filePath));
if (ctClass.isFrozen()) ctClass.defrost();
declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod m : declaredMethods) {
if (m.hasAnnotation(ClickOnce.class)) {
System.out.println("found @ClickOnce method: " + m.getName());
// 在当前被@ClickOnce注解的方法体前面执行上面创建的新方法
m.insertBefore("if (!is$Click$Valid()) {" +
"Log.d(\"ClickOnce\", \"Click too fast, ignore this click...\");" +
"return;" +
"}");
}
}
}
ctClass.writeFile(mainModuleClassPath);
ctClass.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 构造一个新的方法,方法体为:
* private boolean is$Click$Valid() {
* long time = System.currentTimeMills();
* boolean canClick = (time - lastClickTime > 600L);
* lastClickTime = time;
* return canClick;
* }
* 如果点击有效,上面的方法返回true,否则返回false
* @param clz
* @throws Exception
*/
private static void createClickOnceMethod(CtClass clz) throws Exception {
// 先给类新增一个成员变量,用于记录上次点击的时间
CtField lastClickField = new CtField(CtClass.longType, "lastClickTime", clz);
lastClickField.setModifiers(Modifier.PRIVATE | Modifier.STATIC);
clz.addField(lastClickField);
// 新的方法体
String body = "{" +
"long time = System.currentTimeMillis();" +
"boolean canClick = (time - lastClickTime > 600);" +
"lastClickTime = time;" +
"return canClick;" +
"}";
// 创建新方法并添加到类中
CtMethod newMethod = CtNewMethod.make(Modifier.PRIVATE, CtClass.booleanType, "is$Click$Valid",
new CtClass[]{}, new CtClass[]{}, body, clz);
clz.addMethod(newMethod);
clz.writeFile(mainModuleClassPath);
}
以上代码注释得比较详细了,其主要功能是判断某个类中是否有被@ClickOnce
注解的方法,如果有,就在当前类中添加一个is$Click$Valid()
方法,用于判断当前点击是否有效,再修改被@ClickOnce
注解的方法体,在其中调用is$Click$Valid()
方法。经过反编译后可以看到MainActivity的代码长下面这样:
public class MainActivity extends AppCompatActivity implements OnClickListener {
private TextView textView;
private static long lastClickTime; // 这是注入的字段
public MainActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
Toast.makeText(this, "MainActivity", 0).show();
super.onCreate(savedInstanceState);
this.setContentView(2131427356);
this.textView = (TextView)this.findViewById(2131230959);
this.textView.setOnClickListener(this);
}
@ClickOnce
public void onClick(View v) {
// 调用注入的方法
if (!this.is$Click$Valid()) {
Log.d("ClickOnce", "Click too fast, ignore this click...");
} else {
this.startActivity(new Intent(this, OtherActivity.class));
}
}
// 这是注入的新方法
private boolean is$Click$Valid() {
long var1 = System.currentTimeMillis();
boolean var3 = var1 - lastClickTime > (long)600;
lastClickTime = var1;
return var3;
}
}
可以看到新的class类中注入了我们的新代码,并完成了600ms的判断,确保使用@ClickOnce
注解的方法在600ms内只执行一次。
统计某个方法执行时长
本例子会实现这样一个功能:使用@CountTime
注解某个方法,会自动统计这个方法执行消耗的时间并通过log输出。
首先还是在annotation中定义一个注解:
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface CountTime {
}
别忘了要重新发布一下annotation到本地仓库,可以不用修改annotation library的版本号。
然后新建一个OtherActivity,在其中插入测试代码如下:
package com.example.aopdemo;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.annotation.CountTime;
public class OtherActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_other);
findViewById(R.id.textView).setOnClickListener(v -> {
doSomething();
});
}
@CountTime
private void doSomething() {
Toast.makeText(this, "fb(30) = " + fb(30), Toast.LENGTH_SHORT).show();
}
// 计算非波拉契数列第n项的值
private int fb(int n) {
if (n == 0 || n == 1) {
return n;
}
return fb(n - 1) + fb(n - 2);
}
}
测试代码很简单,就是点击页面上的文本,触发点击事件计算非波拉契数列第n项的值,并Toast弹出这个值,我们在doSomething方法上加了自定义注解@CountTime
希望统计这个方法执行消耗多长时间。
下面是InjectUtil
类对class字节码的处理方式:
/**
* 通过注入字节码,计算被@CountTime注解的方法执行消耗的时间
* @param filePath class文件路径
*/
private static void calculateMethodTime(String filePath) {
try {
CtClass ctClass = classPool.getCtClass(getFullClassName(filePath));
if (ctClass.isFrozen()) {
ctClass.defrost();
}
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod m : declaredMethods) {
if (m.hasAnnotation(CountTime.class)) {
// 定义long类型的局部变量start
m.addLocalVariable("start", CtClass.longType);
// 在方法体开始处插入代码记录时间
m.insertBefore("start = System.currentTimeMillis();");
// 定义long类型的局部变量end
m.addLocalVariable("end", CtClass.longType);
// 在方法体结束时插入代码记录时间
m.insertAfter("end = System.currentTimeMillis();");
// 获取方法名
String methodName = m.getName();
String logMsg = "execute " + methodName + " use time: ";
m.insertAfter(String.format(Locale.getDefault(), "Log.d(\"%s\", \"%s\" + (end - start));", "CountTime", logMsg));
}
}
ctClass.writeFile(mainModuleClassPath);
ctClass.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
处理的方式很简单,还是匹配到被@CountTime
注解的方法,然后在方法体中原有的代码前后分别插入新的代码,用于记录方法开始执行和结束执行的时间,并通过Log输出,编译项目后可以看到OtherActivity被修改成如下样子:
public class OtherActivity extends AppCompatActivity {
public OtherActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
Toast.makeText(this, "OtherActivity", 0).show();
super.onCreate(savedInstanceState);
this.setContentView(2131427357);
this.findViewById(2131230959).setOnClickListener((v) -> {
this.doSomething();
});
}
@CountTime
private void doSomething() {
long start = System.currentTimeMillis();
Toast.makeText(this, "fb(30) = " + this.fb(30), 0).show();
Object var6 = null;
long end = System.currentTimeMillis();
Object var8 = null;
Log.d("CountTime", "execute doSomething use time: " + (end - start));
}
private int fb(int n) {
return n != 0 && n != 1 ? this.fb(n - 1) + this.fb(n - 2) : n;
}
}
对于这个功能,需要注意的地方有如下几点:
- 插入代码用于记录方法开始执行和结束执行的时间,需要使用CtMethod的
addLocalVariable
方法定义局部变量,然后再插入代码,否则,在方法结束时计算两个时间差时,无法得知之前保存的时间值存在哪个变量里,如果你直接用m.insertBefore("long start = System.currentTimeMillis();");
这种方式插入代码,实际上保存时间的变量并不叫start
,可以自己尝试一下,编译过程中会报错找不到变量 @CountTime
需要注解在没有返回值的方法上,因为以上代码是在方法体执行开始处和末尾处插入代码,如果方法有返回值,肯定是不对的,比如将@CountTime
注解在上面的fb()
方法上,肯定是有问题的。
修复jar包中的bug
假设我们的项目中引用了一个第三方的jar包,这个jar包中有一些错误需要修复,如果我们通过解压修改源码再打包的方式,可以做到但是比较麻烦,一种比较简单的方式是直接修改jar包中的字节码,从而达到目的。
本例中我们会引入一个有问题的jar包,这个jar包中提供了如下方法:
public class BugCalculator {
public BugCalculator() {
}
// add方法返回的是整型,精度会丢失
public static int add(float a, float b) {
System.out.println("result is " + (a + b));
return (int)(a + b);
}
}
我们的目的是去掉上面add
方法中打印的日志,并且将方法返回值改为float而不是int,下面就看看具体的操作方法吧:
/**
* 修复jar包中的bug
* @param jarFilePath jar包路径
*/
public static byte[] fixJarBug(String jarFilePath) throws Exception {
classPool.appendClassPath(jarFilePath);
CtClass ctClass = classPool.getCtClass("com.bug.calculator.BugCalculator");
if (ctClass.isFrozen()) {
ctClass.defrost();
}
// 创建一个新的方法fAdd,接收两个float参数,返回一个float值
CtMethod newMethod = CtNewMethod.make("public static float fAdd(float a, float b) { return a + b; }", ctClass);
ctClass.addMethod(newMethod);
byte[] bytes = ctClass.toBytecode();
ctClass.detach();
return bytes;
}
Javassist提供了删除某个类中已存在的方法的API,但是没有提供API修改某个类中已存在的方法的返回值,所以我们直接在com.bug.calculator.BugCalculator
类中添加了一个新方法fAdd
(直接删除某个类中已有的方法也不太好,因为不确定有没有其他地方调用了这个方法),然后编写测试代码如下:
try {
// 通过反射调用jar包中新增的方法
// 注意不能直接显式调用fAdd方法,否则编译不通过
Class<?> clz = BugCalculator.class;
Method fAdd = clz.getDeclaredMethod("fAdd", float.class, float.class);
Object result = fAdd.invoke(clz, 1.5f, 2.2f);
Log.d("BugCalculator", "fAdd(1.5 + 2.2) = " + result);
// 调用原来的add方法,返回的是丢失了精度的值
Log.d("BugCalculator", "add(1.5 + 2.2) = " + BugCalculator.add(1.5f, 2.2f));
} catch (Exception e) {
e.printStackTrace();
}
将代码跑到真机上,可以看到控制台输出如下:
可以看到我们在类中添加的方法成功执行。
关于修改Jar包中的bug,这里有如下几点注意事项需要说明:
- 针对jar包的修改,需要先在自定义Transform中拿到输入的jar包并匹配到需要修改的jar包,再通过
JarFile
JarOutputStream
JarEntry
等API来获取并修改jar包中有问题的class文件 - 在类中添加了新的方法后,不能直接在代码中显式调用,这样编译都不会通过,需要使用反射来调用新增的方法。
总结
本篇通过4个小例子记录了使用Gradle插件+TransformAPI+Javassist处理字节码的一些方式,实际上本篇仅仅使用了Javassist,但是某些功能也可以使用AspectJ和ASM去处理。