对于后端常用框架的技术整理,其实框架在平时就是会用就行,但面试时多半需要描述实现原理,这个要靠自己理解,不推荐死记硬背。
这篇和另外几篇文章区分开,主要用于规整Java后端各种框架,面试使用及原理相关问题。
一:Spring
Spring是一个轻量级开源的后端框架,是为Java应用程序提供基础性服务的一套框架。核心功能为IOC容器管理和AOP面向切面编程。
早期使用Spring需提供XML配置,Java5引入注解后,使用大量注解替换原有XML配置。但随着Spring注解的数量越来越多,尤其是相同注解会用到各个地方,就导致了繁琐的配置及大量冗余的代码。
Spring是一个生态,包含了SpringMVC、JPA、Security安全框架等,也是SpringBoot的基石,后续在它的基础上扩展的。
1:Spring IOC的理解
总:控制反转,即原来的对象是由使用者控制(手动new创建),有了Spring之后,可以把整个对象交给Spring来帮我们管理。降低代码耦合性。
容器:存储Bean对象,底层使用map结构来存储。singletonObjects存放完整的Bean对象。
(可引出Bean的生命周期,从创建到销毁的过程都是容器管理。生命周期可引出循环依赖问题,进而可引出三级缓存处理及原理。)
容器管理Bean的生命周期,并提供重要的AOP特性,AOP也属于IOC的一部分。
BeanFactory:基础容器,提供基础DI功能,延迟加载(使用时才实例化Bean)
ApplicationContext(更常用):扩展自BeanFactory,支持更多企业级功能(AOP、事件、国际化等)预加载单例Bean(容器启动时实例化所有非懒加载的Bean)。通常说的容器就指这个。
如果直接使用BeanFactory创建Bean,需手动提供IOC加载过程的所需资源,如BeanDifinition。而ApplicationContext则封装了整个IOC的加载过程,更加自动。
1:实例化容器对象
- ClassPathXmlApplicationContext:基于 XML 配置的应用程序上下文实现。它从类路径中加载 XML 配置文件,这些文件包含了 Spring Bean 的定义和配置信息。开发人员需要在 XML 文件中使用特定的标签(如 <bean>)来定义和配置 Spring Bean。
- AnnotationConfigApplicationContext:基于注解配置的应用程序上下文实现。它不需要 XML 配置文件,而是使用配置类(例如通过 @Configuration 注解标记的类)来定义和配置 Spring Bean。这种方式将配置信息直接嵌入到 Java 代码中。
通过构造函数初始化容器,触发核心方法 refresh() 创建并启动容器。
二者都会调用核心方法refresh,区别于配置方式不同(xml,注解),前置准备有所不同,但都是读取和扫描文件或配置类,处理一些初始化和环境的设置。之后Spring会统一进行处理。
//Spring容器创建流程
//1: 使用AnnotationConfigApplicationContext(类.class) ClassPathXmlApplicationContext(管理Bean的XML文件) 创建
AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext(Empvo.class);
ClassPathXmlApplicationContext classPathContext = new ClassPathXmlApplicationContext("XXX.xml");
2:DI 依赖注入
在需要使用Spring容器对象时,可将对应的属性值注入到具体的对象中,依赖注入有三种方式。
1:构造器注入
通过类的构造方法传递依赖对象,将一个配置类的方法添加@Bean注解,在返回的对象中设置一个final对象,通过构造器赋值。
官方推荐使用构造器注入,因为它强制依赖不可变,并且保证完全初始化的对象。符合不可变对象设计原则。好处是有助于保持代码的清晰和可测试性。缺点是不够灵活,不够简洁。
2:Setter注入
通过类的 Setter 方法设置依赖对象,在XML中配置需注入的属性位置及信息,在指定类下创建set方法,并添加 @Autowired 注解。
Setter注入方式,相比构造器更加灵活,依赖对象可以在对象创建后动态设置。但需谨慎使用,仅在依赖可能变化的场景使用。对象可能在未完全初始化时被使用(需注意空指针问题)。
3:字段注入
使用@Autowired(Spring注解) 或 @Resourse(JSR-250规范,Java EE注解),直接通过字段或成员变量注入依赖,无需构造方法或 Setter。
目前使用较多的一种方式,代码简介,适合快速开发。通过反射进行注入,有两种注入方式:
@Autowired 默认按类型注入,可配合 @Qualifier 按名称注入。@Resource 默认按名称注入,找不到名称时按类型注入。
该方式缺点是依赖关系不明确,需避免重复依赖。单元测试不方便。
3:Bean的生命周期
总:总体分为以下几个环节,其中可以通过 BeanPostProcessor 插入扩展逻辑(贯穿全流程)
实例化 → 属性注入 → Aware → 前置处理 → 初始化 → 后置处理 → 使用 → 销毁
Spring容器在启动时会创建好所有单例Bean的实例,并将其存储在单例池中。当客户端请求获取单例Bean时,Spring容器会直接从单例池中获取已经创建好的Bean实例,而不需要每次都重新创建。
1:Bean加载定义
不论通过何种方式(解析XML、Java Config、注解如@ComponentScan等),解析获取的Bean对象,Spring会把bean的配置信息(如BeanCLass、scope等)统一存储在BeanDefinition对象中,该对象是一个接口,封装了所有Bean的定义信息。一个Bean会对应一个BeanDefinition。
Spring维护了一个 beanDefinitionMap,用来存储所有的BeanDefinition,使用ConcurrentHashMap进行存储,用bean的名称作为键,对象作为值。
另外,Spring还维护了一个List<String> beanDefinitionNames,用于记录所有Bean的名称(保证注册顺序)。
2:BeanFactoryPostProcessor处理
该对象是Spring容器的一个扩展点,允许在Bean实例化之前修改Bean的定义(BeanDefinition),
通常用于在应用程序上下文加载时调整配置,比如修改属性值、添加属性等。
BeanFactoryPostProcessor 只会执行一次(整个容器生命周期内),且作用于所有Bean的定义,而非针对单个Bean实例。在容器启动的 refresh() 阶段通过 invokeBeanFactoryPostProcessors() 方法统一触发。
很关键的是 SpringBoot 在该阶段提供了一个实现类,用于配置文件解析和自动配置。
1:解析配置文件占位符
解析配置文件(如 application.properties)中的 ${...} 占位符
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="location" value="classpath:config.properties"/>
</bean>
2:动态覆盖 Bean 定义
根据环境变量或条件修改 Bean 的元数据(如类名、作用域、属性值)。
public class DynamicBeanProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
BeanDefinition beanDef = beanFactory.getBeanDefinition("dataSource");
beanDef.getPropertyValues().add("url", "jdbc:new-url");
}
}
3:根据条件注册或移除Bean
根据运行时条件(如系统参数)动态注册或移除 Bean。
public class ConditionalBeanProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (!System.getProperty("env").equals("prod")) {
((BeanDefinitionRegistry) beanFactory).removeBeanDefinition("prodOnlyBean");
}
}
}
其他还有:Profile 激活扩展、配置类增强、自定义注解解析等场景。
3:Bean实例化
总体的实例化可以分为两大步骤,预实例化阶段和单例池检查,在每个阶段有额外的处理。
预实例阶段:
在容器刷新的最后阶段,遍历加载时维护的 beanDefinitionNames 名称集合,确定哪些Bean需要预实例化。仅针对于非抽象、单例、非懒加载的Bean,调用getBean方法进入实例化流程。
多例Bean和懒加载,都是在使用时才创建实例。并且多例Bean每次使用会创建新的实例。
- 是否懒加载:xml设置:lazy-init="true",注解设置:@Lazy,可加在配置类或@Bean方法。
- 是否多例:xml设置:scope="prototype",注解设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)。
public void preInstantiateSingletons() {
// 获取Bean名称集合并遍历
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
for (String beanName : beanNames) {
// 合并BeanDefinition(处理父子定义)
RootBeanDefinition bd = getMergedBeanDefinition(beanName);
// 仅处理非抽象、单例、非懒加载的Bean
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
getBean(beanName); // 调用getBean()触发实例化
}
}
}
单例池检查:
上面判断通过,进入 getBean(beanName) 方法后 ,会进入 doGetBean(beanName) 进一步处理。
会优先检查单例池中是否已存在,如果有则直接使用,没有则调用 createBean、doCreateBean进入创建流程。
查单例池是确保单例Bean全局唯一,直接复用已有实例。这里可以提一下实例化会往三级缓存放,提前暴露对象以及处理AOP代理对象等。
protected <T> T doGetBean(String name, Class<T> requiredType, Object[] args) {
// 1. 检查单例池
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null) {
return (T) sharedInstance;
}
// 2. 若未命中,创建Bean
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> createBean(beanName, mbd, args));
return (T) sharedInstance;
}
}
进入创建流程后,则根据Bean的定义配置判断实例化方式,常用的是构造器实例化方式。其他的几个例如静态或实例工厂方法、FactoryBean自定义都是XML格式的配置方式。
1:构造器实例化
通过反射,使用无参构造器或带参数构造器创建对象,默认使用无参。如果存在多个构造器,需通过@Autowired或<constructor-arg>指定参数。
XML配置为直接指定需创建的类路径,注解方式则是在类上添加注解,例如 @Component,@Configuration + @Bean等。
用的最多的:先根据BeanName获取到BeanDefinition实例,然后通过 getBeanClass() 获取到 Class 类对象,随后使用Class实例的无参构造创建Bean对象实例:getConstractor().newInstance();此时创建的Bean称为纯净Bean(未完成)。
<!-- XML配置 -->
<bean id="userService" class="com.example.UserServiceImpl"/>
2:静态工厂方法
通过类的静态方法返回Bean实例,在XML配置中,指定类路径,以及 factory-method 静态方法返回Bean实例。
<!-- XML配置 -->
<bean id="clientService" class="com.example.ClientServiceFactory" factory-method="createInstance"/>
// 静态工厂类
public class ClientServiceFactory {
public static ClientService createInstance() {
return new ClientService();
}
}
3:实例工厂方法
通过某个已存在的Bean的非静态方法创建对象,在XML中,指定已经存在的Bean,以及其下的非静态方法返回实例对象。
<!-- XML配置 -->
<bean id="serviceFactory" class="com.example.ServiceFactory"/>
<bean id="accountService" factory-bean="serviceFactory" factory-method="createAccountService"/>
// 实例工厂类
public class ServiceFactory {
public AccountService createAccountService() {
return new AccountServiceImpl();
}
}
4:FactoryBean方式
通过实现FactoryBean接口自定义Bean的创建逻辑,在XML中指定自定义的Bean创建类,在重写的getObject方法中返回实例对象。
<bean id="userService" class="com.example.MyFactoryBean"/>
4:属性注入
创建Bean实例后,Spring容器会根据BeanDefinition中的信息来配置Bean,例如设置属性值、注入依赖等。尤其是处理@Autowired,此时会再走到 getBean 方法处,尝试从单例池获取,获取不到就创建。因为之前创建的纯净Bean依赖这个实例。
进入 populateBean() 方法,注入Bean的属性和依赖:
- 调用 postProcessProperties() 方法注入依赖,例如@Autowired。
- 调用 applyPropertyValues() 方法设置属性值,例如@Value。
这个节点可以引出循环依赖问题,进而可以引出三级缓存处理。
// 简化版源码逻辑
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
// 1. 处理 @Autowired/@Value 注解
if (hasInstAwareBpps) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
// 触发注解注入
((InstantiationAwareBeanPostProcessor) bp).postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
}
}
}
// 2. 处理 XML/Java Config 显式属性定义
applyPropertyValues(beanName, mbd, bw, pvs);
}
5:Aware接口回调
属性注入完成后,调用invokeAwareMethod()方法,实现接口可设置Bean的上下文信息,完成对象的属性设置。常见的例如有:
BeanNameAware => setBeanName()
BeanClassLoaderAware => setBeanClassLoader()
BeanFactoryAware => setBeanFactory()
ApplicationContextAware => setApplicationContextAware() 需注意此时容器未初始化,只有原始的实例对象。这里可以实现策略模式的实现类统一管理。
6:BeanPostProcessor前置处理
初始化前置处理,执行所有实现了BeanPostProcessor下的 postProcessBeforeInitialization() 方法。
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("🟡 前置处理 Bean: " + beanName + " | 类型: " + bean.getClass());
// 可在此修改 Bean 属性或返回代理对象
return bean;
}
}
场景运用场景,日志监控(耗时),属性校验(是否非空)等。
以及做其他个性化操作,但是要注意,避免在其中执行耗时操作(影响启动速度)。
7:初始化方法
前置处理完成后,调用初始化方法,并判断是否实现了InitializingBean,如果有则调用afterPropertiesSet() 方法,如果没有则不调用。
AbstractAutowireCapableBeanFactory#initializeBean() {
// 1️⃣ BeanPostProcessor 前置处理
applyBeanPostProcessorsBeforeInitialization();
// 2️⃣ 执行 InitializingBean 逻辑(核心阶段)
invokeInitMethods() {
if (bean instanceof InitializingBean) {
((InitializingBean) bean).afterPropertiesSet();
}
// 3️⃣ 执行自定义 init-method(XML/Java Config 定义)
invokeCustomInitMethod();
}
// 4️⃣ BeanPostProcessor 后置处理
applyBeanPostProcessorsAfterInitialization();
}
调用初始化有三种方式,且有先后顺序,推荐使用 @PostConstruct 注解方式。
避免在 afterPropertiesSet() 执行数据库连接等阻塞操作,可结合 @Lazy 注解优化启动速度。
@Service
public class PaymentService implements InitializingBean {
// 组合使用三种初始化方式
@PostConstruct
public void validateConfig() {
System.out.println("🟢 @PostConstruct 优先执行");
}
@Override
public void afterPropertiesSet() {
System.out.println("🟠 InitializingBean 次之执行");
}
@Bean(initMethod = "initMethod") //xml方式在Bean标签处添加 init-method 指定方法
public void initMethod() {
System.out.println("🔵 init-method 最后执行");
}
}
8:BeanPostProcessor后置处理
初始化后置处理,执行所有实现了BeanPostProcessor下的 postProcessAfterInitialization() 方法。
Spring的AOP动态代理,就是在此处实现的。
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("🟡 后置处理 Bean: " + beanName + " | 类型: " + bean.getClass());
// 可在此生成代理对象,例如AOP动态代理
return bean;
}
}
9:Bean就绪
创建后的Bean会放在单例池中:DefaultSingletonBeanRegistry类下的 singletonObjects 集合。存储结构为ConcurrentHashMap,其中键是Bean的名称,值是对应的Bean实例对象。
可通过依赖注入,或直接 getBean() 的方式来进行实例的获取及使用。
10:销毁阶段
容器关闭或手动调用close方法时,执行销毁流程。如果提供了多个销毁方法,按照顺序执行。
注意在执行销毁方法前,会优先执行:DestructionAwareBeanPostProcessor(接口)下的 postProcessBeforeDestruction 方法。
@Component
public class DemoApplicationTests implements DestructionAwareBeanPostProcessor {
@Override
public void postProcessBeforeDestruction(Object o, String s) throws BeansException {
//销毁前置处理,优先执行
}
}
随后,按照顺序执行提供的销毁方法。
// 实现销毁逻辑的三种方式
public class DemoBean implements DisposableBean {
@PreDestroy
public void preDestroy() {
System.out.println("1注解方式优先执行,@PreDestroy 方法");
}
@Override
public void destroy() {
System.out.println("2接口实现次之执行,DisposableBean#destroy()");
}
//注解或XML配置的自定义方法,最后执行 XML:destroy-method="customDestroy"
public void xmlDestroy() {
System.out.println("XML 配置的 destroy-method");
}
@Bean(destroyMethod = "customDestroy")
public ExampleBean exampleBean() {
return new ExampleBean();
}
}
// 测试销毁流程
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(App.class);
ctx.close(); // 触发销毁
}
}
Spring 不触发Prototype作用域(多例)的 Bean,需手动触发多例Bean的销毁。例如 BeanFactory.destroyBean(beanInstance) 。
若某个销毁方法抛出异常,Spring 会记录错误(WARN 级别)但继续执行后续销毁逻辑。
若未显式指定 destroyMethod,Spring 会自动检测 close() 或 shutdown() 方法(可通过 @Bean(destroyMethod = "") 禁用)。
4:循环依赖及三级缓存
前文提过,在Bean实例化之后,属性注入阶段,可能发生循环依赖问题。
总:循环依赖问题是:A引用B,B引用A,造成一直实例化并注入依赖的过程。
处理方式:三级缓存,提前暴露对象,AOP
三级缓存其实就是不同的三个Map集合,用于存放不同Bean的相关对象。
缓存级别 | 存储内容 | 作用 |
---|---|---|
一级缓存(singletonObjects) | Map<BeanName, 完全体Bean> 完整的单例Bean | 对外提供可用的Bean |
二级缓存(earlySingletonObjects) | Map<BeanName, 原始对象/代理对象> 提前暴露的未初始化Bean(半成品) | 临时存储,解决循环依赖的核心 |
三级缓存(singletonFactories) | Map<BeanName, () -> getEarlyBeanReference()> Bean工厂(ObjectFactory) | 早期引用,处理动态代理逻辑 |
解决思路:在B引用A时候,此时其实A已经实例化了,但未初始化,所以可以设法先拿到A的实例化对象,将B初始化完成之后,再将A的属性补全。
所以,循环依赖的核心处理逻辑就是:将Bean的实例化与初始化分离,三级缓存作为中间态解决依赖注入时序问题。
为什么要三级缓存:三级缓存的设计是为了效率和正确性,避免重复创建和保证单例。三级缓存催在的意义是保证在整个容器的运行过程中同名的Bean对象只能有一个。
若Bean需要AOP代理,三级缓存的ObjectFactory会提前生成代理对象,确保依赖注入的正确性。
Spring 之所以需要三级缓存而不是简单的二级缓存,主要原因在于AOP代理和Bean的早期引用问题。二级缓存虽然可以解决循环依赖的问题,但在涉及到动态代理(AOP)时,直接使用二级缓存不做任何处理会导致我们拿到的Bean是未代理的原始对象。如果二级缓存内存放的都是代理对象,则违反了Bean的生命周期(正常代理对象的生成是在后置处理器)。
所以,针对循环依赖问题,所有创建的Bean都要优先放到三级缓存中,后续向一二级缓存提升。
针对上述问题,调整后的实例化和属性注入流程为:
1:实例化A
A实例化完毕后,但未进行属性注入时,将创建A的ObjectFactory并存入三级缓存。
// 源码位置:AbstractAutowireCapableBeanFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean, beanDefinition));
Lambda表达式即为ObjectFactory,该对象只负责生成Bean的早期引用(非BeanFactory)。
注意需优先判断对象是否需要被代理,如果是代理对象,则覆盖原来对象,重新创建。
// 伪代码:AbstractAutoProxyCreator
protected Object getEarlyBeanReference(String beanName, Object bean) {
//...
return wrapIfNecessary(bean, beanName);
}
随后进行三级缓存的操作,注意有其他的后续操作。
// DefaultSingletonBeanRegistry
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) { //加锁,确保三级缓存-二级缓存的原子性
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory); // 存入三级缓存
this.earlySingletonObjects.remove(beanName); //清除二级缓存
this.registeredSingletons.add(beanName); //记录所有单例Bean的注册状态
}
}
}
上述涉及到几个知识点:
1:加锁,为了保证三级缓存到二级缓存的唯一性,避免并发问题。
2:earlySingletonObjects.remove(beanName),清除二级缓存。是为了保证获取的是最新的早期对象。比如AOP代理对象的唯一性、多个线程的并发安全。
3:registeredSingletons.add(beanName),将当前正在创建的 Bean 名称(beanName
)添加到一个有序集合中,记录所有已注册的单例 Bean。使用LinkedHashSet有序存储,为了保证销毁时逆序执行。
// 源码参考:DefaultSingletonBeanRegistry
public void destroySingletons() {
String[] singletonNames = this.registeredSingletons.toArray(new String);
for (int i = singletonNames.length - 1; i >= 0; i--) {
destroySingleton(singletonNames[i]); // 逆序销毁
}
}
2:初始化B
A依赖于B,此时按照顺序从1、2、3级缓存查找,如果没有则进入B的创建流程(实例化并放入三级缓存中)
之后进行属性注入,发现依赖于A,此时从1、2、3级缓存查找A,发现此时A在三级缓存。
从三级缓存中获取A的早期对象,getSingleton("a")。
// 源码位置:DefaultSingletonBeanRegistry#getSingleton()
public Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName); // ① 查一级缓存
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName); // ② 查二级缓存
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); // ③ 查三级缓存
if (singletonFactory != null) {
// 命中三级缓存,触发后续操作...
}
}
}
}
return singletonObject;
}
注意在这之后,还会调用 getEarlyBeanReference() 判断是否代理,确保代理对象的一致性。
随后,将 A 的早期引用存入二级缓存,清空三级缓存。后续都从二级缓存获取A的引用。
// 源码位置:DefaultSingletonBeanRegistry#getSingleton()
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject(); // 生成早期引用
this.earlySingletonObjects.put(beanName, singletonObject); // ⑤ 存入二级缓存
this.singletonFactories.remove(beanName); // ⑥ 清空三级缓存
}
最后,B 完成属性注入和初始化,并存入一级缓存。添加锁,避免出现后续A获取不到B的情况。
// 源码位置:DefaultSingletonBeanRegistry#addSingleton()
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject); // ⑦ 写入一级缓存
this.singletonFactories.remove(beanName); //清除该Bean三级缓存
this.earlySingletonObjects.remove(beanName); //清除该Bean二级缓存
this.registeredSingletons.add(beanName); //实例化Bean记录
}
}
3:初始化A
当 B 通过 addSingleton 完成初始化后,返回到 A 的 populateBean 方法继续执行。
B已初始化成功并存入一级缓存,还需补充A的属性注入及初始化。(注意此时A的引用在二级缓存中,在上述B的属性注入过程中,从三级缓存提升)
之后,A注入B,完成自身初始化。并存入一级缓存,清空二三级缓存。添加锁。
// DefaultSingletonBeanRegistry#getSingleton
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 标记Bean正在创建(防止重复进入)
beforeSingletonCreation(beanName);
try {
// 关键恢复点:B完成初始化后返回此处
singletonObject = singletonFactory.getObject();
}
finally {
afterSingletonCreation(beanName);
}
// 存入一级缓存
addSingleton(beanName, singletonObject);
}
return singletonObject;
}
}
至此,A和B都完成了初始化,且都存入一级缓存中。后续其他Bean注入AB也会直接查询获取。
补充:构造器注入无法解决循环依赖(需使用Setter/字段注入),或添加@Lazy注解延迟加载。
非单例Bean(如prototype)无法解决循环依赖。
注意清空二级、三级缓存,都指的是清空当前Bean的实例,并非整个缓存数据。
缓存操作的代码,获取实例,或缓存创建删除,全部都会加同步锁,避免不一致情况。
5:Spring AOP
AOP是一种强大的编程范式,和OOP一样是一种思想。Spring AOP只是其的一种实现。
Spring框架中的AOP称为面向切面编程。在传统的OOP面向对象的编程中,程序关注的是竖向扩展,属性复用。而AOP则是用于横向公共模块的抽取解耦,实现横切关注点的模块化,提升代码的可维护性和复用性。
Spring AOP就是位于Bean生命周期的初始化后置处理阶段,算是其的一个扩展点。
常见场景有:日志切面,权限切面,耗时切面,业务数据切面(分片)等。
1:核心概念
基本术语 | 描述及使用 |
---|---|
切面(Aspect) | 封装横切逻辑的模块,通过注解@Aspect定义在类上,才可使用AOP功能。 |
连接点(Join Point) | 概念术语,指程序执行时的某个具体位置。 说白了就是我们的代码类或方法,理论上所有可能被增强的位置都是连接点。 |
切点(Pointcut) | 实际处理,从上方筛选出需要被增强的特定连接点。 使用时可通过表达式(如AspectJ语法),或注解切点指示符方式定义拦截方法。 @Pointcut本身是一个注解,定义切点后可以其他可以调用方法复用。 |
通知(Advice) | AOP的核心处理,切面在特定连接点执行的操作,共有五种。 使用时可以调用定义好的切点方法,或直接内联表达式。 切点可以被多个通知(Advice)共享。 |
- @Before:方法执行前
- @After(后置通知):方法执行后(无论是否异常)
- @AfterReturning:方法正常返回后
- @AfterThrowing:方法抛出异常后
- @Around:包裹目标方法,需手动调用ProceedingJoinPoint.proceed()
其他的术语:目标对象即指被代理的原始对象,代理即为Spring底层生成的代理对象,织入则是Spring在运行时将切面应用到目标对象的过程。
2:切点表达式
使用AspectJ语法定义拦截规则,或使用切点指示符匹配注解。
execution是匹配指定路径下的方法执行,比较常用。
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
切点指示符:
@within:拦截被指定注解标注的类中的所有方法,粒度粗,适用处理整个类方法的场景。
@annotation:拦截带有指定注解的方法,粒度细,适用于需要针对具体方法做拦截的场景。
这两个指示符在代码中点击不进去,因为它们并不是实际定义的注解,而是类似于语法糖,告诉AOP框架如何匹配目标或类。
这两个指示符在我的项目中用到了,搭配自定义注解简化AOP切面流程复用,并且他们还可以一起用,中间添加逻辑关系符进一步指定。
3:实现原理
使用Spring AOP的流程:首先确认自己的业务连接点,需要在哪里切入。确认后需在启动类开启AOP支持,然后创建切面类(需放入容器),设置切点和通知类型即可。
Spring AOP的底层是动态代理实现,动态代理选择优先级是:如果目标类实现了接口,则默认使用JDK动态代理,否则使用CGLib动态代理。可通过注解强制使用CGLIB动态代理。
4:注意事项
Spring AOP仅支持方法级别的连接点。切面不能应用于final类或方法(CGLIB无法代理)。
同类内部方法调用(如A方法调用B方法)不会触发AOP(需通过代理对象调用)。
Spring AOP 和 AspectJ 对比:
总结:两者都是AOP思想的具体实现,提供对目标对象的代理增强处理。不同点:
1:不同框架
Spring AOP依赖于容器,只能代理由Spring容器创建的Bean;AspectJ是另外一个框架,由 Eclipse 基金会维护,可拦截任意对象(如通过new创建的对象)。
2:实现方式不同
Spring AOP基于动态代理实现(运行时增强),会有额外开销;AspectJ是直接修改字节码,性能更高。
3:控制粒度不同
Spring AOP只支持方法级的连接点;AspectJ支持方法、字段、构造器等更细粒度的控制。
4:织入阶段不同
Spring AOP是在运行时织入,将切面应用到目标对象;AspectJ支持编译时、编译后、加载时三种织入方式,能够处理更复杂的横切逻辑。
5:适用场景不同
Spring AOP与Spring生态无缝集成,适合简单场景;AspectJ功能更强大,但需要额外学习成本。
具体可以根据项目中的业务复杂度、拦截粒度等选择使用,或结合使用。
6:Spring 事务
Spring 事务管理是 Spring 框架中用于保证数据库操作ACID的核心功能。提供了声明式事务和编程式事务两种方式,简化了事务管理代码,提高了开发效率。
优化:事务应尽可能短,避免长时间占用数据库连接。根据业务场景选择具体传播行为。设置回滚方式并确保异常场景下事务能正确回滚。
1:为什么需要事务
1:首先,需要先弄清除代码为什么需要事务?
如果代码不加事务,在同一个方法内插入多条数据,一部分成功,一部分失败,违反了数据库的A原子性(要么全成功要么全失败),后续重新执行会导致数据错误(重复插入)。所以同一个代码事务中如果失败则需要整体回滚。
2:针对上述问题,如何在代码中处理事务相关操作?
传统的事务处理是通过 Connection 数据库连接对象,设置自动提交为false,成功时调用commit,失败时调用rollback,最后还需要关闭资源。
3:总结,需要使用Spring事务的原因
Spring 事务管理不是替代数据库事务,而是在应用层对数据库事务的增强和抽象。
1:简化事务代码
手动设置的方式,代码可能会在每个事务操作(方法)中反复出现,导致代码冗余且忘记关闭资源;Spring的声明式事务通过 AOP 自动管理事务生命周期,开发者只需关注业务逻辑。编程式事务通过回调机制封装事务代码,减少重复。
2:多数据源或分布式事务
如果涉及多个数据库,手动协调多个资源的事务极其复杂;Spring声明式事务对业务代码透明,并且提供了分布式事务,支持多资源事务协调。
3:灵活控制事务传播行为
之前,事务的传播逻辑需要手动通过代码控制;Spring通过注解定义事务传播行为,灵活定义事务的边界,支持按方法定义隔离级别和超时,并将业务异常与事务回滚绑定,实现一致性。
4:支持其他功能扩展
直接操作数据库的方式,事务逻辑与业务代码高度耦合,难以复用或动态调整;Spring通过通过 AOP 代理将事务逻辑与业务代码解耦,并可结合其他Spring功能增强事务行为。
2:事务管理方式
Spring事务管理中,分为编程式事务和声明式事务两种方式,都可用来实现事务控制。
1:编程式事务
相当于传统操作数据库的优化,通过 TransactionTemplate 或 PlatformTransactionManager 手动管理事务,减少重复代码。优点是可以精准控制事务边界,缺点是代码侵入性强,不利于扩展维护。
PlatformTransactionManager 是核心接口,常用实现类有JpaTransactionManager(JPA/Hibernate 事务),JtaTransactionManager(JTA分布式事务)。
2:声明式事务
通过 @Transactional 注解或 XML 配置实现事务管理,优点是代码简洁,不会侵入代码。
缺点是粒度较粗,不能灵活控制边界。默认基于AOP代理实现,会有性能消耗。需注意自调用问题。
底层:对 @Transactional 注解的方法,Spring 通过动态代理(JDK 或 CGLIB)生成代理对象。代理对象在方法执行前后通过 TransactionInterceptor 管理事务(开启、提交、回滚)。
自调用问题:同一类中的方法调用 @Transactional 方法时,事务不生效。
原因:Spring 基于代理实现事务,自调用绕过代理,同一个类中无法获取代理对象。
解决:将方法移到另一个 Bean。或使用 AopContext.currentProxy() 获取当前代理对象。
3:事务注解
@Transactional 注解是使用最多的,简洁有效,并且包含一些关键属性。
propagation | 事务传播行为,提供了枚举类。定义多个事务方法相互调用时的事务边界。 |
isolation | 事务隔离级别(如 Isolation.READ_COMMITTED)。 默认使用数据库的,当出现了隔离级别无法控制的问题时,可以手动提高隔离级别。 |
timeout | 超时时间(秒),默认 -1 表示使用数据库默认。 |
readOnly | 是否只读事务(优化数据库操作)。 |
rollbackFor / noRollbackFor | 指定触发回滚的异常类型,开发中经常用到,保证业务的一致性。 |
使用就是直接在方法或类上添加注解即可,一般指定异常回滚参数,加在类上表示所有方法事务。
@Transactional
public void transferMoney(Account from, Account to, double amount) {
from.withdraw(amount);
to.deposit(amount);
}
事务注解失效的常见原因:
- 方法非 public。
- 自调用问题。
- 异常类型非 RuntimeException 且未配置 rollbackFor。
- 多线程环境下事务上下文丢失。
4:事务传播行为
事务传播行为定义了多个事务方法相互调用时,事务的边界如何划分。Spring提供了7种传播行为,核心解决问题是如果调用者已经有事务,该如何处理。
注意,注解的传播行为是针对于被调用时处理,而不是调用其他方法时控制。
事务传播行为 | 规则及场景 |
---|---|
REQUIRED(默认) | 如果当前调用方存在事务,则加入该事务;如果不存在事务,则新建一个事务。 适用于大多数业务方法,保证操作在同一个事务中(如订单创建和库存扣减)。 |
REQUIRES_NEW | 无论怎样都新建一个独立事务,如果当前调用方存在事务,则挂起当前事务。 适用于需要独立提交的操作(如日志记录、审计),即使主事务回滚,子事务仍提交。 |
SUPPORTS | 如果当前存在事务,则加入。如果不存在,则以非事务方式执行。 适用于查询方法,需要在事务中执行以保证一致性,但不需要强制事务。 |
NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,则挂起该事务。 适用于需要避免事务影响的操作(如发送消息、调用外部 API)。 |
MANDATORY | 必须在一个已有的事务中执行,否则抛出 IllegalTransactionStateException。 确保方法必须由事务调用(如关键数据更新)。 |
NEVER | 必须在非事务状态下执行,如果当前存在事务,则抛出异常。 可用来防止事务中的某些操作(如性能监控)。 |
NESTED | 如果当前存在事务,则在嵌套事务(基于保存点)中执行;否则新建事务。 适用于部分操作需要回滚,但整体事务可继续(如订单中部分商品库存不足)。 |
MANDATORY:作用只是校验,不仅保证自己需在事务内,还确保调用方必须在事务内,可以用来保证多个操作强关联一致。
NESTED:可理解为在调用NESTED事务标注的方法时,记录保存点。发生异常时,外层事务可继续。
5:事务回滚方式
默认是仅在抛出 RuntimeException 或 Error 时回滚,检查型异常(如 IOException)不触发回滚。
生产环境使用时,一般是指定发生异常Exception时回滚。
@Transactional(rollbackFor = Exception.class)
7:Spring常用注解
如果问到Spring注解的问题,回想下Spring大致有四大方面注解:IOC、DI、AOP和事务相关注解,每个注解简单介绍然后等提问。
主要是针对于Spring容器和功能相关的注解,并记录每个注解的功能和区别。AOP和事务注解上面已经整理,相关问题可以找到答案。
1:容器注解
@Component | 通用组件注解,声明为 Spring 管理的 Bean。 |
@Service | 声明Bean,语义化标识业务逻辑组件,无额外功能,在Service类上使用。 |
@Repository | 声明Bean,语义化标识数据访问组件,有异常统一的特殊功能,在DAO 类上使用。 |
@ComponentScan | 配置Spring框架自动扫描并注册组件到容器,默认当前类所在包及子包,可指定配置。 |
@Import | 用于显式导入一个或多个配置类,动态加载 Bean 定义。SpringBoot的自动装配依赖于该注解。 |
@Configuration | 声明一个配置类,替代 XML 配置。内部有Component注解,会注入容器。 |
@Bean | 声明方法返回一个 Bean,并将对象交由Spring容器管理,只会创建一次。常用于配置类,默认Bean的id是方法名。 |
@Scope | 定义 Bean 的作用域(如 singleton、prototype),控制Bean的生命周期。 |
问题描述:
@Component和@Service、@Repository的区别及为什么使用?
都是声明Bean交由Spring管理。可以明确代码分层角色,提高可读性和可维护性。其中@Service无任何特殊处理,@Repository还会将数据库异常转换为统一数据访问异常。
@Component 和 @Bean 的区别?
都可以声明Bean实例,但作用对象和使用场景不同。@Component 作用于类,自动扫描注册 Bean。@Bean 作用于方法,显式声明 Bean,常用于第三方库或复杂配置。
@ComponentScan 自定义扫描路径?
用于配置组件扫描路径,自动发现并注册【添加了容器注解的类】。
默认扫描范围为主类所在包及其子包。如果自定义了扫描路径,默认路径将会失效,如果需要保留,可以设置多个扫描路径。
2:DI注解
@Autowired | 按类型自动注入 Bean。 |
@Qualifier | 按名称指定注入的 Bean,一般配合 @Autowired 使用。 |
@Primary | 标记优先注入的 Bean,可解决多个同类型 Bean 冲突。 |
@Value | 动态注入外部配置值,在运行时获取配置信息。 |
常见问题:
@Autowired和@Resource的区别?
二者都可用来依赖注入Bean,来源和方式不同。@Autowired是Spring注解,默认按类型注入。@Resource是JSR-250规范注解,属于Java社区,默认按名称注入。
二:Spring MVC
Spring MVC 是基于 Java 的 Web 开发框架,是Struts2加上Spring的整合。遵循 MVC(Model-View-Controller) 设计模式,用于构建灵活、松耦合的 Web 应用程序。
在之前,web开发使用 @WebServlet 方式,接口交互使用 HttpServletRequest 和 HttpServletResponse,每个接口都需要配置,代码冗余且不好维护,web页面和后端代码高度耦合(获取字段),增加沟通成本。
SpringMVC是Spring生态的一部分,就是Spring的一个子模块,自然拥有Spring的容器和AOP功能,使用注解和视图处理接口的请求和响应,封装了完整的运行流程,优化了接口交互,提高了开发效率。
早期使用SpringMVC时,需要同Spring手动通过XML方式配置视图解析器、静态资源等。Java引入注解后简化了配置流程,但是造成了代码冗余和配置类繁琐。
MVC设计模式:注意不是传统的Java23种设计模式,而是Web页面的框架设计模式。
M-Model 模型,执行具体的业务逻辑代码,执行后返回给控制器。
V-View 视图,前端展示业务,渲染视图后返回给控制器。
C-Controller 控制器,用于接收请求和响应请求,流程的中转站。
1:核心注解
MVC的常用注解包括控制器、接口交互、全局异常相关。这些注解就构成了SpringMVC的使用。
1:控制器
@Controller | 添加在类上,标记该类为 MVC 控制器,用于Web 层处理 HTTP 请求。 |
@RestController | 同样标记该类为 MVC 控制器,遵循RESTful API 开发规范。 |
二者的区别可能会问到,在生产环境中,使用后者较多。
@Controller 返回视图,需配合视图解析器(默认返回视图到页面),返回数据时,需手动加@ResponseBody。
@RestController 直接返回 JSON/XML(相当于 @Controller + @ResponseBody)。
2:交口交互
@RequestMapping | 定义请求路径和方法,映射 HTTP 请求到控制器方法。 可通过 method 属性指定请求类型,例如:method=RequestMethod.GET。 |
@GetMapping @PostMapping | 简化特定 HTTP 方法的映射,替代 @RequestMapping 的属性设置,精简划分层级,提高代码可读性。 |
@RequestBody | 将请求体反序列化为对象,接收 JSON/XML 数据。 |
@ResponseBody | 将返回值序列化为响应体,直接返回数据而非默认视图。 |
@RequestParam | 从请求参数中获取值,可不加,默认获取同参数名的数据。 一般搭配@RequestBody获取前端页面传递的Json报文转对象,可设置必填和默认值等属性。 |
@PathVariable | 从 URL 路径中获取变量,将其绑定到参数上,常用于RESTful 风格的接口设计。 一般在 get 请求中使用,参数名一致时,可省略name和value,但是不能省略注解。 |
@ModelAttribute | 绑定请求参数到模型对象,实现表单数据绑定。还可用于初始化模型对象。 |
1:@PathVariable使用
一般在get请求中使用的原因是,需要在路径后显式拼接参数,页面可以看到,数据不安全。并且get请求较小,可以避免拼接大批量参数。生产中使用较少,除非有参数传递在路径的特殊需求
@GetMapping("/user/{id}")
public String getUserById(@PathVariable Long id) {
return "UserID: " + id;
}
3:全局处理
Spring提供了一个重要的全局处理的注解@ControllerAdvice,可用于全局异常处理、全局数据绑定和全局数据预处理。
@ControllerAdvice注解内部使用了 @Component 元注解,因此使用时无需额外添加容器注解。
核心原理是AOP思想的实现,当在一个类上定义该注解时,表示需要使用全局的拦截功能。提供拦截规则,然后设置@ExceptionHandler、@InitBinder 或 @ModelAttribute这三个注解以及被其注解的方法来自定义拦截后的处理。
提供了多种拦截规则的定义,默认是拦截所有Controller,可自定义为拦截包、注解等。
1:@ExceptionHandler
单个Controller接口使用时,表示处理 Controller 控制器内的异常,当发生异常时,Spring会调用该注解标注的方法处理异常。
默认处理方法所有异常,可接收一个 Throwable 数组作为参数,传入指定的一个或多个异常进行处理。使用时,可以指定Execption或自定义异常,丰富提示信息,便于排查。
在 @ControllerAdvice 修饰的全局控制器处理类中,添加 @ExceptionHandler 注解修饰的方法,可实现全局异常处理,返回自定义异常或错误页面。
2:@ModelAttribute
添加在参数上时,和RequestParam用处一样,绑定值到参数上,并且可以设置别名。
这个注解的特殊地方时,可添加在方法上,可以用来初始化模型数据,为视图提供初始数据。说白了往model中注入属性,可以供Controller中注有 @RequestMapping 的方法获取使用。
@ModelAttribute("user") //默认属性名,可以自定义名称
public User getUser() {
return new User();
}
这样表示,这个方法会在每个请求处理方法执行前被调用,初始化一个User对象并将其添加到模型中。在接口中就可以获取使用,直接将其赋值给参数或者手动获取。
@GetMapping("methodTwo") //直接获取并赋值到参数上
public String methodTwo(@ModelAttribute("user") String globalAttr){
return globalAttr;
}
@GetMapping("methodThree")
public String methodThree(ModelMap modelMap) {
return (String) modelMap.get("user"); //手动获取
}
使用时需注意,被@ModelAttribute注解的方法会在每个请求处理方法执行前被调用。因此,在一个控制器映射多个URL时,要谨慎使用,以避免不必要的重复执行。
在 @ControllerAdvice 修饰的全局控制器处理类中,添加 @ModelAttribute 注解修饰的方法,可以实现全局预设变量数据,达到默认视图及参数默认值的效果。
3:@InitBinder
可以在Controller中定义一个该注解修饰的方法,该方法会在控制器处理请求之前被调用,用于自定义数据绑定逻辑。
使用:注解中默认处理所有参数,可传入对象名指定处理对象。方法中必须包含一个 WebDataBinder 类型的参数,且方法的返回类型为 void。
@Controller
public class MyController {
@InitBinder //@InitBinder("user") 只针对user对象
public void customizeBinding(WebDataBinder binder) {
// 指定前缀
binder.setFieldDefaultPrefix("p.");
// 自定义绑定逻辑
}
}
在 @ControllerAdvice 修饰的全局控制器处理类中,添加 @InitBinder 注解修饰的方法,可以实现全局初始数据处理,字段赋值转换等操作。
实际使用场景有对参数进行格式化转换(日期、字符串等),简化代码,避免在每个接口重复处理。以及多个参数包含相同字段时,可以在方法中指定前缀区分。
2:运行流程
面试一定会问到的运行流程,包括目前使用的SpringBoot,SpringCloud都是基于这个原理。
把核心组件记下来,然后在回答时,整体串起来描述。
1:DispatcherServlet
前端控制器,dispatcherServlet是整个流程控制的中心。
作用是接收请求,响应结果,相当于转发器或中央处理器,属于mvc模式中的c。
所有HTTP请求都首先由 DispatcherServlet 处理,由它调用统一调度其他组件执行,降低组件之间的耦合性,提高每个组件的扩展性。
2:HandlerMapping
处理器映射器,负责根据用户请求找到对应的控制器(Controller),注意只是查找,不执行。
SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。
HandlerMapping 将会把请求映射为 HandlerExecutionChain 对象,包含一个Handler处理器对象,和多个 HandlerInterceptor 拦截器对象。
映射的这个操作,用到了策略模式。新增映射策略时,只需实现 HandlerMapping 接口,便于扩展,并且达到了解耦的效果。
HandlerMapping作为策略接口,定义了统一的映射方法。其下实现类提供不同的映射算法。
- RequestMappingHandlerMapping:基于注解(如 @RequestMapping)的映射策略,处理控制器方法的动态匹配。
- BeanNameUrlHandlerMapping:根据Bean名称与URL路径匹配的简单策略。
- SimpleUrlHandlerMapping:通过显式配置URL路径与处理器的映射关系。
DispatcherServlet 作为上下文,负责按顺序选择并使用具体的 HandlerMapping 策略。
- 在初始化时,加载所有 HandlerMapping 实现类的实例。
- 处理请求时,遍历这些实例,直到找到第一个能返回 HandlerExecutionChain 的策略。
- 最终将请求委托给选中的策略处理,结束循环。
// DispatcherServlet 中简化后的逻辑
for (HandlerMapping hm : handlerMappings) {
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
3:HandlerAdapter
处理器适配器,执行具体的业务逻辑,返回 ModelAndView 或数据。
在Spring MVC中,HandlerAdapter 的作用是将不同类型的处理器,统一适配到 DispatcherServlet 可调用的接口,随后执行具体的处理器。
用到了适配器模式,新增处理器类型时,只需添加新的 HandlerAdapter 实现类,并在适配器做对应处理即可。无需与具体处理器耦合,提高了框架的灵活性和可维护性。
HandlerAdapter 作为目标接口定义了统一的处理器调用方法(handle)和判断处理器是否支持方法(supports)。
Spring MVC提供了多种 HandlerAdapter 实现类,每种适配一种处理器类型。
- RequestMappingHandlerAdapter:适配带有 @RequestMapping 注解的控制器方法(现代Spring MVC的默认方式)。
- SimpleControllerHandlerAdapter:适配实现了旧版 Controller 接口的处理器(如 AbstractController)。
- HttpRequestHandlerAdapter:适配实现了 HttpRequestHandler 接口的处理器(如静态资源处理)。
- SimpleServletHandlerAdapter:适配 Servlet 类型的处理器。
在获取到上一步的处理器映射器之后,Spring会遍历所有 HandlerAdapter,通过 supports() 方法找到支持该处理器的适配器。
最后通过 handle() 方法统一调用处理器,屏蔽具体处理器的差异。
// DispatcherServlet 中的简化逻辑
HandlerAdapter ha = getHandlerAdapter(handler);
ModelAndView mv = ha.handle(request, response, handler);
4:Handler
后端控制器,就是我们编写的具体Controller接口,内部执行真正业务逻辑代码。
调用 handle 方法后,会返回 ModelAndView 对象,包含模型数据、逻辑视图名。
但是这里需要区分两个场景:接口默认返回ModelAndView,但添加@ResponseBody表示返回数据。
底层实现:
进入handle执行方法后,SpringMVC会通过 HandlerMethodReturnValueHandler 组件机制,判断是否走视图渲染或消息转换逻辑。
1:模型生成
若方法返回 ModelAndView 或视图名称,其会最终生成 ModelAndView 对象,并分情况交给 DispatcherServlet 进行视图渲染。
这里有一个重要的概念,当接口返回字符串时,SpringMVC会隐式创建一个ModelAndView,将返回的字符串解析为视图名称,并将参数合并到模型中。通过该方式创建的模型对象,和代码显式创建的是一致的。
然后可以引出另一个重要的概念,当返回的字符串以 redirect: 重定向或 forward: 转发开头时,Spring MVC会直接创建视图对象(view实现类)!redirect 会创建 RedirectView 对象,forward会创建 InternalResourceView 对象。并将其放入ModelAndView中的视图部分,同时处理模型数据。
转发:通过服务器端请求转发跳转页面,属于内部资源访问。URL不变,共享作用域,模型数据可以自动传递。在服务端内部完成,客户端仅收到一次响应。效率高,适用于大多数页面切换及数据传递。
重定向:触发客户端发起新的HTTP请求,URL会变为目标URL并显式,模型数据默认不传递到重定向后的请求(除非通过 RedirectAttributes 显式传递),客户端会发起第二次请求到新URL。适用于防止重复提交。
随后,会在下方代码检查视图是否是重定向或转发视图,如果是,则不设置视图解析标记。只有为普通字符串时,才会设置解析标记供后续解析。
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
// 将字符串作为视图名称设置到 ModelAndViewContainer
String viewName = (String) returnValue;
mavContainer.setViewName(viewName);
// 标记需要后续处理视图解析
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// 支持返回类型为 String 且非 @ResponseBody 注解的方法
return CharSequence.class.isAssignableFrom(returnType.getParameterType());
}
}
2:数据转换
内部会检查方法是否有 @ResponseBody 或类是否有 @RestController,如果有,则会触发消息转换逻辑。使用 HttpMessageConverter 将Java对象转换为HTTP响应体的内容(如JSON、XML),并写入响应。
将 ModelAndViewContainer 的属性 requestHandled 标记为true,告知 DispatcherServlet 无需后续视图渲染。
所以,这里可以看出来,其实最终都会返回ModelAndView,但是返回数据的场景下,全局控制器会根据标记直接跳过后续流程,结束请求处理。
5:ViewResolver
返回 ModelAndView 后,流程进入视图解析和渲染阶段,依赖 ViewResolver 和 View 实现。
如果是注解标注的返回数据,则不再调用 ViewResolver 和 View.render(),直接结束请求处理。
1:视图解析
视图解析的目的就是得到一个View视图,已经有视图或数据的,就会根据标记跳过该步骤,只有返回字符串且不为指定前缀时,才需要解析获取视图。
通过 ViewResolver (视图解析器)对象将逻辑视图名称(如 "index")映射为具体的 View 实现类(如 JstlView、ThymeleafView),SpringMVC框架提供了很多的View视图类型。
根据我们的视图解析器配置,拼接前缀后缀,组成完整路径,例如:
根据上图配置,最终路径会解析为:/WEB-INF/views/index.jsp
2:视图渲染
这一步是对View进行渲染将处理结果,通过 DispatcherServlet 返回响应给用户。
除了包含对字符串解析得到的 View 视图渲染,还包含转发或重定向的 View 实现类视图,只是调用方法不一样。
解析得到的 View 视图:DispatcherServlet 会调用 View.render() 方法,将模型数据(Model)填充到视图模板中。
RedirectView 重定向视图,触发 response.sendRedirect()。
InternalResourceView 转发视图,触发 RequestDispatcher.forward()。
对于重定向视图,模型数据也会被保留,只是默认无法传递,只有两种场景下会传递数据。
路径参数:通过 RedirectAttributes.addAttribute() 添加,拼接到URL。
Flash属性:通过 RedirectAttributes.addFlashAttribute() 添加,临时存储于Session。
转发视图的模型数据完全保留,所有属性通过请求作用域传递。
6:完整流程
梳理完整运行流程,细节可抛出锚点,全局控制器在讲解时描述,对象都会返回给它统一调度。
1:用户发起请求,前端控制器收到请求后,通过映射器的策略模式,将请求转换为包含处理器映射器的对象。
2:得到映射器后,通过处理器适配器的装饰器模式,获取到支持该处理器的适配器,调用统一执行方法,执行Handler后端控制器。
3:执行具体的业务逻辑代码,最终根据是否含有@ResponseBody注解区分视图和数据响应,隐式创建 ModelAndValue 模型对象并设置视图和模型数据。
4:如果是视图响应,进入视图解析渲染过程。这一步会根据是否重定向或转发,跳过视图解析操作,随后所有View进行视图渲染,处理模型数据返回结果。如果是数据响应,则这两步都跳过,直接返回响应数据。
5:解析渲染后的视图通过 DispatcherServlet 返回响应给用户。
3:拦截器
HandlerInterceptor 是 SpringMVC 框架原生提供的接口,用于拦截 HTTP 请求的处理流程。它允许你在请求到达控制器(Controller)之前、之后,以及整个请求完成之后插入自定义逻辑(如权限校验、日志记录等)。
底层是基于 Servlet 规范 的拦截机制,由SpringMVC管理,拦截逻辑嵌入到 DispatcherServlet 的处理流程中。
接收到请求获取处理器映射器时,会生成一个 HandlerExecutionChain 对象,该对象包含:
- Handler:具体处理器对象。
- Interceptors:拦截器列表,所有注册的拦截器实现类。
随后,DispatcherServlet 直接遍历 HandlerExecutionChain 中的拦截器,依次调用它们的 preHandle、postHandle 和 afterCompletion 方法。
属于是责任链的调用,正常代码流转。不涉及代理或字节码。
1:定义拦截器
实现 HandlerInterceptor 接口,定义具体拦截逻辑。
定义好拦截器之后,还需要将拦截器添加到 Spring MVC 的配置中。有XML配置和注解两种方式。
仅拦截SpringMVC处理的请求,经常会和 Filter 过滤器做比较,下面描述。
2:定义过滤器
Filter 是 Java Servlet 规范的核心组件,不依赖 Spring 框架,属于 Servlet 容器的原生功能。
过滤器用于在 HTTP 请求到达 Servlet(如 Spring MVC 的 DispatcherServlet)之前执行预处理,或响应返回客户端之后执行后处理逻辑。
生命周期由 Servlet 容器(如 Tomcat)管理,随 Web 应用启动而初始化,销毁时调用 destroy()。执行顺序是通过 web.xml 或注解定义的顺序依次执行,遵循“先进后出”的链式调用。
Filter比较好的一点是拦截面较广,兼容新老代码,可用来处理跨域问题。
跨域处理还可使用 Spring 的 @CrossOrigin 或专用 Filter,但不要同时使用两者。
创建过滤器有三种方式,分别为XML、基于 Servlet 的注解、以及基于 Spring 中配置类方式。
上面是规范的 SpringBoot 的过滤器显式注册,但直接使用 @Component 注解并实现Filter接口也可以实现过滤器功能,原因是因为SpringBoot的自动装配机制。
启动时扫描,如果类实现了 Filter 接口且标注了 @Component,SpringBoot 会通过 FilterRegistrationBean 自动注册该 Filter。
一般更推荐使用显式注册,因为自动注册的默认为/*拦截所有,并且顺序不可控,无法配置初始化参数,不利于扩展维护。
3:拦截器 VS 过滤器
总:二者都可以对请求进行拦截处理,但是所属和粒度有区别。
1:功能所属
拦截器属于SpringMVC管理,依赖Spring容器支持;过滤器不依赖Spring,是Servlet 容器原生支持的功能。
2:作用范围
拦截器只会拦截SpringMVC处理的请求(Controller层面);过滤器作用范围大,可控制所有请求(Servlet 容器层面)。
3:执行时机
拦截器作用在 DispatcherServlet 内部执行(控制器前后),不会拦截原生Servlet接口;过滤器作用在 DispatcherServlet 之前/之后执行(容器初始关闭),提供了每次请求的过滤方法。
4:访问Bean
拦截器可以直接注入Bean进行使用;过滤器无法直接注入,需通过原始方式手动获取Bean(例如WebApplicationContext)。
5:读取参数
拦截器更加灵活,可访问控制器对象、ModelAndView视图信息等;过滤器只能操作 Servlet 请求和响应对象。
常见场景:
拦截器:权限控制、Token校验、参数记录、详细请求参数设置等。
过滤器:兼容历史代码、跨域处理、全局日志记录、字符编码统一、过滤请求参数等。
开发中,可将拦截器与 Servlet 原生的 Filter 结合使用,能够实现更全面的请求控制逻辑,各司其职。
三:SpringBoot
Spring Boot 是一个基于 Spring 框架的开源 Java 开发框架,也属于Spring生态的一部分。并不是要取代Spring,而是简化 Spring 应用的初始搭建和开发流程。
通过约定大于配置的理念和一系列自动化工具,显著减少了传统 Spring 开发中的复杂配置,使开发者能够快速构建独立、生产级的应用程序。
核心特性包括:自动配置、Starter依赖、嵌入式服务器、多环境配置、安全控制五大特性。
换用 SpringBoot 之后,直接创建项目,引入依赖添加配置,编写主启动类就可以直接运行!
1:核心注解
对于原始的Spring、SpringMVC,注解很繁琐且冗余,所以SpringBoot在其基础上做了二次封装,定义了一些新的组合注解来实现一些Spring注解的组合,从而极大地简化了Spring框架本身的繁琐配置,实现快速的集成和开发。
Spring的很多注解都可以作为元注解,允许定义在其他注解上,其自身也有很多组合注解。这也是SpringBoot实现的基础。
1:条件注解
@Conditional,Spring提供的基于条件控制的注解,条件满足时,被注解标注的组件才会注册到容器中,可以标注在类或方法上。
可以用来控制 Bean 的创建行为,这一点是 SpringBoot 实现自动配置的核心基础能力。
Spring Boot中以@Conditional为元注解又重新定义了一组针对不同场景的组合条件注解,这里只列举一些常用的,除了这些还有Web、资源等条件的判断注解。
@ConditionalOnBean | 当容器中有指定Bean的条件下进行实例化,保证Bean的创建顺序。 |
@ConditionalOnMissingBean | 当容器里没有指定Bean的条件下进行实例化,避免重复配置。 |
@ConditionalOnClass | 当classpath类路径下有指定类的条件下进行实例化。 |
@ConditionalOnMissingClass | 当类路径下没有指定类的条件下进行实例化。 |
SpringBoot的核心功能就是基于@Conditional为元注解的组合注解实现的,在SpringBoot的源码中,可以经常看到条件注解的使用,这就是 SpringBoot 实现高度自动化配置的原因。
2:启动注解
核心的启动类注解,其实也就是组合注解,后续的启动流程描述就会从这里作为入口。
@SpringBootApplication | 启动类注解,作为Spring Boot 入口类。 组合注解:@Configuration + @EnableAutoConfiguration + @ComponentScan。 |
@EnableAutoConfiguration | 核心注解,开启自动配置功能,自动加载 Spring 和第三方库的配置。 |
@ConfigurationProperties | 将外部配置绑定到 Java Bean,将配置文件中的属性批量绑定到Java对象,不用单个@Value。 这个正式项目中用到了,可搭配自定义配置,在启动时绑定值到全局Java对象类中。 |
@EnableConfigurationProperties | 启用配置绑定功能,可添加在启动类或配置类上。 另外,在配置类上单独添加 @Component 注入容器也可生效。 |
@SpringBootTest | 标记 Spring Boot 集成测试类。 访问外部依赖时,需注入容器并添加@RunWith(SpringRunner.class)。 |
2:启动流程
Spring Boot 的启动流程包括初始化、环境准备、上下文创建、自动配置加载等步骤。
自动配置只是启动流程中的一个关键部分。启动流程更广泛,涵盖整体过程,自动配置则专注于条件化配置Bean的机制。
其实这里内部描述的是 Spring 的启动流程,SpringBoot只是整合并且加了自动配置、容器等功能。
核心问题:当我们创建一个 SpringBoot 项目,通过主启动类启动,启动流程是什么?
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
首先,会进入 SpringApplication 的 run 方法,分为两个操作:实例化对象和调用实际运行方法。
1:推断应用类型
进入实例化构造方法,核心步骤之一是推断应用类型。
根据类路径是否存在指定核心类,判断是 Servlet 应用(如 Tomcat)、Reactive 应用(如 Netty)还是普通 Java 应用。
原理是通过依赖自动推导,并做不同的处理:
- 引入 spring-boot-starter-web,自动推断为 SERVLET。需启动嵌入式 Servlet 容器(如 Tomcat)并注册 DispatcherServlet。
- 引入 spring-boot-starter-webflux,自动推断为 REACTIVE。需初始化 Netty 服务器和响应式编程模型。
- 无 Web 相关依赖,推断为 NONE。应用不加载任何 Web 相关配置。
SpringBoot默认选择 Servlet 类型,同时指定上述依赖时,也会优先选择 Servlet,但可通过配置显式指定为 REACTIVE(配置文件指定,或在启动类方法中设置 setWebApplicationType)。
为什么需要推断应用类型?
1:无需用户手动配置,指定应用容器类型等,符合约定大于配置思想。
2:避免冲突,确保不加载冲突的配置,配置多个时,按默认或指定配置选择一个。
3:性能优化,精准加载仅适用于当前场景的配置,避免冗余资源消耗。
我这里本地是2.3的版本,比较低一点,后续2.7的代码做了优化,可读性更高一点,可以对比下。
2:加载核心扩展类
推断应用类型之后,返回到初始化对象的构造方法,紧接着读取 META-INF/spring.factories 中定义的两个核心组件。
这里只是从工厂中尽早获取组件,跟自动装配没有关系,因为SpringBoot是使用工厂统一管理。
尽早加载这两个组件,是为了允许开发者在 Spring Boot 启动的最早阶段介入,自定义上下文或监听关键事件,避免部分事件在启动流程的极早期触发。
1:ApplicationContextInitializer
上下文初始化器:在 ApplicationContext 创建完成后但未刷新(refresh)前,对上下文进行定制化初始化(如添加属性源、注册 Bean 等)。
使用:实现接口并重写方法,并在META-INF/spring.factories中声明。其源码会循环执行所有初始化方法。
org.springframework.context.ApplicationContextInitializer=com.example.CustomContextInitializer
2:ApplicationListener
应用事件监听器:核心作用是监听 Spring Boot 的生命周期事件(如启动、刷新、失败等),并在事件发生时执行处理逻辑,提供了多种事件类型(如应用启动前、应用就绪等)。
使用方式同样为实现接口,并在spring.factories中声明。
ApplicationListener用到了观察者模式,通过事件驱动机制实现模块间解耦,监听器只需关注事件内容,无需知道事件如何触发。提高了可维护性和扩展性。
其实这个监听器类似于工作流的监听,覆盖整个启动环节,监听调度。可以通过实现某个事件接口,重写方法添加个性化处理。
3:触发启动事件
进入运行阶段,调用 run 方法,这是 SpringBoot 的核心方法,后续的所有操作都在这个方法中。
首先会调用 getRunListeners 方法,触发启动事件,且返回一个监听器汇总对象。
内部有一个从容器中获取的监听器集合。然后使用事件监听器发布状态(观察者思想)。
4:准备环境
随后调用 prepareEnvironment() 方法,准备运行环境,这是启动的核心步骤之一,这一阶段负责加载和整合所有配置信息(如命令行参数、配置文件、系统变量等),为后续创建 ApplicationContext 和自动配置提供基础数据支持。
1:加载配置源
包括命令行参数、application.properties/application.yml、系统变量等。
2:激活配置文件
激活指定的 Profiles(通过 spring.profiles.active 配置),一般表示不同环境的配置文件。
3:发布事件
激活配置之后发布事件,监听器会监听此事件并加载所有配置文件,越靠前的配置源优先级越高。
优先级顺序:命令行参数 → 系统变量 → 指定配置文件 → 默认配置文件 → 默认属性。
针对于配置文件的优先级,由两个维度决定:文件路径和文件格式。
1:不同路径下
路径优先级高于文件格式优先级(项目根目录大于类根目录),随后再根据文件格式判断。
2:相同路径下
不同格式的配置优先级为:指定前缀配置文件 > properties > .yml > .yaml。
生产中,避免混合使用 .properties 和 .yml,减少优先级冲突。可利用 Profile 隔离不同环境的配置。
5:创建上下文
根据应用类型创建不同的上下文,实现原理是根据初始化推断的应用类型,反射获取处理类。
6:准备上下文
该阶段也称为准备阶段、配置类处理阶段。负责完成上下文的初始配置、注册关键 Bean 定义,并触发扩展点。
根据前面创建的具体的上下文处理对象,调用 prepareContext()。
准备阶段:仅收集和注册 Bean 定义,将其放入 BeanDefinitionMap,不触发任何实例化逻辑。
原理是:Bean 的实例化逻辑被封装在 BeanFactory 中,只有在调用 getBean() 时才会触发实例化。在准备阶段,Spring 仅操作 BeanDefinitionRegistry,不主动调用 getBean()。
准备阶段和刷新阶段都会处理注解,这是Spring设计上的一种分层处理机制。
记住该阶段只处理显式注册的类和初步扫描,例如@Configuration、@Bean、@Component等。生成基础的Bean定义,其他注解不会被处理。
首尾的这五个方法为关键步骤,这一块还涉及Bean的处理,是核心代码逻辑。
1:环境变量
将之前准备的Environment对象,绑定到上一步创建的上下文对象中。
2:后置处理
对上下文进行后置处理,例如设置 Bean 名称生成器、资源加载器、类加载器等。Bean名称生成器用于生成 @Configuration 类中 @Bean 方法的名称。
3:自定义和监听
使用之前提前注册的上下文初始化器,执行自定义上下文初始化逻辑,可以允许开发者手动定义Bean。
通知监听器上下文准备状态,发布初始化创建完成的事件,此时尚未加载Bean定义。
4:load 方法
核心的方法,解析注解并生成Bean定义。注意这里名词是只Bean定义,不实例化!
- 将主类解析为 @Configuration 类,并注册其 Bean 定义,将其作为一个入口。
- 先执行@ComponentScan,扫描用户定义的Bean(如@Component、@Service等),将其放入 BeanDefinitionMap 中。
- 处理@EnableAutoConfiguration 中的 @Import 注解,将其直接引用的配置类注册为 Bean 定义。
这里最关键的是只定义 @Import 引入的类,例如将引入的 MyConfig.class 注册为Bean定义。
1:我们在平时创建项目,会习惯把主启动类放在根目录,因为其默认扫描主类的包及子包,就会把我们的添加容器注解的类扫描注入,无需配置即可简单生效。
2:注解的顺序执行,为了确保用户自定义的 Bean 优先于自动配置的Bean被注册,使得自动配置可以基于用户已有的Bean进行条件化调整(如覆盖默认配置)。
5:监听事件
Bean定义加载完成后,执行 listeners.contextLoaded(context),通知监听器上下文已加载完成,但尚未刷新(refresh)。
该阶段对应的事件为 ApplicationPreparedEvent,可以实现该接口,并重写其方法。
实现在 Bean 定义加载完成后、上下文刷新前执行逻辑(如修改 Bean 定义)。
7:刷新上下文
该阶段也称为刷新阶段,SpringBoot的最核心处理节点,通过 refreshContext(context) 触发,最终会执行到 refresh 核心方法。
刷新环节是 Spring 中 Bean 的生命周期最核心的处理。
这一阶段负责初始化并配置应用上下文,加载 Bean 定义,执行自动配置,最终启动内嵌服务器。
刷新阶段也会解析注解,目的是扫描更多组件并注册Bean定义。
1:这是Spring的设计理念,并不会重复解析,而是职责分离,可简单理解为处理不同注解。
2:通过扩展机制全面处理配置类、自动配置和条件逻辑注解,例如@ComponentScan、@Import 等,并将其注册Bean定义。
3:会先执行@ComponentScan扫描包,然后才处理自动配置。如果是自定义自动配置类,需避免组件扫描冲突,不在@ComponentScan扫描路径下,条件注解才可生效。
1:获取BeanFactory
在进入方法时,会执行初始化上下文,校验环境变量等。
随后获取 BeanFactory 对象,并对其进行准备,供后续使用,提供了供子类扩展的后处理方法。
2:Bean工厂后置处理
最关键的处理,调用 invokeBeanFactoryPostProcessors 方法,执行所有 BeanFactoryPostProcessor 处理。
这块是Spring的逻辑,该节点是Bean生命周期的一部分,允许在实例化之前修改或扩展 Bean 定义(自定义)。
1:调用方法时,需要传递 BeanFactory 参数。
内部从Bean工厂中获取所有 BeanFactoryPostProcessor 的Bean名称,并通过getBean实例化这些处理器,最后汇总到集合中。包括系统或依赖内部定义的,还有我们自定义扩展的。
2:循环后置处理器,由Bean工厂统一执行每个处理器的方法,优先执行 Registry 级别的方法。
// PostProcessorRegistrationDelegate.java
public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactorybeanFactory, List<BeanFactoryPostProcessor> postProcessors) {
// 处理 BeanDefinitionRegistryPostProcessor(优先级最高)
for (BeanFactoryPostProcessor postProcessor : postProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
((BeanDefinitionRegistryPostProcessor) postProcessor).postProcessBeanDefinitionRegistry(registry);
}
}
// 处理普通 BeanFactoryPostProcessor
for (BeanFactoryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanFactory(beanFactory);
}
}
3:自动配置
上述的 BeanFactoryPostProcessor 处理是 Spring 提供的,SpringBoot的自动配置就依赖于此。
注意 SpringBoot 就只是新增了一个实现类:ConfigurationClassPostProcessor,内部实现了解析配置类、处理动态 @Import、自动加载配置类、注册添加 Bean 定义。
每一个Bean工厂后置处理器,在容器生命周期中只会执行一次,且作用于所有Bean的定义。
排除特定自动配置类:@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
流程:
1:@SpringBootApplication 组合了 @EnableAutoConfiguration,其中 @Import(AutoConfigurationImportSelector.class) 引入自动配置逻辑。
2:SpringBoot内部维护了一个处理器实现 ConfigurationClassPostProcessor,从 @SpringBootApplication 标注的主类开始解析,解析所有 @Configuration 配置类。
3:当发现主类上的 @EnableAutoConfiguration 注解后,进而解析其 @Import(AutoConfigurationImportSelector.class)。
4:然后通过 AutoConfigurationImportSelector.selectImports() 加载所有 META-INF/spring.factories 中的自动配置类。
5:解析完成后,通过 @ConditionalOnClass 等条件注解,过滤无效或按指定规则配置对应类,将其注册进容器中,在后续阶段实例化。
// ConfigurationClassPostProcessor.java 实现类
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 解析配置类,处理 @Import、@ComponentScan 等
processConfigBeanDefinitions(registry);
}
// ConfigurationClassParser.java 解析配置类,处理 @Import
protected void processImports(ConfigurationClass configClass, SourceClass currentSourceClass) {
// 处理 AutoConfigurationImportSelector
if (selector instanceof AutoConfigurationImportSelector) {
// 加载自动配置类
String[] imports = selector.selectImports(currentSourceClass.getMetadata());
// 注册自动配置类的 Bean 定义
for (String importClassName : imports) {
this.importStack.registerImport(importClassName);
}
}
}
4:BeanPostProcessor
在Spring的刷新阶段,调用 registerBeanPostProcessors 注册 BeanPostProcessor。
BeanPostProcessor 是一个接口,提供了两个方法,供实现类重写处理。作用是在初始化前后对Bean进行扩展,例如AOP。
在类内部,会调用 getBean() 方法创建 BeanPostProcessor 实例,此时会触发它们的依赖注入和初始化。
但是在这个阶段所有的Bean还是定义状态(用不上),为什么要提前实例初始化?
原因:其本身也是一个Bean,需要自己先被实例化为 Bean,才能对其他 Bean 生效。所以提前实例化。
最主要是实例化流程依赖于它(如依赖注入),需确保后续 Bean 的创建过程可以应用这些处理器。
BeanPostProcessor 针对于每个Bean实例,每个Bean初始化前后都会执行所有处理器。
5:初始化容器
嵌入式服务器的初始化发生在刷新阶段的 onRefresh() 方法中,是一个空方法,由具体子类实现。
实现原理:Spring Boot 根据类路径中的依赖自动配置对应的工厂类,然后会调用核心方法 factory.getWebServer()。
- 根据上述步骤设置的配置参数初始化服务器。
- 注册 Servlet 和 Filter。
- 启动服务器,调用服务器的 start() 方法,开始监听端口。
onRefresh 在设置环境和加载配置之后执行,是为了确保参数已加载,可以安全读取 server.port 等配置。
嵌入式服务器的配置参数,读取优先级为:命令行参数 => 应用配置文件 => 默认值。
选择在 Bean 初始化之前执行的原因是:
1:服务器需要在单例 Bean 初始化之前启动,以便在应用完全就绪时,服务器已可处理请求。
2:若服务器启动失败(如端口被占用),应用会在初始化阶段快速失败,避免后续无效操作。
6:创建Bean对象
在处理其他初始化和注册监听之后,会调用 finishBeanFactoryInitialization 方法,创建所有单例Bean对象。
这里就到达了之前Spring描述的 Bean 的整个创建过程了,通过遍历BeanName集合,然后判断,最后调用 getBean 进入Bean的创建过程。
7:完成刷新
所有Bean创建完成之后,会调用完成刷新方法 finishRefresh()。
内部会发布 ContextRefreshedEvent 事件,标记容器初始化完成,通知监听器。
8:调用 Runner 接口
刷新完成后,调用 callRunners 方法,用于在应用启动后触发自定义初始化逻辑。此时所有Bean和上下文都已准备就绪。
Runner 接口提供了两个组件供自定义实现,ApplicationRunner 和 CommandLineRunner。两个组件接收参数不一致,App接收 ApplicationArguments 对象(封装了命令行参数),Com则是接收原始的 args 数组。
流程:
1:从应用上下文中获取所有实现这两个接口的Bean,分开各自整理,但最终合并为一个统一列表,按 Order 排序(@Order或实现Ordered 接口)。
2:优先执行所有 ApplicationRunner,再执行 CommandLineRunner。同一类型的Runner按Order值升序排列(值越小优先级越高)。
3:若某个Runner抛出异常,会中断后续Runner的执行,并导致应用启动失败。
注意事项:
同一个Bean不应该同时实现两种Runner接口,否则会重复执行(各自整理)。
需避免长时间阻塞,长时间运行的逻辑可考虑异步处理(如结合 @Async),以免拖慢启动速度。
方法执行完之后,返回到启动run方法中,最后通过监听器发布启动完成事件,项目成功启动。
3:Starter 依赖
Starter 是 Spring Boot 的核心特性之一,用于简化项目的依赖管理和自动配置。
早期使用 Spring 和 SpringMVC 时,需要手动添加所需Jar包,很麻烦而且效率不高,还容易出问题。
而 Starter 将某功能相关的依赖打包,开发者无需手动添加多个依赖或编写冗长配置,即可快速集成特定功能,统一管理便于后续扩展维护。
1:使用规范
我们一般会称其为项目管理工具,就是统一管理Jar包和工具。
支持两种格式 Maven/Gradle,创建简单 SpringBoot 项目时,默认为 Maven 格式,即 pom.xml 文件。如果是Gradle,则需手动创建 build.gradle 文件。
1:Maven 使用
Maven的生态比较好,用的比较多,有一个Maven的查询网站,可以很容易的找到所需依赖及版本。
使用:添加所需依赖的 Gav(Maven坐标),即可简单声明依赖。刷新Maven将其引入到项目中。
排除:在Maven中可使用 <exclusions> <exclusion> 指定gav坐标,排除指定依赖。
官方 Starter 命名规范:spring-boot-starter-{功能}
第三方 Starter 命名规范:{框架}-spring-boot-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.18</version>
</dependency>
微服务架构中,支持多项目构建管理子类。以及定义规范和版本,供子类使用。
<modules>
<module>cloud-common-business</module>
</modules>
<!-- 统一管理jar包版本 -->
<properties>
<mysql.version>8.0.23</mysql.version>
</properties>
<!-- 子模块继承之后,不声明版本号使用父类的,声明则使用自己的 -->
<dependencyManagement>
<dependencies>
<!--springboot依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.10.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
子类中只需要指定父类,生命依赖即可使用,默认用父类规定的版本号。
<parent>
<artifactId>springcloud_alibaba</artifactId>
<groupId>com.aaa</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
2:Gradle 使用
Gradle在 Android 语言开发中普遍使用,大型项目和微服务架构也选择。我的公司就用的这个。
Gradle 是一款基于 Groovy/Kotlin DSL 的现代构建工具,结合了 Maven 的约定优于配置,提供了更高效、动态、兼容的特性。
核心特性:支持多语言开发(Java、Kotlin、Android、C++ 等),兼容Maven生态,以及增量构建等启动时的优化。
优点就是启动速度优化、动态灵活、可扩展性强等。
- 增量构建:仅处理变化的代码和资源。
- 构建缓存:重用历史构建结果(如已编译的类)。
- 支持动态依赖版本和条件化引入,例如指定最小版本3.+
缺点是生态不如Maven,Java中用的比较少,有学习成本,问题不好排查等。
3:Maven 和Gradle 的区别
1:配置文件
Maven使用 XML 格式的pom文件,SpringBoot默认;Gradle需手动创建build.gradle文件,基于Groovy/Kotlin DSL。
2:依赖管理
Maven是固定声明式添加依赖,版本号需精确指定或默认;Gradle可动态控制依赖,指定最小版本号控制等,更加灵活。
3:构建速度
Maven每次都会重新构建所有,速度较慢;Gradle有增量构建和缓存机制,仅重新编译改动部分,加快构建速度,会减少JVM启动消耗。
4:场景不同
Maven适用于传统 Java 项目、简单构建需求,生态好,没有大的学习成本可快速使用;Gradle适用于大型项目、负责构建流程、依赖拖慢启动速度等场景,学习成本较高。
2:自定义 Starter
自定义实现自动配置类,首先就需要用到项目管理工具,使用工具的打包功能。
1:新建项目
新建Maven或Gradle项目,因为需要改动 spring.factories 文件。
添加 spring-boot-autoconfigure 依赖,因为需要使用注入容器、条件属性等注解。
2:创建自动配置类
注入容器,并添加条件注解或代码逻辑。
@Configuration
@ConditionalOnClass(MyService.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService() {
return new DefaultMyService();
}
}
3:注册配置类
在新建的这个项目中,在 src/main/resources/META-INF/spring.factories 文件中,添加 EnableAutoConfiguration 并指定我们自动配置类的全路径。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyAutoConfiguration
4:使用
Spring Boot 在启动时扫描所有 jar 包中的 META-INF/spring.factories 文件。
加载文件中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键对应的配置类。
根据条件注解判断是否应用配置,最后将所有Bean注册,包含我们的自动配置类。
4:嵌入式服务器
SpringBoot 支持Tomcat(默认)、Jetty、Undertow。默认内置Tomcat服务器,默认使用Servlet应用类型,可以根据业务进行更换服务器。
实现方式就是排除默认的服务器依赖,然后引入其他容器的 Starter(依赖)。
5:多环境和安全控制
多环境就很简单,指定不同的配置文件,然后每个环境激活不同端口等配置文件。
安全控制就是提供了健康检查、指标监控等配置。
四:MyBatis
MyBatis是一个开源的,半自动的ORM持久层框架,通过 XML 或注解配置 SQL 映射,将 Java 对象与数据库操作解耦。支持普通sql,关联查询,嵌套查询等。
它的核心思想是让开发者通过更简单的方式操作数据库,同时保留对 SQL 语句的完全控制权,避免了传统 JDBC 代码的复杂性。
1:核心架构
Mybatis提供的核心架构组件,熟悉各个组件才能明白其运行流程。
SqlSessionFactory | 通过 SqlSessionFactoryBuilder 解析配置文件生成的,用于创建 SqlSession。 |
SqlSession | 表示一次数据库会话,通过 Executor 执行 SQL,线程不安全。 |
Mapper(接口) | 通过动态代理生成实现类,将接口方法与映射文件中的 SQL 绑定。 |
StatementHandler | 处理 SQL 预编译、参数设置和结果集映射。 |
Executor | SQL 执行器,分为 SimpleExecutor(默认)、ReuseExecutor(重用预处理语句)、BatchExecutor(批处理)。 |
在项目资源目录下,创建XML配置文件,并添加注解 @MapperScan 扫描XML文件。
这些组件在原生MyBatis中需要手动编写使用的,与Spring集成之后,不需要再手动调用。
2:运行流程
Mybatis的使用规范是创建Dao层接口,提供Sql调用方法,编写对应XML语句实现Sql。
底层是使用 JDK 动态代理技术为 Mapper 接口生成代理对象,因为接口才能使用JDK的动态代理,而CGLIB适用于类。
注意:Mybatis分为原生和Spring集成,这两种方式对代理对象的处理不一致!
代理的作用:将接口方法调用转发给 MyBatis 核心组件(如 Executor),与 SQL 执行解耦。
1:解析文件
原生 MyBatis 在初始化时会解析所有 Mapper XML 文件,将每个 SQL 语句封装为 MappedStatement 对象,并注册到全局 Configuration 对象中。
Mapper XML 文件的 namespace 必须与接口的全限定名一致,XML 中的 SQL 语句的 id 必须与接口方法名一致。否则会抛出绑定错误异常。
MappedStatement 的键为:接口全路径+方法名,例如com.aaa.xxx.selectUser。
所以,Mybatis不支持方法重载。如果接口中存在同名方法(即使参数不同),MyBatis 会因无法区分 MappedStatement 的键而报错。
1:原生框架代理
当通过 SqlSession 调用 getMapper 方法时,MyBatis 会从 Configuration 中获取对应的 MapperProxyFactory(维护了所有Mapper接口的代理工厂),动态生成代理对象。
所以原生的Mybatis框架,代理对象在getMapper调用时创建,且通常只需创建一次。
2:Spring集成代理
而Spring通过 MapperScannerConfigurer 扫描 Mapper 接口,在 Spring 容器初始化时,为每个 Mapper 接口生成代理对象,并注册为 Spring Bean。此时代理对象已创建完毕。
在 Service 层通过 @Autowired 注入 Mapper 接口时,实际注入的是启动时已创建的代理对象。
3:Sql执行
上述获取到代理对象后,当调用接口方法时,代理对象会触发 MapperProxy.invoke() 拦截方法。
注意:如果接口方法有默认实现,动态代理会直接调用默认方法,而不会执行 SQL。
1:根据(接口全路径+方法名)从 Configuration 中获取对应的 MappedStatement。
2:将方法参数转换为 SQL 参数。
3:通过 SqlSession 内部的 Executor 执行 SQL,处理参数映射和结果集转换。
3:Sql参数
MyBatis比较关注于Sql的执行,提供了动态Sql、参数映射、以及负责映射等功能。
1:动态Sql
开发使用中,尽量避免在动态 SQL 中过度嵌套,可能有性能问题且代码可读性差。可以将判断逻辑放在业务层。
2:参数映射
#{} 和 ${},两者都是Mybatis框架提供的,用于动态 SQL 参数替换的实现。
#{}用于处理数据值(如字符串);${}用于处理SQL 结构(如动态表名、列名、排序字段)。
不同点:
#符是预编译占位符,在执行时,会将sql中的 #{} 替换为 ? 号,之后调用 PreparedStatement 的set方法进行赋值。可以有效防止sql注入。操作数据优先使用该方式。
是在编译期替换为 ? 号,变量的替换是在DBMS数据库中,并且加双引号。
适用场景:普通参数操作,如where id = #{id}。
$符是字符串替换,在处理时,就只是把 $() 替换为变量的值,不防Sql注入,语句不会加双引号。
sql语句变量的替换是在动态sql解析阶段,存在sql注入问题,在预编译之前变量可被替换。
适用场景:动态表名、列名、排序字段(如 ORDER BY ${sortField})。
<!-- 高危操作:用户输入直接拼接 -->
<select id="login" resultType="User">
SELECT * FROM user
WHERE username = '${username}' AND password = '${password}'
</select>
<!-- 若 username = "admin' --",会绕过密码验证! -->
3:结果映射
可使用 resultMap 处理字段不一致情况,将实体类字段和数据库字段做映射,在查询方法上指向该 ResultMap 即可。
并且提供了高级嵌套查询:嵌套 association(一对一)、collection(一对多)。
4:缓存机制
MyBatis的缓存分为一级缓存和二级缓存,目的是通过减少数据库查询次数提升性能。两者的作用域、配置方式及失效策略不同。
所以它的缓存机制就是针对于查询的Sql语句,避免查询数据库。
1:一级缓存
一级缓存是 SqlSession 级别,同一个会话中有效,其生命周期随 SqlSession 的创建而创建,关闭或提交事务(commit、rollback)时清空。
无需配置,默认开启,存在 BaseExecutor 中的 localCache 属性,PerpetualCache 实例,该对象内部为 HashMap 结构,每个SqlSession有自己独立的缓存。
SqlSession非线程安全,需确保每个线程使用独立的 SqlSession(例如ThreadLocal)。
脏读问题(缓存一致性问题):若其他会话修改数据,当前会话无法感知,需通过更新操作或关闭会话强制刷新。
流程:
1:执行查询时,MyBatis 根据 MappedStatement、参数、分页等信息生成缓存键(CacheKey)。然后通过键查询一级缓存是否有数据,如果有则直接返回结果,如果没有就查询数据库,并将结果存入一级缓存。
2:执行 INSERT/UPDATE/DELETE 或调用 SqlSession.commit()/rollback() 时,清空一级缓存。这里的清空是指清空当前会话下的所有缓存,不会对其他会话造成影响。
2:二级缓存
二级缓存是 Mapper 级别,跨 SqlSession 共享(如多个会话访问同一命名空间)。
需要手动开启,在Mapper中添加XML标签指定缓存信息,生命周期跟随容器。
默认使用 PerpetualCache(内存缓存),可集成第三方缓存(如 Ehcache、Redis),引入依赖和配置,在添加 cache 标签时指定 type 即可,
若缓存对象需要跨会话传输,实体类需实现 Serializable 接口。
<!-- UserMapper.xml -->
<mapper namespace="com.example.dao.UserMapper">
<cache/> <!-- 默认配置 --> <!--集成:type="org.mybatis.caches.ehcache.EhcacheCache"-->
<!-- 或自定义参数 -->
<cache
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
</mapper>
- eviction:淘汰策略(LRU、FIFO、SOFT、WEAK)。
- flushInterval:自动刷新时间(毫秒)。
- size:最大缓存对象数量。
- readOnly:是否只读(若为 false,缓存返回对象的拷贝)。
流程:
1:执行查询时,根据缓存键先查询一级缓存,再查询二级缓存,如果都没有,再查询数据库。查询结果按照SqlSession(会话)、一级缓存、二级缓存的顺序填充保存。
2:当执行 INSERT/UPDATE/DELETE 时,清空当前 Mapper 的二级缓存。
3:多表关联可能有脏数据问题,导致Mapper缓存不会自动失效。可以通过禁用关联Mapper的缓存,或使用cache-ref建立缓存引用,确保关联Mapper的更新触发当前缓存清空。
3:注意事项
如果发现查询速度过慢,优先考虑数据库索引优化,缓存应作为补充手段,不可过度依赖。
缓存数据不一致问题:分布式架构下,可能会有多个线程同时要修改数据,最终可能导致每台机器数据不一致,此时就无法使用二级缓存处理。
但是如果用第三方插件(例如Redis)来实现二级缓存,又没有必要。
因为 Redis 一般是在Mybatis之前做缓存的,而不是嵌套在Mybatis里面使用的,
所以在分布式架构下,应优先考虑在数据查询前缓存控制,Mapper层只用来专注查询数据。
一级缓存适用场景:
- 短会话操作:如单个请求中多次读取同一数据。
- 事务内操作:确保事务内数据一致性。
二级缓存适用场景:
- 读多写少数据:如配置表、静态数据。
- 跨会话共享数据:如热门查询结果。
二级缓存应根据场景慎用,高频更新查询不要用二级缓存。另外Mybatis缓存也会有缓存穿透、缓存雪崩问题,处理方式同Redis一样。
1:缓存穿透,频繁查询不存在的数据,绕过缓存直接访问数据库。
处理:对空结果进行短期缓存,或使用布隆过滤器。
2:缓存雪崩,大量缓存同时失效,导致数据库压力激增。
解决:给不同Mapper设置随机过期时间,或使用集群化缓存。
5:扩展插件
Mybatis提供了扩展插件,用于简化开发以及个性化处理。
1:拦截器插件
基于 JDK 动态代理,可以按照 @Intercepts 注解指定的规则拦截方法,按照 @Signature 指定类、方法等详细信息。
提供了接口 Interceptor,供开发人员实现接口并重写方法,提供了三个核心方法。
- Interceptor:拦截之后的主要处理实现,从参数中获取MappedStatement对象(Sql语句)。
- plugin:创建目标对象的代理,如果要拦截,就生成代理对象,不拦截就返回原生对象。
- setProperties:读取配置文件中的参数。
当我们实现接口后,底层会创建一个该接口的代理实现类,在指定Sql语句执行前,对其进行拦截,通过代理对象执行拦截器方法。
使用场景:可根据 SqlCommandType 语句类型,自动生成主键id、创建人、创建时间等功能,简化调用和自动填充。
2:分页插件
PageHelper 是 MyBatis 的一个分页插件,其核心实现依赖于 MyBatis 的拦截器机制,通过动态代理拦截 SQL 执行过程,自动添加分页逻辑。
原理:PageHelper 实现 MyBatis 的 Interceptor 接口,拦截 Executor 的 query() 方法,在执行 SQL 前动态修改 SQL 语句并添加分页参数。
流程:
1:在代码中直接使用 PageHelper.startPage() 方法时,会通过 ThreadLocal 将分页参数(页码、每页条数)绑定到当前线程,确保同一线程内的后续查询自动分页。
2:代理对象拦截方法后,会检查当前线程是否有分页参数,如果有,就改写 SQL,执行分页查询和 COUNT 查询。如果没有则直接通过。
3:改写Sql时,会根据数据库类型(如 MySQL、Oracle)的方言(Dialect),将原始 SQL 改写为分页 SQL(例如Oracle不支持Limit)。
4:维护了一个总条数字段,自动生成并执行 COUNT(*) 查询,获取总记录数,用于计算总页数。
5:改写Sql和总条数查询完毕之后,清除 ThreadLocal 中的分页参数,避免污染后续查询,以及防止内存泄漏。