UNREMITTINGLY

V1

2022/02/15阅读:39主题:雁栖湖

spring浅析

本篇就对spring做一个整体的浅析,后面会陆续对流程中的每个环节的源码进行梳理和讲解。

1spring是什么

关于spring是什么,相信有一部分同学很清楚了,但一定也有一部分同学虽然清楚,如果用语言表达出来总是会差强人意。

因此这里先简单说一下spring概念以及存在的意义。

spring是一个容器,容器里面存放的是bean(对象),spring同时提供了对于bean创建和管理的一套机制,spring ioc是其实现思想,Spring Framework是具体项目名称,此项目就是具体实现的源码。

而spring mvc,spring boot 以及spring整个生态,都是建立在spring容器的基础之上。

Spring容器的意义是什么呢?

其实spring容器最大意义是通过依赖注入实现控制反转,从而降低耦合度,提高可维护性,说的通俗一点就是把对象的创建和对象与对象之间的依赖关系不再由程序员来维护,而是由spring负责创建对象,并管理这些对象,程序员只需要在使用的时候取出来就可以了。

只不过spring在实现这个容器的过程中,会额外做很多的处理,我们接着分析spring如何实现容器。

2spring容器如何实现

如果让你来实现一个spring容器你会如何设计呢?

不难想到实现的步骤应该如下面所示:

  1. 找到class文件
  2. 类加载处理
  3. 调用构造方法创建普通对象
  4. 放入map缓存普通对象
  5. 调用map.get()获取普通对象

其实spring实现的容器就是这几个步骤,只不过每个步骤的实现会更加细分一些,我们来看下spring的步骤:

  1. 构造BeanDefinition对象,并保存到缓存map1
  2. 推断构造方法创建普通对象
  3. 对普通对象依赖注入(属性赋值)
  4. 普通对象初始化前
  5. 普通对象初始化
  6. 普通对象初始化后(AOP)
  7. 初始化后的普通对象最终作为bean放入缓存map2
  8. 调用getBean()获取bean

我们对每一步骤进行一个简单的介绍:

构造BeanDefinition对象

早些时候我们在使用spring,是用配置xml文件的方式,而如今我们用的最多的是注解的方式配置:

  • < bean />
  • @Bean
  • @Component(@Service,@Controller)

首先我们说下为什么要先生成BeanDefinition对象,我们知道配置bean的时候spring提供了很多的属性,比如:Conditional,Scope、Lazy、Primary、DependsOn、Role、Description等等。

每个bean配置的属性不一样,spring必须有一个可以对每个bean配置的描述信息,而这个描述信息在spring中呈现形式就是BeanDefinition对象,这样每次创建bean的时候,spring只需要找到对应的BeanDefinition对象,按照其描述创建bean即可。

spring会缓存创建好的BeanDefinition对象,那为什么要缓存呢?因为spring只会对单例的对象只创建一次并保存到单例池,对于非单例的对象,spring需要在每一次用到的时候都去重新创建一次bean,这样的话每一次都需要BeanDefinition对象,把BeanDefinition对象放入缓存,就是为了下次使用的时候不需要去重新解析类,可以直接在缓存中取出使用即可。

xml的方式中,我们需要把每个需要spring管理的类都要在xml文件中配置一下,因为spring默认会去解析这个xml文件,把每个bean标签解析成BeanDefinition对象,spring实现解析xml文件并生成BeanDefinition对象利用的是BeanDefinition解析器:XmlBeanDefinitionReader。

注解配置的方式中,不需要去解析固定的xml文件,而是每个类通过配置指定的注解来标示当前这个类的对象需要spring管理,而对于spring就需要去找到这些需要管理的类,而spring实现的方式是扫描某个路径下的所有类,去解析每个类上是否有相应的注解,如果有,则表示这个类需要spring管理,spring就会将其生成一个BeanDefinition对象,spring实现扫描并创建BeanDefinition对象利用的是BeanDefinition扫描器:classPathBeanDefinitionScanner。 除了扫描某个路径下所有的类,如果只想对某一个类创建BeanDefinition对象,spring提供了注解解析器:AnnotatedBeanDefinitionReader

以上三种方式对象的代码如下:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(context);
int i = xmlBeanDefinitionReader.loadBeanDefinitions("spring.xml");

System.out.println(context.getBean("user"));
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.refresh();

ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.scan("com.zhouyu");

System.out.println(context.getBean("userService"));
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

AnnotatedBeanDefinitionReader annotatedBeanDefinitionReader = new AnnotatedBeanDefinitionReader(context);

// 将User.class解析为BeanDefinition
annotatedBeanDefinitionReader.register(User.class);

System.out.println(context.getBean("user"));

我们自己实现的时候步骤中是先找到class文件,然后类加载器加载成类,那么spring中是否也是如此呢,spring最终生成bean的也是通过jvm类加载器加载,但是在最终的bean生成之前,spring需要知道哪个类需要spring管理,实现逻辑就是要看类上是否有相应注解,如果为了实现这个动作,那就不得不先让jvm去加载所有的类,这样与jvm类加载器的延迟加载策略相悖,而且jvm加载的方式效率很低,所以在spring的注解解析器和类路径扫描器中的处理是通过ASM技术实现的,ASM技术是spring对class字节码文件进行解析一项技术,旨在提高加载效率和避开使用jvm类加载器加载类而获取类信息。从而提高效率。

推断构造方法

找到对应的类后,就要根据其对象的构造方法创建初始对象,如果我们手动创建对象,我们可以自由选择用哪个构造方法,但是spring是自动创建对象,如果一个类有多个构造方法的话,spring就要考虑用哪一个构造方法了。

Spring的判断逻辑如下:

  • 如果一个类只存在一个构造方法,不管该构造方法是无参构造方法,还是有参构造方法,Spring都会用这个构造方法

  • 如果一个类存在多个构造方法

    1. 这些构造方法中,存在一个无参的构造方法,那么Spring就会用这个无参的构造方法

    2. 这些构造方法中,不存在无参的构造方法,那么Spring就会报错

Spring的设计思想是这样的:

  1. 如果一个类只有一个构造方法,那么没得选择,只能用这个构造方法

  2. 如果一个类存在多个构造方法,Spring不知道如何选择,就会看是否有无参的构造方法,因为无参构造方法本身表示了一种默认的意义。

  3. 不过如果某个构造方法上加了@Autowired注解,那就表示程序员告诉Spring就用这个加了注解的方法,那Spring就会用这个加了@Autowired注解构造方法了

需要重视的是,如果Spring选择了一个有参的构造方法,Spring在调用这个有参构造方法时,需要传入参数,那这个参数是怎么来的呢?

Spring会根据入参的类型和入参的名字去Spring单例池中找Bean对象(以单例Bean为例,Spring会从单例池Map中去找):

  1. 先根据入参类型找,如果只找到一个,那就直接用来作为入参
  2. 如果根据类型找到多个,则再根据入参名字来确定唯一一个
  3. 最终如果没有找到,则会报错,无法创建当前Bean对象

确定用哪个构造方法,确定入参的Bean对象,这个过程就叫做推断构造方法。

对普通对象依赖注入

依赖注入就是指对象中成员属性的自动赋值,以前程序员在创建对象后,会手动给属性赋值,spring中需要对成员属性进行自动赋值,

 @Autowired
 public User user;

spring中对加了Autowired注解的属性进行赋值,上例中,user属性需要一个User类型的对象作为值,那么spring就要去找一个User类型的对象对user赋值,spring底层会先通过byType的方式去单例池寻找一个User对象,如果存在多个User对象,就会再按byName的方式确定一个User对象。

正常说属性赋值就是这样的流程,但是存在一种特殊情况“循环依赖”,这也是面试中经常被问到的,spring中解决循环依赖是通过三级缓存实现。这里不细讲解,后面会有专门文章介绍。

普通对象初始化

为什么会有初始化阶段,可以理解为spring在发展的过程中发现需要对上面生成的对象做一些加工处理,实现一些自动化方法,比如,spring想让某个对象在创建后就具备某个功能后作为工具类来用,也有可能程序员想让某个对象在创建后具备一些特定能力,比如,我们有一个配置类,所有配置的值都在数据库,我们想让这个配置类生成的对象创建完后就已经完成了从数据库取值并赋值给当前对象的操作。实现这个功能就需要额外处理,spring在这个阶段提供了InitializingBean接口,接口中有一个afterPropertiesSet方法,此阶段spring会对实现了这个接口的对象执行此方法。

普通对象初始化前后

显然随着spring的发展,spring对对象的处理不断增加,如果都堆积在初始化阶段显然会有问题,因此spring提供了bean的后置处理器BeanPostProcessor接口,这个接口提供了两个方法,postProcessBeforeInitialization和postProcessAfterInitialization,spring会判断对象是否实现了BeanPostProcessor接口,如果实现了此接口,spring会分别在初始化阶段的前后执行对应方法,类似于InitializingBean,此接口也会在spring内部使用,同时也提供给程序员使用。

spring aop 也是在初始化后阶段,此阶段spring会判断对象是否需要进行aop,如果需要就会生成代理对象,如果不需要,到此bean就创建结束,另外spring事物的基础是aop,因此事物也是发生在此阶段,后续我们详细讲解spring aop 和 spring事物。

spring aop 判断规则:

1.找到所有的切面bean; 2.遍历每个切面bean的每个方法; 3.如果方法上切点表达式与当前bean匹配,进行aop; 4.把匹配到的方法存入map内。

此阶段之后的对象就是spring最终创建的bean,spring会把最终生成的bean方法单例池map。

调用getBean()获取bean

一般情况下,当需要使用某个单例bean时候,就会调用此方法,但是spring底层并非直接在单例池中取出这么简单,这里涉及到FactoryBean接口。

如果说InitializingBean和BeanPostProcessor提供了程序员对spring创建bean的干预能力,那么FactoryBean就是提供了程序员完全自定义创建bean的能力,这个接口也是spring整合其他框架的基础,比如spring整合dubbo,spring整合mybatis等等。

FactoryBean接口中主要提供了两个方法getObject()和getObject()。

我们用一个例子来说明下FactoryBean的使用

public class UserFactoryBean implements FactoryBean{

  @Override
 public Object getObject() {
  return new User();
 }

 @Override
 public Class<?> getObjectType() {
  return User.class;
 }


}

spring在调用getBean()前不会去判断UserFactoryBean是否实现了FactoryBean,只会把UserFactoryBean当做一个没有实现任何接口的类处理。

当spring创建完UserFactoryBean的bean对象后,在单例池是这样存储的:

{“userFactoryBean”,UserFactoryBean}

当调用getBean(“userFactoryBean”),这个时候才会去判断UserFactoryBean是否实现了FactoryBean,因为这里实现了,所以spring会去调用getObject()方法,获取到一个程序员自己定义的对象,spring会这个对象存入另一个缓存map,如下面这样存储:

{“userFactoryBean”,User}

如果再次调用getBean(“userFactoryBean”),就会在缓存中找到User对象返回。

那如何获取之前单例池中的bean呢?

spring中的规则是在key前加&,如下获取方式:

getBean(“&userFactoryBean”)

3总结

spring博大精深,本篇只是简单浅析,底层还有很多细节,后面章节会介绍所有细节,感兴趣的可以接下来跟着我一起探究。

了解更多:

分类:

后端

标签:

Java

作者介绍

UNREMITTINGLY
V1