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

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

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

何为AOP

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

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的方式支持,那么何为编程风格。

  1. Annotation注解方式;对应@AspectJ

  2. JavaConfig;对应Schema-based AOP

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

Spring AOP的应用

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

Aspect:切面;使用@Aspect注解的Java类来实现,集合了所有的切点,做为切点的一个载体,做一个比喻就像是我们的一个数据库。

Tips:这个要实现的话,一定要交给Spirng IOC去管理,也就是需要加入@Component。

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;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.will")
@EnableAspectJAutoProxy(proxyTargetClass =  false)
public class AppConfig {

}

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

package com.will.config;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 定义一个切面的载体
 */
@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 william");
    }

}

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进入到DefaultSingletonBeanRegistry的getSingleton方法。

我们观察下执行完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是如何完成的织入。

你可能感兴趣的:(Spring AOP从零单排-织入时期源码分析)