Spring AOP从入门到精通

目录

[1. AOP的演化过程](#1. AOP的演化过程)

[1. 代理模式](#1. 代理模式)

[2. 动态代理](#2. 动态代理)

[2.1 JDK动态代理](#2.1 JDK动态代理)

[2.2 Cglib动态代理](#2.2 Cglib动态代理)

[3. Spring模式](#3. Spring模式)

[3.1 ProxyFactory](#3.1 ProxyFactory)

[3.2 ProxyFactoryBean](#3.2 ProxyFactoryBean)

[3.3 AbstractAutoProxyCreator](#3.3 AbstractAutoProxyCreator)

[2. Spring AOP抽象](#2. Spring AOP抽象)

[1. 核心术语](#1. 核心术语)

[1.1 连接点(JoinPoint)](#1.1 连接点(JoinPoint))

[1.2 切点(Pointcut)](#1.2 切点(Pointcut))

[1.3 增强(Advice)](#1.3 增强(Advice))

[1.4 切面(Aspect)](#1.4 切面(Aspect))

[1.5 目标对象(Target)](#1.5 目标对象(Target))

[1.6 代理对象(Proxy)](#1.6 代理对象(Proxy))

[2. 核心组件](#2. 核心组件)

[2.1 AutoProxyCreator工作原理](#2.1 AutoProxyCreator工作原理)

[2.2 @EnableAspectJAutoProxy工作原理](#2.2 @EnableAspectJAutoProxy工作原理)

[3. Spring AOP案例](#3. Spring AOP案例)


1. AOP的演化过程

编程中我们经常会遇到这样的场景,有一段通用逻辑横跨多个业务,不能用继承来解决,比如方法耗时、数据库事务、通用日志等等。一个典型的DAO方法调用分为下面3个步骤:

每一次都手工的开启和提交事务,即显得啰嗦,又影响业务的可读性。AOP就十分擅长解决这类问题,它把开启和提交事务的逻辑抽取到Advice当中,在任务DAO上复用。那么AOP到底是怎么实现的呢?

1. 代理模式

熟悉设计模式的人这时候会想到代理模式,通过新增一个代理类实现,负责处理事务的开启和提交。假设我们要处理的是下面的类图,Staff是我们要处理的实体类,StaffDao是接口,定义两个方法employ、paySalary,StaffDaoImpl是实际的DAO实现,StaffDaoProxy是代理类,负责事务开启和提交。

Dao的操作用伪代码实现,StaffDaoImpl和代理类,核心代码如下

java 复制代码
public class StaffDaoImpl implements StaffDao {
    public void employ(Staff p) {
        System.out.println("employ staff: " + p);
    }
    public void paySalary(Staff p) {
        System.out.println("pay salary :" + p);
    }
}
public class StaffDaoProxy implements StaffDao {
    private StaffDao staffDao;
    public StaffDaoProxy(StaffDao staffDao) {
        this.staffDao = staffDao;
    }
    public void employ(Staff p) {
        System.out.println("start transaction...");
        staffDao.employ(p);
        System.out.println("commit transaction...");
    }
    public void paySalary(Staff p) {
        System.out.println("start transaction...");
        staffDao.paySalary(p);
        System.out.println("commit transaction...");
    }
}

通过如下代码进行测试,StaffDaoImpl只是完成了数据操作,StaffDaoProxy会额外事务开启和提交操作

java 复制代码
Staff staff = new Staff("zhangsan",18); 

StaffDao staffDao = new StaffDaoImpl();  // 直接调用
staffDao.employ(staff);
staffDao.paySalary(staff);

StaffDaoProxy proxy = new StaffDaoProxy(staffDao); // 通过代理类使用
proxy.employ(staff);
proxy.paySalary(staff);

2. 动态代理

代理模式确实达成了我们想要的效果,如果每一个需要横切逻辑的类都需要通过代理模式类实现的话,人力成本过高,而且也不便于维护,后续每一次修改都需要改大量的代理类。通过JDK提供的动态代理,或者字节码操作的第三方库能解决这个问题。

2.1 JDK动态代理

我们来看一个通过JDK的Proxy类实现动态代理的示例,首先要定义一个InvocationHandler的实现

java 复制代码
public class TransactionInvocationHandler implements InvocationHandler {
    private StaffDao staffDao;
    public TransactionInvocationHandler(StaffDao staffDao) {
        this.staffDao = staffDao;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("start transaction...");
        Object result = method.invoke(staffDao,args);
        System.out.println("commit transaction...");
        return result;
    }
}

接着就可以通过Proxy.newInstance来创建代理类实例,并测试,其中staff和staffDao的实例和代理模式里的创建方式一样,通过proxy.employ和proxy.paySalary调用输出也同之前的案例。

java 复制代码
InvocationHandler invocationHandler = new TransactionInvocationHandler(staffDao);
StaffDao proxy = (StaffDao) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{StaffDao.class},invocationHandler);
proxy.employ(staff);
proxy.paySalary(staff);
2.2 Cglib动态代理

JDK动态代理存在的一个问题是,生成代理对象必须基于接口,如果一个类没有实现接口就无法创建代理。好在还有大量的第三方库支持无接口的类生成动态代理。我们来看一个cglib的示例。和JDK代理类似,首先我们要实现一个回调类,不过cglib里回调类接口是MethodInterceptor。

java 复制代码
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TransactionMethodInterceptor implements MethodInterceptor {

    private StaffDao staffDao;

    public TransactionMethodInterceptor(StaffDao staffDao) {
        this.staffDao = staffDao;
    }


    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("start transaction...");
        Object result = method.invoke(staffDao, args);
        System.out.println("commit transaction...");
        return result;
    }
}

紧接着使用Enhancer类创建代理类对象,并使用测试,也能得到同样的测试结果。

java 复制代码
TransactionMethodInterceptor interceptor = new TransactionMethodInterceptor(staffDao);
Enhancer enhancer = new Enhancer();
enhancer.setCallback(interceptor);
enhancer.setSuperclass(StaffDao.class);
StaffDao proxy = (StaffDao) enhancer.create();

3. Spring模式

动态代理解决代理模式带来的部分问题,我不需要再为每个类手动创建子类,所有横切逻辑都集中到InvocationHandler或MethodInterceptor中了。但是每次使用的时候都需要手工创建代理对象,还是比较麻烦。Spring基于自己的能力对这个问题做了简化

3.1 ProxyFactory

Spring提供了ProxyFactory来简化代理对象的创建,频闭通过JDK或Cglib创建代理对象的差异(涉及类如下图),除此以外并没有其他改善。

同样需要实现一个回调类,实现aopaliance提供的MethodInterceptor接口,具体代码如下

java 复制代码
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class SpringTransactionMethodInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("start transaction...");
        Object result = invocation.proceed();
        System.out.println("commit transaction...");
        return result;
    }
}

然后使用ProxyFactory来创建代理对象,并进行测试,测试结果同之前的示例

java 复制代码
ProxyFactory factory = new ProxyFactory();
factory.setTarget(staffDao);
factory.addAdvice(new SpringTransactionMethodInterceptor());
StaffDao proxy = (StaffDao) factory.getProxy();

Staff staff = new Staff("zhangsan", 18);
proxy.employ(staff);
proxy.paySalary(staff);
3.2 ProxyFactoryBean

到目前为止我们依然需要手动创建代理类对象,通过Spring本身的FactoryBean机制,Spring AOP为我们提供了ProxyFactoryBean实现。首先通过xml配置Bean实现

XML 复制代码
<bean id="staffDao" class="com.lws.designPattern.StaffDaoImpl"/>
<bean id="springTransaction" class="com.lws.spring.SpringTransactionMethodInterceptor"/>
<bean id="staffDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="staffDao"/>
    <property name="interceptorNames">
        <list>
            <value>springTransaction</value>
        </list>
    </property>
</bean>

接着通过创建ApplicationContext,获取Bean实例,测试代码如下

java 复制代码
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
StaffDao proxy = context.getBean("staffDaoProxy", StaffDao.class);
proxy.employ(staff);
proxy.paySalary(staff);
3.3 AbstractAutoProxyCreator

ProxyFactoryBean已经基本可以使用了,只是每一个Bean要做代理时都需要手工配置一个ProxyFactoryBean,使用起来还是略显繁琐。Spring提供了AbstractAutoProxyCreator来完成AOP代理的自动创建。这里我们以BeanNameAutoProxyCreator为例,看看AutoProxyCreator是如何使用,它是怎么工作的。同样先基于xml配置BeanNameAutoProxyFactory,使用相同interceptorNames的所有Bean都可以通过这一个配置自动创建代理,而且staffDao不需要和之前那样创建staffDao、staffDaoProxy两个Bean。

XML 复制代码
<bean id="staffDao" class="com.lws.designPattern.StaffDaoImpl"/>
<bean id="springTransaction" class="com.lws.spring.SpringTransactionMethodInterceptor"/>

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
        <list>
            <value>staffDao</value>
        </list>
    </property>
    <property name="interceptorNames" value="springTransaction"/>
</bean>

接着只需要正常的获取staffDao的Bean实例,并调用测试即可

java 复制代码
Staff staff = new Staff("zhangsan", 18);
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
StaffDao proxy = context.getBean("staffDao", StaffDao.class);
proxy.employ(staff);
proxy.paySalary(staff);

应该承认BeanNameAutoProxyCreator的使用已经比较简单了,对使用相同interceptorNames的AOP场景,只需要把Bean实例名称添加到beanNames就行了。再进一步简化就是通过AnnotationAwareAspectJAutoProxyCreator来实现了,它会自动判断Bean是否匹配AspectJ表达式,对符号条件的Bean完成代理。后面的代码中我们会进行源码级解释,现在我们先来讲讲Spring对AOP做的抽象,以及它实现AOP的核心组件。

2. Spring AOP抽象

1. 核心术语

1.1 连接点(JoinPoint)

连接点是程序中客观存在的特定位置,比如类初始化前、类初始化后、方法执行前、方法执行后等等。Spring AOP中只支持方法的连接点。连接到有两组信息组成,一个是程序的执行点,比如哪个类的哪个方法;一个是相对位置,比如方法执行前、方法返回后等等。Spring使用切点(Pointcut)表示执行点,用增强(Advice)表示相对位置。

1.2 切点(Pointcut)

连接点是程序中客观存在的事务,比如一个程序内有2个方法,方法执行前、方法执行后、抛出异常时,连接点个数就已经确定了,如果只看这3类相对位置的话,连接点就是6个。切点用来表示我们关心的连接点。Spring中使用Pointcut类表示切点。下图是Pointcut类的定义,它有两个成员变量,分别是ClassFilter实例、MethodMatcher实例。通过如下的UML图中的方法定义可以看到,ClassFilter用来表示哪些类是满足当前切点要求,MethodMatcher用来确定哪些方法满足当前切点要求。

Spring内部的核心实现类有下面这些,后面我们会挑几个看一下如何使用。

1.3 增强(Advice)

在介绍连接点的时候我们提供到过,连接点相对位置是通过增强(Advice)来表示的,此外Advice还包含相对位置要执行的++代码++。下图是Spring提供的Advice的核心类。我们日常使用的较多的就是在图的中心位置的4个类:

  1. MethodBeforeAdvice,方法执行前增强
  2. MethodInterceptor,方法环绕增强
  3. AfterReturningAdvice,方法返回后增强
  4. ThrowsAdvice,方法抛异常是的正常,这里需要特别注意ThrowsAdvice是一个标记接口,Spring通过反射调用,查看类JavaDoc有描述对方法签名的限制
1.4 切面(Aspect)

从之前的定义中可知,Pointcut和Advice已经完整的定义了在哪里执行什么增强逻辑。切面有切点(Pointcut)和增强(引介)组成,即包括横切逻辑的定义,也包括连接点的定义。

1.5 目标对象(Target)

要进行增强逻辑的目标类,在我们前面的例子里,StaffDao类的实例就是目标对象。

1.6 代理对象(Proxy)

对目标对象进行增强后生成的对象,就称为代理对象,这个对象已经包含增强的执行逻辑。

2. 核心组件

这张图里给出了Spring AOP中的核心流程涉及的组件,包括怎么定义切点(Pointcut),增强(Advice),并且通过组合Pointcut和Advice获得Advisor的定义。拿到Advisor后,我们可以使用ProxyFactory将Advisor和目标对象(target)组合,生成代对象。Spring默认支持两种生成代理对象的实现,一种是基于JDK的实现,JdkDynamicAopProxy,一种是基于Cglib的实现,ObjenesisCglibAopProxy。

每一个组件都有大量的实现类,这里不进行展开。接下来我们对几个关键点做详细的说明

2.1 AutoProxyCreator工作原理

BeanNameAutoProxyCreator继承自AbstractAutoProxyCreator,实现了Bean生命周期里的InstantiationAwareBeanFactoryProcessor、BeanPostProcessor,覆写的是两个方法: postProcessBeforeInstantiation、postProcessAfterInitialization,核心流程图如下:

对于定义了customTargetSource的AbstractAutoProxyCreator,Bean不会走正常的实例化流程,在postProcessBeforeInstantiation就通过TargetSource实例化并创建代理返回。日常开发中我们极少使用TargetSource,TargetSource适用于aop时期望目标对象(target)支持池化或者热替换的场景,更多TargetSource信息可以阅读Spring官方文档中关于++Using TargetSources++ 章节。

BeanNameAutoProxy主要覆写了AbstractAutoProxyCreator以下几个方法:

  1. isSupportedBeanName,基于配置的beanNames判断当前bean是否支持自动创建代理对象
  2. getAdvicesAndAdvisorsForBean,支持的时候返回空数组,不支持返回null

BeanNameAutoProxyCreator的Advice和Advisor对象的注入主要时通过interceptorNames做为bean名称从容器中查找的,具体实现看AbstractorAutoProxyCreator的buildAdvisors实现

AnnotationAwareAspectJAutoProxyCreator基于AbstractAutoProxyCreator实现,不适用interceptorNames,核心逻辑在于查找bean匹配的Advisor,通过findCandidateAdvisors查找候选Advisor。

以下是AbstractAutoProxyCreator的骨干代码,实现类基于下面的模板修改部分逻辑。

java 复制代码
    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
        ...
        TargetSource targetSource = getCustomTargetSource(beanClass, beanName);                // 自定义customTargetSourceCreator的Bean
        if (targetSource != null) {
            if (StringUtils.hasLength(beanName)) {
                this.targetSourcedBeans.add(beanName);
            }
            Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); // 通过自定义TargetSource直接返回Bean
            Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }
        ...
    }


    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyBeanReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }
2.2 @EnableAspectJAutoProxy工作原理

EnableAspectJAutoProxy通过在自身注解@Import引入ImportBeanDefinitionRegistrar实现,ImportBeanDefinitionRegistrar接口允许用户通过代码注册自己的BeanDefinition。

java 复制代码
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy 

AspectJAutoProxyRegistrar的核心代码如下:

java 复制代码
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
        AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        ...
    }
}

进一步往AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary下面追踪,我们发现自动注册了AnnotationAwareAspectJAutoProxyCreator,结合上面我们对AutoProxyCreator的了解,整个基于注解的配置AOP的流程就通顺了。

3. Spring AOP案例

基于上面对原理的探究,我们能理解怎么从Advice、Pointcut开始创建Advisor,怎么利用ProxyFactoryBean创建代理,通过AutoProxyCreator让Spring自动创建代理,Spring对所有这些元素的抽象,以及相互之间如何协同。然而在显示工作中,仅仅知道这些是不够的,我们不可能每次都从最原始的MethodBeforceAdvice来创建我们的Advice,不可能从ClassFilter、MethodMatcher开始定义Pointcut,这样的话使用成本太高了,Spring提供了大量的内置类型,降低我们使用Spring AOP的成本。下一篇中我们会专注提供实战案例,包括但不限于如下主题

1.使用Advice

2.使用Advisor

3.使用BeanNameAutoProxyCreator

4.使用AnnotationAwareAspectJAutoProxyCreator

5.使用aop名称空间

6.使用@Aspect注解

相关推荐
程序员劝退师_4 分钟前
优惠券秒杀的背后原理
java·数据库
java小吕布19 分钟前
Java集合框架之Collection集合遍历
java
一二小选手21 分钟前
【Java Web】分页查询
java·开发语言
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ33 分钟前
idea 弹窗 delete remote branch origin/develop-deploy
java·elasticsearch·intellij-idea
Code成立35 分钟前
《Java核心技术 卷I》用户图形界面鼠标事件
java·开发语言·计算机外设
Lucifer三思而后行42 分钟前
YashanDB YAC 入门指南与技术详解
数据库·后端
鸽鸽程序猿1 小时前
【算法】【优选算法】二分查找算法(下)
java·算法·二分查找算法
瓜牛_gn1 小时前
Spring Security概述
spring
遇见你真好。1 小时前
自定义注解进行数据脱敏
java·springboot
NMBG221 小时前
[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
java·开发语言·面试·java-ee·intellij-idea