前言

前面两篇 如何实现 AOP(上)如何实现 AOP(中) 做了一些 AOP 的核心基础知识简要介绍,本文进入到了实战环节了,去实现一个基于 XML 配置的简易版 AOP,虽然是简易版的但是麻雀虽小五脏俱全,一些核心的功能都会实现,通过实现这个简易版的 AOP,相信你会对 AOP 有深入的理解,不止知其然,还能知其所以然。AOP 的顶层接口规范和底层依赖基础组件都是由一个叫 AOP Alliance 的组织制定的,我们经常听到的 AspectJ、ASM、CGLIB 就是其中被管理的一些项目,需要明确的一点是,在 Spring 中只是使用了 AspectJ 的核心概念和核心类,并不是像 AspectJ 那样在编译期实现的 AOP,而是在运行期。话不多说,下面开始进入主题。

解析 XML 中的 pointcut 定义及方法解析

假设有一个 OrderService 类(P.S. 这里的 @Component 是我自定义的注解,详见 这篇),其中有一个下单的方法 placeOrder(),我们想实现的效果是想给这个 placeOrder() 方法加上 数据库事务,即执行方法之前开启事务,执行过程中发生异常回滚事务,正常执行完成提交事务。OrderService 类的代码如下:

/**
* @author mghio
* @since 2021-06-06
*/
@Component(value = "orderService")
public class OrderService { public void placeOrder() {
System.out.println("place order");
} }

很明显,这里的 pointcut 就是 placeOrder() 方法,在 XML 配置文件中的配置如下:

<aop:pointcut id="placeOrder" expression="execution(* cn.mghio.service.version5.*.placeOrder(..))"/>

我们需要一个类去表达这个概念,pointcut 要实现的功能是给定一个类的方法,判断是否匹配配置文件中给定的表达式。总的来看 pointcut 由方法匹配器和匹配表达式两部分组成,方法匹配器可以有各种不同的实现,所以是一个接口,pointcut 同样也可以基于多种不同技术实现,故也是一个接口,默认是基于 AspectJ 实现的,类图结构如下:

实现类 AspectJExpressionPointcut 是基于 AspectJ 实现的,方法的匹配过程是委托给 AspectJ 中的 PointcutExpression 来判断给定的方法是否匹配表达式,该类的核心实现如下:

/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJExpressionPointcut implements Pointcut, MethodMatcher { private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>(); static {
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
} private String expression;
private ClassLoader pointcutClassLoader;
private PointcutExpression pointcutExpression; @Override
public MethodMatcher getMethodMatcher() {
return this;
} @Override
public String getExpression() {
return expression;
} @Override
public boolean matches(Method method) {
checkReadyToMatch(); ShadowMatch shadowMatch = getShadowMatch(method);
return shadowMatch.alwaysMatches();
} private void checkReadyToMatch() {
if (Objects.isNull(getExpression())) {
throw new IllegalArgumentException("Must set property 'expression' before attempting to match");
}
if (Objects.isNull(this.pointcutExpression)) {
this.pointcutClassLoader = ClassUtils.getDefaultClassLoader();
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
}
} private PointcutExpression buildPointcutExpression(ClassLoader classLoader) {
PointcutParser pointcutParser = PointcutParser
.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(SUPPORTED_PRIMITIVES, classLoader);
return pointcutParser.parsePointcutExpression(replaceBooleanOperators(getExpression()));
} private String replaceBooleanOperators(String pcExpr) {
String result = StringUtils.replace(pcExpr, " and ", " && ");
result = StringUtils.replace(result, " or ", " || ");
result = StringUtils.replace(result, " not ", " ! ");
return result;
} private ShadowMatch getShadowMatch(Method method) {
ShadowMatch shadowMatch;
try {
shadowMatch = this.pointcutExpression.matchesMethodExecution(method);
} catch (Exception e) {
throw new RuntimeException("not implemented yet");
}
return shadowMatch;
} // omit other setter、getter ... }

到这里就完成了给定一个类的方法,判断是否匹配配置文件中给定的表达式的功能。再来看如下的一个完整的 AOP 配置:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.e3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/beans/spring-context.xsd"> <context:scann-package base-package="cn.mghio.service.version5,cn.mghio.dao.version5" /> <bean id="tx" class="cn.mghio.tx.TransactionManager"/> <aop:config>
<aop:aspect ref="tx">
<aop:pointcut id="placeOrder" expression="execution(* cn.mghio.service.version5.*.placeOrder(..))"/>
<aop:before pointcut-ref="placeOrder" method="start"/>
<aop:after-returning pointcut-ref="placeOrder" method="commit"/>
<aop:after-throwing pointcut-ref="placeOrder" method="rollback"/>
</aop:aspect>
</aop:config>
</beans>

在实现各种 XXXAdvice 之前需要定位到这个 Method,比如以上配置文件中的 start、commit、rollback 等方法,为了达到这个目标我们还需要实现的功能就是根据一个 Bean 名称(比如这里的 tx)定位到指定的 Method,然后通过反射调用这个定位到的方法。实际上也比较简单,这个类命名为 MethodLocatingFactory,根据其功能可以定义出目标 Bean 的名称 targetBeanName、需要定位的方法名称 methodName 以及定位完成后得到的方法 method 这三个属性,整体类图结构如下所示:

根据名称和类型定位到方法主要是在 setBeanFactory() 方法中完成的,前提是对应的目标 Bean 名称和方法名称要设置完成,方法定位的类 MethodLocatingFactory 类的代码如下所示:

/**
* @author mghio
* @since 2021-06-06
*/
public class MethodLocatingFactory implements FactoryBean<Method>, BeanFactoryAware { private String targetBeanName; private String methodName; private Method method; public void setTargetBeanName(String targetBeanName) {
this.targetBeanName = targetBeanName;
} public void setMethodName(String methodName) {
this.methodName = methodName;
} @Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!StringUtils.hasText(this.targetBeanName)) {
throw new IllegalArgumentException("Property 'targetBeanName' is required");
}
if (!StringUtils.hasText(this.methodName)) {
throw new IllegalArgumentException("Property 'methodName' is required");
} Class<?> beanClass = beanFactory.getType(this.targetBeanName);
if (Objects.isNull(beanClass)) {
throw new IllegalArgumentException("Can't determine type of bean with name '" + this.targetBeanName);
} this.method = BeanUtils.resolveSignature(this.methodName, beanClass);
if (Objects.isNull(this.method)) {
throw new IllegalArgumentException("Unable to locate method [" + this.methodName + "] on bean ["
+ this.targetBeanName + "]");
}
} @Override
public Method getObject() {
return this.method;
} @Override
public Class<?> getObjectType() {
return Method.class;
}
}

实现各种不同类型的 Advice

各种不同类型的 Advice(BeforeAdvice、AfterAdvice 等)目标都是需要在指定对象的指定方法执行前后按指定次序执行一些操作(称之为 拦截器),比如以上示例中的一种执行次序为:BeforeAdvice -> placeOrder -> AfterAdvice。这里的一个关键问题就是如何去实现按照指定次序的链式调用?,这里先卖个关子,这个问题先放一放等下再介绍具体实现,先来看看要如何定义各种不同类型的 Advice,我们的 Advice 定义都是扩展自 AOP Alliance 定义的 MethodInterceptor 接口,Advice 部分的核心类图如下:

其实到这里如果有了前面两篇文章(如何实现 AOP(上)如何实现 AOP(中))的基础了,实现起来就相对比较简单了,就是在方法执行之前、之后以及发生异常时调用一些特定的方法即可,AbstractAspectJAdvice 类定义了一下公共的属性和方法,核心实现源码如下:

/**
* @author mghio
* @since 2021-06-06
*/
public abstract class AbstractAspectJAdvice implements Advice { protected Method adviceMethod;
protected AspectJExpressionPointcut pc;
protected AopInstanceFactory adviceObjectFactory; public AbstractAspectJAdvice(Method adviceMethod, AspectJExpressionPointcut pc, AopInstanceFactory adviceObjectFactory) {
this.adviceMethod = adviceMethod;
this.pc = pc;
this.adviceObjectFactory = adviceObjectFactory;
} @Override
public Pointcut getPointcut() {
return pc;
} protected void invokeAdviceMethod() throws Throwable {
adviceMethod.invoke(adviceObjectFactory.getAspectInstance());
} public Object getAdviceInstance() throws Exception {
return adviceObjectFactory.getAspectInstance();
} // omit getter ... }

有了这个公共抽象父类之后其它几个 Advice 的实现就很简单了,AspectJBeforeAdvice 就是在执行拦截方法之前调用,核心源码如下:

/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJBeforeAdvice extends AbstractAspectJAdvice { // omit constructor ... @Override
public Object invoke(MethodInvocation mi) throws Throwable {
this.invokeAdviceMethod();
return mi.proceed();
}
}

同理,AspectJAfterReturningAdvice 就是在方法正常执行结束后调用,核心源码如下:

/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice { // omit constructor ... @Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object result = mi.proceed();
this.invokeAdviceMethod();
return result;
}
}

剩下的 AspectJAfterThrowingAdvice 想必你已经猜到了,没错,就是在方法执行过程中发生异常时调用,对应 Java 的异常机制也就是在 try{...}catch{...} 的 catch 中调用,核心源码如下:

/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice { // omit constructor ... @Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
} catch (Throwable t) {
this.invokeAdviceMethod();
throw t;
}
}
}

我们支持的三种不同的 Advice 已经定义好了,接下来就是如何组装调用的问题了,同时也处理了如何去实现按照指定次序的链式调用?的问题,这里的方法调用我们也是扩展 AOP Alliance 定义的规范,即方法调用 MethodInvocation 接口。

由于这里的方法调用是基于反射完成的,将该类命名为 ReflectiveMethodInvocation,要使用反射来调用方法,很显然需要知道目标对象 targetObject、targetMethod 以及方法参数列表 arguments 等参数,当然还有我们的拦截器列表(也就是上文定义的 Advice)interceptors,因为这个是一个类似自调用的过程,为了判断是否已经执行完成所有拦截器,还需要记录当前调用拦截器的下标位置 currentInterceptorIndex,当 currentInterceptorIndex 等于 interceptors.size() - 1 时表示所有拦截器都已调用完成,再调用我们的实际方法即可。核心的类图如下:

其中类 ReflectiveMethodInvocation 的核心源码实现如下,强烈建议大家将 proceed() 方法结合上问定义的几个 Advice 类一起看:

/**
* @author mghio
* @since 2021-04-05
*/
public class ReflectiveMethodInvocation implements MethodInvocation { protected final Object targetObject;
protected final Method targetMethod;
protected Object[] arguments;
protected final List<MethodInterceptor> interceptors;
private int currentInterceptorIndex = -1; public ReflectiveMethodInvocation(Object targetObject, Method targetMethod,
Object[] arguments, List<MethodInterceptor> interceptors) {
this.targetObject = targetObject;
this.targetMethod = targetMethod;
this.arguments = arguments;
this.interceptors = interceptors;
} @Override
public Object proceed() throws Throwable {
// all interceptors have been called.
if (this.currentInterceptorIndex == interceptors.size() - 1) {
return invokeJoinpoint();
} this.currentInterceptorIndex++;
MethodInterceptor methodInterceptor = this.interceptors.get(this.currentInterceptorIndex);
return methodInterceptor.invoke(this);
} private Object invokeJoinpoint() throws Throwable {
return this.targetMethod.invoke(this.targetObject, this.arguments);
} // omit other method ... }

至此,各种不同类型的 Advice 的核心实现已经介绍完毕,本来打算在这边介绍完 AOP 剩下部分的实现的,但是鉴于文章长度太长,还是放到下一次再开一篇来介绍吧。

总结

本文主要介绍了 AOP 在 XML 配置的 pointcut 解析实现、方法匹配定位以及各种不同类型的 Advice 的实现,特别是 Advice 的实现部分,建议自己动手实现一版,这样印象会更加深刻,另源码已上传至 GitHub,可自行下载参考,有任何问题请留言交流讨论。

如何实现一个简易版的 Spring - 如何实现 AOP(下)的更多相关文章

  1. 如何实现一个简易版的 Spring - 如何实现 AOP(上)

    前言 本文是「如何实现一个简易版的 Spring 系列」的第五篇,在之前介绍了 Spring 中的核心技术之一 IoC,从这篇开始我们再来看看 Spring 的另一个重要的技术--AOP.用过 Spr ...

  2. 如何实现一个简易版的 Spring - 如何实现 AOP(中)

    前言 在上篇 如何实现 AOP(上) 介绍了 AOP 技术出现的原因和一些重要的概念,在我们自己实现之前有必要先了解一下 AOP 底层到底是如何运作的,所以这篇再来看看 AOP 实现所依赖的一些核心基 ...

  3. 如何实现一个简易版的 Spring - 如何实现 AOP(终结篇)

    前言 在 上篇 实现了 判断一个类的方式是符合配置的 pointcut 表达式.根据一个 Bean 的名称和方法名,获取 Method 对象.实现了 BeforeAdvice.AfterReturni ...

  4. 如何实现一个简易版的 Spring - 如何实现 Constructor 注入

    前言 本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML 的简单 Setter 注入,这篇来看看要如何去实现一个简单的 Constructor 注入功 ...

  5. 如何实现一个简易版的 Spring - 如何实现 @Component 注解

    前言 前面两篇文章(如何实现一个简易版的 Spring - 如何实现 Setter 注入.如何实现一个简易版的 Spring - 如何实现 Constructor 注入)介绍的都是基于 XML 配置文 ...

  6. 如何实现一个简易版的 Spring - 如何实现 @Autowired 注解

    前言 本文是 如何实现一个简易版的 Spring 系列第四篇,在 上篇 介绍了 @Component 注解的实现,这篇再来看看在使用 Spring 框架开发中常用的 @Autowired 注入要如何实 ...

  7. 如何实现一个简易版的 Spring - 如何实现 Setter 注入

    前言 之前在 上篇 提到过会实现一个简易版的 IoC 和 AOP,今天它终于来了...相信对于使用 Java 开发语言的朋友们都使用过或者听说过 Spring 这个开发框架,绝大部分的企业级开发中都离 ...

  8. 手动实现一个简易版SpringMvc

    版权声明:本篇博客大部分代码引用于公众号:java团长,我只是在作者基础上稍微修改一些内容,内容仅供学习与参考 前言:目前mvc框架经过大浪淘沙,由最初的struts1到struts2,到目前的主流框 ...

  9. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

随机推荐

  1. HTTP/1.1、HTTP/2、HTTP/3 演变

    HTTP/1.1 相比 HTTP/1.0 提高了什么性能? 针对 HTTP/1.1 的性能瓶颈,HTTP/2 做了什么优化? HTTP/2 有哪些缺陷?HTTP/3 做了哪些优化? HTTP/1.1 ...

  2. 【Java】说说你对ThreadLocal的理解

    思路: 0.ThreadLocal是什么?有什么用? 1.ThreadLocal用在什么地方? 2.ThreadLocal的一些细节 3.ThreadLocal的最佳实践 一.ThreadLocal用 ...

  3. hdu1316 大数

    题意:      给你一个区间,问这个区间有多少个斐波那契数. 思路:      水的大数,可以直接模拟,要是懒可以用JAVA,我模拟的,打表打到1000个就足够用了... #include<s ...

  4. Python中数据类型的转换

    bytes<-->str a="hello" #str字符型 #str转换为bytes类型 b=a.encode("utf-8") 或 b=byte ...

  5. POJ3160强连通+spfa最长路(不错)

    题意:       给你一个有向图,每个点上有一个权值,可正可负,然后给你一些链接关系,让你找到一个起点,从起点开始走,走过的边可以在走,但是拿过权值的点就不能再拿了,问最多能拿到多少权值? 思路: ...

  6. node-OS&Domain&Net&Path

    OS--------------------------------------------- Node.js os 模块提供了一些基本的系统操作函数. var os = require(" ...

  7. ajax提交session超时跳转页面使用全局的方法来处理

    来自:http://www.jb51.net/article/43770.htm 如果是ajax提交,超时时从服务器发出的跳转命令就不会起作用,所以如果是session超时,而且是在ajax请求,就在 ...

  8. .NET之默认依赖注入

    介绍 不要依赖于具体的实现,应该依赖于抽象,高层模块不应该依赖于底层模块,二者应该依赖于抽象.简单的说就是为了更好的解耦.而控制反转(Ioc)就是这样的原则的其中一个实现思路, 这个思路的其中一种实现 ...

  9. Mac TouchBar 自定义工具-MTMR

    Github Install brew cask install mtmr 官网

  10. GDI编程基础

    窗口和视口 视口是基于设备的采用的是设备坐标(单位:像素),窗口是基于程序的采用的是逻辑坐标(单位:像素/毫米/厘米等). 在默认的映射模式下,视口是与窗口等同的.但是如果改变其映射模式,则其对应的单 ...