什么是AOP

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。—<摘自百度百科>


AOP 采用横向抽取机制,取代了传统纵向继承体系的重复代码 Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码.

大家获取看了这个概念依旧是处于懵逼的状态,对AOP是什么东西还是不理解,不要着急下面我们通过实际的例子来告诉你什么是AOP,以及为什么使用AOP可以帮助我们解决重复的代码降低耦合度? 横向抽取和纵向继承有什么区别?
假设我们在开发过程中,要求在保存数据和更新数据的时候添加校验,如下图所示:
假设项目中存在这无数的dao那就需要在每个dao中添加校验方法. 一般面对这样的需求,都采用纵向继承的方式解决.
image.png

下面我们来看一下纵向继承: 如下图所示
创建一个基础的BaseDaoImpl类,使每个DAO都继承此类,这种方式在实际开发中经常会用到.
image.png

既然纵向继承能够满足我们的需求,那为什么还要引入AOP呢? 在纵向继承中每个DAO都要继承BaseDao同时还要调用校验方法,如果代码发生改动,那么所有DAO类都需要改动.这样是肯定不行的,而AOP 就是帮助我们解决这个问题,AOP 通过动态代理机制,生成DAO的代理类,通过代理类调用save() update()等方法,这样我们就可以在执行方法前或执行方法后做想要做的任何处理. 我们还是通过流程图来查看,如下图所示:

其实AOP是很好理解的,利用AOP对你的类生成一个代理类,这个代理类和你的类拥有一样的方法,只不过方法内部没有代码的,通过代理类调用你想调用的方法比如:save(),这时候会先回调到invoke()回调方法中,在这个回调方法中进行权限校验,校验完毕后,在通过你原来的类调用save()方法.这样做的好处就是不用修改DAO类中的任何代码,只需要处理代理类调用方法的回调即可.

image.png

AOP相关术语

  • Joinpoint(连接点):所谓连接点是指那些被拦截到的点.在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点.
  • Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截定义.
  • Advice(通知/增强):所谓通知是指拦截到Joinpoint之后说要做的事情就是通知.通知分为前置通知、后置通知、异常通知、最终通知、环绕通知(切面要完成的功能)
  • Introduction(引介):引介是一种特殊的通知不在修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field
  • Target(目标):代理的目标对象
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程.Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入.
  • Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类.
  • Aspect(切面):是切入点和通知(引介)的结合

    Spring采用动态代理方式织入;AspectJ采用编译器织入和类装载区织入的方式.

通过上述UserDao这个类,来看看这些相关术语如何应用到类中. 这些相关术语就会非常容易理解

  • Joinpoint连接点:增删改查这些方法都可以增强就是连接点。
  • Pointcut切入点:真正想拦截到的点。只想增强save方法 save方法就是切入点。
  • Advice:拦截后要做的事情。拦截save做权限校验,这个权限校验的方法就是通知:比如在save方法之前进行校验就是:前置通知
  • 比如在save执行之后做校验就是后置通知。比如在执行save方法之前和之后都进行校验叫环绕通知。
  • Target目标:被增强的对象,UserDaoImpl。
  • Weaving织入:将Advice应用到target的过程。将权限校验应用到UserDaoImpl的save方法的这个过程就是织入。
  • Proxy代理:被应用了增强后,产生一个代理类 代理对象。
  • Aspect切面:就是切入点和通知的组合。

AOP 的底层实现

理解这些概念,我们在实际通过代码进行理解. 还是以上述的例子为例,在DAO的save方法中增加权限校验

  1. public interface UserDao {
  2. void save();
  3. void update();
  4. void delete();
  5. void find(); //
  6. }
  7. public class UserDaoImpl implements UserDao {
  8. @Override
  9. public void save() {
  10. //保存用户
  11. System.out.println("UserDaoImpl.save");
  12. }
  13. @Override
  14. public void update() {
  15. //更新信息
  16. System.out.println("UserDaoImpl.update");
  17. }
  18. @Override
  19. public void delete() {
  20. //删除信息
  21. //比如在删除后 要加一个日志的记录
  22. System.out.println("UserDaoImpl.delete");
  23. }
  24. @Override
  25. public void find() {
  26. //查找信息
  27. System.out.println("UserDaoImpl.find");
  28. }

动态代理有两种不同的实现机制:JDK 动态代理 CGLib动态代理

JDK 动态代理

注意JDK动态代理必须是接口.如果不是接口类会产生错误.

Proxy.newProxyInstance 对目标类对象生成一个代理类对象.

  1. public class AopImpl implements InvocationHandler {
  2. private Object proxyObj;
  3. private Object targetObj;
  4. public AopImpl(Object targetObj) {
  5. this.targetObj = targetObj;
  6. }
  7. /**
  8. * 创建一个代理对象
  9. * @return
  10. */
  11. public Object createProxy() {
  12. proxyObj = Proxy.newProxyInstance(targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(), this);
  13. return proxyObj;
  14. }
  15. @Override
  16. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  17. if ("save".equals(method.getName())) {
  18. System.out.println("权限校验.....");
  19. return method.invoke(targetObj, args);//做完权限校验后在调用目标对象的方法
  20. }
  21. return method.invoke(targetObj, args);//直接调用目标对象的方法
  22. }
  23. }

下面来看如何使用

  1. UserDao userDao = new UserDaoImpl();
  2. //通过代理对象调用方法
  3. AopImpl aop = new AopImpl(userDao);
  4. UserDao proxy = (UserDao) aop.createProxy();
  5. proxy.save();
  6. proxy.update();

输出结果如下:

通过动态代理,不需要去修改已经写好的DAO代码,就可以实现在save()方法执行之前进行权限校验.实现了对UserDao的类增强.

image.png

CGLIB 动态代理

  • 对于不使用接口的业务类,无法使用JDK动态代理
  • CGlib采用非常底层字节码技术,可以为一个类创建子类,解决无接口代理问题

CGlib 是对目标类,产生子类所以如果目标类中的方法是private 或 final 是不能进行代理的.
ProductDao 是没有接口类的业务类.

  1. public class MyCglibProxy implements MethodInterceptor {
  2. private Object targetObj;
  3. public MyCglibProxy(Object obj) {
  4. this.targetObj = obj;
  5. }
  6. public Object createProxy() {
  7. //创建核心类
  8. Enhancer enhancer = new Enhancer();
  9. //设置父类//通过父类 目标类 产生子类
  10. enhancer.setSuperclass(targetObj.getClass());
  11. //设置回调
  12. enhancer.setCallback(this);
  13. //生成代理
  14. return enhancer.create();
  15. }
  16. @Override
  17. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  18. if ("save".equals(method.getName())) {
  19. System.out.println("MyCglibProxy.intercept -> 权限校验");
  20. }
  21. //默认调用父类的方法 也就是原来类的代码
  22. return methodProxy.invoke(targetObj, objects);
  23. }
  24. }
  25. @Test
  26. public void testCglib() {
  27. ProductDao productDao = new ProductDao();
  28. ProductDao proxy = (ProductDao) new MyCglibProxy(productDao).createProxy();
  29. proxy.save();
  30. }

调用结果如下:
image.png

还需要重点注意,动态代理的一些限制

Spring在运行期,生成动态代理对象,不需要特殊的编辑器 Spring AOP的底层就是通过JDK动态代理或CGlib动态代理技术为目标bean执行横向织入

  1. 若目标对象实现了若干接口,Spring使用JDK的java.lang.reflect.Proxy类代理
  2. 若目标对象没有实现任何接口,Spring使用CGlib库生成目标对象的子类

程序中应该优先对接口创建代理,便于程序解耦维护 标记的final方法,不能被代理,因为无法进行覆盖

  1. JDK动态代理,是针对接口生成子类,接口中方法不能使用final修饰
  2. CGlib是针对目标类生产子类,因此类或方法不能使final的

Spring只支持方法连接点,不提供属性连接点

Spring AOP 增强类型

  • AOP联盟为通知Advice定义了org.aopalliance.aop.Interface.Advice
  • Spring 按照通知Advice在目标类方法的连接点位置,可分为5类

    • 前置通知 org.springframework.aop.MethodBeforeAdvice 在目标方法执行前实施增强
    • 后置通知 org.springframework.aop.AfterReturningAdvice 在目标方法执行后实施增强
    • 环绕通知 org.springframework.aop.MethodInterceptor 在目标方法执行前后实施增强
    • 异常抛出通知 org.springframework.aop.ThrowsAdvice 在方法抛出异常后实施增强
    • 引介通知 org.springframework.aop.IntroductionInterceptor 在目标类中添加一些新的方法和属性

      Spring AOP切面类型

  • Advisor : 代表一般切面,Advice本身就是一个切面,对目标类的所有方法进行拦截

  • PointcutAdvisor: 代表具有切点的切面,可以指定连接目标类哪些方法
  • IntroductionAdvisor:代表引介切面,针对引介通知而使用切面

Advisor切面案例

下面来看如何定义切面类,也就是增强类,增强目标类

  • proxyTargetClass:是否对类代理而不是对接口,设置为true时,使用CGlib代理
  • interceptorNames:需要织入目标的Advice
  • singleton:返回代理是否为单例,默认为单例
  • optimize:当设置为true时,强制使用CGlib.

定义切面类

  1. /**
  2. * @author prim
  3. * 切面类 增强类
  4. */
  5. public class MyBeforeAdvice implements MethodBeforeAdvice {
  6. @Override
  7. public void before(Method method, Object[] objects, Object o) throws Throwable {
  8. System.out.println("MyBeforeAdvice.before 前置增强");
  9. }
  10. }

定义要增强的目标类

  1. public interface StudentDao {
  2. void save();
  3. void find();
  4. void delete();
  5. void update();
  6. }
  7. public class StudentDaoImpl implements StudentDao {
  8. @Override
  9. public void save() {
  10. System.out.println("StudentDaoImpl.save");
  11. }
  12. @Override
  13. public void find() {
  14. System.out.println("StudentDaoImpl.find");
  15. }
  16. @Override
  17. public void delete() {
  18. System.out.println("StudentDaoImpl.delete");
  19. }
  20. @Override
  21. public void update() {
  22. System.out.println("StudentDaoImpl.update");
  23. }
  24. }

在applicationContext.xml中进行配置,配置的一些属性,需要进行理解.主要配置目标类bean以及增强类bean
然后通过Spring AOP 配置属性.

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  5. <!-- 配置目标类 -->
  6. <bean id="studentDao" class="com.prim.spring.aop.StudentDaoImpl"/>
  7. <!-- 配置增强类 -->
  8. <!-- 前置增强 -->
  9. <bean id="beforeAdvice" class="com.prim.spring.aop.MyBeforeAdvice"/>
  10. <!-- Spring AOP 产生代理对象
  11. ProxyFactoryBean 可配置属性
  12. -->
  13. <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
  14. <!-- 配置目标类 -->
  15. <property name="target" ref="studentDao"/>
  16. <!-- 配置目标类的接口类 -->
  17. <property name="proxyInterfaces" value="com.prim.spring.aop.StudentDao"/>
  18. <!-- 采用拦截的增强类bean -->
  19. <property name="interceptorNames" value="beforeAdvice"/>
  20. <!-- 强制使用CGlib的方式 -->
  21. <property name="optimize" value="false"/>
  22. </bean>
  23. </beans>

配置完毕以后,当我们调用studentDao的任何方法(public)都会通过增强类进行增强.

注意: studentDao的注入是使用studentDaoProxy Spring AOP 给我们生成的代理类

   @Resource(name = "studentDaoProxy") //设置前置增强类
    private StudentDao studentDao;

    @Test
    public void test1() {
        studentDao.find();
        studentDao.save();
        studentDao.update();
        studentDao.delete();
    }

运行结果:
image.png

在上述的例子中,给目标类设置了增强类,但是调用所有方法都会经过增强类的前置增强处理.如果我们只想在save方法中进行增强如何处理呢? 既然可以定义切面,那么也可以定义这个面上的点也就是切点.

PointcutAdvisor 切点切面

  • 使用普通Advice作为切面,将对目标类所有方法进行拦截,不够灵活,在实际开发中常才做 带有切点的切面
  • 常用PointcutAdvisor实现类
    • DefaultPointcutAdvisor最常用的切面类型,它可以通过任意Pintcut和Advice组合定义切面
    • JdkRegexpMethodPointcut 构造正则表达式切点

使用没有接口的业务类,Spring会自动判断使用CGlib的方式进行动态代理

public class CustomerDao {
    public void find() {
        System.out.println("CustomerDao.find");
    }

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

    public void update() {
        System.out.println("CustomerDao.update");
    }

    public void delete() {
        System.out.println("CustomerDao.delete");
    }
}

然后定义切面,切面类必须要定义的,因为这个类中用到了业务逻辑,如下代码定义一个环绕通知的切面,methodInvocation.proceed();会调用代理的方法.

/**
 * @author prim
 * 环绕通知
 */
public class MyAroundAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        //在执行之前进行一下操作
        System.out.println("MyAroundAdvice.invoke 环绕前增强");

        //执行目标方法
        Object proceed = methodInvocation.proceed();

        System.out.println("MyAroundAdvice.invoke 环绕后增强");
        return proceed;
    }
}

定义切点,主要在配置文件中定义:
向上述的例子一样,首先配置目标类bean和通知bean,然后通过RegexpMethodPointcutAdvisor配置切点,如下面代码通过正则表达式只增强save()和delete()方法,然后配置使用哪种通知.最后在产生代理类的配置中,配置切点切面配置,注意如果没有实现接口需要配置proxyTargetClass=true

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置目标类 -->
    <bean id="customerDao" class="com.prim.pointcut.CustomerDao"/>

    <!-- 配置通知 -->
    <bean id="aroundAdvice" class="com.prim.pointcut.MyAroundAdvice"/>

    <!--
     一般切面使用通知作为切面,因为要对目标类的某个方法进行增强,就需要配置一个带有切入点的切面
     -->
    <bean id="myAdvice" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <!-- 配置正则表达式:. 任意字符;* 任意次数 根据某个切入点进行增强 -->
        <!--        <property name="pattern" value=".*save*"/>-->
        <!-- 多个匹配规则 -->
        <property name="patterns" value=".*save.*,.*delete.*"/>
        <!-- 使用哪种通知 -->
        <property name="advice" ref="aroundAdvice"/>
    </bean>

    <!-- 配置产生代理 -->
    <bean id="coustomerDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="customerDao"/>
        <!-- 没有实现接口需要这样配置 -->
        <property name="proxyTargetClass" value="true"/>
        <!-- 切面连接的规则配置 -->
        <property name="interceptorNames" value="myAdvice"/>
    </bean>
</beans>

看运行结果:

image.png

上述的配置是Spring传统的AOP 需要针对每个bean写一个代理的bean,如果bean有100多个,那么就需要写100多个代理bean,会非常麻烦且难以维护,在实际开发中不会使用这样的方式.

Spring 传统的AOP 需要针对每个bean去写一个代理bean,非常麻烦和重复的代码.所以Spring可以自动创建代理

Spring 自动创建代理

在上述案例中,每个代理都是通过ProxyFactoryBean织入切面代理,在实际开发中,非常多的Bean每个都配置ProxyFactoryBean 开发维护量巨大. spring 为了解决这个问题开发了:自动创建代理

  • BeanNameAutoProxyCreator 根据Bean名称自动创建代理
  • DefaultAdvisorAutoProxyCreator 根据Advisor本身包含信息自动创建代理
  • AnnotationAwareAspectJAutoProxyCreator 基于Bean中的AspectJ注解进行自动代理

BeanNameAutoProxyCreator

比如对所有以Dao结尾Bean所有方法使用代理,基于Bean名称的自动代理方式

dao类我就不再重复写了,注意通过 BeanNameAutoProxyCreator 对所有的以Dao为结尾的类进行了自动产生代理,并且每个bean目标类都进行了前置增强

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置目标类 -->
    <bean id="customerDao" class="com.prim.autoproxy.CustomerDao"/>
    <bean id="productDao" class="com.prim.autoproxy.ProductDao"/>
    <bean id="studentDao" class="com.prim.autoproxy.StudentDaoImpl"/>

    <!-- 配置通知 增强类的方式 -->
    <bean id="beforeAdvice" class="com.prim.autoproxy.MyBeforeAdvice"/>
    <bean id="aroundAdvice" class="com.prim.autoproxy.MyAroundAdvice"/>

    <!-- 配置自动产生代理 基于bean名称的自动代理方式 -->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!-- 连接的beanName -->
        <property name="beanNames" value="*Dao"/>
        <!-- 连接的bean都进行前置增强 -->
        <property name="interceptorNames" value="beforeAdvice"/>
    </bean>
</beans>
    @Resource(name = "customerDao")
    private CustomerDao customerDao;

    @Resource(name = "productDao")
    private ProductDao productDao;

    @Resource(name = "studentDao")
    private StudentDao studentDao;

    @Test
    public void test() {
        customerDao.find();
        customerDao.save();
        customerDao.delete();
        customerDao.update();

        productDao.save();
    }

image.png

在实际开发中,一般不会使用bean名称来自动创建代理,不知道大家有没有发现使用BeanNameAutoProxyCreator自动产生的代理,所有的匹配的bean都会进行增强,而且不支持切点切面只针对某一个方法增强.

DefaultAdvisorAutoProxyCreator

DefaultAdvisorAutoProxyCreator 是根据Advisor创建的通知自动产生代理,可以支持切点切面

如下,DefaultAdvisorAutoProxyCreator根据配置的RegexpMethodPointcutAdvisor切点切面自动产生代理,而且是只针对CustomerDao.save()方法进行增强

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 配置目标类 -->
    <bean id="customerDao" class="com.prim.pointcut.autoproxy.CustomerDao"/>
    <bean id="productDao" class="com.prim.pointcut.autoproxy.ProductDao"/>
    <bean id="studentDao" class="com.prim.pointcut.autoproxy.StudentDaoImpl"/>

    <!-- 配置通知 增强类的方式 -->
    <bean id="beforeAdvice" class="com.prim.pointcut.autoproxy.MyBeforeAdvice"/>
    <bean id="aroundAdvice" class="com.prim.pointcut.autoproxy.MyAroundAdvice"/>

    <!-- 配置一个切点切面 -->
    <bean id="myAutoAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="pattern" value="com\.prim\.pointcut\.autoproxy\.CustomerDao\.save"/>
        <!-- 配置通知方式 -->
        <property name="advice" ref="aroundAdvice"/>
    </bean>

    <!-- 基于切面信息产生代理 根据切面信息 自动产生代理 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
</beans>
    @Resource(name = "customerDao")
    private CustomerDao customerDao;

    @Resource(name = "productDao")
    private ProductDao productDao;

    @Resource(name = "studentDao")
    private StudentDao studentDao;

    @Test
    public void pointcutTest() {
        customerDao.find();
        customerDao.save();
        customerDao.delete();
        customerDao.update();

        productDao.save();

        studentDao.save();
    }

如下运行结果:可以看到只针对了CustomerDao.save()进行了增强.

image.png

总结

通过上述的一大段的讲解了,Spring传统的AOP 不管是Spring自动创建代理的方法还是一般的方法,当Bean类增多时,都会在配置文件中难以维护,在实际开发中并不推荐使用,这里也就引出了Spring 基于AspectJ 的AOP方式,这种方式在实际开发项目中大量的使用通过注解的方式大大减少了配置文件的维护成本,当然学习Spring 传统AOP也是非常必要的,了解AOP的底层原理和Spring传统AOP的痛点,更容易理解Spring 基于AspectJ 的AOP方式