在上一篇,我们已经知道如何配置自己的Transform。这一篇,我们将在此基础上,通过一个例子完成面向切面(AOP)编程。
1 什么是AOP
随着现在项目越来越庞大,越来越多的团队会采取分模块的方式进行开发,如果此时需要给不同模块添加某一方法的埋点,那么最暴力的方式就是在每个模块中去添加。但这样势必会增加很多工作量,并且需求方如果往后又想增加新的点或者去掉某个点将是一场灾难。
有没有一种方式,可以让我们在某个节点统一去完成这类操作呢?答案是肯定的:AOP。
比较形象的一个比喻:如果把各个业务模块比作一个一个长方形,放入到一个容器中。程序执行打包过程是从左到右的方向,而切面就像一刀切下去,我们可以在切面进行功能的添加,从而达到每个业务模块都能进行统一需求的改动。
2 AOP实战
在上一篇我们已经编写好FreeCoderTransform类了,接下来我们需要在其transform方法中添加相应的处理,以达到统一修改某一类class文件。
注意:此时我们是已经拿到了所有class文件,然后利用字节码编辑工具,插入相应代码即可完成某一类型的操作。
2.1 编译后class类的处理
// FreeCoderTransform.java
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
inputs.forEach(transformInput -> {
transformInput.getJarInputs().forEach(jarInput -> {
File dest = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
try {
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
});
transformInput.getDirectoryInputs().forEach(directoryInput -> {
File dir = directoryInput.getFile();
try {
// 处理class文件
traverse(dir);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 1
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
});
}
这一段代码相比上一篇,增加了一个traverse(dir)方法来处理class文件。
- 1 处标记:把复制工作放入
finally中,防止处理字节码过程中出错,而导致不执行复制工作,既而出现classnotfound的错误。
接下来看看traverse方法
// FreeCoderTransform.java
private void traverse(File file) throws IOException {
if (file == null || !file.exists()) return;
// 1
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
traverse(f);
}
} else {
System.out.println("find class: " + file.getName());
if (!file.getName().endsWith(".class")) return;
// 2
ClassReader classReader = new ClassReader(AppFileUtils.fileToBytes(file));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new FreeCoderClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
byte[] bytes = classWriter.toByteArray();
FileOutputStream outputStream = new FileOutputStream(file.getPath());
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
这里我们暂时不用管像ClassReader等等类是怎么来的,下面将要介绍,因为到这里为止,跟Gradle插件开发的流程就已经结束了,后面都是使用ASM进行字节码的修改工作。
这段代码的主要工作,就是遍历需要修改的class文件,进行逐一修改。
-
1 处标记:利用递归便利出所有
file文件夹中的所有文件。 -
2 处标记:把
file转化成byte数组,扔给ASM进行处理。
2.2 引入ASM
接下来,在build.gradle中添加引用
implementation("org.ow2.asm:asm:7.1")
implementation("org.ow2.asm:asm-commons:7.1")
自定义ClassVisitor类,注意引入的所有类都是org.objectweb.asm这个包里的内容。
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class FreeCoderClassVisitor extends ClassVisitor {
private String mClassName;
private String mSuperName;
public FreeCoderClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
System.out.println("visit name: " + name + " superName: " + superName);
mClassName = name;
mSuperName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("visit name: " + name + " signature: " + signature + " access: " + access);
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
// 1
if (mSuperName.equals("androidx/appcompat/app/AppCompatActivity")) {
// 2
if (name.startsWith("onCreate")) {
return new FreeCoderMethodVisitor(mv, mClassName, name);
}
}
return mv;
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
首先ClassVisitor类是负责访问.class文件中的各个元素,它可以解析字节码中的方法、变量,当遇到这些标记时,它会自动调用内部相应的visitMethod或者visitField方法。
-
1 处标记:当遇到父类名为
androidx/appcompat/app/AppCompatActivity时,进入判断; -
2 处标记:当遇到
onCreate方法时,利用MethodVisitor的具体子类来增加相应的字节码。
那么接下来我们看看MethodVisitor的子类应该如何编写
public class FreeCoderMethodVisitor extends MethodVisitor {
private String mClassName;
private String mMethodName;
public FreeCoderMethodVisitor(int api) {
super(api);
}
public FreeCoderMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
super(Opcodes.ASM5, methodVisitor);
mClassName = className;
mMethodName = methodName;
}
@Override
public void visitCode() {
super.visitCode();
// 1
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(mClassName + "---->" + mMethodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log"
, "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
}
- 1 处标记:通过
FreeCoderClassVisitor类传递过来的mv(MethodVisitor)对象,添加相应的字节码。
这里需要读者对字节码有一定的了解,如果对其不是很了解,我们可以尝试用一些字节码生成工具获得自己想要的字节码,这里就不做展开了。
最后,关于ClassReader、ClassWriter这两个类,它们的作用:ClassReader负责解析class文件中的字节码,而ClassWriter是生成字节码的工具类。
看看app的MainActivity,并没有添加任何日志打印的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun helloClick(view: View) {
startActivity(Intent(this, SecondActivity::class.java))
}
}
此时如果运行将会得到如下结果:
可以看到,在代码中我们并没有添加日志的打印,但是最终还是输出了日志。
3 写在最后
至此,通过从gradle插件引入到aop的系列文章也就暂时告一段落,如果文章有些许帮助到你,这就便有了它的价值。
创造不易,感谢点赞、分享。