Spring从成神到升仙系列 三2023年再不会 AOP 源码,就要被淘汰了

Posted 爱敲代码的小黄

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring从成神到升仙系列 三2023年再不会 AOP 源码,就要被淘汰了相关的知识,希望对你有一定的参考价值。

  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙、Spring从成神到升仙系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,和大家一起学习,一起进步👀

文章目录

Spring AOP源码解析

一、引言

对于Java开发者而言,关于 Spring ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 Spring 源码解析系列文章,将带你领略 Spring 源码的奥秘

本期源码文章吸收了之前 Kafka 源码文章的错误,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

废话不多说,发车!

本文流程图可关注公众号:爱敲代码的小黄,回复:AOP 获取
贴心的小黄为大家准备的文件格式为 POS文件,方便大家直接导入 ProcessOn 修改使用

二、适合人群

本文采用的 Spring 版本为:4.3.11.RELEASE

本文适合人群:使用过Spring AOP的功能,了解过切面编程

本文前置知识:

如果你都已经满足,那么跟我一起了解一下抽象的 AOP 吧。

三、Spring AOP配置

首先创建我们的核心类:BeanA

public class BeanA 
    public void do1() 
        System.out.println("I am a do1, I start....");
    

我们日志配置类:LogUtil

public class LogUtil 

    private void before(JoinPoint joinPoint) 
        //获取方法签名
        Signature signature = joinPoint.getSignature();
        //获取参数信息
        Object[] args = joinPoint.getArgs();
        System.out.println("log---" + signature.getName() + "I am before");
    

    private void after(JoinPoint joinPoint) 
        //获取方法签名
        Signature signature = joinPoint.getSignature();
        //获取参数信息
        Object[] args = joinPoint.getArgs();
        System.out.println("log---" + signature.getName() + "I am before");
    

我们 xml 配置:application.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
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="beanA" class="com.hls.aop.BeanA"/>

    <bean id="logUtil" class="com.hls.aop.LogUtil"/>

    <aop:config>
        <aop:aspect ref="logUtil">
            <aop:pointcut id="myPoint"  expression="execution(* com.hls.aop.BeanA.do*(..))"/>
            <aop:before method="before" pointcut-ref="myPoint"/>
            <aop:after method="after" pointcut-ref="myPoint"/>
        </aop:aspect>
    </aop:config>
</beans>

编写我们的测试类:AOPTest

public class AOPTest 
    public static void main(String[] args) 
        ApplicationContext context = new GenericXmlApplicationContext("application.xml");

        BeanA beanA = context.getBean(BeanA.class);

        beanA.do1();
    

运行输出结果:

log---do1I am before
I am a do1, I start....
log---do1I am before

四、Spring AOP 组件分析

AOP 的组件在源码中比较重要的有四个:

  • Pointcut:定义切面的匹配点,主要是类和方法
  • Advice:定义切面的行为,即在匹配点执行的操作。
  • Advisor:将 Pointcut 和 Advice 组合成一个对象,表示一个完整的切面。
  • Aspect:使用注解或 XML 配置方式定义切面,通常包含多个 Advisor

我们依次讲解这些组件是什么意思

1、Pointcut

一般情况下我们的 Pointcut 都是以这种形式出现:

<aop:pointcut id="myPoint" expression="execution(* com.mashibing.hls.aop.BeanA.do*(..))"/>

主要就是指明切入点的表达式,比如上述表达式:* com.hls.aop.BeanA.do*(..)

代表 com.hls.aop 包下的 BeanA 类以 do 开头的方法都可以进行切入

我们看下源码中的形式:

public interface Pointcut 
	/**
	 * 在类级别上限定joinpoint的匹配范围
	 *
	 * Return the ClassFilter for this pointcut.
	 * @return the ClassFilter (never @code null)
	 */
	ClassFilter getClassFilter();

	/**
	 * 在方法级别上限定joinpoint的匹配范围
	 *
	 * Return the MethodMatcher for this pointcut.
	 * @return the MethodMatcher (never @code null)
	 */
	MethodMatcher getMethodMatcher();


	/**
	 * 用于匹配上的一个实例,永远返回true
	 *
	 * Canonical Pointcut instance that always matches.
	 */
	Pointcut TRUE = TruePointcut.INSTANCE;




// Pointcut 的实现
public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware 
    // 类的匹配
    public boolean matches(Class<?> targetClass) 
    
    // 方法的匹配 
    public boolean matches(Method method, Class<?> targetClass, boolean hasIntroductions) 

通过源码我们可以发现,这个类提供的功能主要是用来匹配类与方法。

2、Advice

一般情况下,我们的 Advice 以这种情况出现:

<aop:before/>
<aop:after/>

主要的目的就是定义我们切面的行为,比如:方法前切入、方法后切入、环绕切入等

我们看下源码的形式:

public interface Advice 



// 前置切入实现
public interface BeforeAdvice extends Advice 

// 后置切入实现
public interface AfterAdvice extends Advice 

一般我们的 Advice 都与 Pointcut 相结合使用

3、Advisor

一般情况下,我们的 Advisor 以这种情况出现:

<aop:before method="before" pointcut-ref="myPoint"/>
<aop:after method="after" pointcut-ref="myPoint"/>

AdvicePointcut 结合起来使用

我们看下源码的形式:

public interface Advisor 
   // 返回切面对应的通知
	Advice getAdvice();

很明显可以看出,这个类主要包装了 Advice,后续我们会继续讲到

4、Aspect

一般情况下,我们的 Aspect 以这种情况出现:

<aop:aspect ref="logUtil">
</aop:aspect>

定义一个日志配置类,其方法作为实际的切入业务逻辑

5、总结

从我们上述的描述得知,我们几个组件的关系如下:

大家这里心里有个大致的了解即可,我们后面会详细讲到。

五、Spring AOP 源码剖析

同样和我们的 IOC 类似,我们从入口开始:

ApplicationContext context = new GenericXmlApplicationContext("application.xml");

1、beanDefinition的注册

同样和我们 IOC 流程一样,先进行 xml 文件的解析

我们直接找到 DefaultBeanDefinitionDocumentReader 类的 parseBeanDefinitions 方法

这里从业务来说,做了两件事:

  • 注册了 AspectJAwareAdvisorAutoProxyCreator 的类,方便之后代理的扩展
  • 解析 xml<aop:config> 的配置,最终生成 AspectJPointcutAdvisorBeanDefinition 注册至 DefaultListableBeanFactoryBeanDefinitionMap

1.1 AspectJAwareAdvisorAutoProxyCreator的注册

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) 
		if (delegate.isDefaultNamespace(root)) 
			NodeList nl = root.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) 
				Node node = nl.item(i);
				if (node instanceof Element) 
					Element ele = (Element) node;
					if (delegate.isDefaultNamespace(ele)) 
                   // 正常的标签,IOC中讲过
						parseDefaultElement(ele, delegate);
					
					else 
                   // <AOP>、<dubbo> 类的标签
						delegate.parseCustomElement(ele);
					
				
			
		
	

public BeanDefinition parseCustomElement(Element ele) 
    return parseCustomElement(ele, null);


public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) 
    // 获取对应的命名空间
    String namespaceUri = getNamespaceURI(ele);
    
    // 根据命名空间找到对应的NamespaceHandlerspring
    //  因为我们这里处理的是 AOP 的标签,所以会有 AopNamespaceHandler 的执行
    NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
    
    // 调用自定义的NamespaceHandler(AopNamespaceHandler)进行解析
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));


public BeanDefinition parse(Element element, ParserContext parserContext) 
    // 获取元素的解析器
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    return (parser != null ? parser.parse(element, parserContext) : null);



public BeanDefinition parse(Element element, ParserContext parserContext) 
    CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element));
		parserContext.pushContainingComponent(compositeDef);
		// 注册自动代理模式创建器,AspectjAwareAdvisorAutoProxyCreator
		configureAutoProxyCreator(parserContext, element);



private void configureAutoProxyCreator(ParserContext parserContext, Element element) 
    AopNamespaceUtils.registerAspectJAutoProxyCreatorIfNecessary(parserContext, element);


public static void registerAspectJAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) 
		// 注册名为org.springframework.aop.config.internalAutoProxyCreator的beanDefinition,其中的class类为`AspectJAwareAdvisorAutoProxyCreator`,其也会被注册到bean工厂中
		BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAutoProxyCreatorIfNecessary(
				parserContext.getRegistry(), parserContext.extractSource(sourceElement));
		// 如果指定proxy-target-class=true,则使用CGLIB代理,否则使用JDK代理
		// 其实其为AspectJAwareAdvisorAutoProxyCreator类的proxyTargetClass属性
		useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
		// 注册到spring的bean工厂中,再次校验是否已注册
		registerComponentIfNecessary(beanDefinition, parserContext);
	

public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary( BeanDefinitionRegistry registry, @Nullable Object source) 
    return registerOrEscalateApcAsRequired(AspectJAwareAdvisorAutoProxyCreator.class, registry, source);


private static BeanDefinition registerOrEscalateApcAsRequired( Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) 
       // cls = AspectJAwareAdvisorAutoProxyCreator
		RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
		beanDefinition.setSource(source);
		beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
		beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
      // key = org.springframework.aop.config.internalAutoProxyCreator
      // value = org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator
      // 这里正式注册了 !!!
		registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
		return beanDefinition;
	

有读者可能疑惑,这里为什么要大费周折注册一个 AspectJAwareAdvisorAutoProxyCreator 这个类,他有什么特殊的地方嘛

这里先卖个关子,后续我们会讲到

1.2、AspectJPointcutAdvisor 的注册

由于这一段的注册逻辑写的很绕、很晦涩难懂,这里博主带大家大致的浏览下流程

不然,直接劝退了大部分人

我们跟着上面这部分源码继续往下看:

public BeanDefinition parse(Element element, ParserContext parserContext) 
    CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element));
		parserContext.pushContainingComponent(compositeDef);
		// 注册自动代理模式创建器,AspectjAwareAdvisorAutoProxyCreator
		configureAutoProxyCreator(parserContext, element);
      // 解析aop:config子节点下的aop:pointcut/aop:advice/aop:aspect
		List<Element> childElts = DomUtils.getChildElements(element);
		for (Element elt: childElts) 
			String localName = parserContext.getDelegate().getLocalName(elt);
			if (POINTCUT.equals(localName)) 
				parsePointcut(elt, parserContext);
			
			else if (ADVISOR.equals(localName)) 
				parseAdvisor(elt, parserContext);
			
          // 因为我们上面是 <aop:aspect/> 的标签,会走这个业务逻辑
			else if (ASPECT.equals(localName)) 
				parseAspect(elt, parserContext);
			
		

		parserContext.popAndRegisterContainingComponent();
		return null;


private void parseAspect(Element aspectElement, ParserContext parserContext) 
		// <aop:aspect> id属性
		String aspectId = aspectElement.getAttribute(ID);
		// aop ref属性,必须配置。代表切面
		String aspectName = aspectElement.getAttribute(REF);

		try 
			List<BeanDefinition> beanDefinitions = new ArrayList<>();
			List<BeanReference> beanReferences = new ArrayList<>();
          
			// 解析其下的advice节点
			NodeList nodeList = aspectElement.getChildNodes();
			boolean adviceFoundAlready = false;
			for (int i = 0; i < nodeList.getLength(); i++) 
				Node node = nodeList.item(i);
				// 是否为advice:before/advice:after/advice:after-returning/advice:after-throwing/advice:around节点
             // 这个就是 advice 的种类,不明白的小伙伴可以百度一下
				if (isAdviceNode(node, parserContext)) 
					// 校验aop:aspect必须有ref属性,否则无法对切入点进行观察操作
					if (!adviceFoundAlready) 
						adviceFoundAlready = true;
						if (!StringUtils.hasText(aspectName)) 
							return;
						
						beanReferences.add(new RuntimeBeanReference(aspectName));
					
					// 解析advice节点并注册到bean工厂中
					AbstractBeanDefinition advisorDefinition = parseAdvice(
							aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences);
					beanDefinitions.add(advisorDefinition);
				
			

			AspectComponentDefinition aspectComponentDefinition = createAspectComponentDefinition(
					aspectElement, aspectId, beanDefinitions, beanReferences, parserContext);
			parserContext.pushContainingComponent(aspectComponentDefinition);

			// 解析aop:point-cut节点并注册到bean工厂
			List<Element> pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT);
			for (Element pointcutElement : pointcuts) 
				parsePointcut(pointcutElement, parserContext);
			

			parserContext.popAndRegisterContainingComponent();
		
		finally 
			this.parseState.pop();
		
	

到这里我们先停一下,从上述源码中我们得到了一些信息,这部分源码的注册基本就三个方面:

  • 解析 Advice 节点并注册到 bean 工厂中
  • 解析 Advisor 节点注册到 bean 工厂中
  • 解析 Pointcut 节点并注册到 bean 工厂
// 整体的注册方法
AbstractBeanDefinition advisorDefinition = parseAdvice(aspectName, i, aspectElement,  
 




2019.11.15

好味廚 
20:30-22:00


「CSSAUG」財神到~硬幣兌換來啦!


「CSSAUG」財神到~硬幣兌換來啦!

亨利 亞當斯

人在香港,剛下飛機,還沒上公交車。

身上只有一張百萬英鎊支票

誰能幫我換下硬幣

一百萬,公交車司機實在找不開

在線等,急

「CSSAUG」財神到~硬幣兌換來啦!

59分钟前

「CSSAUG」財神到~硬幣兌換來啦!
「CSSAUG」財神到~硬幣兌換來啦!

馬克 吐溫, 平安香港, CSSAUG

CSSAUG :還等什麼~快來我們的硬幣兌換活動吧!


「CSSAUG」財神到~硬幣兌換來啦!

五阿哥

謝CSSAUG解決了我的大麻煩!

感恩!

「CSSAUG」財神到~硬幣兌換來啦!

30分钟前

「CSSAUG」財神到~硬幣兌換來啦!
「CSSAUG」財神到~硬幣兌換來啦!

皇上, 三阿哥, 公主, 駙馬爺, 羅天師, CSSAUG

CSSAUG : 感謝永琪老鐵的宣傳!我們11.15恭候您的到來!


「CSSAUG」財神到~硬幣兌換來啦!

CSSAUG

為滿足CSSAUG會員對不同種類錢幣的需求,方便會員手中錢幣的儲存及使用,我們為會員們搭建了一個硬紙幣(HKD)兌換的平臺--硬幣兌換活動~!


我們可以幫大家把紙幣兌換成硬幣,或將硬幣兌換成紙幣~


對於以下幾種情況,需要您點擊下方閱讀全文進行登記哦~

1. 硬幣兌換紙幣金額超過100HKD

2. 紙幣兌換硬幣超過20枚

3. 有需要特定面額硬幣的要求

※ 建議兌換其他金額的會員填寫預約表格(可選),我們會優先保證有預約會員的兌換。

活動時間:2019年11月15日(星期五)20:30~22:00

活動地點:學生宿舍區Hall Canteen 好味廚 (好妹子)

預約截止時間 :2019年11月10日23:59



「CSSAUG」財神到~硬幣兌換來啦!

26分钟前

「CSSAUG」財神到~硬幣兌換來啦!
「CSSAUG」財神到~硬幣兌換來啦!

五阿哥,小燕子,容嬤嬤, 亨利 亞當斯,馬克吐溫, CSSAUG

五阿哥:老鐵666沒毛病!

 亨利 亞當斯 :現在我旁邊有一卡車硬幣,誰能來幫我搬一下,在線等,急。


「CSSAUG」財神到~硬幣兌換來啦!

CSSAUG 福利部 徐陳晨

大家好呀~!

我是此次硬幣兌換的pic徐陳晨,大家有什麼問題可以聯繫我呢~左邊是我的微信!

郵箱:chennchenn16@gmail.com

電話:46400710


右邊是福利部部長姬皓冉的微信哦~

大家有什麼疑問也可以問他呢!

郵箱:1565877096ji@gmail.com

電話:46405474


「CSSAUG」財神到~硬幣兌換來啦!
「CSSAUG」財神到~硬幣兌換來啦!

26分钟前

亨利 亞當斯

亨利 亞當斯:能不能來幫我搬下硬幣...


                                                                                                   文案&排版:吳子揚


以上是关于Spring从成神到升仙系列 三2023年再不会 AOP 源码,就要被淘汰了的主要内容,如果未能解决你的问题,请参考以下文章

2023年再不会 IOC 源码,就要被淘汰了

Spring从成神到升仙系列 四从源码分析 Spring 事务的来龙去脉

Netty 从成神到升仙系列 三Netty 凭什么成为国内最流行的网络通信框架?

Kafka从成神到升仙系列 四你真的了解 Kafka 的缓存池机制嘛

Kafka从成神到升仙系列 五面试官问我 Kafka 生产者的网络架构,我直接开始从源码背起.......

回首2022,展望2023