Spring AOP实现原理详解(JDK和CGLib的动态代理)

Spring AOP实现原理详解

Spring的AOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理

Spring AOP实现原理

1. JDK动态代理

Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理需要两个组件:
(1)InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。
(2)Proxy这个类。我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。

2. CGLib动态代理

JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。CGLib直接操作字节码生成类的子类,重写类的方法完成代理

以上就是Spring实现动态的两种方式,下面我们具体来谈一谈这两种生成动态代理的方式。

JDK 动态代理

1. 实现原理

JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类(记住这个结论,后面测试要用)。这就是JDK动态代理大致的实现方式。

2. 优点

JDK动态代理是JDK原生的,不需要任何依赖即可使用;
通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;

3. 缺点

如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;
  
4. 代码实现

JDK Proxy 是通过实现 InvocationHandler 接口来实现的,代码如下:

interface Animal {
    void eat();
}
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("The dog is eating");
    }
}
class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("The cat is eating");
    }
}

// JDK 代理类
class AnimalProxy implements InvocationHandler {
    private Object target; // 代理对象
    public Object getInstance(Object target) {
        this.target = target;
        // 取得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("调用前");
        Object result = method.invoke(target, args); // 方法调用
        System.out.println("调用后");
        return result;
    }
}

public static void main(String[] args) {
    // JDK 动态代理调用
    AnimalProxy proxy = new AnimalProxy();
    Animal dogProxy = (Animal) proxy.getInstance(new Dog());
    dogProxy.eat();
}

如上代码,我们实现了通过动态代理,在所有请求之前和之后打印了一个简单的信息。
注意: JDK Proxy 只能代理实现接口的类(即使是extends继承类也是不可以代理的)

JDK Proxy 为什么只能代理实现接口的类?

这个问题要从动态代理的实现方法newProxyInstance 源码说起:

@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
// 省略其他代码

来看前两个源码参数说明:

  1. @param loader the class loader to define the proxy class
  2. @param interfaces the list of interfaces for the proxy class to implement
    loader:为类加载器,也就是 target.getClass().getClassLoader()
    interfaces:接口代理类的接口实现列表

所以这个问题的源头,在于 JDK Proxy 的源码设计。如果要执意动态代理,非接口实现类就会报错:

Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to xxx

CGLib 动态代理

1. 实现原理
  CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
2. 优点
  使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类
  CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
  CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理;
3. 缺点
  由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;
  由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;
  CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;
4. 实现代码
Cglib 可以通过 Maven 直接进行版本引用,Maven 版本地址:https://mvnrepository.com/artifact/cglib/cglib

本文使用的是 3.2.9 版本的 Cglib,在 pom.xml 添加如下引用:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.9</version>
</dependency>

Cglib 代码实现,如下:

class Panda {
    public void eat() {
        System.out.println("The panda is eating");
    }
}
class CglibProxy implements MethodInterceptor {
    private Object target; // 代理对象
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        // 设置父类为实例类
        enhancer.setSuperclass(this.target.getClass());
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("调用前");
        Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
        System.out.println("调用后");
        return result;
    }
}

public static void main(String[] args) {
    // CGLIB 动态代理调用
    CglibProxy proxy = new CglibProxy();
    Panda panda = (Panda)proxy.getInstance(new Panda());
    panda.eat();
}

cglib 的调用通过实现 MethodInterceptor 接口的 intercept 方法,调用 invokeSuper 进行动态代理的,可以直接对普通类进行动态代理。

代码实现与测试

  1. 测试JDK动态代理

下面我们通过一个简单的例子,来验证上面的说法。首先我们需要一个接口和它的一个实现类,然后再为这个实现类的方法配置切面,看看Spring是否真的使用的是JDK的动态代理。假设接口的名称为Human,而实现类为Student:

public interface Human {
    void display();
}

@Component
public class Student implements Human {

    @Override
    public void display() {
        System.out.println("I am a student");
    }
}

然后我们定义一个切面,将这个display方法作为切入点,为它配置一个前置通知,代码如下:

@Aspect
@Component
public class HumanAspect {
    // 为Student这个类的所有方法,配置这个前置通知
    @Before("execution(* cn.tewuyiang.pojo.Student.*(..))")
    public void before() {
        System.out.println("before student");
    }
}

下面可以开始测试了,我们通过Java类的方式进行配置,然后编写一个单元测试方法:

// 配置类
@Configuration
@ComponentScan(basePackages = "cn.tewuyiang")
@EnableAspectJAutoProxy
public class AOPConfig {
}

// 测试方法
 @Test
public void testProxy() {
    ApplicationContext context =
        new AnnotationConfigApplicationContext(AOPConfig.class);
	// 注意,这里只能通过Human.class获取,而无法通过Student.class,
    // 因为在Spirng容器中使用JDK动态代理,Ioc容器中,存储的是一个类型为Human的代理对象
    Human human =  context.getBean(Human.class);
    human.display();
    // 输出代理类的父类,以此判断是JDK还是CGLib
    System.out.println(human.getClass().getSuperclass());
}

注意看上面代码中,最长的那一句注释。由于我们需要代理的类实现了接口,则Spring会使用JDK的动态代理,生成的代理类会实现相同的接口,然后创建一个代理对象存储在Spring容器中。这也就是说,在Spring容器中,这个代理bean的类型不是Student类型,而是Human类型,所以我们不能通过Student.class获取,只能通过Human.class(或者通过它的名称获取)。这也证明了我们上面说过的另一个问题,JDK动态代理无法代理没有定义在接口中的方法。假设Student这个类有另外一个方法,它不是Human接口定义的方法,此时就算我们为它配置了切面,也无法将切面织入。而且由于在Spring容器中保存的代理对象并不是Student类型,而是Human类型,这就导致我们连那个不属于Human的方法都无法调用。这也说明了JDK动态代理的局限性。

我们前面说过,JDK动态代理生成的代理类继承了Proxy这个类,而CGLib生成的代理类,则继承了需要进行代理的那个类,于是我们可以通过输出代理对象所属类的父类,来判断Spring使用了何种代理。下面是输出结果:

before student
I am a student
class java.lang.reflect.Proxy	// 注意看,父类是Proxy

通过上面的输出结果,我们发现,代理类的父类是Proxy,也就意味着果然使用的是JDK的动态代理。

  1. 测试CGLib动态代理

前面说过,只有当需要代理的类没有实现接口时,Spring才会使用CGLib动态代理,于是我们修改Student这个类的定义,不让他实现接口:

@Component
public class Student {
    public void display() {
        System.out.println("I am a student");
    }
}

由于Student没有实现接口,所以我们的测试方法也需要做一些修改。之前我们是通过Human.class这个类型从Spring容器中获取代理对象,但是现在,由于没有实现接口,所以我们不能再这么写了,而是要写成Student.class,如下:

@Test
public void testProxy() {
    ApplicationContext context =
        new AnnotationConfigApplicationContext(AOPConfig.class);
	// 修改为Student.class
    Student student = context.getBean(Student.class);
    student.display();
    // 同样输出父类
    System.out.println(student.getClass().getSuperclass());
}

因为CGLib动态代理是生成了Student的一个子类,所以这个代理对象也是Student类型(子类也是父类类型),所以可以通过Student.class获取。下面是输出结果:

before student
I am a student
class cn.tewuyiang.pojo.Student		// 此时,父类是Student

可以看到,AOP成功生效,并且代理对象所属类的父类是Student,验证了我们之前的说法。下面我们修改一下Student类的定义,将display方法加上final修饰符,再看看效果:

@Component
public class Student {
    // 加上final修饰符
    public final void display() {
        System.out.println("I am a student");
    }
}

输出结果如下:

I am a student
class cn.tewuyiang.pojo.Student

可以看到,输出的父类仍然是Student,也就是说Spring依然使用了CGLib生成代理。但是我们发现,我们为display方法配置的前置通知并没有执行,也就是代理类并没有为display方法进行代理。这也验证了我们之前的说法,CGLib无法代理final方法,因为子类无法重写父类的final方法。下面我们可以试着为Student类加上final修饰符,让他无法被继承,此时看看结果。运行的结果会抛出异常,因为无法生成代理类,这里就不贴出来了,可以自己去试试。

通过上面的测试我们会发现,CGLib的动态代理好像更加强大,而JDK的动态代理却限制颇多。而且前面也提过,CGLib的代理对象,执行代理方法的速度更快,只是生成代理类的效率较低。但是我们使用到的bean大部分都是单例的,并不需要频繁创建代理类,也就是说CGLib应该会更合适。但是为什么Spring默认使用JDK呢?这我也不太清楚,网上也没有找到相关的描述(如果有人知道,麻烦告诉我)。但是据说SpringBoot现在已经默认使用CGLib作为AOP的实现了。

那我们可以强制Spring使用CGLib,而不使用JDK的动态代理吗?答案当然是可以的。我们知道,如果要使用注解(@Aspect)方式配置切面,则需要在xml文件中配置下面一行开启AOP:

<aop:aspectj-autoproxy />

如果我们希望只使用CGLib实现AOP,则可以在上面的这一行加点东西:

<!-- 将proxy-target-class配置设置为true -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

当然,如果我们是使用Java类进行配置,比如说我们上面用到的AOPConfig这个类,如果是通过这种方式配置,则强制使用CGLib的方式如下:

@Configuration
@ComponentScan(basePackages = "cn.tewuyiang")
// 如下:@EnableAspectJAutoProxy开启AOP,
// 而proxyTargetClass = true就是强制使用CGLib
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AOPConfig {

}

如果我们是在xml文件中配置切面,则可以通过以下方式来强制使用CGLib:

<!-- aop:config用来在xml中配置切面,指定proxy-target-class="true" -->
<aop:config proxy-target-class="true">
	<!-- 在其中配置AOP -->
</aop:config>

上面我们就对Spring中AOP的实现原理做了一个大致的介绍。归根到底,Spring AOP的实现是通过动态代理,并且有两种实现方式,分别是JDK动态代理和CGLib动态代理。Spring默认使用JDK动态代理,只有在类没有实现接口时,才会使用CGLib。

总结

在Spring中的AOP是依靠动态代理来实现切面编程的,而这两者又是有区别的。

JDK是基于反射机制,生成一个实现代理接口的匿名类,然后重写方法,实现方法的增强,它生成类的速度很快,但是运行时因为是基于反射,调用后续的类操作会很慢,而且他是只能针对接口编程的。

CGLIB是基于继承机制,继承被代理类,所以方法不要声明为final,然后重写父类方法达到增强了类的作用,它底层是基于asm第三方框架,是对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理,生成类的速度慢,但是后续执行类的操作时候很快,可以针对类和接口。

因为jdk是基于反射,CGLIB是基于字节码,所以性能上会有差异。在老版本CGLIB的速度是JDK速度的10倍左右,但是CGLIB启动类比JDK慢8倍左右,但是实际上JDK的速度在版本升级的时候每次都提高很多性能,而CGLIB仍止步不前。

在对JDK动态代理与CGlib动态代理的代码实验中看,1W次执行下,JDK7及8的动态代理性能比CGlib要好20%左右。

参考:浅析Spring中AOP的实现原理——动态代理

  • 19
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值