Spring AOP

Spring AOP

先贴官方文档地址:spring aop官方文档

什么是AOP

面向切面编程(Aspect-oriented Programming)

  • 功能:让关注点代码和业务代码分离
  • 面向切面编程就是指: 对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上横向插入”切面代码“。

传统的OOP过程通过纵向的继承方式完成代码的封装、可重用等,每一个类有对应的功能或完成指定的业务。这纵向拓展的方式解决了大部分场景的代码冗余问题,但随着业务系统的越发复杂,OOP也显出了一些弊端。
核心业务中总掺杂着一些不相关联的特殊业务,如日志记录,权限验证,事务控制,性能检测,错误信息检测等等,这些特殊业务可以说和核心业务没有根本上的关联而且核心业务也不关心它们。例如对于用户管理模块我们需要记录日志、要关心事务控制、验证权限等,但这些都不应该是用户管理模块需要处理的,同样,其他业务模块同样需要这些操作,那么,就会造成以下问题:

  • 代码混乱:核心业务模块可能需要兼顾处理其他不相干的业务外围操作,这些外围操作可能会混乱核心操作的代码,而且当外围模块有重大修改时也会影响到核心模块,这显然是不合理的。
  • 代码分散和冗余:同样的功能代码,在其他的模块几乎随处可见,导致代码分散并且冗余度高。
  • 代码质量低扩展难:由于不太相关的业务代码混杂在一起,无法专注核心业务代码,当进行类似无关业务扩展时又会直接涉及到核心业务的代码,导致拓展性低。

如何解决?事实上我们知道诸如日志,权限,事务,性能监测等业务几乎涉及到了所有的核心模块,如果把这些特殊的业务代码直接写到核心业务模块的代码中就会造成上述的问题,而工程师更希望的是这些模块可以实现热插拔特性而且无需把外围的代码入侵到核心模块中,这样在日后的维护和扩展也将会有更佳的表现,假设现在我们把日志、权限、事务、性能监测等外围业务看作单独的关注点(也可以理解为单独的模块),每个关注点都可以在需要它们的时刻及时被运用而且无需提前整合到核心模块中,这种形式相当下图:
v24800c187e3a7ddf36523311db7f914c6_r.jpg
每个关注点与核心业务模块分离,作为单独的功能,横切几个核心业务模块,这样的做的好处是显而易见的,每份功能代码不再单独入侵到核心业务类的代码中,即核心模块只需关注自己相关的业务,当需要外围业务(日志,权限,性能监测、事务控制)时,这些外围业务会通过一种特殊的技术自动应用到核心模块中,这些关注点有个特殊的名称,叫做“横切关注点”,上图也很好的表现出这个概念,是的,这种抽象级别的技术就是AOP。
参考链接(建议阅读):从POP到OOP再到AOP

AOP相关概念

  • Target(目标对象):代理的目标对象
  • Proxy:产生的结果代理类
  • Join Point:Spring AOP只支持方法切入,可以理解为方法执行
  • Pointcut:切点,可以理解为使用切点表达式指定方法
  • Advice:自己写的用于增强目标方法的方法
  • Weaving:与其他应用程序类型或对象链接以创建代理对象
  • Introduction:在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

Spring检测所有方法执行(Join Point),若匹配切入点(Pointcut),与其他应用程序类型或对象链接以创建代理对象(Weaving),执行自己写的代码(Advice)用以增强原方法

Spring AOP实现原理

从上文我们可以看出,AOP是在不改动原代码基础上(我们调用的方法没变),在原功能的基础上实现更多的控制或功能(实际执行的代码变了),这是代理模式的典型应用。

具体来说,如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。

Spring中若类实现了接口,则使用JDK动态代理,否则使用CGLib动态代理,也可以设置@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLib代理(默认为fasle)。

关于两者如何实现以及优缺点,可以参考Java动态代理

Spring AOP注解

以下所有注解支持JoinPoint参数

  1. @Before:在增强方法执行前
  2. @AfterReturning:在增强方法正常返回后,类似try块中未抛出异常,returning参数可指定获取返回值
  3. @AfterThrowing:在增强方法抛出异常后,类似catch块,throwing参数可指定获取异常对象
  4. @After:在增强方法结束后,类似finally
  5. @Around:前面所有注解的组合,支持ProceedingJoinPoint参数

切点表达式语法

基本语法

[切点表达式指示符] [访问修饰符] 返回值类型 包名.类名.方法名(参数列表)

  1. 访问修饰符可省略
  2. 返回值类型、包名、类名、方法名可以使用*代替
  3. .代表当前包下,…代表当前包及其子包下
  4. 参数列表可以使用…表示任意个数、任意类型的参数列表
  5. 使用部分有包含语义的指示符时,方法部分可省略
  6. 默认访问符为public

切点表达式指示符

  • execution:表示匹配方法,没什么其他语义。这是使用Spring AOP时主要的切点表达式指示符。

    1. execution(public int com.rufeng.aop.TargetClass.targetMethod(int, String))
      对应包下的对应类的targetMethod方法,该方法两个参数,int和String类型,返回值int,访问权限public
    2. execution(* com.rufeng.*.*(..))
      com.rufeng包下的所有方法(不包括子包)
    3. execution(* com.rufeng..*.to*())
      com.rufeng包及其子包下的所有方法名to开头的无参方法
    4. execution(* *..*.*(..))
      任意包下的任意方法
  • within:匹配在某些类型内的连接点,例如匹配包下所有方法或者类下所有方法

    1. within(com.xyz.myapp.trading…*)
      trading包下的所有方法
    2. within(com.xyz.service.*)
      service包下的所有方法,不包含子包
  • this:匹配代理对象
    this(com.xyz.service.AccountService)
    如果某个对象的代理对象是AccountService类型,该对象中的代理对象的方法会被切入

  • target:匹配目标对象
    target(com.xyz.service.AccountService)
    目标对象是AccountService,其方法会被切入

  • args:方法参数
    args(com.rufeng.aoptest.service.TestService, java.io.Serializable, …)
    前两参数类型为Testservice、Serializable,后面随意

  • @target:target注解版本

  • @args:args的注解版本,参数有指定注解时切入

  • @within:within的注解版本

  • @annotation:具有指定注解的方法

  • bean(beanName):指定bean的所有方法

部分aspectJ中的指示符在Spring中不支持,具体可查看官方文档。

关于this和target

target匹配目标对象的类型,即被代理对象的类型,例如A继承了B接口,则使用target(“B”),target(“A”)均可以匹配到A

this匹配的是代理对象的类型,例如存在一个接口B,使用this(“B”),如果某个类A的JDK代理对象类型为B,则A实现的接口B的方法会作为切点进行织入。

this的匹配结果和最终生成的代理对象互不干涉,对于this(M),当M为类时,若一个类的CGLIB代理类型为M,该类的所有方法作为连接点织入到CGLIB代理对象中,当M为接口时,若一个类的JDK代理类型为M,该类实现的接口方法作为连接点织入JDK代理对象中,this的匹配结果不会影响spring生成代理对象的方式,若一个类继承了接口,则使用JDK代理,否则使用CGLIB代理,这就会出现下列情况:

A继承了B接口,this(“A”)会匹配到A的CGLIB代理,接着将A的所有方法织入到A的CGLIB代理对象中,但是因为A继承了B接口,所以spring生成的是JDK代理对象,并不会因为this(“A”)的存在而生成CGLIB代理。

@Around注解

spring官方代码(略有修改):对业务函数进行增强,

  1. 业务正常执行,返回执行结果
  2. 当业务发生异常时重试,超出指定重试次数后抛出异常
@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
	Object result = null;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
	    /* 前面部分相当于@Before */

		/* 执行目标方法 */
                result = pjp.proceed();

            }
	   /* @AfterThrowing */
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
	if (result == null){
		throw lockFailureException;
	}
	/* @After和@AfterReturning */
	return result;
    }
}

切面执行的顺序

同其他在Spring中需要保证顺序的情况一样,在切面类上@Order注解,值越小,优先级越高。

Spring AOP配置原理

在Spring中,使用AOP功能需要加上这个@EnableAspectJAutoProxy注解,Spring中有许多类似的注解,其原理都是相似的。
点进@EnableAspectAutoProxy注解看,可以发现他只是@Import了一个AspectJAutoProxyRegistrar,作为bean注入到容器,这个类实现了ImportBeanDefinitionRegistrar接口,可以向容器中注入bean。
那么,我们只需要弄清楚它向容器中注入了哪些bean以及那些bean的生命周期和功能就可以了。

@Override
public void registerBeanDefinitions(
    AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    /* 注入了AnnotationAwareAspectJAutoProxyCreator */
    AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

    AnnotationAttributes enableAspectJAutoProxy =
        	AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
    if (enableAspectJAutoProxy != null) {
	/* 是否强制使用CGLib代理 */
        if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
        }
	/* 是否暴露当前线程的代理对象 */
        if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
        }
}

跟进registerAspectJAnnotationAutoProxyCreatorIfNecessary方法,最后发现在AOPConfigUtis类的registerOrEscalateApcAsRequired方法中,将AnnotationAwareAspectJAutoProxyCreator添加到了beanDefininationMap中,名字为org.springframework.aop.config.internalAutoProxyCreator
这就是@EnableAspectJAutoProxy注解的任务,剩下的全在AnnotationAwareAspectJAutoProxyCreator身上了。

AnnotationAwareAspectJAutoProxyCreator

顾名思义,这个类它肯定要拦截类的创建,然后生成代理类。
在Spring中,需要拦截类,自然会想到BeanPostProcessor接口,实现这个接口,在拦截的方法中,拦截原对象,返回代理对象。所以AnnotationAwareAspectJAutoProxyCreator应该是一个BeanPostProcessor。
当然,这些都是我们的猜想,看一下它的继承树。
AnnotationAwareAspectJAutoProxyCreator.png

可以看到,它实现了两个xxxAware接口,并且是一个BeanPostProcessor,猜对了。
按照Spring容器refresh方法的流程(不清楚的可以Spring 容器初始化过程),registerBeanPostProcessors在Bean的创建和实例化之前注册了BeanPostProcessor,我们可以在这里下个断点,查看源码。
在registerBeanPostProcessors方法中调试跟踪,我们很容易看到一个名为org.springframework.aop.config.internalAutoProxyCreator的bean。
关于这个方法的更多细节,可以查Spring 容器初始化过程

那么AnnotationAwareAspectJAutoProxyCreator已经完成初始化并加入到容器中,在接下来的容器初始化流程中,每个bean都会在初始化之前经过每个postProcessor的postProcessBeforeInstantiation和postProcessAfterInstantiation。那么,我们只需要关注该类的这两个方法即可。

这两个方法一直要追溯到父类AbstractAutoProxyCreator中,postProcessBeforeInitialization中啥也没干,查看执行过程。
注意:该类中还有一个postProcessBeforeInstantiation方法,这个是在实例化之前调用的。

关注postProcessorAfterInitialization方法,发现它调用了wrapIfNecessary方法,这里产生了代理,主要代码如下

...
/* 获取所有的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));
    this.proxyTypes.put(cacheKey, proxy.getClass());
    return proxy;
}
...

最后是在DefaultAopProxyFactory中的createAopProxy中生成了代理类,具体源码可自行查看。

从AOP注解到Advisor

整个流程大概理清楚了,其实还有最后一个问题,怎么获取从@Before等注解产生Advisor的?也就是上面getAdvicesAndAdvisorsForBean方法的具体细节。
跟进这个方法,最后发现这些工作是由ReflectiveAspectJAdvisorFactory完成的,以下是其getAdvisors方法的部分代码

...
/* aspectClass即为@Aspect注解的类 */
for (Method method : getAdvisorMethods(aspectClass)) {
    Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
    if (advisor != null) {
        advisors.add(advisor);
    }
    ...
    // Find introduction fields.
    for (Field field : aspectClass.getDeclaredFields()) {
        Advisor advisor = getDeclareParentsAdvisor(field);
        if (advisor != null) {
            advisors.add(advisor);
	}
    }
    ....
}

在这里,扫描了方法上是否有@Before、@After等注解。

aspect_anno.png

AOP实现的完整调用栈

AOP完整堆栈

在buildAspectJAdvisors方法中,会调用ReflectiveAspectJAdvisorFactory解析注解、生成Advisor并缓存结果。

总结

其实整个大致流程还是比较简单的:

  1. 添加@EnableAspectJAutoProxy注解,注入AnnotationAwareAspectJAutoProxyCreator,而这个类是一个BeanPostProcessor,也是SmartInstantiationAwareBeanPostProcessor。
  2. 在容器refresh流程中,通过registerBeanPostProcessors方法完成了所有BeanPostProcessor的初始化,供后续初始化普通Bean使用。
    在这个方法中,作为框架内部使用的SmartInstantiationAwareBeanPostProcessor,在postProcessBeforeInstantiation方法和postProcessAfterInitialization方法中,它可以对普通的BeanPostProcessor决定是否代理。
  3. 在finishBeanFactoryInitialization方法实例并初始化普通单例时,作为BeanPostProcessor,在初始化完成后,在postProcessAfterInitialization方法中,可以将原对象替换成代理对象。

其实还是Spring容器那一套,只不过在其中多注了一个特殊的Bean,如果对Spring容器和Java动态代理熟悉的话,弄清楚这个很快的。

参考:https://blog.csdn.net/dhaiuda/article/details/82317005

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值