SpringBoot面经——SpringBean部分
SpringBoot面经——SpringBean部分
Spring组成
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的生命周期
大致分为四个过程:实例化 -> 属性赋值 -> 初始化 -> 销毁
-
创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
-
Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如@Autowired 等注解注入的对象、@Value 注入的值、setter方法或构造函数注入依赖和值、@Resource注入的各种资源。
-
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() 方法。
-
销毁 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 的三级缓存包括:
- 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
- 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中
ObjectFactory
产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()
都是会产生新的代理对象的。 - 三级缓存(singletonFactories):存放
ObjectFactory
,ObjectFactory
的getObject()
方法(最终调用的是getEarlyBeanReference()
方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
接下来说一下 Spring 创建 Bean 的流程:
- 先去 一级缓存
singletonObjects
中获取,存在就返回; - 如果不存在或者对象正在创建中,于是去 二级缓存
earlySingletonObjects
中获取; - 如果还没有获取到,就去 三级缓存
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