Código de bytes y ASM: implementación de AOP

Un ejemplo muy simple de AOP está involucrado en el artículo anterior. Como miembro de uno de los marcos comúnmente utilizados en JAVA, AOP estará expuesto al concepto de AOP ya sea que esté involucrado en el back-end o en Android. Como usuarios, ¿Tenemos alguna idea?¿Cómo se implementa la tecnología como AOP? Si nunca lo ha pensado, este artículo brindará una implementación detallada de un conjunto de bibliotecas AOP, incluidas las implementaciones comunes de AOP, como pre, post, surround, salida normal y salida anormal. Comencemos con la consideración más simple, cómo se implementan los requisitos previos, y tómese un tiempo para pensar primero. Creo que la idea más fácil y rápida es registrar la clase actual y la información del método, lo que se puede lograr invocando las llamadas de método relevantes a través de ASM antes de la ejecución del método que se debe conectar. Sustituya otras funciones por pensar de acuerdo con el mismo método, no es difícil encontrar que este método parece ser bastante adecuado, pero el entorno no parece funcionar. Debido a la particularidad del entorno, ¿necesitamos encontrar un nuevo ¿camino? Darle un poco de pensamiento de implementación, y luego regresar y dar la respuesta. La solución a la apelación es implementar la lógica de corte a través de la invocación del método, ¿podemos registrar el código de operación y el operando correspondiente al método e ingresarlo en el estado real apropiado? La respuesta es, por supuesto, sí. Necesitamos recopilar todos los códigos de operación y operandos para la posición previa, la posición posterior, la salida normal y la salida anormal, pero el ajuste parece ser diferente, por lo que tanto esta solución como la solución anterior parecen necesitar resolver el problema del ajuste. No hablemos de las soluciones relevantes aquí, comencemos directamente a implementar otras funciones, y finalmente daremos la respuesta cuando implementemos el wraparound. Por supuesto, es posible que ya tenga la respuesta si es inteligente.

Definición de anotación

En primer lugar, debemos definir las anotaciones relacionadas con pre, post, surround, salida normal y salida anormal, y la anotación debe llevar los datos de la clase y el método que debe coincidir.

parte delantera

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Before {
    String clzName();
    String methodDesc();
}
复制代码

trasero

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface After {
    String clzName();
    String methodDesc();
}
复制代码

rodear

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Around {
    String clzName();
    String methodDesc();
}
复制代码

Salir normalmente

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface AfterReturning {
    String clzName();
    String methodDesc();
}
复制代码

anormal

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
    String clzName();
    String methodDesc();
}
复制代码

Todas las anotaciones anteriores contienen clzNamey methodDescse utilizan para hacer coincidir clases y métodos. Aquí nos basamos en la descripción del tipo básico, que clzNamedebe coincidir con el nombre completo de la clase y methodDesccon el nombre del método y la firma del método. Además, necesitamos una clase de etiqueta de anotación que nos facilite recopilar las anotaciones anteriores.

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Aop{}
复制代码

通过以上的注解定义已经形成AOP库的核心类了,在使用上我们需要在类头通过@Aop标记,内部方法通过@Before,@After,@Around,@AfterReturning,@AfterThrowing标记,例如:

@Aop
public class TestAOP {
    @Around(clzName = "com/river/easyaop/Test", methodDesc = "say()")
    public void say() {
        System.out.println("hello2");
    }

    @Before(clzName = "com/river/easyaop/Test", methodDesc = "hello()")
    public void hello() {
        System.out.println("before");
    }

    @After(clzName = "com/river/easyaop/Test", methodDesc = "run()")
    public void run() {
        System.out.println("after");
    }

    @AfterReturning(clzName = "com/river/easyaop/Test", methodDesc = "ret()")
    public void ret() {
        System.out.println("ret");
    }

    @AfterThrowing(clzName = "com/river/easyaop/Test", methodDesc = "ret()")
    public void ret2() {
        System.out.println("error");
    }

    @Before(clzName = "com/river/easyaop/Test", methodDesc = "callback()")
    public void callback() {
        System.out.println("callback before");
    }

    @After(clzName = "com/river/easyaop/Test", methodDesc = "onCreate()")
    public void onCreate() {
        System.out.println("onCreate");
    }
}
复制代码

通过观察我们发现@Around好像哪里不对,环绕注解我们需要在原函数的前后都炽入代码,而现在这个样子并不能控制对原函数的调用。由此我们新增一个接口表示对原函数的调用

public interface Method {
    void invoke();
}
复制代码

通过该接口进一步修改上面被@Around标记的方法

@Aop
public class TestAOP {
    @Around(clzName = "com/river/easyaop/Test", methodDesc = "say()")
    public void say(Method method) {
        System.out.println("hello1");
        method.invoke();
        System.out.println("hello2");
    }
}
复制代码

目前为止我们已经写好了给使用者的相关代码了。我们再次需要思考,我们应该如何处理这些注解并且如何实现AOP呢?这里呢我给出三个步骤,第一收集注解相关的信息,第二应对收集的注解信息去匹配需要处理的类的方法,第三就是将匹配成功的方法进行处理,将需要炽入的代码在合适的时机进行炽入。有了三个步骤那么我们就一一去实现它。

收集注解

首先我们需要对类进行分析,该类是否被@Aop标记,如果标记我们需要进一步去分析类的方法,将类的方法逐一分析方法是否被我们的注解标记,如果标记那么我们需要将注解信息收集。这里我们关心的数据为切入类的全限定名、切入方法的方法名及方法描述、待炽入的操作码及操作数和切入注解类

@Data
public class MethodPointCut {
    //切入类全限定名
    private String clzName;
    //切入方法名及描述
    private String methodDesc;
    //前置炽入操作码及操作数
    private List<Insn> beforeInstructions;
    //后置炽入操作码及操作数
    private List<Insn> afterInstructions;
    //注解类
    private Class<?> annoClass;
}
复制代码

这个类将是我们这个AOP库的核心类,维护这核心数据,而这个核心类中最为重要的便是携带操作码及操作数的两个集合beforeInstructionsafterInstructions,这里需要两个集合是因为考虑到了环绕注解需要我们分别存储方法执行前及执行后的操作码及操作数。

public abstract class Insn {
	private int opcode;
    
    public Insn(int opcode) {
    	this.opcode = opcode;
    }
    
    public int getOpcode() {
    	return opcode;
    }
    
    public abstract void accept(MethodVisitor methodVisitor);
}
复制代码

Insn抽象类表示不同的操作码,而不同的操作码有不同的子类具体实现细节。accept方法用于将该操作码及操作数炽入到对应的方法体内。介绍完我们收集的存储结构后我们正式开始收集相关的代码吧。

public class CollectAnnoClassVisitor extends ClassVisitor {
    private String clzName;
    private boolean needReduce;
    private ArrayList<MethodPointCut> methodPointCuts = new ArrayList<>();

    public CollectAnnoClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM6, 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);
        clzName = name;
    }


    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        needReduce = descriptor.equals(Type.getDescriptor(Aop.class));
        return super.visitAnnotation(descriptor, visible);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (needReduce) {
            mv = new InstructionsCollectMethodNode(this, access, name, descriptor, signature, exceptions);
        }
        return mv;
    }

    public ArrayList<MethodPointCut> getMethodPointCuts() {
        return methodPointCuts;
    }

    public void setMethodPointCuts(ArrayList<MethodPointCut> methodPointCuts) {
        this.methodPointCuts = methodPointCuts;
    }
}
复制代码

通过visitAnnotation方法我们判断注解的类型描述是否和Aop相同,相同代表这个类被@Aop标记,那么我们需要对方法进行进一步的处理。这里的代码不难发现有一处BUG,就是如果我们的类被多个注解标记按目前的写法只要不是最后访问的注解为@Aop就会出现Bug,所以如果需要严谨些你可以自行修改。 对于方法操作码操作数的收集是核心逻辑,我们知道ASM通过MethodVisitor的各种visitXXX方法描述了不同操作码及操作数。我们便可以依据此来实现不同的Insn实现类来收集操作码操作数。比如方法MethodVisitor#visitVarInsn用于访问方法局部变量表,我们根据此方法抽象出对应的访问局部变量表的VarInsn类用于存储同样的功能,在ASM调用该方法时我们只需创建一个VarInsn的实例并添加进集合。

public class VarInsn extends Insn {
    private int var;

    public VarInsn(int opcode, int var) {
        super(opcode);
        this.var = var;
    }

    @Override
    public void accept(MethodVisitor methodVisitor) {
        methodVisitor.visitVarInsn(getOpcode(), var);
    }
}
复制代码

MethodVisitor#visitXX的其他方法如法炮制创建对应的XXInsn。幸运的是ASM框架提供了类似的功能,无需我们一个一个去实现。然而更令人惊喜的是收集操作数及操作码的功能我们也可以通过ASM提供的类。惊不惊喜?意不意外?这里我们就直接通过使用ASM提供的类来实现我们的功能了。MethodNode是ASM中树API的实现的一员,树API是ASM基于ASM核心API上层封装的一层。读到这里建议打开IDE阅读下MethodNode的源码,源码不多,逻辑大致类似。通过MethodNode我们可以轻松的获取我们需要的操作码及操作数。通过阅读源码我们发现改类仅仅提供了方法体所有的操作码及操作数,那么好像不加特殊的处理我们好像实现不了相应的环绕逻辑呀!遇到问题我们不妨再先思考下应如何处理呢?聪明的你不知是否发现我们加入的Method#invoke这时就是这个问题的钥匙,在ASM访问方法调用的时候我们通过拦截判断是否为调用Method#invoke,以这个节点为时间点是不是就区分出来了相应的前置和后置操作码操作数了呀?答案无疑是肯定的。

public class InstructionsCollectMethodNode extends MethodNode {
    private CollectAnnoClassVisitor cv;
    private boolean isMatchAnno = false;
    private MethodPointCut methodPointCut = new MethodPointCut();

    public InstructionsCollectMethodNode(CollectAnnoClassVisitor cv, int access, String name, String descriptor, String signature, String[] exceptions) {
        super(Opcodes.ASM6, access, name, descriptor, signature, exceptions);
        this.cv = cv;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        isMatchAnno = Util.matcherAnno(descriptor);

        AnnotationVisitor av = super.visitAnnotation(descriptor, visible);

        if (isMatchAnno) {
            methodPointCut.setAnnoClass(Util.matcherAnnoClass(descriptor));
            av = new AnnotationVisitor(Opcodes.ASM6, av) {
                @Override
                public void visit(String name, Object value) {
                    super.visit(name, value);
                    try {
                        name = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
                        java.lang.reflect.Method setField = methodPointCut.getClass().getDeclaredMethod(name, String.class);
                        setField.invoke(methodPointCut, value);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            };
        }

        return av;
    }

    @Override
    public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) {
        if (isMatchAnno && opcodeAndSource == Opcodes.INVOKEINTERFACE && owner.equals(Type.getInternalName(Method.class)) && name.equals("invoke") && descriptor.equals("()V") && isInterface) {
            instructions.remove(instructions.getLast());
            InsnList insnList = new InsnList();
            insnList.add(instructions);
            methodPointCut.setBeforeInstructions(insnList);
            instructions.clear();
            return;
        }
        super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
    }


    @Override
    public void visitEnd() {
        super.visitEnd();
        if (isMatchAnno) {
            InsnList insnList = new InsnList();
            instructions.remove(instructions.getLast());
            insnList.add(instructions);

            if (methodPointCut.getAnnoClass() == Before.class) {
                methodPointCut.setBeforeInstructions(insnList);
            } else {
                methodPointCut.setAfterInstructions(insnList);
            }

            cv.getMethodPointCuts().add(methodPointCut);
        }
    }
}
复制代码

现在我们已经完成了AOP三部曲中的第一步,通过扫描依次收集后我们现在手里就有了所有的数据

匹配方法

在我们掌握到数据后我们需要再次开启扫描去将方法和收集到的信息依次匹配,如果匹配成功我们再次通过MethodVisitor将方法进一步处理。

public class ImplantCodeClassVisitor extends ClassVisitor {
    private List<MethodPointCut> methodPointCuts;
    private String clzName;

    public ImplantCodeClassVisitor(ClassVisitor classVisitor, List<MethodPointCut> methodPointCuts) {
        super(Opcodes.ASM6, classVisitor);
        this.methodPointCuts = methodPointCuts;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        clzName = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        List<MethodPointCut> methodPointCuts = Util.matcherMethodPointCuts(this.methodPointCuts, clzName, name, descriptor);

        if (mv != null && !methodPointCuts.isEmpty()) {
            mv = new ImplantCodeMethodVisitor(mv, access, name, descriptor, methodPointCuts);
        }
        return mv;
    }
}
复制代码

核心的逻辑就是Util#matcherMethodPointCuts方法将收集的数据和现在访问的方法进行对比过滤出符合条件的数据,如果数据不为空意味着该方法需要进一步处理。匹配方法的复杂度意味着AOP库的通用性,所以作为例子我们这里直接比较类全限定名、方法名、方法签名是否相同作为判断条件。

炽入代码

炽入代码就非常简单了,我们通过将收集的数据在合适时机炽入就好了。

public class ImplantCodeMethodVisitor extends AdviceAdapter {
    private List<MethodPointCut> methodPointCuts;
    private Label handler;

    protected ImplantCodeMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor, List<MethodPointCut> methodPointCuts) {
        super(Opcodes.ASM6, methodVisitor, access, name, descriptor);
        this.methodPointCuts = methodPointCuts;
    }

    @Override
    protected void onMethodEnter() {
        for (MethodPointCut methodPointCut : methodPointCuts) {
            implantBeforeCode(methodPointCut);
        }
        super.onMethodEnter();
    }

    @Override
    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
        super.visitTryCatchBlock(start, end, handler, type);
        this.handler = handler;
    }

    @Override
    public void visitLabel(Label label) {
        super.visitLabel(label);
        if (label == handler) {
            for (MethodPointCut methodPointCut : methodPointCuts) {
                if (methodPointCut.getAnnoClass() == AfterThrowing.class) {
                    implantAfterCode(methodPointCut);
                }
            }
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        for (MethodPointCut methodPointCut : methodPointCuts) {
            if (methodPointCut.getAnnoClass() != AfterThrowing.class) {
                implantAfterCode(methodPointCut);
            }
        }
        super.onMethodExit(opcode);
    }

    private void implantBeforeCode(MethodPointCut methodPointCut) {
        if (methodPointCut.getBeforeInstructions() != null) {
            methodPointCut.getBeforeInstructions().accept(this);
        }
    }


    private void implantAfterCode(MethodPointCut methodPointCut) {
        if (methodPointCut.getAfterInstructions() != null) {
            methodPointCut.getAfterInstructions().accept(this);
        }
    }
}
复制代码

至此AOP的核心代码已经全部完成了。不知是否你已经发现了我们这种实现的缺点,给你一些时间思考下。这种方案通过炽入大量相同的操作码操作数会使编译后的文件大小增大,而这个一般不是我们想要看到的,而炽入所有操作码操作数相对来说代码编译时效率也是一个问题。所以不妨回头想想我们的第一个方案,不难发现现在的缺点都将不复存在。这里主要需要思考的是对环绕处理的解决方案,第一种呢我们通过拆分原方法将代码一份为二,在方法进入和退出分别调用拆分的方法,不难发现这个方案中我们需要解决的是局部变量作用域由原来的相同作用域到不同作用域的改变带来的问题,虽说技术上可以实现,但是还是容易出问题的。第二种我们通过MethodHandle方法句柄调用原方法。首先我们将原方法改名内容不变,其次我们需要克隆原方法同名的方法,并且内部仅仅通过调用@Around标记的方法,传入的Method我们通过实现其子类通过invoke间接通过MethodHandle调用到改名后的方法。

最后贴一个Android端对应的实现github.com/lyqiai/easy…

Supongo que te gusta

Origin juejin.im/post/7083805782264774693
Recomendado
Clasificación