Spring AOP从入门到精通

目录

1. AOP的演化过程

1. 代理模式

2. 动态代理

2.1 JDK动态代理

2.2 Cglib动态代理

3. Spring模式

3.1 ProxyFactory

3.2 ProxyFactoryBean

3.3 AbstractAutoProxyCreator

2. Spring AOP抽象

1. 核心术语

1.1 连接点(JoinPoint)

1.2 切点(Pointcut)

1.3 增强(Advice)

1.4 切面(Aspect)

1.5 目标对象(Target)

1.6 代理对象(Proxy)

2. 核心组件

2.1 AutoProxyCreator工作原理

2.2 @EnableAspectJAutoProxy工作原理

3. Spring AOP案例


1. AOP的演化过程

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

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

1. 代理模式

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

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

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会额外事务开启和提交操作

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的实现

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调用输出也同之前的案例。

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。

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类创建代理类对象,并使用测试,也能得到同样的测试结果。

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接口,具体代码如下

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来创建代理对象,并进行测试,测试结果同之前的示例

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实现

<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实例,测试代码如下

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。

<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实例,并调用测试即可

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的骨干代码,实现类基于下面的模板修改部分逻辑。

    @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。

@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy 

AspectJAutoProxyRegistrar的核心代码如下:

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注解

  • 44
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值