In the previous article, we learned how to configure our own Transform. In this article, we’ll build on that, using an example to complete aspect Oriented (AOP) programming.

1 What is AOP

As projects get bigger, more and more teams develop in modules, and if you need to add a method to each module, the most violent way is to add it to each module. But that would add a lot of work, and it would be a disaster if the demand side wanted to add a new point or remove a point later.

Is there a way that we can do this on a single node? The answer is yes: AOP.

A more vivid metaphor: if each business module is compared to a rectangle, put into a container. The packaging process of program execution is from left to right, and the sections are cut like one. We can add functions in the sections, so that each business module can make unified changes.

2 the AOP of actual combat

Now that we’ve written the FreeCoderTransform class in the last article, we need to add processing to its transform method to uniformly modify a class file.

Note: At this point we have all the class files, and we can use the bytecode editing tool to insert the corresponding code to complete a certain type of operation.

2.1 Processing of class after compilation

// 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 {
                // Process the class file
                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(); }}}); }); }Copy the code

This code adds a traverse(dir) method to handle class files.

Mark 1: Put the copy work in finally to prevent errors in the bytecode processing, which may result ina ClassNotfound error when the copy work is not performed.

Let’s take a look at traverse method

// 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 = newFileOutputStream(file.getPath()); outputStream.write(bytes); outputStream.flush(); outputStream.close(); }}Copy the code

We don’t have to worry about how classes like ClassReader come into being, but we’ll see what happens next, because at this point, the process of developing Gradle plug-ins is over, and we’ll use ASM to modify the bytecode.

The main job of this code is to iterate through the class files that need to be modified, making each modification.

1 place mark: use recursion to facilitate all files in all file folder.

2: convert the file into a byte array and throw it to ASM for processing.

2.2 introduced the ASM

Next, add references to build.gradle

implementation("Org. Ow2. Asm: asm: 7.1")

implementation("Org. Ow2. Asm: asm - Commons: 7.1")
Copy the code

Customize the ClassVisitor class, noting that all classes introduced are contents of the org.objectWeb.asm package.

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 newFreeCoderMethodVisitor(mv, mClassName, name); }}return mv;

    }

    @Override
    public void visitEnd(a) {
        super.visitEnd(); }}Copy the code

First, the ClassVisitor class is responsible for accessing the elements in the.class file. It parses the methods and variables in the bytecode, and automatically calls the corresponding visitMethod or visitField method internally when these tags are encountered.

Marks: 1 when the parent class called androidx appcompat/app/AppCompatActivity, enter judgment;

2: Use a concrete subclass of MethodVisitor to add the bytecode when the onCreate method is encountered.

So let’s look at how do we write a subclass of 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(a) {
        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); }}Copy the code

1: Add the bytecode to the MV (MethodVisitor) object passed through the FreeCoderClassVisitor class.

Some knowledge of bytecode is required here, and if not, we can try to use some bytecode generation tools to get the bytecode we want, which will not be expanded here.

Finally, the ClassReader and ClassWriter classes do what they do: ClassReader is responsible for parsing the bytecode in the class file, and ClassWriter is the utility class that generates the bytecode.

Take a look at the app’s MainActivity, which doesn’t add any code for log printing:

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))
    }
}
Copy the code

If run at this point, you will get the following result:

As you can see, we didn’t add the print of the log in the code, but the log was printed anyway.

I’ll put the 3 at the end

This concludes the series of articles introducing AOP from the Gradle plugin, and if it helped you in any way, it’s worth it. It’s not easy to create, thanks for sharing.

In addition, if there is any mistake, welcome to point out.