【Spring专题】Spring底层核心原理解析——引导篇

  • Post author:
  • Post category:其他




前言

Spring啊,可以说是我们大部分Java玩家【最熟悉的陌生人】了吧。八个字形容:似懂非懂,会也不会

在这里插入图片描述

你说简单应用,我们大家都会,那真要展开说两句的话,那只能来这么两句:这是第一句,接着是第二句,好了我说完了。

在这里插入图片描述

但是啊xdm,

据说Spring是一份非常非常非常优秀的源码,不但有丰富的设计模式应用场景,代码写的也很优美,有条理,所以非常推荐大家学习。除了能在日常装逼以外,还能丰富一下见识,提升自己写代码的能力。



阅读导航

阅读对象:有过Spring开发经验的人



前置知识



Q1:你能描述一下JVM对象创建过程吗?

答:看图说话:

在这里插入图片描述

  1. 类加载:在使用一个类之前,Java虚拟机需要先将类的字节码加载到内存中。类加载是Java虚拟机的核心过程,它负责查找类的字节码文件并加载到内存的方法区。类加载包括加载、验证、准备、解析和初始化这五个阶段。
  2. 分配内存:在类加载完成后,Java虚拟机会为对象分配内存空间。内存分配通常在堆(Heap)上进行,但也有一些特殊情况下的对象可以在栈(Stack)上分配内存,例如线程栈上的局部对象。
  3. 实例化(初始化零值):在分配内存后,Java虚拟机会将对象的内存空间初始化为零值。这包括基本类型的默认值(例如0、false等)和引用类型的默认值(null)。
  4. 设置对象头:Java对象在内存中的布局包括对象头和实例数据两部分。对象头存储了一些元数据,如对象的哈希码、锁状态等。在对象创建过程中,Java虚拟机会设置对象头的值。
  5. 执行构造方法:对象创建的最后一步是执行构造方法。构造方法用于初始化对象的实例数据,并执行其他必要的初始化操作。构造方法可以是类的默认构造方法,也可以是自定义的构造方法。
  6. 返回对象引用:对象创建完成后,Java虚拟机会返回一个指向该对象的引用。通过引用,程序可以操作对象的属性和方法。



PS:为什么要问这个问题?因为Spring是IOC技术,就算再怎么玩出花来,他也要按照这个基本流程来创建对象。只不过,可以提前告知大家的是,SpringIOC在这个流程之中,新增了很多槽点,通过热插拔的方式,丰富了IOC的功能!



Q2:Spring的特性是什么?

答:Spring的特性就是IOC跟AOP两大概念!甚至可以这么说:

Spring就是实现了AOP技术的IOC容器




(容器,容器,容器)

Q3:什么是IOC,什么是AOP?

答:下面答案来源于百度【文心一言】:

  • IOC(控制反转)是一种设计模式(思想),它允许


    将对象的创建和管理交给Spring容器来处理,而不是在代码中直接创建对象


    。通过使用IOC,可以将对象的依赖关系从代码中解耦,使得代码更加灵活、可维护和可测试。
  • AOP(面向切面编程)也是一种设计模式(思想),


    它通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态添加功能


    。AOP解决了面向对象编程中无法解决的问题,例如事务管理、安全性、日志记录等。

Spring框架通过实现IOC和AOP,使得程序更加模块化、灵活和易于维护。同时,Spring还提供了许多其他模块和功能,如DAO、ORM、WebMVC等,使得它成为一个功能强大的Java开发框架。



前置知识总结

从上面的问题里面,我们提到了一个很重要的东西,即:

Spring就是实现了AOP技术的IOC容器

。并且,也概括地描述了IOC跟AOP的概念。既然我们也知道了,IOC其实也管理了对象的创建,那么说到对象创建,肯定也离不开我们在Q1说的,对象创建的过程。而且,无论对象怎么创建,谁创建,都没办法离开上面的流程的。

事实上,可以提前告诉大家的是,IOC在创建对象的过程,无非就是在上面的对象创建流程中,丰富了一些细节,新增了一些拓展点,为Spring功能实现提供支持。



课程内容

为了展开对Spring源码的研究,我们这里线大致地串讲一下Spring的一些核心知识点,让大家对Spring的底层一些基础逻辑有个清晰的认知。



一、Spring容器的启动

我想,经历过SSM/SSH时代的朋友,对下面的代码都不会陌生:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();
System.out.println(userService);
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

<!--    <import resource="引入其他bean xml配置文件" />-->
    <bean id="userService" class="org.example.spring.bean.UserService"/>
</beans>

如果真的很陌生也没关系,下面这个可能就相对熟悉一点了:(

后面也会围绕这个启动方式的Spring讲解。除了是下面的比较主流,也因为,下面这种方式使用更广、更新,内容相对丰富点!

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();
System.out.println(userService);
@Component
public class UserService {

    public void test() {
        System.out.println("这是一个测试方法");
    }
}

哈哈,我估计很多直接进入了Java【SpringBoot】时代的朋友,可能连上面这个都没看到过。

那上面段代码是干啥的呢?很简单,就是启动一个Spring容器而已。上面两个不同的启动方式,也仅仅是Bean注册方式不一样。比如前者是通过读取

xml

里面的

<bean>

标签定义,后者是读取的注解式Bean。

到这里,想问大家一个问题,那就是,通过上面第二种方式的代码,你发现了什么?我的发现是:我仅仅只是调用了一行代码,就可以开始使用Spring定义的Bean了,什么依赖注入,AOP啥的,我都没管,直接就可以了。这证明了啥?其实很粗浅,也有点废话,那就是证明:

通过这一行代码,里面就帮我完成了所有我们平时使用过的,Spring的基础能力



二、一般流程推测

根据我们之前学习过Spring相关的操作,简单推测一下,这一行代码里面干了什么。

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);



2.1 扫描

首先是第一点【扫描】。我们在项目中,写了这么多Bean,或者说,我项目里面这么多类,哪些是Bean,哪些是普通类,Spring是怎么识别到的?其实道理很简单的,Spring它也没那么智能,想要获取这个类的信息,Spring肯定要【亲自】去看一眼,才能知道这个类的具体信息。你有这么多少个文件,他就要扫描多少个类。关键代码如下:

// 定义需要扫描的基础包名
@ComponentScan("org.tuling.spring")
public class AppConfig {
}



2.2 IOC

扫描完了所有的文件,那基本上Spring已经能确定,哪些是Bean,哪些是普通类了。接下来,就可以开始创建Bean了,这里,就是所谓的IOC过程



2.3 AOP

AOP肯定是发生在IOC之后的,如果你们了解设计模式里面的【代理模式】的话,理解这一点并不难。毕竟,如果目标对象功能不完整,代理对象的功能也会收到影响。



2.4 小结

在这里插入图片描述



三、【扫描】过程简单推测

之前我们也说了,扫描,就是需要Spring亲自去看一看,哪些是需要被创建Bean的,哪些是不需要的。就拿我们举例用的

new AnnotationConfigApplicationContext(AppConfig.class)

来说,大概步骤如下:(简单推测,不详)

  1. 它需要先看

    AppConfig.class

    ,读取扫描包的基础路径
  2. 根据上一步读到的基础路径,遍历包下所有的文件,如果类上存在

    @Component



    @Service

    等注解,则确认为是一个Bean
  3. 筛选完后将读取的Bean信息记录下来,比如说存到一个Map里面,方便后续遍历

在这里插入图片描述



四、【IOC】过程简单推测

IOC过程,其实在Spring中有个比较专业的术语,叫做:Bean的生命周期。简单的几个字,包含了很多内容。在此之前,大家先看一看【前置知识】里面【JVM对象创建过程】,加深一下印象。

其中有:

  1. 利用该类的构造方法来实例化得到一个对象(但是如何一个类中有多个构造方法,Spring则会进行选择,这个叫做推断构造方法)
  2. 得到一个对象后,Spring会判断该对象中是否存在被@Autowired注解了的属性,把这些属性找出来并由Spring进行赋值(依赖注入)
  3. 依赖注入后,Spring会判断该对象是否实现了BeanNameAware接口、BeanClassLoaderAware接口、BeanFactoryAware接口,如果实现了,就表示当前对象必须实现该接口中所定义的setBeanName()、setBeanClassLoader()、setBeanFactory()方法,那Spring就会调用这些方法并传入相应的参数(Aware回调)
  4. Aware回调后,Spring会判断该对象中是否存在某个方法被@PostConstruct注解了,如果存在,Spring会调用当前对象的此方法(初始化前)
  5. 紧接着,Spring会判断该对象是否实现了InitializingBean接口,如果实现了,就表示当前对象必须实现该接口中的afterPropertiesSet()方法,那Spring就会调用当前对象中的afterPropertiesSet()方法(初始化)
  6. 最后,Spring会判断当前对象需不需要进行AOP,如果不需要那么Bean就创建完了,如果需要进行AOP,则会进行动态代理并生成一个代理对象做为Bean(初始化后)

另外需要注意的是,Bean对象创建出来后:

  1. 如果当前Bean是单例Bean,那么会把该Bean对象存入一个Map<String, Object>,Map的key为beanName,value为Bean对象。这样下次getBean时就可以直接从Map中拿到对应的Bean对象了(实际上,在Spring源码中,这个Map就是单例池);
  2. 如果当前Bean是原型Bean,那么后续没有其他动作,不会存入一个Map,下次getBean时会再次执行上述创建过程,得到一个新的Bean对象。

在这里插入图片描述



4.1 推断构造方法过程细讲

Spring在基于某个类生成Bean的过程中,需要利用该类的构造方法来实例化得到一个对象,但是如果一个类存在多个构造方法,Spring会使用哪个呢?

Spring的判断逻辑如下:

  1. 如果一个类只有一个构造函数,不管构造函数是有参,还是无参,Spring都会使用这个构造函数创建对象,因为没得选了;
  2. 如果这个类存在多个构造函数:

    • 如果存在无参构造函数,则使用无参构造函数。因为在Java里面,无参构造函数本身就具有默认的意思在里面;
    • 如果没有无参构造函数,则看多个无参构造函数,哪个有

      @Autowired

      修饰,有就选择;没有就只能报错了

还有一个问题。如果Spring选择了一个有参的构造方法,Spring在调用这个有参构造方法时,需要传入参数,那这个参数是怎么来的呢?答案是:Spring会根据入参的类型和入参的名字去Spring中找Bean对象。

3. 先根据入参类型找,如果只找到一个,那就直接用来作为入参;

4. 如果根据类型找到多个,则再根据入参名字来确定唯一一个;

5. 最终如果没有找到,则会报错,无法创建当前Bean对象。



五、【AOP】过程简单推测

AOP就是进行动态代理,在创建一个Bean的过程中,Spring在最后一步(

放入单例池之前

)会去判断当前正在创建的这个Bean是不是需要进行AOP,如果需要则会进行动态代理。

那么,如何判断一个Bean是否需要被AOP代理呢?步骤如下:

  1. 找出所有的切面Bean(切面也是Bean来的,或者叫做:特殊的Bean)
  2. 遍历切面中的每个方法,看是否写了@Before、@After等注解(通知)
  3. 如果写了,则判断所对应的Pointcut是否和当前Bean对象的类是否匹配
  4. 如果匹配则表示当前Bean对象有匹配的的Pointcut,表示需要进行AOP

利用cglib进行AOP的大致流程:(看上面的代理范式大概就知道了)

  1. 新增一个代理类XxxProxy,继承自被代理对象XxxTarget,并且持有一个XxxTarget成员变量(这个成员变量需要经过一个Bean的声明周期,即,完成了IOC等)
  2. 在代理类中重写父类的方法
  3. 执行代理类的方法时,调用的代理类的方法,但同时也需要执行切面的逻辑

在这里插入图片描述

然后这里给大家一个【代理模式】的范式:

// 被代理对象
public class ProxyTarget {
    public void run() {
        System.out.println("这是普通对象的run");
    }
}


// 代理对象
public class ProxyModel extends ProxyTarget {
    private ProxyTarget proxyTarget;

    public void setProxyTarget(ProxyTarget proxyTarget) {
        this.proxyTarget = proxyTarget;
    }

    @Override
    public void run() {
        System.out.println("我代理对象可以在这里做加强---1");
        super.run();
        System.out.println("我代理对象也可以在这里做加强---2");
    }
}



六、Spring事务

当我们在某个方法上加了@Transactional注解后,就表示该方法在调用时会开启Spring事务,而这个方法所在的类所对应的Bean对象会是该类的代理对象。

Spring事务的代理对象执行某个方法时的步骤:

  1. 判断当前执行的方法是否存在@Transactional注解
  2. 如果存在,则利用事务管理器(TransactionMananger)新建一个数据库连接
  3. 修改数据库连接的autocommit为false
  4. 执行target.test(),执行程序员所写的业务逻辑代码,也就是执行sql
  5. 执行完了之后如果没有出现异常,则提交,否则回滚


Spring事务是否会失效的判断标准:某个加了@Transactional注解的方法被调用时,要判断到底是不是直接被代理对象调用的,如果是则事务会生效,如果不是则失效。

(PS:这一点很容易被疏忽)

另外,还有个经典例子,那就是

@Bean

在有跟没有

@Configuration

的时候,结果是不一样的,如下:

声明Bean的方法:

@ComponentScan("org.tuling.spring")
@Configuration
public class AppConfig {
    
    @Bean
    public UserService userService() {
        return new UserService(walletService());
    }

    @Bean
    public UserService userService1() {
        return new UserService(walletService());
    }

    @Bean
    public WalletService walletService() {
        return new WalletService();
    }
}

// UserService声明
public class UserService {
    private WalletService walletService;

    public UserService() {}
    public UserService(WalletService walletService) {
        this.walletService = walletService;
    }

    public WalletService getWalletService() {
        return walletService;
    }

    /**
     * 自我介绍
     */
    public void selfIntroduction() {
        System.out.println("你好,我是阿通,我有好多钱");
        walletService.showMyBalance();
    }
}

大家看上面的声明Bean的方法,按照设想,

WalletService

肯定也是一个单例嘛,所以,

userService



userService1

持有的

walletService

对象肯定是一样的。

调用方法:

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        UserService userService = (UserService)context.getBean("userService");
        System.out.println(userService);
        System.out.println(userService.getWalletService());


        System.out.println("--------------------------------");
        UserService userService1 = (UserService)context.getBean("userService1");
        System.out.println(userService1);
        System.out.println(userService1.getWalletService());

结果输出如下:

org.tuling.spring.bean.UserService@2c34f934
org.tuling.spring.bean.WalletService@12d3a4e9
--------------------------------
org.tuling.spring.bean.UserService@240237d2
org.tuling.spring.bean.WalletService@12d3a4e9

看结果,没什么问题,如期输出。但如果我们把声明Bean的方法里面的

@Configuration

去掉,结果会变成这样:

@ComponentScan("org.tuling.spring")
//@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserService(walletService());
    }

    @Bean
    public UserService userService1() {
        return new UserService(walletService());
    }

    @Bean
    public WalletService walletService() {
        return new WalletService();
    }
}
org.tuling.spring.bean.UserService@710726a3
org.tuling.spring.bean.WalletService@646007f4
--------------------------------
org.tuling.spring.bean.UserService@481a15ff
org.tuling.spring.bean.WalletService@78186a70

为什么,只是简单注释了一个

@Configuration

结果就不一样了呢?分析如下:


  1. @Bean

    注解可以将方法返回的对象注册为一个 Bean,并且该 Bean 会被 Spring 容器管理。仅此而已
  2. 所以,在

    userService()

    方法重调用

    walletService()

    方法,实际上就是一个普通Java调用而已,肯定会重新

    new WalletService()
  3. 而被

    @Configuration

    注解之后,所有方法都将被代理(暂时还没找到源码证据,等后面我看懂了再附上)



学习总结

  1. 简单学习了Spring启动的流程
  2. 通过一些常见Spring操作的串讲,大概了解了一下IOC和AOP的大致流程



版权声明:本文为qq_32681589原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。