【框架源码篇 03】Spring源码手写篇-手写AOP

news2024/11/18 7:33:01

Spring源码手写篇-手写AOP

  手写IoC和DI后已经实现的类图结构。

image.png

一、AOP分析

image.png

1.AOP是什么?

   AOP[Aspect Oriented Programming] 面向切面编程,在不改变类的代码的情况下,对类方法进行功能的增强。

2.我们要做什么?

  我们需要在前面手写IoC,手写DI的基础上给用户提供AOP功能,让他们可以通过AOP技术实现对类方法功能增强。

image.png

3.我们的需求是什么?

  提供AOP功能!,然后呢?… 没有了。关键还是得从上面的定义来理解。

image.png

image.png

二、AOP概念讲解

  上面在分析AOP需求的时候,我们介绍到了相关的概念,Advice、Pointcuts和weaving等,首先我们来看看在AOP中我们会接触到的相关的概念都有哪些。

image.png

更加形象的描述

image.png

然后对于上面的相关概念,我们就要考虑哪些是用户需要提供的,哪些是框架要写好的?

image.png

思考:Advice,Pointcuts和Weaving各自的特点

image.png

三、切面实现

  通过上面的分析,我们要设计实现AOP功能,其实就是要设计实现上面分析的相关概念对应的组件。

image.png

1.Advice

1.1 面向接口编程

  Advice:通知,是由用户提供的,我们来使用,主要是用户提供就突出了 多变性。针对这块我们应该怎么设计?这里有两个问题:

  1. 我们如何能够识别用户提供的东西呢?用户在我们写好框架后使用我们的框架
  2. 如何让我们的代码隔绝用户提供的多变性呢?

针对这种情况我们定义一套标准的接口,用户通过实现接口类提供他们不同的逻辑。是否可行?image.png

这里有个重要的设计原则大家要注意: 如何应对变化,通过面向接口编程来搞定!!!

我们先定义一个空的接口,可以先思考下我们为什么定义一个空的接口呢?

image.png

1.2 Advice的特点分析

  Advice的特点:可选时机,可选择在方法执行前、后、异常时进行功能的增强

image.png

结合上面的情况我们可以分析出Advice通知的几种情况

  • 前置增强-Before
  • 后置增强-AfterReturn
  • 环绕增强-Around
  • 最终通知-After
  • 异常通知-Throwing

有这么多的情况我们应该要怎么来实现呢?我们可以定义标准的接口方法,让用户来实现它,提供各种具体的增强内容。那么这四种增强相关的方法定义是怎样的呢?我们一一来分析下。

1.3 各种通知分析

1.3.1 前置增强

前置增强:在方法执行前进行增强。

问题1:它可能需要的参数?

  目的是对方法进行增强,应该需要的是方法相关的信息,我们使用它的时候能给如它的就是当前要执行方法的相关信息了

问题2:运行时方法有哪些信息?

  1. 方法本身 Method
  2. 方法所属的对象 Object
  3. 方法的参数 Object[]

问题3:前置增强的返回值是什么?

  在方法执行前进行增强,不需要返回值!

public interface MethodBeforeAdvice extends Advice {

	/**
	 * 实现该方法进行前置增强
	 * 
	 * @param method
	 *            被增强的方法
	 * @param args
	 *            方法的参数
	 * @param target
	 *            被增强的目标对象
	 * @throws Throwable
	 */
	void before(Method method, Object[] args, Object target) throws Throwable;
}
1.3.2 最终通知

  最终通知:在方法执行后进行增强

问题1:它可能需要的参数?

  • 方法本身 Method
  • 方法所属的对象 Object
  • 方法的参数 Object[]
  • 方法的返回值 Object 可能没有

问题2:它的返回值是什么?

  这个就需要看是否允许在After中更改返回的结果,如果规定只可用、不可修改返回值就不需要返回值

public interface AfterAdvice extends Advice {
	/**
	 * 实现该方法,提供后置增强
	 * 
	 * @param returnValue
	 *            返回值
	 * @param method
	 *            被增强的方法
	 * @param args
	 *            方法的参数
	 * @param target
	 *            方法的所属对象
	 * @throws Throwable
	 */
	void after(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
}
1.3.3 后置通知

  后置增强:在方法执行后进行增强

问题1:他可能需要的参数

  • 方法本身 Method
  • 方法所属的对象 Object
  • 方法的参数 Object[]
  • 方法的返回值 Object

问题2:它的返回值是什么?

  这个就需要看是否允许在After中更改返回的结果,如果规定只可用、不可修改返回值就不需要返回值

public interface AfterReturningAdvice extends Advice {
	/**
	 * 实现该方法,提供AfterRetun增强
	 * 
	 * @param returnValue
	 *            返回值
	 * @param method
	 *            被增强的方法
	 * @param args
	 *            方法的参数
	 * @param target
	 *            方法的所属对象
	 * @throws Throwable
	 */
	void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable;
}
1.3.4 环绕通知

Around环绕增强:包裹方法进行增强

问题1:他可能需要的参数

  • 方法本身 Method
  • 方法所属的对象 Object
  • 方法的参数 Object[]

问题2:它的返回值是面试?

  方法被它包裹,即方法将由它来执行,它需要返回方法的返回值

public interface MethodInterceptor extends Advice {
	/**
	 * 对方法进行环绕(前置、后置)增强、异常处理增强,方法实现中需调用目标方法。
	 * 
	 * @param method
	 *            被增强的方法
	 * @param args
	 *            方法的参数
	 * @param target
	 *            方法所属对象
	 * @return Object 返回值
	 * @throws Throwable
	 */
	Object invoke(Method method, Object[] args, Object target) throws Throwable;
}
1.3.5 异常通知

异常通知增强:对方法执行时的异常,进行增强处理

问题1:它可能需要什么参数?

  • 一定需要Exception
  • 可能需要方法本身 Method
  • 可能需要方法所属的对象 Object
  • 可能需要方法的参数 Object[]

问题2:它的返回值是什么?

  这个就需要看是否允许在After中更改返回的结果,如果规定只可用、不可修改返回值就不需要返回值

public interface ThrowsAdvice extends Advice {

    void afterThrowing(Method method, Object[] args, Object target, Exception ex) throws Throwable;
}

1.4 Advice设计

  结合上面的分析,我们就可以得出Advice的体系图了

image.png

2.Pointcut

2.1 Pointcut的特点有:

  • 用户性:由用户指定
  • 变化性:用户可灵活指定
  • 多点性:用户可以选择在多个点上进行增强

2.2 Pointcut分析

  为用户提供一个东西,让他们可以灵活地指定多个方法点,而且我们还能看懂!

思考:切入点是由用户来指定在哪些方法点上进行增强,那么这个哪些方法点如何来表示能满足上面的需求呢?

分析:

  1. 指定哪些方法,是不是一个描述信息?
  2. 如何来指定一个方法?
  3. 如果有重载的情况怎么办?
  4. 123要求的其实就是一个完整的方法签名
com.boge.spring.aop.Girl.dbj(Boy,Time)

com.boge.spring.aop.Girl.dbj(Boy,Girl,Time)

我们还得进一步分析:如何做到多点性和灵活性,在一个描述中指定一类类的某些方法?

  • 某个包下的某个类的某个方法
  • 某个包下的所有类中的所有方法
  • 某个包下的所有类中的do开头的方法
  • 某个包下的以service结尾的类中的do开头的方法

也就是我们需要有这样一个表达式能够灵活的描述上面的这些信息。

这个表达式表达的内容有:

image.png

而且每个部分的要求是怎么样的呢?

  • 包名:有父子特点,要能模糊匹配
  • 类名:要能模糊匹配
  • 方法名:要能模糊匹配
  • 参数类型:参数可以有多个

那么我们设计的这个表达式将被我们用来决定是否需要对某个类的某个方法进行增强,这个决定过程应该是怎么样的?

image.png

针对需求我们的选择是:

image.png

AspectJ官网:http://www.eclipse.org/aspectj

image.png

切入点表达式要匹配的对象就是目标方法的方法名。所以,execution表达式中明显就是方法的签名。注意,表达式中加[ ]的部分表示可省略部分,各部分间用空格分开。在其中可以使用以下符号

image.png

举例:

execution(public * (. .))
指定切入点为:任意公共方法。
execution(
set (. .))
指定切入点为:任何一个以“set”开始的方法。
execution(
com.xyz.service..(. .))
指定切入点为:定义在service包里的任意类的任意方法。
execution(* com.xyz.service. ..(. .))
指定切入点为:定义在service包或者子包里的任意类的任意方法。“…”出现在类名中时,
后面必须跟“”,表示包、子包下的所有类。
execution(
.service..(. .))
指定只有一级包下的serivce子包下所有类(接口)中的所有方法为切入点
execution(
. .service..*(. .))
指定所有包下的serivce子包下所有类(接口)中的所有方法为切入点

2.3 Pointcut设计

  通过分析完成我们就该对Pointcut类设计了,接口,类。

思考1:首先考虑切入点应该具有的属性—>切入点表达式

思考2:切入点应对外提供什么行为

思考3:切入点被我们设计用来做什么?

  对类和方法进行匹配,切入点应该提供匹配类,匹配方法的行为

思考4:如果在我们设计的框架中要能灵活的扩展切点,我们应该如何设计?

  这又是一个要支持可多变的问题,像通知一样,我们定义一套标准接口,定义好基本行为,面向接口编程,屏蔽掉具体的实现。不管哪些方案,都实现匹配类,匹配方法的接口。

image.png

案例代码

public interface Pointcut {

	boolean matchsClass(Class<?> targetClass);

	boolean matchsMethod(Method method, Class<?> targetClass);
}

然后来看看AspectJ的实现

image.png

案例代码

public class AspectJExpressionPointcut implements Pointcut {

	private static PointcutParser pp = PointcutParser
			.getPointcutParserSupportingAllPrimitivesAndUsingContextClassloaderForResolution();

	private String expression;

	private PointcutExpression pe;

	public AspectJExpressionPointcut(String expression) {
		super();
		this.expression = expression;
		pe = pp.parsePointcutExpression(expression);
	}

	@Override
	public boolean matchsClass(Class<?> targetClass) {
		return pe.couldMatchJoinPointsInType(targetClass);
	}

	@Override
	public boolean matchsMethod(Method method, Class<?> targetClass) {
		ShadowMatch sm = pe.matchesMethodExecution(method);
		return sm.alwaysMatches();
	}

	public String getExpression() {
		return expression;
	}

}

3.切面Aspect

搞定了两个难点后,我们来看看用户该如何使用我们提供的东西

image.png

为此我们需要创建对应的接口来管理。

4. Advisor

为用户提供更简单的外观,Advisor(通知者)组合Advice和Pointcut。

image.png

当然扩展的形式比较多:

image.png

或者:

image.png

四、织入实现

1. 织入的分析

  织入要完成的是什么?织入其实就是要把用户提供的增强功能加到指定的方法上。

image.png

思考1:在什么时候织入?

  创建Bean实例的时候,在Bean初始化后,再对其进行增强。

思考2:如何确定bean要增强?

  对bean类及方法挨个匹配用户配置的切面,如果有切面匹配就是要增强

思考3:如何实现织入?

  代理方式

2.织入的设计

  为了更好的去设计织入的实现,先整理下AOP的使用流程。

image.png

这里我们要考虑匹配、织入逻辑写到哪里?是写在BeanFactory中吗?

这时我们要考虑如果我们直接在BeanFactory中来处理,后续如果还有其他的需求是不是也要在BeanFactory中处理呢?这样操作有什么不好的地方呢?

  • BeanFactory代码爆炸,不专情
  • 不易扩展

那我们应该要怎么来设计呢?

我们先来回顾下Bean的生产的过程

image.png

在这个过程中, 将来会有更多处理逻辑加入到Bean生产过程的不同阶段。我们现在最好是设计出能让我们后面不用再改BeanFactory的代码就能灵活的扩展。

这时我们可以考虑用观察者模式,通过在各个节点加入扩展点,加入注册机制。

image.png

那么在这块我们就应用观察者模式来加入一个Bean的后置处理器 BeanPostProcessor

image.png

具体的我们在代码中来看看。

3.织入的实现

3.1 分析

  我们先定义了 BeanPostProcessor 接口,在这个接口中我们定义了相关的行为,也就是初始化之前和初始化之后要执行的方法。

image.png

  那么在此处我们需要在BeanFactory对创建的Bean对象做初始化前后要校验是否需要做相关的增强操作。

image.png

  在BeanFactory中我们提供了BeanPostProcessor的注册方法。

image.png

image.png

那么结合BeanFactory要实现相关的Bean增强操作,我们要做的行为就是两方面

  1. 创建相关的BeanPostProcessor,并注册到BeanFactory中
  2. BeanFactory在初始化Bean前后判断是否有相关BeanPostProcessor,如果有做相关的增强处理

  有了上面的分析,那么我们要实现具体的织入就需要来看看在对应前置和后置方法中我们要实现的功能

image.png

3.2 判断是否需要增强

  我们如何判断Bean对象是否需要增强呢?其实就是需要判断该Bean是否满足用户定义的切入点表达式。也就是我们需要简单Bean所属的类和所有方法。然后遍历Advisor。取出advisor中的Pointcut来匹配类和方法。

image.png

代码层面

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws Throwable {

		/*逻辑
		1 判断Bean是否需要增强
		2 创建代理来实现增强
		*/

        //1 判断Bean是否需要增强
        List<Advisor> matchAdvisors = getMatchedAdvisors(bean, beanName);

		// 2如有切面切中,创建代理来实现增强
		if (CollectionUtils.isNotEmpty(matchAdvisors)) {
			bean = this.createProxy(bean, beanName, matchAdvisors);
		}

        return bean;
    }

image.png

3.3 代理对象

  通过上面的分析如果Bean需要被增强,那么我们就需要创建Bean对应的代理对象了。代理模式:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在调用者和目标对象之间起到中介的作用;

image.png

  动态代理的实现方法有哪些?

image.png

JDK动态代理:

在运行时,对接口创建代理对象

image.png

cglib动态代理:

image.png

3.4 代理实现层设计

  动态代理的实现方式有很多种,如何能够做到灵活的扩展呢?在这里我们同样可以通过 抽象面向接口编程来设计一套支持不同代理实现的代码

image.png

  有了上面的设计,然后就是需要考虑代理对象的创建了。

image.png

3.5 增强逻辑实现

  代理对象搞定后我们需要考虑核心的问题就是怎么来实现我们要增强的逻辑呢?首先不管你用哪种方式来生成代理对象最终增强的逻辑代码是一样的。所以我们可以把这部分内容提炼出来。

image.png

  然后具体的应用Advice增强实现的逻辑为:

image.png

注意此处用到了责任链模式

	public static Object applyAdvices(Object target, Method method, Object[] args, List<Advisor> matchAdvisors,
			Object proxy, BeanFactory beanFactory) throws Throwable {
		// 这里要做什么?   
		// 1、获取要对当前方法进行增强的advice
		List<Object> advices = AopProxyUtils.getShouldApplyAdvices(target.getClass(), method, matchAdvisors,
				beanFactory);
		// 2、如有增强的advice,责任链式增强执行
		if (CollectionUtils.isEmpty(advices)) {
			return method.invoke(target, args);
		} else {
			// 责任链式执行增强
			AopAdviceChainInvocation chain = new AopAdviceChainInvocation(proxy, target, method, args, advices);
			return chain.invoke();
		}
	}

然后我们前面的Creator要怎么使用AopProxy呢?这块我们可以通过工厂模式来处理

image.png

public interface AopProxyFactory {

	AopProxy createAopProxy(Object bean, String beanName, List<Advisor> matchAdvisors, BeanFactory beanFactory)
			throws Throwable;

	/**
	 * 获得默认的AopProxyFactory实例
	 *    
	 * @return AopProxyFactory
	 */
	static AopProxyFactory getDefaultAopProxyFactory() {
		return new DefaultAopProxyFactory();
	}
}

到这儿,完整的增强逻辑就梳理通了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1118402.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Spring] SpringBoot2 简介(一)—— 基础配置

目录 一、SpringBoot 简介 1、Spring 的缺点 2、SpringBoot 功能 二、SpringBoot 入门案例 1、实现步骤 2、访问服务器 3、入门小结 4、Idea 快速构建 SpringBoot 工程 5、起步依赖无需版本号 6、主启动类的在项目中的位置&#xff08;*重要*&#xff09; 三、Sprin…

[AUTOSAR][诊断管理][$10] 会话模式控制

文章目录 一、简介二、指令格式请求: 10 SF会话参数记录有P2Server_max(2byte)和P2*Server_max(2byte),高位在前的表示方式。否定相应:7F SID NRC(否定相应码)三、示例代码(1) uds10_session_ctl.c一、简介 $10服务是Diagnostic Session Control诊断会话控制,子功能有01…

机器学习 | Python决策树算法

基本原理 决策树的基本原理是将数据分成不同的子集,使每个子集尽可能纯净。 这意味着子集中的数据属于同一类别或具有相似的属性。 为了做到这一点,决策树会选择一个特征,并根据该特征将数据分成两个子集。 它会选择那个特征,该特征在划分后的子集中具有最好的纯度,通…

Python获取微信公众号文章数据

这是一个通过 Python mitmproxy 库 实现获取某个微信公众号下全部文章数据的解决方案。首先需要创建一个 Python 虚拟环境&#xff0c;并进入虚拟环境下&#xff1a; $ python -m venv venv $ venv/Scripts/activate我们需要使用 mitmproxy 库 来建立一个网络代理&#xff0c;…

设计模式篇---组合模式

文章目录 概念结构实例总结 概念 组合模式&#xff1a;组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。 当我们开发中遇到树形结构的业务时&#xff0c;可以考虑使用组合模式。&#xff08;我也没有想明白为啥…

基于springboot实现财务管理系统项目【项目源码+论文说明】

基于springboot实现财务管理系统演示 摘要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#x…

Ubuntu的EFI分区无法删除

本文解决的问题&#xff1a;双系统装完后需要删除ubuntu的分区&#xff0c;但是EFI系统分区无法删除。 第一步&#xff1a;cmd中输入命令 diskpart 并回车&#xff0c;如图中①&#xff1b; 第二步&#xff1a;在弹出窗口②中依次输入如下命令即可删除EFI分区&#xff1b; /…

基于springboot实现藏区特产销售平台项目【项目源码+论文说明】

基于springboot实现藏区特产销售管理平台演示 摘要 “互联网”的战略实施后&#xff0c;很多行业的信息化水平都有了很大的提升。但是目前很多藏区特产销售信息仍是通过人工管理的方式进行&#xff0c;需要在各个岗位投入大量的人力进行很多重复性工作&#xff0c;使得对人力物…

USRP-2944 配件讲解,如何选择对应的配件

USRP-2944 产品图片 产品官网价格信息 查看附件和价格 硬件服务 NI硬件服务计划通过简化物流&#xff0c;延长正常运行时间以及根据业界标准维护数据的可追溯性&#xff0c;帮助您节省系统组装、设置和维护所需的时间和金钱。这些计划涵盖多年期维修服务&#xff0c;同时还提…

Python 循环

Python有两个基本的循环命令&#xff1a; while循环for循环 while循环 使用while循环&#xff0c;我们可以在条件为真的情况下执行一组语句。 示例&#xff0c;打印i&#xff0c;只要i小于6&#xff1a; i 1 while i < 6:print(i)i 1注意&#xff1a;记得增加i的值&a…

微机原理:汇编语言语句类型与格式

文章目录 壹、语句类型1、语句分类2、常用伪代码和运算符2.1数据定义伪指令2.1.1字节定义伪指令DB&#xff08;8位&#xff09;2.1.2字定义伪指令DW&#xff08;16位&#xff09;2.1.3双字节伪指令DD2.1.4 多字节定义DF/DQ/DT&#xff08;了解&#xff09; 2.2 常用运算符2.2.1…

数据结构-- 并查集

0. 引入 并查集是来解决等价问题的数据结构。 离散数学中的二元关系。 等价关系需满足自反性、对称性、传递性。 a ∈ S , a R a a R b & b R a a R b ∩ b R c > a R c a \in S, aRa \\ aRb \& bRa \\ aRb \cap bRc >aRc a∈S,aRaaRb&bRaaRb∩bRc>a…

【MATLAB源码-第53期】m代码基于粒子群算法(PSO)的三维路径规划,显示最优路径和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 粒子群算法&#xff08;Particle Swarm Optimization&#xff0c;简称PSO&#xff09;是一种模拟鸟群觅食行为的启发式优化方法。以下是其详细描述&#xff1a; 基本思想&#xff1a; 鸟群在寻找食物时&#xff0c;每只鸟都会…

拦截器以及统一功能的实现

目录 引言 实现一个简单的拦截器 拦截器小结 统一访问前缀 统一异常处理 统一返回参数 ControllerAdvice 引言 HandlerInterceptor是Spring MVC框架提供的一个拦截器接口&#xff0c;它用于对请求进行拦截和处理。在Spring MVC中&#xff0c;拦截器可以用于实现一些通用的功能…

什么是t检验?

t检验&#xff08;t-test&#xff09;是一种统计方法&#xff0c;用于比较两组数据之间的平均值是否存在显著差异。它通常用于分析两组样本的平均值是否具有统计学上的显著性差异。t检验基于正态分布的假设&#xff0c;它计算两组数据之间的t值&#xff0c;然后通过与t分布表进…

YOLO目标检测——人脸口罩佩戴数据集【(含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;公共场所监控场景下的大密度人群检测是否佩戴口罩&#xff0c;以及戴口罩的人证比对&#xff08;安检刷脸不用摘口罩&#xff09;、手机解锁、刷脸考勤等身份认证场景。数据集说明&#xff1a;人脸口罩佩戴检测数据集&#xff0c;真实场景的高质量图片…

reactnative 底部tab页面@react-navigation/bottom-tabs

使用react-navigation/native做的页面导航和tab‘ 官网&#xff1a;https://reactnavigation.org/docs/getting-started 效果图 安装 npm install react-navigation/nativenpm install react-navigation/bottom-tabs封装tabbar.js import { View, StyleSheet, Image } from …

【JavaEE】死锁问题 -- 多线程篇(5)

死锁问题 1. 死锁是什么?2. 如何避免死锁? 1. 死锁是什么? 概念 死锁是这样一种情形: 多个线程同时被阻塞, 它们中的一个或者全部都在等待某个资源被释放, 由于线程被无限期的阻塞, 因此程序不能正常终止。 死锁的三种常见的场景 一个线程, 一把锁, 但是是不可重入锁, 该线程…

新手上路:学会使用SELinux保护你的系统

1 Selinux的介绍 SELinux是为了提高系统安全性的机制。 它对系统的每一个程序、文件都引入了安全上下文。安全上下文标签&#xff0c;用于唯一标识文件、进程和资源。这些标签包括了安全策略的信息&#xff0c;允许SELinux强制执行策略。 1.1 Selinux关闭状态下 getenforce …

个微多账号聚合聊天管理如何实现?

在日常工作中&#xff0c;我经常遇到以下问题&#xff1a; 1. 微信号众多&#xff0c;需要频繁切换设备和账号&#xff0c;导致工作效率低下。 2. 无法及时回复客户消息&#xff0c;客户体验不尽如人意。 3. 难以随时掌握员工与客户的沟通情况&#xff0c;导致员工沟通质量难…