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

881 阅读4分钟

上一篇,我们已经知道如何配置自己的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的系列文章也就暂时告一段落,如果文章有些许帮助到你,这就便有了它的价值。

创造不易,感谢点赞、分享。