[Day20] spring八股文复习
Spring 面试
目录
[toc]
Spring #⭐new
讲讲什么是 IOC
IOC 字面理解是控制反转, 而实际上他要实现的效果也是这样的; 在没有 Sping 的情况下, 如果我们一个类中想要引入另一个类并使用方法, 我们只能手动的 new 然后手动管理这个对象; 当业务场景越来越复杂, 类和类之间的关系越来越复杂的情况下那么整体程序就会呈现出紧耦合的情况。而通过 IOC 我们将对象的实例化初始化和销毁的过程都交给 Spring 容器进行管理, 实现程序解耦。
讲讲什么是 AOP
AOP 就是为了实现无侵入式的功能增强和公共功能的抽取
AOP 的实现是在 Bean 生命周期中实例化和初始化完成之后通过 BeanPostProcessor接口进行实现
在每个 Bean 初始化完成之后都会调用后置处理器, 然后为需要进行代理的 bean 创建代理对象; AOP 的底层是基于动态代理实现的
- AOP 的实现有 jdk 和 cglib
- jdk 实现的 AOP 要求被代理的类必须实现一个或者多个接口
讲讲什么是 DI; Spring 有哪些依赖注入的方式?
DI 就是依赖注入, 在 spring 中当 Bean 之间存在依赖关系的时候, 容器会自动进行注入并且可以通过三级缓存来解决循环依赖的问题。
- 依赖注入的方式
- 字段注入
- 构造器注入
- setter 注入
构造器注入
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired // Spring 4.3+ 单一构造器可省略
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
setter 注入
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
字段注入
public class ProductService {
@Autowired
private InventoryService inventoryService;
}
@Autowired 和@Resource 的区别
- 两者的相同点都是实现依赖注入的
@Autowired
是通过类型来注入的; 当容器中有两个相同通类型的 Bean 是则会报错, 但是这个报错可以通过@Primary
和@Qualifier(name)
来解决@Parimary
是写在被注入 Bean 上的; 表示冲突时当前 Bean 优先注入Qualifier
是写在字段上的; 表示冲突时注入指定 name 的 Bean- 可以写在构造、字段和 setter 上
@Resource
- 这个注解是 JDK 中提供的, 只不过 Spring 提供了对他的支持
@Recource
可以指定 ByName 和 ByType 进行准确的注入; 默认是根据 name 优先匹配, 如果没有匹配上再根据类型进行匹配- 只能写在字段和 setter 上
Bean 注入 IOC 的方式又哪些
- 通过 xml
- 通过 ComponentScan 扫描 Controller、Service、Repository、Component 定义的类
- 通过 Configuration 配置类+Bean 的方法 (是 xml 的一种演变是 spring 迈入无配置化的一个标志)
- 使用 import 注解导入 ImportSelector 的实现类或者普通类
@PostConstruct、init-method 和 afterPropertiesSet 执行顺序
- 静态代码块
- 类被加载时执行一次
- 构造方法
- @PostConstruct
- afterPropertiesSet
- init-method
什么是 Bean 的生命周期
Bean 的生命周期和 vue 还有 maven 的生命周期一样, 指的都是一个对象从创建到使用和销毁的过程。 下面我来讲述一下 Bean 从定义到被销毁的整个流程; 首先我们知道我们会用各种方式来定义 Bean ,比如 xml 还有各种注解, 所以第一步要做的就是对这样不同的定义做归一化处理, Spring 中是通过 BeanDefinitionReader 将他们读成结构相同的 BeanDefinition 用 map 存储; 因为目前这些 BeanDefiniton 并不是完整的, 比如有的 Bean 需要从配置文件中或者其他地方引入值, 所以通过 BeanFactoryPostProcess 来执行这些操作得到完整的 BeanDefiniton 对象, 然后就可以正式开始创建 Bean, 大体上可以分为三大步骤实例化 bean, 属性填充, 初始化 Bean; 实例化 Bean 是通过反射实现创建对象的过程,反射会选择默认或者用户指定的构造函数进行实例化,默认是无参构造,或者是唯一的有参构造,如果有多个有参构造那么就会报错; 然后是属性填充, 这一步是实现 Spring 中 DI 的一步, 容器会自动根据 Bean 之间的依赖关系实现注入, spring 是通过三级缓存解决循环依赖问题的; 最后就是初始化; 这一步会首先通过 Aware 接口来补充 bean 的信息,因为 Aware 接口可以然我们的 bean 携带元数据,然后初始化就可以分为三小步,BeanPostProcessor 前置处理器,初始化操作,BeanPostProcessor 后置处理器,我们熟知的 AOP 就是在这一步进行的,因为在到这一步对于 Bean 数据层才是完整地,可以对他进行封装或者功能增强,前置处理器会进行动态代理,然后初始化会执行定义的初始化操作,@PostConstruct, init_method, 还有 initialization 接口中的 afterPropertiesSet 方法;然后是 BeanPostProcess 后置处理器会返回代理后的 bean 对象;经过以上步骤就能创建出所有 Bean; 然后其他类中就可以松耦合地来方便地使用这些 Bean 了; 当 Bean 释放之前也会触发钩子进行释放前的逻辑操作,最后就是释放;
Spring 是如何解决循环依赖问题的
Spring 解决循环依赖问题是通过三级缓存来实现的; 例如 A 和 B 两个 Bean 出现了相互依赖的情况, 那么在 Bean 的创建流程是这样的: 这里补充说明一下三级缓存的结构和分别存放的内容, 一级缓存是 map 存储的是完整可用的 Bean, 二级缓存存储的是属性未完整填充的半成品 Bean, 三级缓存同样是 map, key 是 beanId, value 是一个执行就能获取 Bean 的 lambda 表达式; 创建开始是在 List 中存放着 A 和 B 的 BeanDefiniton, 程序先初始化 A, 从一二三级缓存中查找是否有 A, 发现没有就会通过反射实例化 A 并将 A 存放在三级缓存, 然后开始对 A 进行属性填充, 发现需要 B 那么就开始创建 B 的过程; 依然一开始从一二三级缓存中查找 B, 没有就实例化 B 并将获取 B 的 lambda 表达式存到三级缓存中, 然后开始填充 B 的字段, 发现需要 A, 然后从一二三级缓存中查找是否有 A, 发现三级缓存中有 A 但是是 lambda 表达式通过. get 方法获取 lambda 表达式得到 A 的半成品对象 (如果 A 是被代理对象此时调用 lambda 就会提前代理对象并返回), 现在就可以将 A 存到二级缓冲中, 将三级缓存中的 A 移除, 然后 B 就可以正常完成属性填充得到完整的 B; 再回到 A 进行属性填充 B 这样就可以从一级缓存中找到完整的 B 从而完整填充 A
如果 Bean 不是单例的话会存在那里?
如果 Bean 不是单例的话就不会保存在 IOC 容器的单例池当中,而是存在创建 Bean 的线程中,每创建一次这个 Bean 都会触发一次完整地 Bean 创建流程;
Bean 是线程安全的吗?
如果 Bean 是多例的话那么就是线程安全的,因为每个线程都会独立创建 Bean,存储在线程中了; 对于单例 Bean 来说他是线程共享的,就需要分情况讨论了,如果 Bean 中有成员变量涉及到了修改操作那么就是线程不安全的,我们称之为有状态的 Bean;如果 Bean 中没有涉及成员变量的修改操作的话就是无状态的 Bean,无状态的 Bean 是线程安全的;要想解决有状态 Bean 的线程安全就可以通过将变量定义在方法中,或者通过锁进行同步,因为并发问题的本质就是多个线程同时操作同一块内存上的数据,如果定义在方法中的话,数据就存在本地线程的方法栈中不会有并发问题;
BeanFactory 和 FactroyBean 的关系?
这两个接口都是定义 Bean 的操作, 不同的是 BeanFactory 是定义 ioc 容器中 bean 的基本操作包括创建 bean 获取 bean 信息等操作, 可以将它看做操作 ioc 容器的核心接口 ioc 容器的一个入口,他 ioc 管理 Bean 生命周期的重要接口; FactoryBean 的作用是让我们创建一些动态 Bean 或者复杂 Bean 的时候只需要实现这个接口并编写创建 Bean 的逻辑, 就能将 Bean 交给 spring 为我们管理, 他的核心方法是.getObject (), 在许多框架中使用的很多比如 Mybatis, fegin 等
- BeanFactory
public interface BeanFactory {
// 这个变量在获取一个FactoryBean时使用,后边详细介绍
String FACTORY_BEAN_PREFIX = "&";
// 工厂的核心方法,提供了多种获取单个实例bean的能力
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
// ...省略其他代码
}
- FactoryBean 实现 demo
-- 定义
@Component
public class StudentFactory implements FactoryBean<Student> {
@Override
public Student getObject() throws Exception {
return new Student(888L,"立堂顶针");
}
@Override
public Class<?> getObjectType() {
return Student.class;
}
}
-- 使用
@SpringBootApplication
@Slf4j
public class DemospringResourceLearningApplication {
@Resource
private Student student;
@PostConstruct
public void tt() {
log.info("student: {}", student);
student.run();
}
}
Spring 事务的实现方式
- 按照声明的方式分
- 编程式事务
- 手动管理事务的开启, 回滚和提交
- 声明式事务
- 通过注解实现
- 编程式事务
声明式事务的优点就是实现方便, 对方法的侵入性小; 缺点就是不好控制粒度, 因为声明式的最小控制粒度是方法
Spring 的事务传播机制有哪些 #🎦review
事务传播的场景出现在两个或者更多方法调用过程中, 例如 A 调用 B 方法, 根据不同的情况可以分为以下三大类的传播机制; 对于 B 来说, 第一种就是融入将 B 中的事务融入到 A 中的事务中去; 第二种就是挂起, 即当 B 方法被执行是挂起 A 方法中事务的状态, 当 B 事务提交后继续读取 A 事务的状态并继续运行; 第三种就是嵌套, 底层对于不同的数据库来讲实现嵌套事务的方式有很多比如 MYSQL 为例就可以通过 savePoint 和 rollbackTo 来实现嵌套事务的效果 spring 中的事务传播级别就基于上面三大类组合而成, 比如有当前事务必须创建新事务, 当前事务执行前必须已经存在事务否则报错, 还要如果当前事务有事务就融入没有就开启, 还有必须存在事务, 必须不存在事务 Spring 的默认事务传播机制是 REQUIRED,就是如果没有事务则开启事务,如果已经有事务那么就将当前方法融入到之前的事务中;
事务失效的场景
- 方法所在类没有注册成 Bean ,没有被 Spring 管理
- 事务所标记的方法是用 private 或者 static 修饰那么就不能被 AOP 增强
- 异常被捕获导致失效; AOP 通过异常感知和判断是否需要回滚
- 异常类型不匹配
- 事务的传播行为指定错误
- 这个案例中因为子方法的事务级别是要求开启独立的事务, 所以没办法做到整体回滚控制
- 同一个类中非事务方法调用事务方法
- 本质就是调用到了原始的方法而并非被 AOP 代理增强的拥有事务管理能力的方法
- 解决方法可以通过暴露代理对象
- 开启动类上开启暴露 (默认开启)
@EnableAspectJAutoProxy
- 开启动类上开启暴露 (默认开启)
public class MyService {
public void doSomething() {
doInternal(); // 自调用方法
}
@Transactional
public void doInternal() {
System.out.println("Doing internal work...");
}
}
事务默认的 REQUIRED; 如果没有事务则开启新事务,如果有事务则融入之前存在的事务;
Spring 中使用了哪些设计模式?
单例模式 spring 中我们不做声明的情况下默认 Bean 都是单例的,所有线程共用一个 bean ,单例 bean 可以减少很大的内存开销;多线程模式下还需要考虑 Bean 的安全性问题,无状态的 bean 是线程安全的,如果是有状态的 bean 那么就不能使用单例模式了,而应该指定 scope 为 prototype 来实现多例
原型模式 scope 指定为 prototype 后就会通过原型模式创建 bean,原型模式也叫做克隆模式,他会通过深拷贝将 Bean 拷贝到每个线程的栈中这样每个线程的 bean 都是隔离的就没有并发问题了
代理模式 AOP 就是代理模式的一个很强大的实现,底层通过 JDK 或则 CGLIB 的方式实现无侵入式的功能增强;
模板模式 父类中将复杂的过程抽象成不同的步骤,每个步骤抽象成一个类,父模板中只定义步骤的前后关系,而每一步的具体实现由子类实现;比如 JDBC 的源码中通过模板模式,将 SQL 的中流程抽象成加载驱动、获取连接通道、构建 sql、执行 sql 和关闭连接
策略模式 策略模式可以避免代码中出现大量的 if else 语句;比如在 Bean 初始化之前我们需要从各种不同的定义渠道中收集 BeanDefiniton 信息我们就是通过策略模式选择 XmlBeanDefinitonReader、PropertiesBeanDifinitionReader;
责任链模式 AOP 的拦截链就说使用的责任链模式
观察者模式 观察者模式实现的发布订阅模式在 SpringBoot 的启动过程中有使用,在环境初始化完成之后会通过观察者模式通知监听器执行相应操作;
介绍一下 Spring 中 Async 注解
@Async 注解可以让标记的方法实现异步调用,让调用方不用阻塞等待; 具体使用就是现在启动类上开启
@EnableAsync
然后一般在使用前定义一个线程池并注册成 Bean 因为如果没有指定线程池的话@Async
自己创建一个容量为一的线程池;然后只需要在需要进行异步调用的方法上加上 async 注解即可,如果需要返回值的话可以通过 Future 或则 CompletablFutue 返回;
Spring 中如何实现事件驱动
Spring 中的事件驱动是基于观察者模式实现的,事件驱动中可以分为三个角色,事件对象、监听者、发布者;定义事件类只需要在类上继承 ApplicationEvent 类,然后重写 RegisterSuccessEvent 方法;然后在监听者方法上加上注解并指定监听的类 class 对象;最后在需要发送事件的地方通过 applicationContext.publishEvent () 方法,或者通过发布器Bean即可向 spring 容器中发布事件;
定义事件
import org.springframework.context.ApplicationEvent;
public class UserRegisteredEvent extends ApplicationEvent {
private String userId;
public UserRegisteredEvent(Object source, String userId) {
super(source);
this.userId = userId;
}
public String getUserId() {
return userId;
}
}
事件监听器
@Component
public class UserRegisteredEventListener {
@EventListener
public void handleUserRegisteredEvent(UserRegisteredEvent event) {
System.out.println("用户注册事件已触发,用户 ID: " + event.getUserId());
// 可以在这里添加后续处理逻辑,比如发送邮件或通知
}
}
发布
-- 通过发布器
@RestController
public class UserController {
@Autowired
private ApplicationEventPublisher eventPublisher;
@PostMapping("/register")
public String registerUser() {
// 业务逻辑...
eventPublisher.publishEvent(new UserRegisteredEvent(this, "123"));
return "success";
}
}
-- 通过容器
如何在 Spring 启动过程中做缓存预热
spring 在启动的过程中会开启很多的注册器和监听器,我们可以通过观察者模式监听这些事件来实现启动过程中的缓存预热;比如我们可以通过
@EventListener(ApplicationReadyEvent.class)
; 或者在某些 Bean 的初始化过程中进行
@Lazy 注解能解决循环依赖吗
传统的三级缓存解决循环依赖无法对构造器注入生效,如果构造器注入的过程中发送了循环依赖,那么我们可以在其中一个构造方法上记上懒加载注解,懒加载的 Bean 只有在第一次被使用的时候才会创建
SpringBoot #⭐new
SpringBoot 的启动流程
- 从有 SpringBootApplication 注解类的 main 方法开始
- SpringApplication 执行 run 方法;1. 推断应用类型默认是 web;2. 加载 Spring. factories 中的拓展类,加载初始器和监听器;3. 准备环境变量,有系统环境变量 JVM,命令行参数等,比如当然项目的运行是开发、测试还是生产;加载完成环境变量后会通过观察者模式向监听器发生信号,环境准备后置处理器等监听器就会做具体的准备;
- 创建并配置应用上下文,也就是应用容器;根据应用的类型不同创建和初始化不同的应用容器;
- 执行自动配置;通过启动类注解中的
@EnableAutuConfiguration
注解触发扫描METE-INF/spring.factories
中指定的配置类; - 启动 web 服务器,这里一般是启动内置的 tomcat;
- 完成启动后刷新容器再完成一些后置处理器操作后就启动完成了;
SpringBoot 的自动配置原理是什么?
自动装配的目的就是让当前模块和包, 在极少量配置的情况下注入第三方组件和包的 Bean。在之前传统的开发中如何要实现注入第三方的过程必须要通过写大量 xml 进行整合实现; 在将正式流程之前应该解释一下 SPI 和 SpringFactories, 他们都是"约定大于配置"这个思想下的产物; 简单理解他们就是 bean 的提供方和消费方要实现分离和解耦, 那么他们就约定特定的存储位置和存储格式化, 这样双方只需要遵守这样的约定就能实现解耦和分离。而实现自动装配的约定就是在类路径在 META-INF/spring. factories 文件中通过 key 和 value 来描述需要进行自动装配 bean 的信息 现在就可以开始讲解自动装配的整个流程了: 首先在启动类启动后会创建 IOC 容器然后将源配置类放进去; 然后开始加载所有的配置类; 这个过程主要由
@ComponentScan
和@Import
进行扫描, 前者主要是扫描指定包下的 bean, 而第三方包中的 bean 路径信息并不知道所以通过@import
的方式进行查找; 由于源配置类上有@SprinBootApplication
注解他又间接包含@EnableAutoConfiguration
和@Import(AutoConfigurationImportSelector.class)
;所以就会通过@Import()
执行AutoConfigurationImportSelector.class
中的方法; 方法中就会通过 SpringFactoriesLoader 读取 spring. factories 中需要进行自动配置的 bean 的路径信息, 将这个集合返回; 还可以通过 Condition 条件注解来筛选,然后将扫描到的符合条件的 Bean 信息封装成 BeanDifinition 然后进行后续 bean 的实例化和初始化;
# 例如:spring-boot-autoconfigure中的配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
如何自定义一个 starter
- 在 starter 所在包下引入 spring 核心注解依赖
- 然后创建配置类,因为 starter 可能需要调用方传入某些配置
- 然后编写 starter 的核心功能并且注册成 bean, 为了避免一个 Bean 重复注册,我们一般在这些记上@ConfidentionalOnMissingBean;
- 然后在 METE-INFI/spring. factories; 文件中编写需要自动配置的 bean;这里定义的 bean 会被使用方通过 import 导入;
Spring 的@Autowired 能用在 Map 上吗?
可以的,spring 不但可以自动注入普通的引用类型,复杂的引用类型也可以;比如定义了一个存放 User 类型的 List 那么 spring 就将容器中所有 User 类型的 bean 放进 List 中;我们还可以在每个 UserBean 直接上加上@Order 来定义他们在 List 中的顺序,还可以通过@Qualifier 来进行过滤
SpringMVC #⭐new
什么是 SpringMVC?
首先聊聊什么是 MVC,MVC 是一种编程思想, 他规定了 Model,view,Controller 三个层次; 而 SpringMVC 是 spring 生态在对 MVC 的一种实现, 他在传统的 MVC 框架上做了拓展, 将 Model 层拆分层了业务模型和数据模型分别是 Service 和 Repository, Contoller 才拆分成了前端控制器和后端控制器分别是 DispatcherServlet 和 Controller; 简单来说 SpringMVC 就是在 Spring 和 Sevlet 的基础上实现的 MVC 模式让我们更方便更清晰的开发网络服务
SpringMVC 的执行流程
- 前端的请求会首先经过 DispatcherServlet, 这个组件是整个 MVC 的核心调度组件
- 然后 DispatcherServlet 会将路径传递给 HandlerMapping 查询当前请求需要被那个具体的方法执行; 当然也可能并不是方法而是其他静态资源; 不只是会将处理方法返回, 而是将处理链返回; 处理链中包括拦截器
- 前端调度器接受到调用链会先执行调用链中的拦截方法, 然后将将请求参数传递给处理器适配器; 处理器适配器主要工作就是做处理器输入和输出参数的转化和封装
- 处理器适配器会将封装好的参数传入处理器也就是 controller 的具体方法中执行并返回 (这里如还会执行拦截器的 postHandle)
- 如果是前后端不分离的, 那么就会先将响应数据传递到视图解析器解析视图
- 最后通过前端调度器进行返回
public interface WebRequestInterceptor {
void preHandle(WebRequest request) throws Exception;
void postHandle(WebRequest request, ModelMap model) throws Exception;
void afterCompletion(WebRequest request, Exception ex) throws Exception;
}
preHandle 的执行是在 handler 方法执行之前; postHandler 是在执行方法之后; afterCompletion 在视图渲染之后执行
SpringMVC 的拦截器和过滤器有什么区别
- 提供者不同
- 过滤器是在 servlet 提供的
- 拦截器是 SpringMVC 提供的
- 实现方式不同
- 过滤器是通 Servlet 实现的
- 拦截器是通过 Sping 的 AOP 实现的