文章

SpringBoot面经——SpringBean部分

SpringBoot面经——SpringBean部分

Spring组成

image

Core Container

Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。

Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。

  • spring-core:Spring 框架基本的核心工具类。
  • spring-beans:提供对 bean 的创建、配置和管理等功能的支持。
  • spring-context:提供对国际化、事件传播、资源加载等功能的支持。
  • spring-expression:提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。

AOP

  • spring-aspects:该模块为与 AspectJ 的集成提供支持。
  • spring-aop:提供了面向切面的编程实现。
  • spring-instrument:提供了为 JVM 添加代理(agent)的功能。 具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。没有理解也没关系,这个模块的使用场景非常有限。

Data Access/Integration

  • spring-jdbc:提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
  • spring-tx:提供对事务的支持。
  • spring-orm:提供对 Hibernate、JPA、iBatis 等 ORM 框架的支持。
  • spring-oxm:提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
  • spring-jms : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。

Spring Web

  • spring-web:对 Web 功能的实现提供一些最基础的支持。
  • spring-webmvc:提供对 Spring MVC 的实现。
  • spring-websocket:提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
  • spring-webflux:提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。

Messaging

spring-messaging 是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用。

Spring Test

Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。

Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。

SpringMVC到底是啥?

使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!

Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!

而SpringMVC是Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

当然,我们Web开发常用的注解基本都来自SpringMVC模块,但MVC模块中的Jsp模板相关内容其实很少使用了现在。一般都使用这个模块提供的Restful相关接口来做前后端分离的设计。

@Component 和 @Bean 的区别是什么?

这个问题可以扩大一点,变成SpringBoot中,有哪些方法定义Bean,各自有什么不同?

基本的用于类的:@Component​表示通用注解、用于特定场合的还包括@Repository @Service @Controller​。

除了这些以外,还包括@Bean,这个是定义在方法上的,表示这个方法构造了一个Bean,方法返回的实例会被Spring放入Ioc容器中。

而这俩又有什么区别呢?

从定义上就能看出,@Component 注解作用于类,而@Bean注解作用于方法。而@Component注解的类会在启动阶段通过类路径扫描自动扫描装载。@Bean则是在某个方法上定义,这样每当这个方法被调用的时候就会产生一个新的Bean。

通常@Bean会用于把某些jar打包的第三方库引入Spring管理。

如何注入Bean,为什么@Autowired有时会被Warning。

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。注入的位置包括直接字段注入、Setter方法注入、构造函数注入。

Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入  SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;

还是建议通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。

@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType。

@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。

从注入方式的角度方式考虑,Spring 官方推荐构造函数注入,这种注入方式的优势如下:

  • 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。
  • 不可变性:有助于创建不可变对象,提高了线程安全性。
  • 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。
  • 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。

构造函数注入适合处理必需的依赖项,而 Setter 注入 则更适合可选的依赖项,这些依赖项可以有默认值或在对象生命周期中动态设置。虽然 @Autowired 可以用于 Setter 方法来处理必需的依赖项,但构造函数注入仍然是更好的选择。

在某些情况下(例如第三方类不提供 Setter 方法),构造函数注入可能是唯一的选择。

Bean的作用域

默认情况下,SpringBean是单例(singleton)模式,其作用域就是全局的,也是唯一单例的。

除了单例模式之外,还有几种:每次调用都创建新的(prototype),每次http请求都创建新的(request),同一个HTTP Session共享一个Bean(session),(global session)。

singleton(默认)

singleton 是单例类型(对应于单例模式),就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用。可以指定Bean节点的 lazy-init=”true” 来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean。

prototype

在每次对该 bean 请求(将其注入到另一个 bean 中,或者以程序的方式调用容器的 getBean() 方法)时都会创建一个新的 bean 实例。根据经验,对有状态的 bean 应该使用 prototype 作用域,而对无状态的 bean 则应该使用 singleton 作用域。

剩下几个先不管

Bean的生命周期

大致分为四个过程:实例化 -> 属性赋值 -> 初始化 -> 销毁

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。

  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value 注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。

  3. Bean 初始化

    • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
    • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
    • 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
    • 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
    • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
    • 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。
    • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
    • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法。
  4. 销毁 Bean
    销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
    如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
    如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过@PreDestroy 注解标记 Bean 销毁之前执行的方法。

Bean线程安全

SpringBean本身没有线程安全特性,如果是prototype则无需考虑这个问题,因为每次请求都使用新的对象,只需要讨论singleton作用域时的线程安全性。

singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。

例如这个bean就是有状态的:

// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List
@Component
public class ShoppingCart {
    private List<String> items = new ArrayList<>();

    public void addItem(String item) {
        items.add(item);
    }
}

这种情况下,就需要做特别处理保证线程安全,当然通常需要尽量避免这种情况。

对于有状态单例 Bean 的线程安全问题,常见的解决办法是:

  • 使用ThreadLocal: 将可变成员变量保存在 ThreadLocal 中,确保线程独立。

  • 使用同步机制: 利用 synchronized 或 ReentrantLock 来进行同步控制,确保线程安全。

    • synchronized关键字可以用来修饰方法或者代码块。当一个方法被synchronized修饰时,那么每次只有一个线程能够执行该方法。如果一个代码块被synchronized修饰,那么在同一时间只有一个线程能够执行该代码块。

    • ReentrantLock类似一个手动操作的锁,如示例所示。

      import java.util.concurrent.locks.ReentrantLock;
      
      public class Counter {
          private int count = 0;
          private final ReentrantLock lock = new ReentrantLock();
      
          public void increment() {
              lock.lock(); // 获取锁
              try {
                  count++;
              } finally {
                  lock.unlock(); // 释放锁
              }
          }
      }
      

Spring循环依赖

循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。

@Component
public class CircularDependencyA {
    @Autowired
    private CircularDependencyB circB;
}

@Component
public class CircularDependencyB {
    @Autowired
    private CircularDependencyA circA;
}

像这样的情况,首先是一个设计问题,设计上不应该出现这样的情况,而且SpringBoot现在的版本在默认情况下也不支持这种情况。

但Spring对这种情况是有解决方案的,核心是使用三层缓存(说是缓存,准确来说应该来说就是三层map)。

简单来说,Spring 的三级缓存包括:

  1. 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
  2. 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中ObjectFactory​产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()​都是会产生新的代理对象的。
  3. 三级缓存(singletonFactories):存放ObjectFactory​,ObjectFactory​的getObject()​方法(最终调用的是getEarlyBeanReference()​方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

接下来说一下 Spring 创建 Bean 的流程:

  1. 先去 一级缓存 singletonObjects 中获取,存在就返回;
  2. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
  3. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotry​ 的 getObject()​ 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。

显然,初始化A的过程中,A是放在三级缓存里面的。此时因为依赖B,所以转而去初始化B,B又需要A,此时产生冲突,而Spring的解决方案是直接去三级缓存里面把A的引用先放过来,这样就解决了循环依赖的问题。

LazyLoad也可以解决循环依赖的问题,还是刚刚的情况,由于在 A 上标注了 @Lazy​ 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性,不会去实际注入B,也就没有循环依赖的问题。


著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html

License:  CC BY 4.0