一盤好書 2021-08-15 13:36:55 阅读数:859
在上一篇,我們已經知道如何配置自己的Transform
。這一篇,我們將在此基礎上,通過一個例子完成面向切面(AOP)編程。
隨著現在項目越來越龐大,越來越多的團隊會采取分模塊的方式進行開發,如果此時需要給不同模塊添加某一方法的埋點,那麼最暴力的方式就是在每個模塊中去添加。但這樣勢必會增加很多工作量,並且需求方如果往後又想增加新的點或者去掉某個點將是一場灾難。
有沒有一種方式,可以讓我們在某個節點統一去完成這類操作呢?答案是肯定的:AOP
。
比較形象的一個比喻:如果把各個業務模塊比作一個一個長方形,放入到一個容器中。程序執行打包過程是從左到右的方向,而切面就像一刀切下去,我們可以在切面進行功能的添加,從而達到每個業務模塊都能進行統一需求的改動。
在上一篇我們已經編寫好FreeCoderTransform
類了,接下來我們需要在其transform
方法中添加相應的處理,以達到統一修改某一類class
文件。
注意:此時我們是已經拿到了所有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
進行處理。
接下來,在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))
}
}
複制代碼
此時如果運行將會得到如下結果:
可以看到,在代碼中我們並沒有添加日志的打印,但是最終還是輸出了日志。
至此,通過從gradle
插件引入到aop
的系列文章也就暫時告一段落,如果文章有些許幫助到你,這就便有了它的價值。創造不易,感謝點贊、分享。
另,如有錯誤,歡迎指出。
版权声明:本文为[一盤好書]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815133652295v.html