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

1、前言

  最近在复习Spring的相关内容,刚刚大体研究了一下Spring中,AOP的实现原理。这篇博客就来简单地聊一聊SpringAOP是如何实现的,并经过一个简单的测试用例来验证一下。废话很少说,直接开始。html


2、正文

2.1 Spring AOP的实现原理

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

(一)JDK动态代理spring

  Spring默认使用JDK的动态代理实现AOP,类若是实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理须要两个组件,首先第一个就是InvocationHandler接口。咱们在使用JDK的动态代理时,须要编写一个类,去实现这个接口,而后重写invoke方法,这个方法其实就是咱们提供的代理方法。而后JDK动态代理须要使用的第二个组件就是Proxy这个类,咱们能够经过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的全部接口,并对接口的方法进行了代理,咱们经过代理对象调用这些方法时,底层将经过反射,调用咱们实现的invoke方法。框架

(二)CGLib动态代理ide

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

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


2.2 JDK的动态代理

(一)实现原理.net

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

(二)优势code

  1. JDK动态代理是JDK原生的,不须要任何依赖便可使用;
  2. 经过反射机制生成代理类的速度要比CGLib操做字节码生成代理类的速度更快;

(三)缺点

  1. 若是要使用JDK动态代理,被代理的类必须实现了接口,不然没法代理;
  2. JDK动态代理没法为没有在接口中定义的方法实现代理,假设咱们有一个实现了接口的类,咱们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,可是因为配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
  3. JDK动态代理执行代理方法时,须要经过反射机制进行回调,此时方法执行的效率比较低;

2.3 CGLib动态代理

(一)实现原理

  CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对须要代理的类的字节码进行操做,生成这个类的一个子类,并重写了类的全部能够重写的方法,在重写的过程当中,将咱们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了加强。而经过字节码操做生成的代理类,和咱们本身编写并编译后的类没有太大区别。

(二)优势

  1. 使用CGLib代理的类,不须要实现接口,由于CGLib生成的代理类是直接继承自须要被代理的类;
  2. CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类能够为原来那个类中,全部可以被子类重写的方法进行代理;
  3. CGLib生成的代理类,和咱们本身编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,因此CGLib执行代理方法的效率要高于JDK的动态代理;

(三)缺点

  1. 因为CGLib的代理类使用的是继承,这也就意味着若是须要被代理的类是一个final类,则没法使用CGLib代理;
  2. 因为CGLib实现代理方法的方式是重写父类的方法,因此没法对final方法,或者private方法进行代理,由于子类没法重写这些方法;
  3. CGLib生成代理类的方式是经过操做字节码,这种方式生成代理类的速度要比JDK经过反射生成代理类的速度更慢;

2.4 经过代码进行测试

(一)测试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的动态代理。


(二)测试CGLib动态代理

  好,测试完JDK动态代理,咱们开始测试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修饰符,让他没法被继承,此时看看结果。运行的结果会抛出异常,由于没法生成代理类,这里就不贴出来了,能够本身去试试。


2.5 强制Spring使用CGLib

  经过上面的测试咱们会发现,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>

3、总结

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

  上面的内容若存在错误或者不足,欢迎指正或补充。也但愿这篇博客对须要了解Spring AOP的人有所帮助。


4、参考

相关文章
相关标签/搜索