Gradle插件系列(三)—— 面向AOP

一盤好書 2021-08-15 13:36:55 阅读数:859

本文一共[544]字,预计阅读时长:1分钟~
gradle 插件 系列 面向 aop

上一篇,我們已經知道如何配置自己的Transform。這一篇,我們將在此基礎上,通過一個例子完成面向切面(AOP)編程。

1 什麼是AOP

隨著現在項目越來越龐大,越來越多的團隊會采取分模塊的方式進行開發,如果此時需要給不同模塊添加某一方法的埋點,那麼最暴力的方式就是在每個模塊中去添加。但這樣勢必會增加很多工作量,並且需求方如果往後又想增加新的點或者去掉某個點將是一場灾難。

有沒有一種方式,可以讓我們在某個節點統一去完成這類操作呢?答案是肯定的:AOP

比較形象的一個比喻:如果把各個業務模塊比作一個一個長方形,放入到一個容器中。程序執行打包過程是從左到右的方向,而切面就像一刀切下去,我們可以在切面進行功能的添加,從而達到每個業務模塊都能進行統一需求的改動。

Lark20210813-084058.png

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是生成字節碼的工具類。

看看appMainActivity,並沒有添加任何日志打印的代碼:

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))
}
}
複制代碼

此時如果運行將會得到如下結果:

asm-log日志新增secondactivity.png

可以看到,在代碼中我們並沒有添加日志的打印,但是最終還是輸出了日志。

3 寫在最後

至此,通過從gradle插件引入到aop的系列文章也就暫時告一段落,如果文章有些許幫助到你,這就便有了它的價值。創造不易,感謝點贊、分享。

另,如有錯誤,歡迎指出。

版权声明:本文为[一盤好書]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815133652295v.html