likes
comments
collection
share

Spring Boot「49」扩展:Spring AOP

作者站长头像
站长
· 阅读数 12

我正在参加「掘金·启航计划」

01-面向切片编程中的核心概念

AOP(面向切片编程)是一种编程范式。它常被拿来与 OOP(面向对象编程)来比较。 它与 OOP 并不是互相替代的关系,而是互相补充、增强的关系。 AOP 可以更为方便地描述 OOP 中不太容易实现、或实现起来比较麻烦的一些需求。 借用《Spring 实战(第四版)》中的一张图来解释下 AOP 与 OOP 的关系。 Spring Boot「49」扩展:Spring AOP OOP 关注的是横向的需求,AOP 关注的是纵向的需求。

Aspect(切面)是 AOP 中的核心概念,或者说基本单元,类比于 OOP(面向对象编程)中的类。 在 AOP 中,程序运行时的某个点(控制流中的某个位置,例如某个方法调用)被称为 join point(连接点,或者增强点),你可以简单地理解为程序中可以或需要被切面增强的地方。 在程序到达连接点时执行的动作(即增强的功能)被称为是 advice(通知)。 通知有多种类型,例如前置(before)通知、后置(after)通知、环绕(around)通知等,它们的类型描述了通知(增强行为)的执行时机。 Pointcut(切入点)是关于连接点的谓词。它通常包含一个表达式,通过表达式能够选定一组连接点;包含一个或多个通知,当程序执行到选定的连接点时执行。 切面有多个切入点和通知组成,用来描述那些跨越多个类的 accrosscut concerns。

谓词(predicate),在计算机语言的环境下,指对条件表达式求值,返回真值或假值的过程。

切入点和通知会动态地影响程序的执行。 AOP 中有另外一种技术,用于静态地修改程序的类结构,称之为 introduction(引介)。 AspectJ 中也称之为 inter-type declarations(类型间声明)。它使开发者能够修改现有类的属性、方法,或者是类之间的关系。

站在 AOP 的角度,OOP 中的原始类是要代理或增强的目标对象,称之为 target object。 经过切面增强过的类,称之为目标对象的 AOP 代理(proxy)对象。 而为目标对象生成其 AOP 代理对象的过程,称之为织入(Weaving)。

01.1-Spring 对 AOP 的支持

Spring 对 AOP 的支持通过 org.springframework.aop 中的 API 接口实现。 其中,核心的一个接口是 org.springframework.aop.Pointcut

public interface Pointcut {
	ClassFilter getClassFilter();
	MethodMatcher getMethodMatcher();
	Pointcut TRUE = TruePointcut.INSTANCE;
}

在前面有提到过,切入点能够选中一组连接点。 Spring 目前支持的连接点类型是函数,即针对 Spring Bean 的函数进行增强。 Pointcut 中定义的方法分为两类,ClassFilter 从类角度判断是否需要增强,依据是 ClassFilter#matches(Class); MethodMatcher 判断某个类的特定方法是否需要增强,依据是 MethodMatcher#matches(Method, Class)。

Pointcut 之间支持 union、intersection 等运算操作,可以通过 Pointcuts#union 和 Pointcuts#intersection 实现,或者通过 ComposablePointcut#union 和 ComposablePointcut#intersection 实现。

另外一个比较核心的接口是 org.springframework.aop.Advisor

public interface Advisor {
	Advice EMPTY_ADVICE = new Advice() {};
	Advice getAdvice();
	boolean isPerInstance();
}

Advisor 是 Spring AOP 中的一个基础接口,用来保存一个 Advice 对象及在哪些连接点应用该 Advice 的过滤器(例如 Pointcut)。 它的一个子类 PointcutAdvisor 就表示使用的过滤器类型为 Pointcut。

public interface PointcutAdvisor extends Advisor {
	Pointcut getPointcut();
}

Advisor 的另一个子类为 IntroductionAdvisor,使用的过滤器为 ClassFilter。

01.2-Spring 对 AspectJ 的支持

AspectJ 是基于 Java 实现的 AOP 框架,是一个功能全面的 AOP 实现。 正如 Spring 官网所说的那样,“Spring AOP 与 AspectJ 从来都不是竞争对手”。 Spring AOP 的目的并不是提供一个完整、全面的 AOP 实现(例如 AspectJ),而是为 Spring IoC 提供必要的支持功能。 Spring 无缝地集成了 AspectJ,允许你使用 AspectJ 中的大部分功能。

在 Spring 中,可以通过 @EnableAspectJAutoProxy 开启对 AspectJ 的支持;例如:

@EnableAspectJAutoProxy
@Configuration
public class Config {}

或者在使用 XML 配置时,可以通过 <aop:aspectj-autoproxy/> 标签。

@EnableAspectJAutoProxy 注解的内容如下:

@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
	/**
	 * true,表示使用 CGLIB 生成代理,基于 类继承
         * false,默认值,表示使用 JDK 生成代理,基于 接口实现
	 */
	boolean proxyTargetClass() default false;

	/**
	 * true,使用 ThreadLocal 变量,可以在 AopContext 访问
         * false,默认值,不保证可以在 AopContext 中正常访问
	 */
	boolean exposeProxy() default false;

}

为何 @EnableAspectJAutoProxy 能够开启 Spring 对 AspectJ 的支持?当你在配置类上标注了这个注解后 Spring 都做了些什么动作?

接下来,我将结合源码与大家一同探究下上面两个问题的答案。 在下面的代码分析过程中,我使用的是 AnnotationConfigApplicationContext 作为应用上下文容器,即采用 Java 类作为配置文件。 它内部包含一个 reader 和 scanner,用于发现、读取 BeanDefinition,并将它们注册到 Registry(BeanFactory)容器中。

首先,AnnotatedBeanDefinitionReader 会向 Registry 中注册一些关键的 BeanDefinition 定义,这其中就包括 org.springframework.context.annotation.ConfigurationClassPostProcessor,用来处理 @Configuration 标注的配置类。 在 AnnotationConfigApplicationContext#refresh 阶段,ConfigurationClassPostProcessor 作为 BeanDefinitionRegistryPostProcessor 接口的一个实现,会被回调到 processConfigBeanDefinitions 方法。 在这个方法里,所有被 @Configuration 注解标注的类,即所有的配置类,会被 ConfigurationClassParser 解析并处理。 解析配置类的过程中,会检查它是否导入其他的配置。 这时就会发现 @EnableAspectJAutoProxy 通过 @Import 导入了 AspectJAutoProxyRegistrar。

AspectJAutoProxyRegistrar 是 ImportBeanDefinitionRegistrar 接口的一个实现,会被添加到解析过的配置类中。

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
                // 向 registry(容器)中注册
                // org.springframework.aop.config.internalAutoProxyCreator -> new RootBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class)
		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

                /**
                * 根据 @EnableAspectJAutoProxy 注解标签中的内容进行设置
                */
		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy != null) {
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                                // 强制使用 CGLIB
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                                // 确保在 AopContext 中可以正常访问到 代理对象
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}
}

然后,ConfigurationClassBeanDefinitionReader#loadBeanDefinitions 会从上述解析过的配置类中加载(读取、注册)所有发现的 BeanDefinition。 在 loadBeanDefinitions 方法中,会调用到前面步骤中发现的 AspectJAutoProxyRegistrar#registerBeanDefinitions 方法。

最后,在 registerBeanDefinitions 中,AnnotationAwareAspectJAutoProxyCreator 会被注册到 BeanFactory 中。 它负责处理所有的 AspectJ 注解,例如 @Aspect、@Before 等等,以及 Spring 中的 Advisor。

01.3-AnnotationAwareAspectJAutoProxyCreator 如何为 Bean 创建代理?

AnnotationAwareAspectJAutoProxyCreator 是如何与 Spring IoC 容器整合在一起的,或者换句话说它是如何为 Bean 创建代理的?

我们看下这个类的继承结构: Spring Boot「49」扩展:Spring AOP 从上图中可以看出,AnnotationAwareAspectJAutoProxyCreator 实现了多个 *BeanPostProcessor 接口。

结合之前在 《# Spring Boot「48」扩展:深入分析 IoC 容器》 中介绍的 Bean 的创建过程,我接下来会分析 AnnotationAwareAspectJAutoProxyCreator 在什么阶段起作用。 在此之前,先回顾 Bean 的创建过程,以及 *BeanPostProcessor 接口作用在什么阶段。

Spring Boot「49」扩展:Spring AOP

从图上可以看到,在创建 Bean 时,首先会先调用 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation,该方法可以返回 null 或者其他对象(可以是代理对象)。 返回非 null 值后,就会立即中止创建流程,后续只有 BeanPostProcessor#postProcessAfterInitialization 会继续执行。 接下来,我们来看 AnnotationAwareAspectJAutoProxyCreator 的“父类” AbstractAutoProxyCreator 对 postProcessBeforeInstantiation 的实现。

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
    Object cacheKey = getCacheKey(beanClass, beanName);

    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
        // 已经代理过的,避免再次代理
        if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
        }
        // 无需代理的类、应当跳过的类
        // 例如 Aspect 类,Advisor 类等
        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
        }
    }

    /**
    * 如果有自定义的 TargetSourceCreator,则根据它来创建代理
    * 注:我们使用 IoC 容器时,一般不会指定 TargetSourceCreator,所以这里的 targetSource == null
    * 如果要使用自定义的 TargetSourceCreator,可以通过 setCustomTargetSourceCreators 来设置
    */
    // Create proxy here if we have a custom TargetSource.
    // Suppresses unnecessary default instantiation of the target bean:
    // The TargetSource will handle target instances in a custom fashion.
    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    if (targetSource != null) {
        if (StringUtils.hasLength(beanName)) {
            this.targetSourcedBeans.add(beanName);
        }
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
        Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

    return null;
}

根据前面的分析,一般代理对象并不在 postProcessBeforeInstantiation 中创建。

我们接着来看后面的流程。 AbstractAutoProxyCreator 还实现了 postProcessAfterInitialization 方法(注:绝大多数代理类,都在这里创建):

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
        // 表示 bean 已经被代理过了,避免重复代理
        return bean;
    }
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
        // 已经处理过,且不需要代理
        return bean;
    }
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
        // 未处理过,不需要代理,保存下来
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }

    /**
    * 由子类实现,用来为 bean 收集可用的 advisor 或 advice
    */
    // Create proxy if we have advice.
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
        this.advisedBeans.put(cacheKey, Boolean.TRUE);
        // 创建代理
        Object proxy = createProxy(
                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));  // 注意这里的 SingletonTargetSource
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }
    // 没找到可用的 advice 或 advisor,不代理
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
}

通过上面的分析,我们了解到 Bean 在被创建完成后(即 createBean 的流程执行完毕后),会在最后 postProcessAfterInitialization 回调时根据 Bean 以及当前的代理信息来确定是否为其生成对应的代理。

还有另外一点需要注意,AbstractAutoProxyCreator 还实现了 SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference 接口。 对 getEarlyBeanReference 的调用,会在 Bean 对象创建完毕后(执行 InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation 之前),根据是否允许 earlySingletonExposure,然后作为 ObejectFactory 加入到 singletonFactories 中。 singletonFactories 是我们之前分析循环引用中提到的三级对象缓存。 添加到 singletonFactories 后,当后续通过 getSingleton(beanName, allowEarlyReference) 获取 Bean 对象时,调用它的 getObject 方法,从而执行 getEarlyBeanReference。

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

可以看到,最终也是由 wrapIfNecessary 根据情况来进行代理类的创建。

wrapIfNecessary 方法中创建代理时使用到了一个 SingletonTargetSource 类。 它是 Spring 提供的 TargetSource 接口实现类,也是 Spring AOP 默认使用的实现类。 TargetSource 的作用是在 AOP 代理类执行时为其提供目标对象。 SingletonTargetSource 中包含了一个 target 属性,用来存储目标对象。 而且它的 isStatic 返回 true,表示多次调用 getTarget 方法,获得的是同一个对象。

到这里为止,总结一下这节的内容。 Spring 通过 @EnableAspectJAutoProxy 注解来开启对 AspectJ(各类注解,允许以 AspectJ 的方式定义 Advice 等)的支持。 @EnableAspectJAutoProxy 通过 @Import 引入了 AspectJAutoProxyRegistrar。 后者负责向 BeanFactory 容器中注册名称为 “org.springframework.aop.config.internalAutoProxyCreator” 的 BeanDefinition,实现类是 AnnotationAwareAspectJAutoProxyCreator。 AnnotationAwareAspectJAutoProxyCreator 是 *BeanPostProcessor 接口的实现类,它会在 Bean 的创建过程中,根据 Bean 的信息决定是否创建代理。

02-Spring AOP 示例程序

在本小节中,我将通过一个 Demo 程序演示前面介绍的东西,帮助你更好地理解面向切面编程。 在正式开始之前,我先来介绍下我演示使用的程序。 假设有一个 IAnimal 接口,它定义了动物捕猎的天性。

/**
 * @author Samson
 */
public interface IAnimal {
    /**
     * 捕猎
     * @return 猎物
     */
    String hunt();
}

然后,两种动物 Cat 和 Tiger 都实现了这一接口。

/**
 * @author Samson
 */
@Component
public class Cat implements IAnimal{
    @Override
    public String hunt() {
        System.out.println("猫咪捕到了一只老鼠!");
        return "老鼠";
    }
}

/**
 * @author Samson
 */
@Component
public class Tiger implements IAnimal{
    @Override
    public String hunt() {
        System.out.println("老虎抓到了一只羊!");
        return "羊";
    }
}

在没有任何 AOP 之前,我先来编写一个程序,来运行一下,然后观察下它们的输出。

@ComponentScan
@Configuration
public class Config {
}

/**
 * @author Samson
 */
public class DemoAnimalApplication {
    public static void main(String[] args) {
        final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        final Cat cat = context.getBean(Cat.class);
        final Tiger tiger = context.getBean(Tiger.class);

        cat.hunt();
        tiger.hunt();
    }
}

输出如下:

猫咪捕到了一只老鼠!
老虎抓到了一只羊!

然后,我们来编写一个切面,它的主要逻辑是在动物捕猎之前,即调用 hunt 方法之前,打印一下动物当前的状态,比如当前比较饥饿。

@Aspect
@Component
public class AnimalHuntAspect {

    @Pointcut("execution (* self.samson.example.spring.aop.demo.animal.IAnimal.hunt(..))")
    public void hunt() {}

    @Before("hunt()")
    public void beforeHunt() {
        System.out.println("(动物)感到一阵饥饿,于是准备捕猎。");
    }
}

然后,需要在 Config 类上增加 @EnableAspectJAutoProxy(proxyTargetClass = true) 注解。 之后再运行一下,就能得到如下的输出。

(动物)感到一阵饥饿,于是下山捕猎。
猫咪捕到了一只老鼠!
(动物)感到一阵饥饿,于是下山捕猎。
老虎抓到了一只羊!

@Pointcut 切入点定义了所有生效的连接点,即所有对 IAnimal#hunt 方法。 我借用 pdai 大佬 网站上的一幅图来帮助大家理解切入点中的表达式含义。 Spring Boot「49」扩展:Spring AOP

注:开启 AOP 支持时,为什么要特别指定 proxyTargetClass = true ?这里我解释下原因,proxyTargetClass = true 的意思是强制使用 CGLIB 生成代理。 我们知道 CGLIB 是通过继承实现的代理,Java 原生代理是通过实现接口实现的代理。 这里如果不指定 proxyTargetClass = true,我们运行时会抛如下的异常:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'self.samson.example.spring.aop.demo.animal.Cat' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1172)
	at self.samson.example.spring.aop.demo.animal.DemoAnimalApplication.main(DemoAnimalApplication.java:11)

更底层的原因在于我从容器中取对象时使用了 context.getBean(Cat.class)context.getBean(Tiger.class)。 如果使用 Java 原生代理,容器中存在的应该是两个 IAnimal 类型的 Bean。 如果不修改 proxyTargetClass = true,也可以通过如下方式:

final Map<String, IAnimal> beansOfType = context.getBeansOfType(IAnimal.class);
beansOfType.forEach((k, v) -> v.hunt());

希望今天的内容能对你有所帮助。

我正在参加「掘金·启航计划」

refs