Spring 系列篇之彻底了解AOP

本篇文章主要介绍如何快速使用Spring AOP,以及Spring AOP的一个基本原理。
首先我们在介绍如何使用XML格式来使用Spring AOP

1. 概念

在使用AOP之前我们简单来介绍一下,不得不提的几个重要概念,官网也有说明

  • Aspect 切面(包括Advice,Pointcut),通常指一个类
  • Join point 连接点,通常指类中需要被代理的方法
  • Advice 通知(建议),通常指需要增强的代码。分为(戳我查看详细):前,后,环绕,返回,抛异常
  • Pointcut 切入点,通常指通知与连接点的关联,也就是定义连接点的表达式。语法戳这里

2. 使用

搞清楚概念之后,我们在来实际操作,我来做几个例子。我们先定义以下几个类


Person and Animal

Person

public interface Person {
    void run();
    int getMoney();
}

Man

public class Man implements Person {
    @Override
    public void run() {
        System.out.println("by car");
    }

    @Override
    public int getMoney() {
        return 1-000-000-000;
    }
}

Animal

public class Animal {
    public void run(){
        System.out.println("四条腿跑");
    }
    public void say(){
        int i = 1 / 0;
    }
}

2.2 XML方式使用AOP

定义好后我们接下来我们想把 run方法添加一个环绕通知,getMoney添加一个返回通知,say添加一个异常通知。那么接下来我们来创建一个切面Aspect

@Component
public class AspectBean {
    public void before(JoinPoint point){
        System.out.println("this is before");
    }
    public void after(JoinPoint point) throws Throwable{
        System.out.println("this is after +" + point);
    }
    public void afterReturn(JoinPoint point,Object aa){
        System.out.println("this is afterReturn +" + point);
    }
    public void round(JoinPoint point) throws Throwable{
        System.out.println("this is round before +" + point);
        ((ProceedingJoinPoint)point).proceed();
        System.out.println("this is round after +" + point);
    }
    public void throwException(JoinPoint point,Throwable exs){
        System.out.println("error +" + point);
    }
}

创建好切面后。我先来看看用XML配置方式,因为这里只测试切面相关的XML配置,其它Bean注入此处省略。

<aop:config >
    <aop:aspect id="aspectBean" ref="aspectBean" >
        <aop:after-returning returning="afterObj" method="afterReturn"  pointcut="execution(* com.lykos.aop..*.getMoney(..))"></aop:after-returning>
        <aop:after-throwing throwing="ex" method="throwException" pointcut="execution(* com.lykos.aop..*.say(..))"></aop:after-throwing>
        <aop:pointcut id="roundPointCut" expression="execution(* com.lykos.aop..*.run(..))"/>
        <aop:around method="round" pointcut-ref="roundPointCut"></aop:around>
    </aop:aspect>
</aop:config>

上面介绍的是用<aop:aspect>在注入我们的切面。其实我们还可以用<aop:config>,如下:

<aop:config>
    <aop:advisor advice-ref="aroundAdvice" pointcut="execution(* com.lykos.aop..*.run(..))"></aop:advisor>
    <aop:advisor advice-ref="afterReturnAdvice" pointcut="execution(* com.lykos.aop..*.getMoney(..))"></aop:advisor>
</aop:config>

不过用<aop:config>这个时,我们的通知需要实现Advice接口,如上面的aroundAdvice,afterReturnAdvice定义分别如下:

public class AroundAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        return invocation.proceed();
    }
}
public class AfterReturnAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("afterReturning");
    }
}

2.2 Annotation方式使用AOP

以上就是用XML格式使用了我们的Spring AOP,接下来我们在用注解方式实现一遍,注意Spring AOP的注解是支
AspectJ的。所以我们用AspectJ的注解。在使用注解之前我们需要先开启支持注解,开启方式也有两种,XML <aop:aspectj-autoproxy/> 和 Annotation @EnableAspectJAutoProxy
以下Annotation事例功能等同于上面,这里需要注意的就是@Component需要和@Aspect一起使用。

@Component
@Aspect
public class AspectJBean {
    @AfterReturning(value = "execution(* com.lykos.aop..*.getMoney(..))",returning = "xx")
    public void afterReturn(JoinPoint point,Object aa){
        System.out.println("this is afterReturn +" + point);
    }

    /**
     * 这里用pointcut修饰方法,以便在@Around上直接使用
     */
    @Pointcut("execution(* com.lykos.aop..*.run(..))")
    public void aroundPointcut(){}


    @Around("aroundPointcut()")
    public void round(JoinPoint point) throws Throwable{
        System.out.println("this is round before +" + point);
        ((ProceedingJoinPoint)point).proceed();
        System.out.println("this is round after +" + point);
    }
    @AfterThrowing(value = "execution(* com.lykos.aop..*.say(..))",throwing = "ex")
    public void throwException(JoinPoint point,Throwable ex){
        System.out.println("error +" + point);
    }
}

3. 原理

Spring AOP是支持JDK动态代理和Cglib动态代理的,默认只要类有实现接口就是用的JDK动态代理,如果没有实现任何接口则使用Cglib动态代理。

3.2 启用流程

3.2.1 @EnableAspectJAutoProxy 启动流程

前面我们讲过,要使用AOP我们需要使用@EnableAspectJAutoProxy,接下来我们看看@EnableAspectJAutoProxy为我们做了什么。根据定义我们发现帮我们引入了AspectJAutoProxyRegistrar

@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    boolean proxyTargetClass() default false;
    boolean exposeProxy() default false;

}

3.2.1.1 AspectJAutoProxyRegistrar

AspectJAutoProxyRegistrar是实现了ImportBeanDefinitionRegistrar接口,其定义如下

public interface ImportBeanDefinitionRegistrar {
    void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}

可以发现他就是一个注册Bean的接口,并且还可以实EnvironmentAware,BeanFactoryAware ,BeanClassLoaderAware,ResourceLoaderAware接口,也就是说我们在使用registerBeanDefinitions方法做Bean注册时可以拿到以上列的相关对象。而他通常和@Import一起使用。类似@Import(AspectJAutoProxyRegistrar.class)。那么到这里我们知道AspectJAutoProxyRegistrar是注册Bean的类,接下来我们看看他到底注册的哪个Bean

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // (1) 注册 AnnotationAwareAspectJAutoProxyCreator
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
        AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        // (2)获取EnableAspectJAutoProxy 属性值
        if (enableAspectJAutoProxy != null) {
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }

}
3.2.1.1.1 (1) 注册 AnnotationAwareAspectJAutoProxyCreator

AnnotationAwareAspectJAutoProxyCreator最终是实现了BeanPostProcessor接口中的postProcessAfterInitialization方法,通过Spring 系列篇之后置处理器我们知道postProcessAfterInitialization是可以改变我们Bean的实际返回值的。那么重点来了,Spring AOP就是在这里帮我们做的代理。

@Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }

上面会走wrapIfNecessary -> AbstractAutoProxyCreator.createProxy
AbstractAutoProxyCreator.createProxy

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
        @Nullable Object[] specificInterceptors, TargetSource targetSource) {
    if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
        AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
    }
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);
    //判断是否强制走Cglib代理
    if (!proxyFactory.isProxyTargetClass()) {
        if (shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
        }
        else {
            //分析需要代理的对象是否实现了接口,如果实现了接口,则使用Jdk 动态代理
            evaluateProxyInterfaces(beanClass, proxyFactory);
        }
    }
    //此处获取到的Advisor 来源在 【4. Advice】 提到
    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);
    proxyFactory.setFrozen(this.freezeProxy);
    if (advisorsPreFiltered()) {
        proxyFactory.setPreFiltered(true);
    }
    //会创建AopProxy对象,并使用他为我们创建实际代理对象
    return proxyFactory.getProxy(getProxyClassLoader());
}

上面我们需要关注两个信息。

  • 第一个是proxyTargetClass这个属性,如果为ture代表强制使用Cglib做代理,如果不是就会调用 evaluateProxyInterfaces分析是否有实现接口,如果有则使用JDK动态代理。
  • 第二个是proxyFactory.getProxy这个方法会先帮我们创建一个AopProxy对象,然后使用他创建实际的代理对象。其实AopProxy是一个接口,真正实现他的,如图:
    AopProxy

    无论是JdkDynamicAopProxy还是CglibAopProxy都会在其代理的方法上使用
    List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass) 并使用chain创建ReflectiveMethodInvocation对象并执行proceed方法,依次链路执行我们切面方法。
3.2.1.1.2 (2)获取EnableAspectJAutoProxy 属性值

当我们添加EnableAspectJAutoProxy时会有两个属性

  • 第一个 proxyTargetClass 前面已经介绍过了,是用来强制设置用Cglib代理的
  • 第二个 exposeProxy 意思是是否暴露代理对象,这个设置为true后,我们可以用AopContext.currentProxy()获取当前代理类。因为无论是使用的JdkDynamicAopProxy还是CglibAopProxy创建代理类,内部都会有,setCurrentProxy的动作
if (this.advised.exposeProxy) {
    // Make invocation available if necessary.
    oldProxy = AopContext.setCurrentProxy(proxy);
    setProxyContext = true;
}

3.2.2 <aop:aspectj-autoproxy> 启动流程

以上讲的是使用@EnableAspectJAutoProxy开启AOP代理,那么<aop:aspectj-autoproxy>又是什么原理呢?其实原理是一样的,至于如何解析的<aop:aspectj-autoproxy>这里就不说了
Spring 系列篇之彻底了解Annotation的使用
中有详细说明。最终也是使用AspectJAutoProxyBeanDefinitionParser注册了AnnotationAwareAspectJAutoProxyCreator对象。那么最终解析过程就是3.2.1.1.1流程。

4. Advice

前面整体讲解了Spring AOP是如何创建我们的一个代理对象,还没有涉及到如何使用,解析我们最开始讲解的Aspect,Advice,Pointcut,Join point那么在这小节我们就来看看Spring 是如何处理@Aspect或者 <aop:aspect>的。

4.1 @Aspect

之前也说过@Aspect需要和@Component一起使用,目的是将这个Bean作为一个普通Spring Bean注入到容器中

4.2 <aop:advisor>

<aop:advisor>是通过ConfigBeanDefinitionParser.parse方法解析并注册到容器中

public BeanDefinition parse(Element element, ParserContext parserContext) {
    CompositeComponentDefinition compositeDef =
            new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element));
    parserContext.pushContainingComponent(compositeDef);

    configureAutoProxyCreator(parserContext, element);
    //此处解析<aop 元素,并构建Advisor BeanDefinition 注入到容器中
    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);
        }
        else if (ASPECT.equals(localName)) {
            parseAspect(elt, parserContext);
        }
    }

    parserContext.popAndRegisterContainingComponent();
    return null;
}

无论是通过@Aspect还是<aop:advisor>最终目的都是在AnnotationAwareAspectJAutoProxyCreator创建代理对象时,能够通过findCandidateAdvisors方法中识别并解析定义的切面。

protected List<Advisor> findCandidateAdvisors() {
    // Add all the Spring advisors found according to superclass rules.
    // 在Spring 容器中找Advisor类型的bean,如<aop:advisor> 注册的bean 
    List<Advisor> advisors = super.findCandidateAdvisors();
    // Build Advisors for all AspectJ aspects in the bean factory.
    //在Spring 容器中找@Aspect修饰的Bean 并将其解析成Advisor
    if (this.aspectJAdvisorsBuilder != null) {
        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
    }
    return advisors;
}

5. 配置代理类class文件生成

如果需要查询动态代理生成的Class文件可以添加以下配置

Cglib 设置生成class目录
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/yourpath");

jdk 动态代理 目录在当前工作空间/com/sun下
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

感谢

感谢各位老铁花时间观看!
欢迎留言指正!
内容持续更新!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354