Spring原理学习(七)JDK动态代理与CGLIB代理底层实现

时间:2022-11-08 07:56:16

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

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

  • 第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中

  • 第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能

  • 作为对比,之前学习的代理是运行时生成新的字节码

简单比较的话:

  • aspectj 在编译和加载时,修改目标字节码,性能较高

  • aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强

  • 但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此没有广泛流行

这里开拓一下视野,在实际开发中,我们还是经常使用代理为主。

一、AOP 实现之 ajc 编译器

需要导入的依赖和插件

注意事项:

  1. 版本选择了 java 8, 因为目前的 aspectj-maven-plugin 1.14.0 最高只支持到 java 16
  2. 一定要用 maven 的 compile 来编译, idea 不会调用 ajc 编译器
    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.14.0</version>
                <configuration>
                    <complianceLevel>1.8</complianceLevel>
                    <source>8</source>
                    <target>8</target>
                    <showWeaveInfo>true</showWeaveInfo>
                    <verbose>true</verbose>
                    <Xlint>ignore</Xlint>
                    <encoding>UTF-8</encoding>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <!-- use this goal to weave all your main classes -->
                            <goal>compile</goal>
                            <!-- use this goal to weave all your test classes -->
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

主程序类

@SpringBootApplication
public class A09Application {

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

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(A09Application.class, args);
        MyService service = context.getBean(MyService.class);

        log.debug("service class: {}", service.getClass());
        service.foo();

        context.close();
    }
}

增强类

@Aspect // 注意此切面并未被 Spring 管理
public class MyAspect {

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

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

被增强类

@Service
public class MyService {

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

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

结果:MyService的类型是原始目标,而不是代理

[main] com.itheima.A09Application : service class: class com.itheima.service.MyService
[main] com.itheima.aop.MyAspect : before()
[main] com.itheima.service.MyService : foo()

这里的增强并不是spring做的增强,因为切面类并未被管理,是通过ajc编译器实现的。 

查看MyService的class文件,发现foo()调用了增强类的方法。

Spring原理学习(七)JDK动态代理与CGLIB代理底层实现

那我们去掉与spring相关的东西,重新再测试

public class A09Application {

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

    public static void main(String[] args) {
        new MyService().foo();
    }
}

发现还是被增强了,原因:修改的是class文件,自然可以生效

[main] DEBUG com.itheima.aop.MyAspect - before()
[main] DEBUG com.itheima.service.MyService - foo()

总结

  1. 编译器也能修改 class 实现增强

  2. 编译器增强能突破代理仅能通过方法重写增强的限制:可以对构造方法、静态方法等实现增强

二、AOP 实现之 agent 类加载

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

需要导入的依赖

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>

主程序类

@SpringBootApplication
public class A10Application {

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

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(A10Application.class, args);
        MyService service = context.getBean(MyService.class);

        log.debug("service class: {}", service.getClass());
        service.foo();
    }
}

 增强类

@Aspect // 注意此切面并未被 Spring 管理
public class MyAspect {

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

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

被增强类

@Service
public class MyService {

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

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

运行时需要在 VM options里加入

-javaagent:C:/Users/manyh/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar

把其中 C:/Users/manyh/.m2/repository 改为你自己 maven 仓库起始地址类加载阶段,在class文件看不到。

结果:MyService的类型是原始目标,而不是代理

[main] com.itheima.A09Application : service class: class com.itheima.service.MyService
[main] com.itheima.aop.MyAspect : before()
[main] com.itheima.service.MyService : foo()

由于是在类加载阶段(即运行期间)被增强,在class文件看不到,我们需要借助Arthas工具查看。可以进入官网下载jar包。

具体操作如下:

Spring原理学习(七)JDK动态代理与CGLIB代理底层实现

 反编译命令:

jad com.itheima.service.MyService 

Spring原理学习(七)JDK动态代理与CGLIB代理底层实现

发现MyService在运行期间被增强

三、AOP 实现之 proxy

代理类与普通类的差别:

  • 普通类:java原代码 -> 字节码 -> 类加载 -> 使用
  • 代理类:运行期间直接生成代理类的字节码

1、JDK动态代理

public class JdkProxyDemo {
    interface Foo {
        void foo();
    }

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

    // jdk 只能针对接口代理
    public static void main(String[] param) throws IOException {
        //目标对象
        Target target = new Target();
        
        //用来加载在运行期间动态生成的字节码
        ClassLoader classLoader = JdkProxyDemo.class.getClassLoader();
        //参数二:代理类要实现的接口 参数三:代理类调用代理类方法时执行的行为
        Foo proxy = (Foo) Proxy.newProxyInstance(classLoader, new Class[]{Foo.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("before...");
                //反射调用目标方法
                Object result = method.invoke(target);
                System.out.println("after...");
                return result; //让代理类返回目标方法执行的结果
            }
        });
        proxy.foo();
    }
}

结果:

before...
target foo
after...

注意:

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

2、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, new MethodInterceptor() {

            @Override
            public Object intercept(Object p, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("before...");
                //用反射调用目标方法
                Object result = method.invoke(target);
                System.out.println("after...");
                return result;
            }
        });

        proxy.foo();
    }
}

结果:

before...
target foo
after...

注意:

cglib 不要求目标实现接口,它生成的代理类是目标的子类,因此代理与目标之间是子父关系

根据上述分析 final 类,final方法无法被 cglib 增强,因为是子父类的关系,相当于代理类对目标类方法的重写。

invoke 与 invokeSuper的区别

与jdk动态代理不同的是,cglib可以避免使用反射调用目标方法。

使用methodProxy的invoke 或 invokeSuper方法

    public static void main(String[] param) {
        Target target = new Target();

        Target proxy = (Target) Enhancer.create(Target.class, new MethodInterceptor() {

            @Override
            public Object intercept(Object p, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("before...");
                Object result = methodProxy.invoke(target, args);
                System.out.println("after...");
                return result;
            }
        });

        proxy.foo();
    }

两者区别

//需要目标对象
methodProxy.invoke(target, args)

//需要代理对象
methodProxy.invokeSuper(p, args);

spring使用的是invoke方法

四、JDK动态代理进阶

学习目标:代理类的内部原理

  1. 方法重写可以增强逻辑
  2. 通过接口回调将【增强逻辑】置于代理类之外
  3. 配合接口方法反射(也是多态),就可以再联动调用目标方法

1、模拟JDK动态代理 

public class A12 {

    interface Foo {
        void foo();
        int boo();
    }

    //目标类
    static class Target implements Foo {

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

        @Override
        public int boo() {
            System.out.println("target boo");
            return 100;
        }
    }

    //提供动态执行增强逻辑的方法
    interface InvocationHandler {
        Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    }

}

代理类

public class $Proxy0 implements Foo {

    private InvocationHandler invocationHandler;

    public $Proxy0() {
    }

    public $Proxy0(InvocationHandler invocationHandler) {
        this.invocationHandler = invocationHandler;
    }
    
    static Method foo;
    static Method boo;

    static {
        try {
            //根据方法名获取Method属性
            foo = Foo.class.getMethod("foo");
            boo = Foo.class.getMethod("boo");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    @Override
    public void foo() {
        try {
            invocationHandler.invoke(this, foo, new Object[0]);
        } catch (RuntimeException | Error e) {
            throw e;
        }catch (Throwable e){
            throw new UndeclaredThrowableException(e);
        }
    }

    @Override
    public int boo() {
        try {
            int result = (int) invocationHandler.invoke(this, boo, new Object[0]);
            return result;
        } catch (RuntimeException | Error e) {
            throw e;
        }catch (Throwable e){
            throw new UndeclaredThrowableException(e);
        }
    }
}

这里说明一下异常处理:

  • 运行时异常:RuntimeException,Error,无需捕获,直接抛出
  • 检查异常:Throwable,接口不一定有Throwable,需要将检查异常转换为运行时异常抛出

测试

    Foo foo = new $Proxy0(new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before...");
            Object result = method.invoke(target);
            System.out.println("after...");
            return result;
        }
    });
    foo.foo();
    foo.boo();

结果:

before...
target foo
after...
before...
target boo
after...

JDK生成代理类时并没有经历源码阶段,编译阶段,而是直接进入字节码阶段,现在看到的java源码是通过Arthas工具对它进行了反编译, 直接生成字节码的底层技术是ASM,被广泛应用于JDK,Spring等框架,它的作用就是在运行期间动态生成字节码。

和我们自己写的代理类进行比较发现比我们多实现了Object中的toString,equals,hashCode方法,并且继承了Proxy类。

package com.itheima.a11;

import com.itheima.a11.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 $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.itheima.a11.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);
        }
    }

    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);
        }
    }
}

Proxy类内部已经定义了 InvocationHandler

public class Proxy implements java.io.Serializable {

    protected InvocationHandler h;

    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
}

注意:代理增强是借助多态来实现,因此成员变量、静态方法、final 方法均不能通过代理实现

2、方法反射优化

由于ASM学习成本比较高,这里直接给出结论,感兴趣的小伙伴可以去看视频。

优化:

  1. 前 16 次反射性能较低

  2. 第 17 次调用会生成代理类,优化为非反射调用

五、cglib 代理进阶

和 JDK 动态代理原理查不多

  1. 回调的接口换了一下,InvocationHandler 改成了 MethodInterceptor

  2. 调用目标时有所改进,见下面代码片段

    1. method.invoke 是反射调用,必须调用到足够次数才会进行优化

    2. methodProxy.invoke 是不反射调用,它会正常(间接)调用目标对象的方法(Spring 采用)

    3. methodProxy.invokeSuper 也是不反射调用,它会正常(间接)调用代理对象的方法,可以省略目标对象

1、模拟cglib代理

目标类

public class Target {
    public void save() {
        System.out.println("save()");
    }

    public void save(int i) {
        System.out.println("save(int)");
    }

    public void save(long j) {
        System.out.println("save(long)");
    }
}

代理类

public class Proxy extends Target{

    private MethodInterceptor methodInterceptor;

    public Proxy() {
    }

    public Proxy(MethodInterceptor methodInterceptor) {
        this.methodInterceptor = methodInterceptor;
    }

    static Method save0;
    static Method save1;
    static Method save2;

    static {
        try {
            save0 = Target.class.getMethod("save");
            save1 = Target.class.getMethod("save", int.class);
            save2 = Target.class.getMethod("save", long.class);
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    @Override
    public void save() {
        try {
            methodInterceptor.intercept(this, save0, new Object[0], null);
        } catch (RuntimeException | Error e){
            throw e;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    @Override
    public void save(int i) {
        try {
            methodInterceptor.intercept(this, save1, new Object[]{i}, null);
        } catch (RuntimeException | Error e){
            throw e;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    @Override
    public void save(long j) {
        try {
            methodInterceptor.intercept(this, save2, new Object[]{j}, null);
        } catch (RuntimeException | Error e){
            throw e;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

测试

public class A13 {
    public static void main(String[] args) {
        Target target = new Target();
        Proxy proxy = new Proxy(new MethodInterceptor() {
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("before...");
                return method.invoke(target, args);
            }
        });
        proxy.save();
        proxy.save(1);
        proxy.save(2L);
    }
}

结果:

before...
save()
before...
save(int)
before...
save(long)

 上面的代码其实和模拟JDK动态代理的代码差不多,重点是下面的避免反射调用的代码。

2、cglib 避免反射调用

给代理类增加原始功能的方法以及MethodProxy

public class Proxy extends Target{

    private MethodInterceptor methodInterceptor;

    public Proxy() {
    }

    public Proxy(MethodInterceptor methodInterceptor) {
        this.methodInterceptor = methodInterceptor;
    }

    static Method save0;
    static Method save1;
    static Method save2;
    static MethodProxy save0Proxy;
    static MethodProxy save1Proxy;
    static MethodProxy save2Proxy;
    static {
        try {
            save0 = Target.class.getMethod("save");
            save1 = Target.class.getMethod("save", int.class);
            save2 = Target.class.getMethod("save", long.class);
            /*
            参数一:目标类型    参数二:代理类型
            参数三:方法参数描述符
            参数四:带增强功能的方法名    参数五:带原始功能的方法名
             */
            save0Proxy = MethodProxy.create(Target.class, Proxy.class, "()V", "save", "saveSuper");
            save1Proxy = MethodProxy.create(Target.class, Proxy.class, "(I)V", "save", "saveSuper");
            save2Proxy = MethodProxy.create(Target.class, Proxy.class, "(J)V", "save", "saveSuper");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    //>>>>>>>>>>>>>>>>>>>>>>>带原始功能的方法
    public void saveSuper(){
        super.save();
    }
    public void saveSuper(int i){
        super.save(i);
    }
    public void saveSuper(long j){
        super.save(j);
    }

    //>>>>>>>>>>>>>>>>>>>>>>>带增强功能的方法
    @Override
    public void save() {
        try {
            methodInterceptor.intercept(this, save0, new Object[0], save0Proxy);
        } catch (RuntimeException | Error e){
            throw e;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    @Override
    public void save(int i) {
        try {
            methodInterceptor.intercept(this, save1, new Object[]{i}, save1Proxy);
        } catch (RuntimeException | Error e){
            throw e;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    @Override
    public void save(long j) {
        try {
            methodInterceptor.intercept(this, save2, new Object[]{j}, save2Proxy);
        } catch (RuntimeException | Error e){
            throw e;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

当调用 MethodProxy 的 invoke 或 invokeSuper 方法时, 会动态生成两个类

  • ProxyFastClass 配合代理对象一起使用, 避免反射
  • TargetFastClass 配合目标对象一起使用, 避免反射 (Spring 用的这种)

这两个类会继承FastClass抽象类,为了演示简单,下面的getIndex 和 invoke是实现它的两个方法。

1)invoke方法的无反射演示

public class TargetFastClass {
    //方法签名
    static Signature s0 = new Signature("save","()V");
    static Signature s1 = new Signature("save","(I)V");
    static Signature s2 = new Signature("save","(J)V");

    // 获取目标方法的编号
    /*
        Target
            save()              0
            save(int)           1
            save(long)          2
        signature 包括方法名字、参数返回值
     */
    public int getIndex(Signature signature) {
        if (signature.equals(s0)){
            return 0;
        } else if (signature.equals(s1)){
            return 1;
        } else if (signature.equals(s2)){
            return 2;
        }
        return -1;
    }

    // 根据方法编号, 正常调用目标对象方法
    public Object invoke(int index, Object target, Object[] args) {
        if (index == 0){
            ((Target) target).save();
            return null;
        } else if (index == 1){
            ((Target) target).save((int) args[0]);
            return null;
        }else if (index == 2){
            ((Target) target).save((long) args[0]);
            return null;
        }else {
            throw new RuntimeException("无此方法");
        }
    }

    //模拟操作
    public static void main(String[] args) {
        //首次使用MethodProxy方法时被创建
        TargetFastClass fastClass = new TargetFastClass();
        //MethodProxy在创建时由于记录了方法签名,所以能够调用getIndex方法得到方法编号
        int i = fastClass.getIndex(new Signature("save", "()V"));
        //调用MethodProxy的invoke方法,间接会调用到fastClass的invoke方法
        fastClass.invoke(i, new Target(), new Object[0]);
    }
}

2)invokeSuper方法的无反射演示

public class ProxyFastClass {
    /*
    调用的是代理类带原始功能的方法而不是增强方法,不然会陷入死循环
     */
    static Signature s0 = new Signature("saveSuper","()V");
    static Signature s1 = new Signature("saveSuper","(I)V");
    static Signature s2 = new Signature("saveSuper","(J)V");

    // 获取代理方法的编号
    /*
        Target
            saveSuper()              0
            saveSuper(int)           1
            saveSuper(long)          2
        signature 包括方法名字、参数返回值
     */
    public int getIndex(Signature signature) {
        if (signature.equals(s0)){
            return 0;
        } else if (signature.equals(s1)){
            return 1;
        } else if (signature.equals(s2)){
            return 2;
        }
        return -1;
    }

    // 根据方法编号, 正常调用目标对象方法
    public Object invoke(int index, Object proxy, Object[] args) {
        if (index == 0){
            ((Proxy) proxy).saveSuper();
            return null;
        } else if (index == 1){
            ((Proxy) proxy).saveSuper((int) args[0]);
            return null;
        }else if (index == 2){
            ((Proxy) proxy).saveSuper((long) args[0]);
            return null;
        }else {
            throw new RuntimeException("无此方法");
        }
    }

    public static void main(String[] args) {
        ProxyFastClass fastClass = new ProxyFastClass();
        int i = fastClass.getIndex(new Signature("saveSuper", "()V"));
        fastClass.invoke(i, new Proxy(), new Object[0]);
    }
}

总结

为什么有这么麻烦的一套东西呢?

  • 避免反射,提高性能,代价是一个代理类配两个 FastClass 类,代理类中还得增加仅调用 super 的一堆方法

  • 用编号处理方法对应关系比较省内存,另外,最初获得方法顺序是不确定,这个过程没法固定死

与JDK动态代理相比,CGLIB代理类数目相对较少,只有两个类。而JDK动态代理在调用到第十七次后会生成代理类去优化为非反射调用,并且是一个方法对应一个代理类。