不一样的视角来学习Spring源码之AOP---上

x33g5p2x  于2022-03-29 转载在 Spring  
字(23.4k)|赞(0)|评价(0)|浏览(367)

系列文章:

不一样的视角来学习Spring源码之容器与Bean—上

不一样的视角来学习Spring源码之容器与Bean—下

AOP

AOP 底层实现方式之一是代理,由代理结合通知和目标,提供增强功能

除此以外,aspectj 提供了两种另外的 AOP 底层实现:

  • 第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中
  • 第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能
  • 作为对比,之前学习的代理是运行时生成新的字节码

简单比较的话:

  • aspectj 在编译和加载时,修改目标字节码,性能较高
  • aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强
  • 但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此没有广泛流行

AOP 实现之 ajc 编译器

准备一个待增强的MyService类

@Service
public class MyService {

    private static final Logger log = LoggerFactory.getLogger(MyService.class);

    public static void foo() {
        log.debug("foo()");
    }
}

准备一个切面类:

@Component
@Aspect 
public class MyAspect {

    private static final Logger log = LoggerFactory.getLogger(MyAspect.class);

    @Before("execution(* com.aop01.service.MyService.foo())")
    public void before() {
        log.debug("before()");
    }
}

引入aspectJ的编译插件acj编译器

<build>
        <plugins>
            <!-- 编译期织入 -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.14.0</version>
                <configuration>
                    <complianceLevel>1.8</complianceLevel>
                    <source>1.8</source>
                    <target>1.8</target>
                    <showWeaveInfo>true</showWeaveInfo>
                    <verbose>true</verbose>
                    <Xlint>ignore</Xlint>
                    <encoding>UTF-8</encoding>
                </configuration>
                <executions>
                    <execution>
                        <configuration>
                            <skip>false</skip>
                        </configuration>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

运行程序,查看被acj编译器动态织入后的class文件:

AspectJ官网
更多AspectJ的内容可以参考本篇文章进行学习

收获💡

  1. 编译器也能修改 class 实现增强
  2. 编译器增强能突破代理仅能通过方法重写增强的限制:可以对构造方法、静态方法等实现增强
    注意
  • 版本选择了 java 8, 因为目前的 aspectj-maven-plugin 1.14.0 最高只支持到 java 16
  • 一定要用 maven 的 compile 来编译, idea 不会调用 ajc 编译器

AOP 实现之 agent 类加载

Java Agent是JVM级别的AOP实现:

相关技术:

  • Java动态字节技术之Javassist

Java动态字节技术之Javassist

javassist基础以及如何使用javassist实现AOP

官方文档

  • Java agent—jvm级别的AOP实现

Javaagent使用指南

Package java.lang.instrument

Java Agent

使用javaagent实现代码无入侵增强logback

构建自己的监测器【3】-instrumentation

构建自己的监测器【6】-agentmain方式

Package java.lang.instrument

  • -java -D参数使用

构建自己的监测器【4】-java -D参数使用

使用演示:

  • 我们需要借助javassist修改字节码,因此引入依赖
<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.28.0-GA</version>
        </dependency>

我们这里通过agent从jvm层面完成对指定方法执行时间的计算功能

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain-1." + agentArgs);
        inst.addTransformer(new MyMonitor());
    }
}

转换器,计算所有类文件在被classLoader加载器,都会回调注册的转换器—inst.addTransformer(new MyMonitor()),可以通过转换器的transform改写类的定义或者给类新增某些定义

/**
 * <p>
 *     增加计算方法执行时间的横切逻辑
 * </p>
 * @author 大忽悠
 * @create 2022/3/28 13:36
 */
public class MyMonitor implements ClassFileTransformer {
    private List<String> getTargetMethods()
    {
       return Arrays.asList("sayHello1","sayHello2");
    }

    /**
     * 从环境变量中读取出指定的类名
     */
    private String getTargetClassName()
    {
        return "com.agent.TestBean";
    }

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        String targetClassName = getTargetClassName();
        //默认传入的形式是java/util/list
        className = className.replace("/", ".");
        if(className.equals(targetClassName))
        {
            System.out.println("==========================>拦截的指定类名为: "+className);
            try {
                //用于取得字节码类,必须在当前classpath中,使用全称
                CtClass ctClass = ClassPool.getDefault().get(className);
                //遍历需要增强的方法列表
                List<String> targetMethods = getTargetMethods();
                for (int i = 0; i < targetMethods.size(); i++) {
                    //获取方法名
                    String methodName=targetMethods.get(i);
                    System.out.println("==========================>当前被增强的方法名为: "+methodName);
                    //准备方法体
                    String outputStr ="\nSystem.out.println" +
                            "(\"this method "+methodName+" cost:\" +(endTime - startTime) +\"ms.\");";
                    //得到方法对象实例
                    CtMethod ctMethod = ctClass.getDeclaredMethod(methodName);
                    //构建新的方法体
                    StringBuilder bodyStr =new StringBuilder();
                    bodyStr.append("{");
                    bodyStr.append("long startTime = System.currentTimeMillis();");
                    //调用原有代码,类似于method();($$)表示所有的参数
                    CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName+"$Proxy", ctClass,null);
                    //在新的方法里面调用旧方法---不能在就方法里面调用旧方法,那样子就无限递归了
                    bodyStr.append(methodName+"();\n");
                    bodyStr.append("long endTime = System.currentTimeMillis();");
                    bodyStr.append(outputStr);
                    bodyStr.append("}");
                    //更换方法体
                    newMethod.setBody(bodyStr.toString());
                    //增加新的方法
                    ctClass.addMethod(newMethod);
                }
                return ctClass.toBytecode();
            } catch (NotFoundException e) {
                e.printStackTrace();
            } catch (CannotCompileException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

MANIFEST.MF文件

Manifest-Version: 1.0
Premain-Class: com.dhy.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

将这个agent打成jar包

待增强类:

public void sayHello1()
    {
        try {
            Thread.sleep(2000);
            System.out.println("你好呀!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void sayHello2()
    {
        try {
            Thread.sleep(2000);
            System.out.println("hhh!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试主类:

public class AgentTest {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
        TestBean testBean=new TestBean();
        Method[] declaredMethods = testBean.getClass().getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            if(declaredMethod.getName().equals("sayHello1$Proxy")||declaredMethod.getName().equals("sayHello2$Proxy"))
            {
                declaredMethod.invoke(testBean,null);
            }
        }
    }
}

还需要在IDEA增加VM参数---》指定agnet的jar包位置: -javaagent:C:\Users\zdh\IdeaProjects\SpringSourceCode\target\myAgnet-1.0-SNAPSHOT.jar

完美!

收获💡

  1. 类加载时可以通过 agent 修改 class 实现增强

java文件是先被编译成class二进制字节码文件,再加载进虚拟机的,Aspectj是在没加载前,直接修改生成的class文件完成aop逻辑织入的,但是这里的agent是通过加载前的回调处理接口,对class文件进行aop逻辑切入的,所以我们查看生成的class文件,会发现并没有变化

我们可以使用Arthas在运行期间,对程序中某个类进行反编译,查看反编译之后生成的class字节码文件,下面我们来尝试一下

Arthas官网

public class AgentTest {
    public static void main(String[] args) {
        TestBean testBean=new TestBean();
        Method[] declaredMethods = testBean.getClass().getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            if(declaredMethod.getName().equals("sayHello1$Proxy")||declaredMethod.getName().equals("sayHello2$Proxy"))
            {
                declaredMethod.invoke(testBean,null);
            }
        }
        //阻塞,方便通过arthas动态观察
        System.in.read();
    }
}

启动arthas

AOP 实现之 proxy

jdk 动态代理使用简单的演示

public class JdkProxyDemo {

    interface Foo {
        void foo();
    }

    static class Target implements Foo {
        @Override
        public void foo() {
            System.out.println("target foo");
        }
    }

    public static void main(String[] param) {
        // 目标对象
        Target target = new Target();
        // 代理对象
        Foo proxy = (Foo) Proxy.newProxyInstance(
                Target.class.getClassLoader(), new Class[]{Foo.class},
                (p, method, args) -> {
                    System.out.println("proxy before...");
                    Object result = method.invoke(target, args);
                    System.out.println("proxy after...");
                    return result;
                });
        // 调用代理
        proxy.foo();
    }
}

收获💡
  • jdk 动态代理要求目标必须实现接口,生成的代理类实现相同接口,因此代理与目标之间是平级兄弟关系

cglib 代理

public class CglibProxyDemo {

    static class Target {
        public void foo() {
            System.out.println("target foo");
        }
    }

    public static void main(String[] param) {
        // 目标对象
        Target target = new Target();
        // 代理对象
        Target proxy = (Target) Enhancer.create(Target.class,
                (MethodInterceptor) (p, method, args, methodProxy) -> {
            System.out.println("proxy before...");
            //不会使用反射
            Object result = methodProxy.invoke(target, args);
            // 另一种调用方法,不需要目标对象实例
//            Object result = methodProxy.invokeSuper(p, args);
            System.out.println("proxy after...");
            return result;
        });
        // 调用代理
        proxy.foo();
    }
}

运行结果与 jdk 动态代理相同

收获💡
  • cglib 不要求目标实现接口,它生成的代理类是目标的子类,因此代理与目标之间是子父关系
  • 限制⛔:根据上述分析 final 类无法被 cglib 增强

jdk 动态代理进阶

模拟 jdk 动态代理

被代理类实现的接口:

public interface Target {
    String sayHello();
}

被代理类:

public class RealTarget implements Target{
    @Override
    public String sayHello() {
        System.out.println("hello world");
        return "hello";
    }
}

代理类呢? 先来模拟一下最简单的实现:

public class $Proxy0 implements Target{

    private Target target;

    public $Proxy0(Target target) {
        this.target = target;
    }

    @Override
    public String sayHello() {
        System.out.println("========>前置切入");
        String hello = target.sayHello();
        System.out.println("后置切入<==========");
        return hello;
    }
}

代理类中肯定需要持有被代理类的实例,这样才能调用其方法,所以正常第一反应应该写成上面这样

测试:

public class MockJdk {
    public static void main(String[] args) {
        Target target=new RealTarget();
        $Proxy0 $Proxy0=new $Proxy0(target);
        $Proxy0.sayHello();
    }
}

问题: 代理类里面的横切逻辑不应该是写死的,应该由用户写好,然后进行织入,但是上面的写法显然是不对的,所以需要优化,这时候就要引入一个中间类,来承担切入逻辑的功能:
联系上面的jdk的动态代理例子,大家应该猜到这个中间类是谁了----》InvocationHandler

我们这边就直接利用jdk提供的这个InvocationHandler接口即可

public interface InvocationHandler {
    public Object invoke(
   //代理对象   当前正在执行的方法  方法参数
   Object proxy, Method method, Object[] args)
            throws Throwable;
}

我们改进一下$Proxy0代理类

/**
 * @author 大忽悠
 * @create 2022/3/29 9:39
 */
public class $Proxy0 implements Target{
    private InvocationHandler h;
    private static Method m1;

    static {
        try {
            m1=Target.class.getMethod("sayHello");
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public $Proxy0(InvocationHandler h) {
        this.h=h;
    }

    @Override
    public String sayHello() {}
        Object ret = null;
        try {
            //这里将方法具体需要被织入什么样的逻辑交给用户决定
            ret = h.invoke(this,m1,null);
        }
        catch (RuntimeException | Error e)
        {
            //运行时异常直接抛出
            throw e;
        }
        catch (Throwable e) {
            //转换成运行时异常抛出
            throw new UndeclaredThrowableException(e);
        }
        return (String) ret;
    }
}

测试类:

public class MockJdk {
    public static void main(String[] args) {
        Target target=new RealTarget();
        $Proxy0 $Proxy0=new $Proxy0(target, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("========>前置切入");
                Object invoke = method.invoke(target, args);
                System.out.println("后置切入<==========");
                return invoke;
            }
        });
        $Proxy0.sayHello();
    }
}

还可以优化,因为实际生成的代理类还继承了一个父类Proxy

public class Proxy implements java.io.Serializable {
...
 protected InvocationHandler h;
 ...
 protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
    ...

在这里类里面已经为我们提供了InvocationHandler 保存的地方,因此可以对上面的代理类继续进行优化

public class $Proxy0 extends Proxy implements Target{
    private static Method m1;

    static {
        try {
            m1=Target.class.getMethod("sayHello");
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public $Proxy0(InvocationHandler h) {
        super(h);
    }

    @Override
    public String sayHello() {
        Object ret = null;
        try {
            //这里将方法具体需要被织入什么样的逻辑交给用户决定
            ret = h.invoke(this,m1,null);
        }
        catch (RuntimeException | Error e)
        {
            //运行时异常直接抛出
            throw e;
        }
        catch (Throwable e) {
            //转换成运行时异常抛出
            throw new UndeclaredThrowableException(e);
        }
        return (String) ret;
    }
}

这样就比较接近jdk动态生成的代理类的杨子了

Proxy主要功能是下面这几个

在newProxyInstance方法中通过getProxyClass0方法生成代理类的字节码

Class<?> cl = getProxyClass0(loader, intfs);

该方法底层通过asm字节码框架生成代理类的字节码后,再通过传入的类加载器加载代理类进jvm中

arthas反编译查看生成的代理类源码

从上面模拟JDK动态代理的简单演示我们可以知道,JDK通过动态代理会生成一个代理类,我们下面通过arthas来反编译一下这个代理类:

public class JdkProxyDemo {

    interface Foo {
        void foo();
    }

    static class Target implements Foo {
        @Override
        public void foo() {
            System.out.println("target foo");
        }
    }

    public static void main(String[] param) throws IOException {
        // 目标对象
        Target target = new Target();
        // 代理对象
        Foo proxy = (Foo) Proxy.newProxyInstance(
                Target.class.getClassLoader(), new Class[]{Foo.class},
                (p, method, args) -> {
                    System.out.println("proxy before...");
                    Object result = method.invoke(target, args);
                    System.out.println("proxy after...");
                    return result;
                });
        // 调用代理
        proxy.foo();
        //输出代理类的class类名
        System.out.println(proxy.getClass());
        //阻塞
        System.in.read();
    }
}

运行程序,而且要确保这个进程不结束,否则arthas就检测不到了

反编译输出结果如下:

[arthas@22824]$ jad com.jdkProxy.$Proxy0

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
  +-sun.misc.Launcher$ExtClassLoader@74b5721

Location:

/*
 * Decompiled with CFR.
 *
 * Could not load the following classes:
 *  com.jdkProxy.JdkProxyDemo$Foo
 */
package com.jdkProxy;

import com.jdkProxy.JdkProxyDemo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

final class $Proxy0
extends Proxy
implements JdkProxyDemo.Foo {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public final void foo() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.jdkProxy.JdkProxyDemo$Foo").getMethod("foo", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}
关于代理类抛出异常的注意事项

对于代理类的某个方法来说,如果该方法抛出了运行时异常,那我们直接抛出即可,如果抛出的是编译时异常,那么对应的方法也必须显示抛出该异常:

显然这就改变了原有的方法签名,因此我们不能这样做,可以采用将编译时异常转换为运行时异常抛出的方法来绕过这个问题。

@Override
    public String sayHello() {
        Object ret = null;
        try {
            //这里将方法具体需要被织入什么样的逻辑交给用户决定
            ret = h.invoke(this,m1,null);
        }
        catch (RuntimeException | Error e)
        {
            //运行时异常直接抛出
            throw e;
        }
        catch (Throwable e) {
            //转换成运行时异常抛出
            throw new UndeclaredThrowableException(e);
        }
        return (String) ret;
    }
使用asm模拟生成代理类字节码

因为使用asm框架的要求比较高,需要使用者熟悉jvm指令集,因此我们先手写一个代理类模板,然后通过asm插件,翻译出该类对应的asm代码:

  • 1.安装asm插件

  • 2.准备代理类
public class $Proxy0 extends Proxy implements Target{
    private static Method m1;

    static {
        try {
            m1=Target.class.getMethod("sayHello");
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public $Proxy0(InvocationHandler h) {
        super(h);
    }

    @Override
    public String sayHello() {
        Object ret = null;
        try {
            //这里将方法具体需要被织入什么样的逻辑交给用户决定
            ret = h.invoke(this,m1,null);
        }
        catch (RuntimeException | Error e)
        {
            //运行时异常直接抛出
            throw e;
        }
        catch (Throwable e) {
            //转换成运行时异常抛出
            throw new UndeclaredThrowableException(e);
        }
        return (String) ret;
    }
}
  • 查看该类对应的asm代码

package asm.com.asm;

import org.springframework.asm.*;

public class $Proxy0Dump implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        RecordComponentVisitor recordComponentVisitor;
        MethodVisitor methodVisitor;
        AnnotationVisitor annotationVisitor0;

        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/asm/$Proxy0", null, "java/lang/reflect/Proxy", new String[]{"com/asm/Target"});

        classWriter.visitSource("$Proxy0.java", null);

        {
            fieldVisitor = classWriter.visitField(ACC_PRIVATE | ACC_STATIC, "m1", "Ljava/lang/reflect/Method;", null, null);
            fieldVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/reflect/InvocationHandler;)V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(24, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitVarInsn(ALOAD, 1);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V", false);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(25, label1);
            methodVisitor.visitInsn(RETURN);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLocalVariable("this", "Lcom/asm/$Proxy0;", null, label0, label2, 0);
            methodVisitor.visitLocalVariable("h", "Ljava/lang/reflect/InvocationHandler;", null, label0, label2, 1);
            methodVisitor.visitMaxs(2, 2);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "sayHello", "()Ljava/lang/String;", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            Label label1 = new Label();
            Label label2 = new Label();
            methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/RuntimeException");
            methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/Error");
            Label label3 = new Label();
            methodVisitor.visitTryCatchBlock(label0, label1, label3, "java/lang/Throwable");
            Label label4 = new Label();
            methodVisitor.visitLabel(label4);
            methodVisitor.visitLineNumber(29, label4);
            methodVisitor.visitInsn(ACONST_NULL);
            methodVisitor.visitVarInsn(ASTORE, 1);
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(32, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitFieldInsn(GETFIELD, "com/asm/$Proxy0", "h", "Ljava/lang/reflect/InvocationHandler;");
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitFieldInsn(GETSTATIC, "com/asm/$Proxy0", "m1", "Ljava/lang/reflect/Method;");
            methodVisitor.visitInsn(ACONST_NULL);
            methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/lang/reflect/InvocationHandler", "invoke", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", true);
            methodVisitor.visitVarInsn(ASTORE, 1);
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(42, label1);
            Label label5 = new Label();
            methodVisitor.visitJumpInsn(GOTO, label5);
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(34, label2);
            methodVisitor.visitFrame(Opcodes.F_FULL, 2, new Object[]{"com/asm/$Proxy0", "java/lang/Object"}, 1, new Object[]{"java/lang/Throwable"});
            methodVisitor.visitVarInsn(ASTORE, 2);
            Label label6 = new Label();
            methodVisitor.visitLabel(label6);
            methodVisitor.visitLineNumber(37, label6);
            methodVisitor.visitVarInsn(ALOAD, 2);
            methodVisitor.visitInsn(ATHROW);
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLineNumber(39, label3);
            methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"});
            methodVisitor.visitVarInsn(ASTORE, 2);
            Label label7 = new Label();
            methodVisitor.visitLabel(label7);
            methodVisitor.visitLineNumber(41, label7);
            methodVisitor.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitVarInsn(ALOAD, 2);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException", "<init>", "(Ljava/lang/Throwable;)V", false);
            methodVisitor.visitInsn(ATHROW);
            methodVisitor.visitLabel(label5);
            methodVisitor.visitLineNumber(43, label5);
            methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            methodVisitor.visitVarInsn(ALOAD, 1);
            methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/String");
            methodVisitor.visitInsn(ARETURN);
            Label label8 = new Label();
            methodVisitor.visitLabel(label8);
            methodVisitor.visitLocalVariable("e", "Ljava/lang/Throwable;", null, label6, label3, 2);
            methodVisitor.visitLocalVariable("e", "Ljava/lang/Throwable;", null, label7, label5, 2);
            methodVisitor.visitLocalVariable("this", "Lcom/asm/$Proxy0;", null, label4, label8, 0);
            methodVisitor.visitLocalVariable("ret", "Ljava/lang/Object;", null, label0, label8, 1);
            methodVisitor.visitMaxs(4, 3);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            Label label1 = new Label();
            Label label2 = new Label();
            methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/NoSuchMethodException");
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(17, label0);
            methodVisitor.visitLdcInsn(Type.getType("Lcom/asm/Target;"));
            methodVisitor.visitLdcInsn("sayHello");
            methodVisitor.visitInsn(ICONST_0);
            methodVisitor.visitTypeInsn(ANEWARRAY, "java/lang/Class");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false);
            methodVisitor.visitFieldInsn(PUTSTATIC, "com/asm/$Proxy0", "m1", "Ljava/lang/reflect/Method;");
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(20, label1);
            Label label3 = new Label();
            methodVisitor.visitJumpInsn(GOTO, label3);
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(18, label2);
            methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/NoSuchMethodException"});
            methodVisitor.visitVarInsn(ASTORE, 0);
            Label label4 = new Label();
            methodVisitor.visitLabel(label4);
            methodVisitor.visitLineNumber(19, label4);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/NoSuchMethodException", "printStackTrace", "()V", false);
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLineNumber(21, label3);
            methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitLocalVariable("e", "Ljava/lang/NoSuchMethodException;", null, label4, label3, 0);
            methodVisitor.visitMaxs(3, 1);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }
}

JDK动态代理底层就是通过asm的上述代码,为代理类生成类似于上面的字节码,然后加载进JVM中

public class TestProxy {
    public static void main(String[] args) throws Exception {
        //得到代理类的二进制流
        byte[] dump = $Proxy0Dump.dump();

        FileOutputStream os = new FileOutputStream("$Proxy0.class");
        os.write(dump, 0, dump.length);
        os.close();
    }
}

将这个代理类生成的class文件,默认会生成到当前的工作目录下面

下面我们通过类加载器把这个生产的代理类二进制流加载进jvm中,然后创建代理类实例对象,执行其方法:

public class TestProxy {
    public static void main(String[] args) throws Exception {
        //得到代理类的二进制流
        byte[] dump = $Proxy0Dump.dump();

        ClassLoader loader = new ClassLoader() {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                return super.defineClass(name, dump, 0, dump.length);
            }
        };

        Class<?> proxyClass = loader.loadClass("com.asm.$Proxy0");

        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);

        //创建出代理类对象----代理类实现了Target接口并且继承了Proxy类----Proxy类的构造函数需要一个参数InvocationHandler
        Target proxy = (Target) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理类执行");
                return null;
            }
        });

        proxy.sayHello();
    }
}

收获💡

代理一点都不难,无非就是利用了多态、反射的知识

  1. 方法重写可以增强逻辑,只不过这【增强逻辑】千变万化,不能写死在代理内部
  2. 通过接口回调将【增强逻辑】置于代理类之外
  3. 配合接口方法反射(是多态调用),就可以再联动调用目标方法
  4. 会用 arthas 的 jad 工具反编译代理类
  5. 限制⛔:代理增强是借助多态来实现,因此成员变量、静态方法、final 方法均不能通过代理实现
jdk反射优化
// 运行时请添加 --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED
public class TestMethodInvoke {
    public static void main(String[] args) throws Exception {
        Method foo = TestMethodInvoke.class.getMethod("foo", int.class);
        for (int i = 1; i <= 17; i++) {
            show(i, foo);
            foo.invoke(null, i);
        }
        System.in.read();
    }

    // 方法反射调用时, 底层 MethodAccessor 的实现类
    private static void show(int i, Method foo) throws Exception {
        Method getMethodAccessor = Method.class.getDeclaredMethod("getMethodAccessor");
        getMethodAccessor.setAccessible(true);
        Object invoke = getMethodAccessor.invoke(foo);
        if (invoke == null) {
            System.out.println(i + ":" + null);
            return;
        }
        Field delegate = Class.forName("jdk.internal.reflect.DelegatingMethodAccessorImpl").getDeclaredField("delegate");
        delegate.setAccessible(true);
        System.out.println(i + ":" + delegate.get(invoke));
    }

    public static void foo(int i) {
        System.out.println(i + ":" + "foo");
    }
}
收获💡
  1. 前 16 次反射性能较低
  2. 第 17 次调用会生成代理类,优化为非反射调用
  3. 会用 arthas 的 jad 工具反编译第 17 次调用生成的代理类
    注意

运行时请添加 --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED

JDK虽然在方法多次反射后,会通过生成代理类进行优化,但是对于每个方法都会生成一个代理类,这有点浪费性能了

相关文章