Spring 源码学习(八) AOP 使用和实现原理(一)

本文涉及的产品
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 我们在业务开发中,使用得最多的是面向对象编程(OOP),因为它的代码逻辑直观,从上往下就能查看完整的执行链路。在这个基础上延伸,出现了面向切面编程(AOP),将可以重复性的横切逻辑抽取到统一的模块中。例如日志打印、安全监测,如果按照 OOP 的思想,在每个方法的前后都要加上重复的代码,之后要修改的话,更改的地方就会太多,导致不好维护。所以出现了 AOP 编程, AOP 所关注的方向是横向的,不同于 OOP 的纵向。所以接下来一起来学习 AOP 是如何使用以及 Spring 容器里面的处理逻辑~

创建用于拦截的 bean

public class TestAopBean {
    private String testStr = "testStr";
    public void testAop() {
        // 被拦截的方法,简单打印
        System.out.println("I am the true aop bean");
    }
}

创建 Advisor

@Aspect
public class AspectJTest {
    @Pointcut("execution(* *.testAop(..))")
    public void test() {
    }
    @Before("test()")
    public void beforeTest() {
        System.out.println("before Test");
    }
    @After("test()")
    public void afterTest() {
        System.out.println("after Test");
    }
    @Around("test()")
    public Object aroundTest(ProceedingJoinPoint joinPoint) {
        System.out.println("around Before");
        Object o = null;
        try {
            // 调用切面的方法
            o = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("around After");
        return o;
    }
}

首先类打上了 @Aspect 注解,让 Spring 认识到这个是一个切面 bean,在方法打上 @Pointcut("execution(* *.testAop(..))"),表示这是一个切点方法,execution() 内部的表达式指明被拦截的方法,BeforeAfterAround 分别表示在被拦截方法的前、后已经环绕执行。


创建配置文件 aop.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启 AOP 功能-->
    <aop:aspectj-autoproxy />
    <bean id="aopTestBean" class="aop.TestAopBean"/>
    <bean class="aop.AspectJTest" />
</beans>

测试 Demo

public class AopTestBootstrap {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop/aop.xml");
        TestAopBean bean = (TestAopBean) context.getBean("aopTestBean");
        bean.testAop();
        // 输出内容 看输出顺序,了解到增强方法的执行顺序 :
        // Around proceed 之前 -> Before -> Around proceed 之后 -> After
        //around Before
        //before Test
        //I am the true aop bean
        //around After
        //after Test
    }
}

根据上面的启动例子,发现在自己写的核心业务方法 testAop() 上,明明只是简单打印了 I am the true aop bean,但执行结果输出了其它内容,说明这个类被增强了,在不修改核心业务方法上,我们对它进行了扩展。证明了  AOP 可以使辅助功能独立于核心业务之外,方便了程序的扩展和解耦。

使用起来很方便,接下来一起来看看 Spring 是如何实现 AOP 功能的吧~


动态 AOP 自定义标签

之前在介绍自定义标签时,提到了 AOP 的实现也借助了自定义注解,根据自定义标签的思想:每个自定义的标签,都有对应的解析器,然后借助强大的开发工具 IDEA 定位功能,找到解析器注册的地方:

0.jpg

  1. 按住 `ctrl`,定位标签对应的 `xsd` 文件
  2. 根据命名文件,在 `META-INF` 目录下找到了 `spring.handlers` 文件
  3. 在处理器文件中发现了处理器 `AopNamespaceHandler`
public class AopNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // In 2.0 XSD as well as in 2.1 XSD.
        registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
        // 注释 8.1 自定义注解,注册解析器,元素名是 aspectj-autoproxy
        registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
        registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
        // Only in 2.0 XSD: moved to context namespace as of 2.1
        registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
    }
}

处理器继承自 NamespaceHandlerSupport,在加载过程中,将会执行 init 初始化方法,在这里,会注册 aspectj-autoproxy 类型的解析器 AspectJAutoProxyBeanDefinitionParser

如何注册自定义解析器之前也了解过了,所以接下来直接来看看,遇到 aspectj-autoproxy 类型的 bean,程序是如何解析的。


注册 AnnotationAwareAspectJAutoProxyCreator

来看下解析时,它的入口方法如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // aop 注解的解析入口,注册 AnnotationAwareAspectJAutoProxyCreator
    AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
    // 对注解中子类的处理
    extendBeanDefinition(element, parserContext);
    return null;
}

入口方法一如既往的简洁,交代了要做的事情,然后具体复杂逻辑再交给工具类或者子类继续实现,所以接下来要看的是如何注册 AnnotationAwareAspectJAutoProxyCreator

public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
        ParserContext parserContext, Element sourceElement) {
    // 通过工具类,注册或升级 AspectJAnnotationAutoProxyCreator
    BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
            parserContext.getRegistry(), parserContext.extractSource(sourceElement));
    // 处理 proxy-target-class 以及 expose-proxy 属性
    useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
    // 注册组件并通知,让监听器进行处理
    registerComponentIfNecessary(beanDefinition, parserContext);
}

可以看到这个方法内部有三个处理逻辑,所以我们来一个一个去分析了解:


注册或者升级 AnnotationAwareAspectJAutoProxyCreator

对于 AOP 的实现,基本上都是靠 AnnotationAwareAspectJAutoProxyCreator 去完成,它可以根据 @Point 注解定义的切点来自动代理相匹配的 bean

由于 Spring 替我们做了很多工作,所以开发 AOP 业务时才可以这么简单,连配置也简化了许多,所以来看下 Spring 是如何使用自定义配置来帮助我们自动注册 AnnotationAwareAspectJAutoProxyCreator

public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
        BeanDefinitionRegistry registry, @Nullable Object source) {
    // 实际注册的 bean 类型是 AnnotationAwareAspectJAutoProxyCreator
    return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}
private static BeanDefinition registerOrEscalateApcAsRequired(
        Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
    if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
        // 如果在 registry 已经存在自动代理创建器,并且传入的代理器类型与注册的不一致,根据优先级判断是否需要修改
        BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
            // 根据优先级选择使用哪一个
            int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
            int requiredPriority = findPriorityForClass(cls);
            if (currentPriority < requiredPriority) {
                // 传进来的参数优先级更大,修改注册的 beanName,使用传进来的代理创建器
                apcDefinition.setBeanClassName(cls.getName());
            }
        }
        // 因为已经存在代理器,不需要之后的默认设置,直接返回
        return null;
    }
    RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
    beanDefinition.setSource(source);
    // 默认的是最小优先级
    beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
    beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    // 自动代理创建器的注册名字永远是 org.springframework.aop.config.internalAutoProxyCreator
    registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
    return beanDefinition;
}

这个步骤中,实现了自动注册 AnnotationAwareAspectJAutoProxyCreator 类,同时能看到涉及到优先级的概念和注册名一直都是 AUTO_PROXY_CREATOR_BEAN_NAME


处理 proxy-target-class 以及 expose-proxy 属性

private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) {
    if (sourceElement != null) {
        // 这方法作用挺简单的,就是解析下面两个属性,如果是 true,将它们加入代理注册器的属性列表中
        // definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE)
        boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE));
        if (proxyTargetClass) {
            // 处理  proxy-target-class 属性
            // 与代码生成方式有关,在之后步骤中决定使用 jdk 动态代理 或 cglib
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
        }
        boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE));
        if (exposeProxy) {
            // 处理 expose-proxy 属性
            // 扩展增强,有时候目标对象内部的自我调用无法实施切面中的增强,通过这个属性可以同时对两个方法进行增强
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
        }
    }
}

关于 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); 方法,它是一个属性设置的过程,如果解析到的属性为 true,将它们加入代理注册器的属性列表中,这里不细说下去。

将这两个属性分开熟悉:


proxy-target-class

Spring AOP 部分使用 JDK 动态代理 (Proxy + InvocationHandler),或者 CGLIB (Code Generation LIB)来为目标对象创建代理。书中提到,推荐使用的是 JDK 动态代理。

如果被代理的目标对象实现了至少一个接口,则会使用 JDK 动态代理。所有该目标类型实现的接口都将被代理。

若该目标对象没有实现任何接口,则创建一个 CGLIB 代理。如果希望代理目标对象的所有方法,而不只是实现自接口的方法,可以通过该属性 proxy-target-class 开启强制使用 CGLIB 代理。

但是强制开启 CGLIB 会有以下两个问题:

  • 无法同时(advise)Final 方法,因为他们不能被覆写
  • 需要将 CGLB 二进制发行包放在 classpath 下面

如果考虑好上面两个方面,那就可以通过以下两个地方来强制开启 CGLIB 代理:

<!-- one -->
<aop:config proxy-target-class="true">...</aop:config>
<!-- two -->
<aop:aspectj-autoproxy proxy-target-class="true"/>

其中有关 CGLIB 代理,这位老哥讲得很透彻,建议大家可以去了解一下~ Cglib及其基本使用


expose-proxy

有时候目标对象内部的自我调用将无法实施切面中的增强。

例如两个方法都加上了事务注解 @Transactional 但是事务类型不一样:

public interface TestService {
    void a();
    void b();
}
public class TestServiceImpl implements TestService {
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void a() {
        this.b();
    }
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void b() {
        System.out.println("Hello world");
    }
}

此处的 this 指向了目标对象, this.b() 方法将不会执行 b 事务的切面,即不会执行事务增强。

为了解决这个问题,使 a()b() 方法同时增强,可以通过 expose-proxy 来实现:

<!-- one -->
<aop:config expose-proxy="true">...</aop:config>
<!-- two -->
<aop:aspectj-autoproxy expose-proxy="true"/>

注册组件并通知

emmmm,这个方法内部逻辑如名字一样清晰,所以不细说啦。



相关文章
|
23小时前
|
Java 测试技术 数据安全/隐私保护
Spring Boot中的AOP编程实践
Spring Boot中的AOP编程实践
|
1天前
|
Cloud Native Java 开发者
深入解析Spring Framework的核心设计原理
深入解析Spring Framework的核心设计原理
|
1天前
|
监控 Java 数据安全/隐私保护
Spring AOP实现原理及其在企业应用中的实际应用
Spring AOP实现原理及其在企业应用中的实际应用
|
1天前
|
监控 Java 数据安全/隐私保护
Spring AOP实现原理及其在企业应用中的实际应用
Spring AOP实现原理及其在企业应用中的实际应用
|
1天前
|
XML 监控 Java
Spring框架的核心原理与应用实践
Spring框架的核心原理与应用实践
|
3天前
|
消息中间件 安全 Java
学习认识Spring Boot Starter
在SpringBoot项目中,经常能够在pom文件中看到以spring-boot-starter-xx或xx-spring-boot-starter命名的一些依赖。例如:spring-boot-starter-web、spring-boot-starter-security、spring-boot-starter-data-jpa、mybatis-spring-boot-starter等等。
17 4
|
4天前
|
Java 开发者 Spring
使用Spring Boot AOP实现日志记录
使用Spring Boot AOP实现日志记录
|
4天前
|
XML 安全 Java
Spring 基础知识学习
Spring 基础知识学习
|
4天前
|
Java Spring 容器
Spring5系列学习文章分享---第六篇(框架新功能系列+整合日志+ @Nullable注解 + JUnit5整合)
Spring5系列学习文章分享---第六篇(框架新功能系列+整合日志+ @Nullable注解 + JUnit5整合)
5 0
|
4天前
|
XML Java 数据库
Spring5系列学习文章分享---第五篇(事务概念+特性+案例+注解声明式事务管理+参数详解 )
Spring5系列学习文章分享---第五篇(事务概念+特性+案例+注解声明式事务管理+参数详解 )
6 0