Spring AOP从零单排-织入时期源码分析

问题:Spring AOP代理中的运行时期,是在初始化时期织入仍是获取对象时期织入?java

织入就是代理的过程,指目标对象进行封装转换成代理,实现了代理,就能够运用各类代理的场景模式。spring

何为AOP

简单点来定义就是切面,是一种编程范式。与OOP对比,它是面向切面,为什么须要切面,在开发中,咱们的系统从上到下定义的模块中的过程当中会产生一些横切性的问题,这些横切性的问题和咱们的主业务逻辑关系不大,假如不进行AOP,会散落在代码的各个地方,形成难以维护。AOP的编程思想就是把业务逻辑和横切的问题进行分离,从而达到解耦的目的,使代码的重用性、侵入性低、开发效率高。sql

AOP使用场景

在这里插入图片描述

  • 日志记录;记录调用方法的入参和结果返参。
  • 用户的权限验证;验证用户的权限放到AOP中,与主业务进行解耦。
  • 性能监控;监控程序运行方法的耗时,找出项目的瓶颈。
  • 事务管理;控制Spring事务,Mysql事务等。

AOP概念点

AOP和Spring AOP的关系

在这里问题中,也有一个相似的一对IOC和DI(dependency injection)的关系,AOP能够理解是一种编程目标,Spring AOP就是这个实现这个目标的一种手段。同理IOC也是一种编程目标,DI就是它的一个手段。数据库

SpringAOP和AspectJ是什么关系

在这里插入图片描述

在Spring官网能够看到,AOP的实现提供了两种支持分别为@AspectJ、Schema-based AOP。其实在Spring2.5版本时,Spring本身实现了一套AOP开发的规范和语言,可是这一套规范比较复杂,可读性差。以后,Spring借用了AspectJ编程风格,才有了@AspectJ的方式支持,那么何为编程风格。编程

  • Annotation注解方式;对应@AspectJ
  • JavaConfig;对应Schema-based AOP

SpringAOP和AspectJ的详细对比,在以后的章节会在进行更加详细的说明,将会在他们的背景、织入方法、性能作介绍。安全

Spring AOP的应用

阅读官网,是咱们学习一个新知识的最好途径,这个就是Spring AOP的核心概念点,跟进它们的重要性,我作了从新的排序,以便好理解,这些会为咱们后续的源码分析起到做用。bash

在这里插入图片描述

Aspect:切面;使用@Aspect注解的Java类来实现,集合了全部的切点,作为切点的一个载体,作一个比喻就像是咱们的一个数据库。 Tips:这个要实现的话,必定要交给Spirng IOC去管理,也就是须要加入@Component。app

Pointcut:切点;表示为全部Join point的集合,就像是数据库中一个表。源码分析

Join point:链接点;俗称为目标对象,具体来讲就是servlet中的method,就像是数据库表中的记录。性能

Advice:通知;这个就是before、after、After throwing、After (finally)。

Weaving:把代理逻辑加入到目标对象上的过程叫作织入。

target:目标对象、原始对象。

aop Proxy:代理对象 包含了原始对象的代码和增长后的代码的那个对象。

Tips 这个应用点,有不少的知识点可让咱们去挖掘,好比Pointcut中execution、within的区别,我相信你去针对性搜索或者官网都未必能有好的解释,稍后会再专门挑一个文章作重点的使用介绍;

SpringAOP源码分析

为了回答咱们的一开始的问题,前面的几个章节咱们作了一些简单的概念介绍作为铺垫,那么接下来咱们回归正题,正面去切入问题。以码说话,咱们以最简洁的思路把AOP实现,咱们先上代码。

项目结构介绍

项目目录结构,比较简单,5个主要的文件;

在这里插入图片描述
pom.xml核心代码;spring-content是核心jar,已经包含了spring全部的基础jar,aspectjweaver是为了实现AOP。
在这里插入图片描述
AppConfig.java;定义一个Annotation,作为咱们Spirng IOC容器的启动类。

package com.will.config;
​
@Configuration
@ComponentScan("com.will")
@EnableAspectJAutoProxy(proxyTargetClass = false)
public class AppConfig {
    
}
复制代码

WilAspect.java ;按照官网首推的方式(@AspectJ support),实现AOP代理。

package com.will.config;
​
/**
 * 定义一个切面的载体
 */
@Aspect
@Component
public class WilAspect {
    /**
     * 定义一个切点
     */
    @Pointcut("execution(* com.will.dao.*.*(..))")
    public void pointCutExecution(){
    }
​
    /**
     * 定义一个Advice为Before,并指定对应的切点
     * @param joinPoint
     */
    @Before("pointCutExecution()")
    public  void before(JoinPoint joinPoint){
        System.out.println("proxy-before");
    }
}
复制代码

Dao.java

package com.will.dao;
public interface Dao {
    public void query();
}
复制代码

UserDao.java

package com.will.dao;
import org.springframework.stereotype.Component;
@Component
public class UserDao implements Dao {
    public void query() {
        System.out.println("query user");
    }
}
复制代码

Test.java

package com.will.test;
import com.will.config.AppConfig;
import com.will.dao.Dao;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
    public static void main(String[] args) {
        /**
         * new一个注册配置类,启动IOC容器,初始化时期;
         */
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
​
        /**
         * 获取Dao对象,获取对象时期,并进行query打印
         */
        Dao dao = annotationConfigApplicationContext.getBean(Dao.class);
        dao.query();
        annotationConfigApplicationContext.start();
    }
}
复制代码

好了,这样咱们总体的AOP代理就已经完成。

问题分析测试

到底是哪一个时期进行对象织入的,好比Test类中,到底是第一行仍是第二行进行织入的,咱们只能经过源码进行分析,假如是你,你会进行如何的分析源码解读。

Spring的代码很是优秀,同时也很是复杂,那是一个大项目,里面进行了不少的代码封装,那么的代码你三天三夜也读不完,甚至于你都不清楚哪一行的该留意的,哪一行是起到关键性做用的,这里教几个小技巧。

  • 看方法返回类型;假如是void返回类型的,看都不看跳过。返回结果是对象,好比T果断进行去进行跟踪。
  • 假设法;就当前场景,咱们大胆假设是第二行进行的织入。
  • 借助好的IDE;IDEA能够帮咱们作不少的事情,它的debug模式中的条件断点、调用链(堆栈)会帮助到咱们。

假设法源码分析

在这里插入图片描述

在这里插入图片描述

debug模式StepInfo(F5)后,进入 AbstractApplicationContext.getBean方法,这个是Spring应用上下文中最重要的一个类,这个抽象类中提供了几乎ApplicationContext的全部操做。这里第一个语句返回void,咱们能够直接忽略,看下面的关键性代码。

在这里插入图片描述

继续debug后,会进入到 DefaultListableBeanFactory 类中,看以下代码

return new NamedBeanHolder<>(beanName, getBean
(beanName, requiredType, args));
复制代码

在该语句中,这个能够理解为 DefaultListableBeanFactory 容器,帮咱们获取相应的Bean。

在这里插入图片描述

进入到AbstractBeanFactory类的doGetBean方法以后,咱们运行完。

Object sharedInstance = getSingleton(beanName);
复制代码

语句以后,看到sharedInstance 对象打印出&Proxyxxx ,说明在getSingleton 方法的时候就已经获取到了对象,因此须要跟踪进入到 getSingleton 方法中,继续探究。

在这里插入图片描述

不方便不方便咱们进行问题追踪到这个步骤以后,我须要引入IDEA的条件断点,不方便咱们进行问题追踪由于Spring会初始化不少的Bean,咱们再ObjectsharedInstance=getSingleton(beanName);加入条件断点语句。

在这里插入图片描述

继续debug进入到DefaultSingletonBeanRegistrygetSingleton方法。 咱们观察下执行完ObjectsingletonObject=this.singletonObjects.get(beanName); 以后的singletonObject已经变成为&ProxyUserDao,这个时候Spring最关键的一行代码出现了,请注意这个this.singletonObjects

this.singletonObjects就是至关IOC容器,反之IOC容器就是一个线程安全的线程安全的HashMap,里面存放着咱们须要Bean。

在这里插入图片描述
在这里插入图片描述

咱们来看下singletonObjects存放着的数据,里面就有咱们的UserDao类。

这就说明,咱们的初始化的时期进行织入的,上图也有整个Debug模式的调用链。

源码深层次探索

经过上一个环节已经得知是在第一行进行初始化的,可是它在初始化的时候是何时完成织入的,抱着求知的心态咱们继续求证。

仍是那个问题,那么多的代码,个人切入点在哪里?

既然singletonObjects是容器,存放咱们的Bean,那么找到关键性代码在哪里进行存放(put方法)就能够了。因而咱们经过搜索定位到了。

在这里插入图片描述
在这里插入图片描述

咱们经过debug模式的条件断点和debug调用链模式,就能够进行探索。

在这里插入图片描述

这个时候借助上图中的调用链,咱们把思路放到放到IDEA帮我定位到的两个方法代码上。

DefaultSingletonBeanRegistry.getSingleton

咱们一步步断点,得知,当运行完singletonObject=singletonFactory.getObject();以后,singletonObject已经得到了代理。

在这里插入图片描述

至此咱们知道,代理对象的获取关键在于singletonFactory对象,因而又定位到了AbstractBeanFactorydoGetBean方法,发现singletonFactory参数是由createBean方法创造的。这个就是Spring中IOC容器最核心的地方了,这个代码的模式也值得咱们去学习。

sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
复制代码

这个第二个参数是用到了jdk8中的lambda,这一段的含义是就是为了传参,重点看下 createBean(beanName,mbd,args);代码。随着断点,咱们进入到这个类方法里面。

AbstractAutowireCapableBeanFactory.createBean中的;

ObjectbeanInstance=doCreateBean(beanName,mbdToUse,args)方法;

doCreateBean方法中,作了简化。

Initialize the bean instance.
        Object exposedObject = bean;
        try {
            populateBean(beanName, mbd, instanceWrapper);
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
        ...
​
        return exposedObject;
复制代码

在这里插入图片描述

当运行完 exposedObject=initializeBean(beanName,exposedObject,mbd);以后,咱们看到exposedObject已是一个代理对象,并执行返回。这一行代码就是取判断对象要不要执行代理,要的话就去初始化代理对象,不须要直接返回。后面的initializeBean方法是涉及代理对象生成的逻辑(JDK、Cglib),后续会有一个专门的章节进行详细介绍。

总结

经过源码分析,咱们得知,Spring AOP的代理对象的织入时期是在运行Spring初始化的时候就已经完成的织入,而且也分析了Spring是如何完成的织入。

公众号
相关文章
相关标签/搜索