Spring
Spring
Review: Source Codes Analysis
Spring IoC
IoC: Inverse of Control 原先调用服务或者DAO的需要自行new出来对象,硬编码,耦合程度高,Spring的Container能够接管对象的创建工作(实际上就是管理Bean) 并且能够根据对象Bean之间的关系进行依赖注入,创建A对象的同时会把B对象创建起来,也就是DI(Dependency Injection)
管理方式:配置文件xml IoC容器的获取:Spring提供接口
把业务接口的实现类交给Spring管理,遇到接口类,Spring就会自动去找Bean中是否有接口的实现类。
BookDao是接口,实现类为BookDaoImpl,Impl交给Spring管理
DI:依赖注入,依赖用方法传参的方式传入
property name 是成员变量的名字
ref 是要引用的bean id/name
IoC 配置
bean 管理
name 别名
ATTRIBUTE
bean name = “s1 s2 s3” alias
ref可以使用name也可以使用id
getBean
scope 作用范围
ATTRIBUTE
Spring默认创建单例bean,scope=”singleton” prototype为多例。
- 适合复用的才作为bean交给IoC容器管理
- 表现层,业务层,DAO层,工具层
- 不适合复用的对象
- 封装的实体域对象
bean 创建方式
构造方法
无参构造器,如果使用构造器进行依赖注入,则走的是有参构造
使用静态工厂实例化Bean
- 工厂的静态方法factoryMethod(return new Bean),不造工厂,调用工厂的静态方法造Bean ATTRIBUTE: factory-method
使用工厂实例化Bean
- 先造工厂bean再调用工厂的实例方法(return newBean) 造bean
FactoryBean 工厂bean
第三方自定义工厂Bean类实现FactoryBean接口,重写方法
- getObject 工厂类的returnNewBean方法
- getObjectType return Bean.class bean的 字节码
- isSingleton 单例
xml 配置
主要用于第三方框架和Spring框架对接,他们创建的对象要配置一些参数,这时就需要一个FactoryBean,工厂bean会提供set对象参数的方法,返回的就是配好参的对象,可以省去手动配参的麻烦
bean 生命周期
Customizing the Nature of a Bean :: Spring Framework
init-method 初始化
ATTRIBUTE 方法名
destroy-method 销毁
ATTRIBUTE 方法名
销毁方式1: 容器关闭 ctx.close()
appctx这个类没有关闭功能,换一个annotationConfigAppctx才有
销毁方式2: 注册关闭钩子ctx.registerShutdownHook()
自定义实体类实现接口
DisposableBean
InitializingBean
分别重写destory() afterPropertiesSet()
属性设置就是在属性注入(调用setter)之后调用的方法
生命周期示意图
DI 依赖注入
注入 ⇔ 给bean的属性赋值
注入多个bean,填写多个property
方式
setter注入
- setter 引用其他的bean property ref = 其他bean的名称 ATTRIBUTE
- setter 注入基本数据类型和简单值 property value = 值 ATTRIBUTE
- property name实际上是根据setter方法 setUserDao 去掉set首字母小写 userDao得到的
- 先无参构造创建bean,再用setter注入依赖
构造器注入
针对有参构造器,必须显式声明有参构造器
ATTRIBUTE <constructor-arg name>
引用其他bean name是构造器形参名,耦合度高,参数先后顺序固定不能变
基本数据类型和简单值
耦合度高解决方案:参数适配
<constructor-arg name>
改成type,解决参数名的高耦合,但是type相同的参数会混淆改成index,index表示参数的位置
直接有参构造创建bean,可以没有无参构造
方式选择
- 强制依赖使用构造器进行,使用setter注入有概率不进行注入导致NullPointerException
- 可选依赖使用setter注入进行,灵活性强
- Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
- 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
- 实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
- 自己开发的模块推荐使用setter注入
依赖自动装配 autowire
只适用于引用类型
ATTRIBUTE
不去手动指定,在容器的bean中自动匹配适合的bean。依赖于有参构造或者setter
byType (依赖setter)
bean属性的type
要去匹配 容器中bean的class
保证相同class的bean唯一 推荐
byName (依赖setter)
bean属性的name
要去匹配 容器中bean的id
保证必须要有指定名称的bean 耦合度高,不推荐
constructor(依赖有参构造器)
default
如果<beans>指定了autowire 此bean跟随beans
no
注意事项
只能自动装配引用类型(IoC容器不会去管理简单类型)包装类bean根本没法写
优先级 < 手动装配
集合注入
集合要注入内容,而不是注一个空壳
1 | <property name="myArray"> |
管理第三方Bean
别人写的对象,创建bean,类型是什么?你要配哪些参数?
加载.properties XML Namespace
创建context命名空间
classpath:*.properties 当前模块下所有的配置文件
容器 ctx
创建容器方式
getBean()
立即加载(饿汉),lazy-init 延迟加载 (懒汉)
注解开发
Bean 的定义
定义bean@Component
加上对应的bean的id ,不加就要加载字节码class
纯注解开发@Configuration @ComponentScan
获取ctx: ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class)
默认xml配置文件只给了beans的命名空间,context还得另外自己加,纯注解开发需要定义一个SpringConfig类,常用的配置都有,不用手动去加命名空间 XML out!
和 @Component 的区别是,能进行代理拦截,保证bean是单例的。
Bean 的管理
作用范围 @Scope
生命周期 @PostConstruct @PreDestroy
探究Spring Boot中@PostConstruct注解的使用场景-腾讯云开发者社区-腾讯云 (tencent.com)
Spring 框架中 @PostConstruct 注解详解-腾讯云开发者社区-腾讯云 (tencent.com)
Instantiate(Constructor)> @Autowired > @PostConstruct
依赖注入完成,被显式调用之前
Using @PostConstruct and @PreDestroy :: Spring Framework
DI 自动装配
自动装配@Autowired
在需要注入依赖的一个属性,与配置文件autowire Attribute of Bean不同,注解Autowired不依赖于setter和有参构造器,直接暴力反射访问private属性,创建对象并注入依赖。
按名称匹配@Qualifier
autowired默认按类型装 配,同一类型多个实现,用Qualifier指定具体bean名称,不加Qualifier就按一定规则选择
先名称匹配,再按照类型匹配@Resource
面试突击78:@Autowired 和 @Resource 有什么区别?-阿里云开发者社区 (aliyun.com)
- @Autowired 先根据类型(byType)查找,如果存在多个(Bean)再根据名称(byName)进行查找;
- @Resource 先根据名称(byName)查找,如果(根据名称)查找不到,再根据类型(byType)进行查找。
注意下方的Bean注解
@Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入
@Autowired 来自 Spring 框架,而 @Resource 来自于(Java)JSR-250;
@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数;
@Autowired 既支持构造方法注入,又支持属性注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入;
为什么属性注入不推荐使用 @Autowired
- @Autowired 注解注入依赖容器,单元测试不如setter和构造器方便。
- 无法实现不可变性:final 属性不支持 @Autowired 注解。
- Autowired默认按照type,如果有两个相同type的bean,就会报错,不如 @Resource
1 | // setter........ |
简单类型注入依赖@Value
需要注入的属性上 写value
导入配置文件@PropertySource
管理第三方Bean
Config配置类中bean的创建@Bean
与@Component不同 这个是方法级别的注解,方法返回的对象将由Spring容器管理
@Bean声明的Bean名称?
spring boot中通过注解@Bean声明的bean的名称是什么?_springboot 声明bean的名称-CSDN博客
不指定name属性,bean名称为方法名
指定name属性,bean名称为name
导入其他Config类到核心配置 @Import
不建议直接把其他的配置写到SpringConfig里面,分出去然后import
DI 依赖注入
最简单的方法:自己new个对象出来,手动配参数,丢给spring (其实xml就是把手动配参的过程从业务代码中解耦出来)
简单类型依赖注入:成员变量@Value
引用类型依赖注入:方法形参
XML vs. Annotation
整合第三方框架
Spring & MyBatis
使用纯注解方式Spring整合MyBatis_spring整合mybatis基于注解-CSDN博客
Spring整合Mybatis(注解方式完整过程,摒弃MyBatis配置文件)_springboot启动去除mybatis-CSDN博客
dao是session用动态代理造出来的,不同业务的内部实现有区别。session也不会一直复用,根源在sqlSessionFactory。
还有一个就是mapper映射,这个跟ssf没什么关系。
MyBatisConfig - SqlSessionFactoryBean
导入mybatis-spring spring-jdbc,mybatis实现了Spring规定的FactoryBean接口,专门用来造sqlSessionFactory对象。
回顾spring创建对象的方法,一种是使用构造器直接得到对象,另一种就是使用factoryBean<E>得到对象E,定义一个造E的工厂Bean,这样spring就知道类型E创对象需要用工厂Bean的方式。
factorybean中提供了很多设置E参数的方法,最终返回的是一个设置好参数的E,思想还是一样的,只不过套了一层工厂的皮,封装进去很多固定的参数set方法,一般这个E需要xml进行配置(跟真正的业务代码解耦),工厂Bean就取代了xml,直接给你返回一个配置好的对象。
需要传参就直接在写上方法参数即可,spring自动匹配
MyBatisConfig - MapperScannerConfigurer
DAO没有实现类了,在原始接口上加Component、Repository给ioc容器标识一下,不标也行。
这个mapperScannerConfigurer是mybatis和spring集成的部分,扫描指定mapper所在的包,mapper生成代理对象,通过factoryBean方式交给Spring容器,所以重点不是让spring知道dao的实现类在哪,重点是要让mybatis知道mapper位置
JdbcConfig - 创建DataSource的Bean交给Spring管理
Spring & JUnit
导入spring-test 在Test
在test.java中测试。
@RunWith @ContextConfiguration
@Autowired @Test
需要引用类型参数直接autowired注入即可,一般是业务类做测试
概念
IoC 容器动作
ApplicationContextInitializer: 容器创建后
- IoC容器初始化器,实现
initialize()
方法(返回 ConfigurableApplicationContext对象) - 在spring.factories中配置自定义实现类的全限定名。
- IOC创建完成后执行,常用于 Environment 环境属性注册。
ApplicationListener: 监听容器发布的事件
设计模式:观察者模式
ioc容器发布事件后回调,通常用于资源加载和定时任务的发布。
onApplicationEvent(ApplicationEvent event)
:- 在spring.factories中配置自定义实现类的全限定名。
ApplicationReadyEvent
, ApplicationFailedEvent
IoC 容器
BeanFactory: 容器根接口
主要是bean的创建、配置、依赖注入等功能。
核心方法是 getBean()
,还包含 isSingleton()/isPrototype()
containsBean()
的功能。
getBean(): spring 循环依赖 | scatteredream’s blog
- BeanFactory:根接口
- AbstractBeanFactory
- DefaultSingletonBeanRegistry
- AbstractAutowireCapableBeanFactory
委托制:AnnotationConfigServletWebServerApplicationContext 将bean的创建配置和依赖注入委托给了 DefaultListableBeanFactory
Bean
BeanDefinition: Bean 相关信息
描述bean,包括名称、属性、行为(初始化方法、销毁方法、类名、构造器参数)、实现的接口、添加的注解等。Bean创建之前都要封装成 BeanDefinition 注册到 BeanDefinitionMap 中。
- BeanDefinition
- ScannedGenericBeanDefinition (标注x类为bean的注解)
- ConfigurationClassBeanDefinition(标注某个方法的返回值是bean)

BeanFactoryPostProcessor: BeanFactory 后处理器
BeanFactory 准备好,正式开始Bean创建之前,postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
经常用来新增 BeanDefinition。只要把name bdf传入即可实现注册。

BeanPostProcessor
Spring-扫描自定义注解 在每个Bean初始化完成之后或者之前都会调用 postProcessBefore/AfterInitialization.
after: 代理对象

Aware

Bean 生命周期
Bean 实例化(仅为构造出对象)
- 触发条件:①容器启动 ②首次请求 Bean 时
getBean() 或者依赖注入
。 - 方式:通过构造函数或工厂方法创建 Bean 的实例。
- 异常:若依赖无法解析或构造函数抛出异常,Bean 创建失败。
- 触发条件:①容器启动 ②首次请求 Bean 时
属性赋值 Populate Properties
- 依赖注入:通过
@Autowired
、@Resource
、XML 配置等方式注入属性。 - 处理
@Value
:解析并注入 SpEL 表达式或占位符的值。
- 依赖注入:通过
Aware 接口回调 与
BeanPostProcessor
前置处理、初始化、后置处理。就绪状态
- Bean 完全初始化,可被应用程序使用。
- Singleton Bean 会被缓存,后续请求直接获取。
- Prototype Bean 每次请求创建新实例(无后续销毁步骤)。
Bean 对象销毁回调
@PreDestroy
JSR-250destroy()->
DisposableBeanclose()
@Bean (destroyMethod = close)
Bean 对象销毁
- 触发条件:容器关闭时(如
close()
方法调用)。 - 作用域影响:仅 Singleton Bean 会执行销毁回调,Prototype Bean 需手动清理。
- 触发条件:容器关闭时(如
扫描并注册被注解的类
基于 Netty 的 RPC 框架 | scatteredream’s blog
IoC容器初始化
循环依赖
Spring循环依赖指的是两个或多个Bean之间相互依赖,形成一个环状依赖的情况。简单来说,就是A依赖B,B依赖C,C依赖A,这样就形成了一个循环依赖的环。
Spring循环依赖通常会导致Bean无法正确地被实例化,从而导致应用程序无法正常启动或者出现异常。因此,Spring循环依赖是一种需要尽量避免的情况。
Spring 使用三级缓存机制部分解决循环依赖问题,但是从 SpringBoot 2.6 开始默认禁止循环依赖,因为这是顶层设计出现问题的表现。
解决方式
使用构造函数注入
构造函数注入是一种相对保险的方式,因为在实例化Bean时,Spring会检查是否存在循环依赖,并在发现循环依赖时抛出异常,避免死循环。示例代码如下:
1 |
|
使用@Lazy注解
@Lazy注解可以延迟Bean的实例化,从而避免循环依赖的问题。示例代码如下:
1 |
|
使用 setter 注入
使用setter方法注入也可以解决循环依赖的问题,但要注意可能出现的空指针异常。示例代码如下:
1 |
|
获取单例对象:三级缓存机制
- 优先查询一级缓存(
singletonObjects
)- 一级缓存也叫单例池,存储的是完全初始化的单例 Bean(例如已注入所有依赖且完成代理增强的对象)。
- 作用:直接获取可用 Bean,避免重复创建。
- 优先级最高:如果找到直接返回,不触发后续缓存查询。
- 未找到则查询二级缓存(
earlySingletonObjects
)- 二级缓存存储的是已实例化但未完成初始化的 Bean(半成品)。
- 作用:在循环依赖中临时暴露早期引用(例如 A 依赖 B 时,B 可能正在创建中,需引用 A 的半成品)。
- 注意:若二级缓存中存在目标 Bean,则直接返回,但此时 Bean 可能尚未完成属性注入或代理。
- 最后查询三级缓存(
singletonFactories
)- 三级缓存是 beanName 到 对象工厂(
ObjectFactory
)的映射,对象工厂是个函数式接口,这个接口用于动态生成 Bean 的早期引用或代理对象。 - 触发条件:仅当一、二级缓存均未找到时,调用工厂生成 Bean 实例,之后将其提升至二级缓存。
- 关键作用:支持 AOP 代理的延迟生成(例如解决代理对象的循环依赖)。
- 三级缓存是 beanName 到 对象工厂(
二级缓存可以解决问题
getBean(a)
,实例化对象 A 以后放入二级缓存(裸对象),然后 A 开始属性注入- 遇到一个属性 B,先从一级缓存里面拿发现没有,瞄一眼二级缓存里面也没有,于是开始
getBean(b)
: - 实例化对象B以后将其放入二级缓存(裸对象),B 开始属性注入,发现 A 不在一级缓存,但是从二级缓存里面拿到了 A 的裸对象注入 B,此时 B 算初始化完成,把 B 从二级缓存里面删掉,放到一级缓存里面,至此 B 创建完成。
- 最后 A 用于注入的方法就能返回一个从缓存里面拿到的 B 对象,A 的注入也就完成了。
二级缓存的不足
但是二级缓存的问题是,A是代理,有B,B有需要注入A,首先A创建出实例,随后A就走到了populateBean这一步
然后去拿B,B创建,又想来获取A了,此时A那边属于是一个刚创建实例的状态,并未走到生成代理对象那一步,因此直接注入就会出问题,所以应该注册一个回调函数,把A的实例注册进去,函数的返回值是A的对象(实例/代理实例),所以就产生了三级缓存。三级缓存的 ObjectFactory
主要是用于提供一个钩子,这个接口的方法返回的就是bean对象,不同之处在于可以在返回裸对象前,给其套上一层代理再返回。如果只有二级缓存,就没有机会返回代理对象。
源码流程
DefaultSingletonBeanRegistry#getSingleton(name,true)
DCL双重校验锁。
1 |
|
流程正式开始——
DefaultListableBeanFactory#preInstantiateSingletons()
用于对解析到的beanNames
一一进行getBean(beanName)
AbstractBeanFactory#doGetBean(name···)
getSingleton(name,true)
获取不到再往下走- 类似双亲委派,找 parent,parent 找不到再自行寻找
- 自行寻找:(dependsOn) 然后
getSingleton(name, singletonFactory)
getSingleton(name, singletonFactory)
: 先一级缓存找,找不到就真正开始创建工作:- 首先将其加到 CreationSet 表明其正在创建。
singletonFactory
实际上就是一个ObjectFactory
,这个函数式接口实现方法通过createBean(name···)
获取对象。- 创建完成将其从 CreationSet 中移除,保证其只存在于一级缓存单例池中。最后返回创建好的 bean
AbstractAutowireCapableBeanFactory#doCreateBean(name,mbd,args)
createBeanInstance()
:创建出裸对象,此时未注入依赖。(实际上返回的是 BeanWrapper,有更完善的功能,本质还是对象实例)- 当且仅当单例+允许循环依赖+这个bean在 CreationSet 中,才加三级缓存
addSingletonFactory
,一定要保证一级和二级缓存里面没有,然后把ObjectFactory加到三级缓存里面。所以单例 bean 加入了三级缓存:lambda:getEarlyBeanReference()
返回创建的裸/代理对象singletonObject
- 当且仅当单例+允许循环依赖+这个bean在 CreationSet 中,才加三级缓存
populateBean()
:进行字段、方法注入。做一些postProcessAfterInstantiation
实例化之后初始化之前的工作。然后就是autowireByName/Name
,本质上就是通过getBean(name···)
获取实例。- 依赖注入就是这里遇到的问题,如果代理对象出现循环依赖,那么其生成应该是初始化之后,所以此阶段断然不能提供出代理对象,因此加入三级缓存提前暴露出一个引用,。
initializeBean()
:调用
BeanPostProcessor#postProcessBeforeInitialization
。调用初始化方法。(PostConstruct-initMethod-afterPropertiesSet)
调用
BeanPostProcessor#postProcessAfterInitialization
(一般到这里才生成代理对象)。
完成上述工作之后,如果当前是存在于三级缓存,则调用下方的
getSingleton(name,true)
: true 代表允许早期引用(主要解决循环依赖)
代理:
AbstractAutoProxyCreator
1 | // 这个方法是用于三级缓存生成对象的时候将bean放到earlyBeanReferences里面, |
无法解决的循环引用
如果是 AB互相依赖,A只有含参构造,那么注入B完成之前就无法创建一个A实例出来,自然也没法加到缓存里面,A尝试注入B,B那边尝试注入A彻底卡死。
但是 从B开始又可以了,然而对于普通的bean来说,注册顺序并不是一个可控的状态,所以尽量避免含参构造的bean之间互相依赖
Spring AOP
AOP的含义
AOP 面向切面编程 不惊动原始设计的情况下增强功能
Spring理念:无侵入式增强功能
- 所有原始方法->连接点(joint point) 在SpringAOP中如此
- save update delete select
- 需要追加功能的方法->切入点(pointcut)
- save update delete
- 具备的共性功能->通知 (advice)
- method1 method2
- 通知和切入点产生关系->切面 (aspect)
- save update delete追加method
- 功能的集合->通知类
连接点包含切入点
Spring中进行AOP编程
导入坐标
MyAdvice
定义通知(功能增强点)
定义切入点 @Pointcut
private
void
空参
绑定通知与切入点
Spring接管此类 @Component
定义AOP @Aspect
SpringConfig @EnableAspectAutoProxy
AOP
工作流程
**基于动态代理**:
匹配失败,new原始对象;
匹配成功,new出来的是原始对象的代理对象;
用获取到的bean执行方法:如果是代理的bean,根据通知和切入点进行方法执行。
切入点表达式
切入点:要对其进行增强的方法
切入点表达式:对切入点的描述方式
execution(public
User
com.itheima.service.UserService.findById
(int
))
public exception 可省略
参数必须有
通配符使用
- ..和*的区别 *用于精准匹配到某个位置
1 |
|
通知类型
前置@Before
后置@After
环绕@Around
ProceedingJoinPoint
原始方法在环绕方法中执行,用
pjp.proceed()
执行原始方法,不出现就能隔离原始方法,(权限校验)pjp能接原始方法的返回值,类型为Object,强转后可以在给他返回去,思想和动态代理里的案例比较像:利用反射invoke调用可以拿到返回值,注意修改通知方法的返回值为Object。没返回值也可以
强制抛Throwable
得到返回值之后@AfterReturning
和after区别:after只要方法结束即可,不管是得到返回值正常结束还是抛异常。AfterReturning需要得到返回值正常结束才能
抛出异常之后@AfterThrowing
案例:JUnit 测量业务层接口执行效率
JUnit 测试服务类就private服务出来,@Autowired
下面写test具体方法。详见JUnit单元测试篇(Java SE)
获取方法签名 (执行信息) getSignature()
通知获取数据
JoinPoint & ProceedingJoinPoint
作为通知的参数,如果出现,必须在第一个参数的位置上,PJP是JP的子类
Object[] getArgs()
: 获取原始方法的参数
Object proceed()
:环绕 PJP专用,调用原始方法同时返回这个方法的返回值
AOP获取原始方法调用参数
这样可以对原始参数进行处理,可以增加程序健壮性。
Object proceed(Object[] args)
可以把处理以后的参数传给原始方法。
案例:网盘提取码去空格
args本身是Object数组,拿进来需要转成字符串toString getArgs 然后遍历参数数组,对每个字符串参数trim,再把处理以后的传给proceed
AOP获取返回值
- 环绕 pjp proceed
- AfterReturning 注解的returning要和形参名字相同
AOP接收异常
环绕 不要往出抛Throwable 内部try-catch
AfterThrowing 注解的throwing和形参名字相同

环绕通知模拟其他四种通知
前置 | 最后调用proceed |
---|---|
后置 | try catch finally 在finally里写 |
AfterReturning | Object接住proceed的返回值 |
AfterThrowing | 不要往出抛Throwable,try catch Throwable |
代理对象的实质—this调用实例方法失效
AOP的核心,是从调用对象的方法时生成代理对象—PROXY,this指向真正的目标对象—TARGET
1 | interface IService{ |
事务的实现基于spring aop,如果直接用this,则会使用target对象进行方法调用,
Proxying Mechanisms :: Spring Framework
Understanding the Spring Framework’s Declarative Transaction Implementation :: Spring Framework
如果是在类内部开启的事务,就需要CGLIB动态代理,实现的基础是方法拦截器,环绕通知,
直接调用方法
通过代理调用方法
也就是说,如果要让代理生效,首先就是要获取代理对象,而通过@Transactional注解的方法zoo()
,如果从外部调用,只要获得了代理对象的引用,事务功能就是生效的,因此直接在main中只要获取了代理对象的引用调用service.zoo()方法是没有问题的。
而在目标对象内部,this就是目标对象本身,肯定不会走代理,因此如果实在
解决方案—在类的内部获取代理对象
AopContext.currentProxy()
1 | synchronized (UserHolder.getUser()) { |
@Autowired 注入服务对象本身
获取service对象即可,这样就能在类内部的上下文中获取proxy代理都象的引用
1 |
|
AOP:动态代理对象的生成时机
关于Spring的两三事:代理对象的生成时机-腾讯云开发者社区-腾讯云 (tencent.com)
1) Bean的实例化:首先,Spring容器会实例化Bean。
2)Bean的属性填充:然后,为Bean填充依赖注入的属性。
3)Bean的初始化:
- 初始化前(postProcessBeforeInitialization):在这一阶段,Spring会调用所有BeanPostProcessor的postProcessBeforeInitialization方法。但此时,代理对象可能还未被创建,因为还需要进一步判断该Bean是否需要被代理。
- 初始化:接着,执行Bean的初始化方法(如@PostConstruct注解的方法或实现了InitializingBean接口的afterPropertiesSet方法)。
- 初始化后(postProcessAfterInitialization):在Bean初始化完成后,Spring会调用所有BeanPostProcessor的postProcessAfterInitialization方法。这通常是**创建动态代理对象的时机(AOP)**,因为此时Bean已经完全初始化并准备使用,而且代理对象可以在这一阶段被创建并替换掉原始的Bean实例。
4)代理对象的创建:
- 在postProcessAfterInitialization方法中,Spring会检查该Bean是否需要被代理(通常基于是否存在对应的Advisor或Aspect、注解)。
- 如果需要,Spring会根据Bean的类型(是否实现了接口)选择合适的代理方式(JDK动态代理或CGLIB代理)来创建代理对象。
- 代理对象会封装原始Bean,并在方法调用时插入增强的逻辑(如前置通知、后置通知等)。
5)Bean的交付:最后,将创建好的代理对象(如果需要的话)或原始Bean实例交付给客户端使用。
因此,Spring AOP的动态代理对象主要是在Bean的初始化后的postProcessAfterInitialization阶段创建的。这一过程确保了代理对象能够封装并增强原始Bean的方法调用,同时保持了Bean的生命周期和依赖注入的完整性。
Spring在完成对象的实例化之前,都会将对象代表的函数式接口放入自身的三级缓存,三级缓存本质就是一个map结构。
spring中autowired注入自己的代理后,最后容器中的对象是原对象还是代理对象_autowired 注入自己-CSDN博客
autowired注入自己的代理后,最后容器中的对象只有一个,而且是代理对象。
Spring 事务
事务用于业务层或者Dao层
【Spring事务三千问】Spring的事务管理与MyBatis事务管理结合的原理_spring transaction和mybatis的整合 原理-CSDN博客
【spring源码深度解析】:spring是如何利用@Transactional注解实现数据库事务的?把握住事务的基本用法你就懂了 - 知乎 (zhihu.com)
事务管理整合
图解Java JDBC和JPA的区别 - 快乐随行 - 博客园 (cnblogs.com)
MySQL
- InnoDB存储引擎支持事务(SQL语句)
原生 JDBC
- 注册驱动,
- 获取Connection,
- 建立Statement执行SQL语句,Connection可以管理事务(本质是执行SQL语句)
DataSource数据源
- 主要用来获取并管理,调度Connection
原生 MyBatis (ORM)
可以调用外部数据源获取Connection,也可以使用原生JDBC来获取,最终这些Connection可以呗SqlSession获取到。
SqlSession同样能管理事务,底层是基于Transaction(也是mybatis的一个类)
1 | SqlSession session = sqlSessionFactory.openSession(); |
Spring联合MyBatis事务管理
SpringManagedTransaction
打通了 MyBatis 的事务管理、连接管理 和 spring-tx 的 事务管理、连接管理,使得 MyBatis 与 Spring 可以使用统一的方式来管理连接的生命周期 和 事务处理。
原生的MyBatis 使用的是JdbcTransaction实现类
在一个非
@Transactional
标记的方法中执行 sql 命令,则事务的管理会通过SpringManagedTransaction
来执行。在一个
@Transactional
标记的事务方法中执行 sql 命令,则SpringManagedTransaction
的commit()/rollback()
方法不会执行任何动作,而事务的管理会走 Spring AOP 事务管理,即通过org.springframework.transaction.interceptor.TransactionInterceptor
来进行拦截处理。
- SqlSessionInterceptor 保证了 MyBatis 的 SqlSession 在执行 sql 时使用的连接与 Spring 事务管理操作使用的连接是同一个连接。具体就是通过 Spring 的事务同步器
TransactionSynchronizationManager
来保证的。 - SpringManagedTransaction 中连接的获取是从 Spring 管理的 DataSource 中获取的,这样,数据库连接池也就和 spring 整合在一起了。
多线程事务
Connection
简单地来说,建立Connection
连接,会消耗数据库系统的如下资源:
资源 | 说明 |
---|---|
线程数 | 线程越多,线程的上下文切换会越频繁,会影响其处理能力 |
创建Connection的开销 | 由于Connection负责和数据库之间的通信,在创建环节会做大量的初始化 ,创建过程所需时间和内存资源上都有一定的开销 |
内存资源 | 为了维护Connection对象会消耗一定的内存 |
锁占用 | 在高并发模式下,不同的Connection可能会操作相同的表数据,就会存在锁的情况,数据库为了维护这种锁会有不少的内存开销 |
事务的执行依赖于JDBC-connection,connection的建立基于tcp连接,需要耗费很多资源,所以在多线程并发的情况下,connection数目远少于thread数,需要尽可能考虑connection的共用和复用。
connection可以显式开启和关闭事务,遵循事务的ACID原则,因此虽然共用connection,但是同一时间同一connection只能有同一个事务正在执行,也就是串行执行,否则会造成事务紊乱。
一个最佳实践:当线程需要做数据库操作时,才会真正请求获取JDBC数据库连接,线程使用完了之后,立即释放,被释放的JDBC数据库连接等待下次分配使用
最简单的方式就是把事务执行的代码块用connection锁对象锁住:事务执行完以后释放(但不销毁)
1 | synchronized(connection){ |
线程如何获取锁对象?为了保证一个线程所有dao操作都是用的同一个connection,使用threadLocal存放属于线程自己的connection,如果是直接从连接池获得的话,多个 DAO 就用到了多个Connection,不能完成一个事务。而连接池负责提供缓存和提供connection
连接池
《深入理解mybatis原理》 Mybatis数据源与连接池_mybatis 连接池-CSDN博客
原生的JDBC会让connection的close()方法执行数据库连接的释放与销毁,为了保证不更改原生的功能,我们可以使用代理对象,让其close方法不会真正执行,而是回收到数据库连接池中。
使用数据库连接池,通常都是得到一个javax.sql.DataSource[接口]的实例对象,它里面包含了Connection,并且数据库连接池工具类(比如C3P0、JNDI、DBCP等)重新定义了getConnection、closeConnection等方法,所以每次得到的Connection,几乎都不是新建立的连接(而是已经建立好并放到缓存里面的连接),调用closeConnection方法,也不是真正的关闭连接(一般都是起到一个标识作用,标识当前连接已经使用完毕,归还给连接池,让这个连接处于待分配状态)【PS:所以说:使用数据库连接池时,还是要显式的调用数据库连接池API提供的关闭连接的方法】。
至于为什么要用ThreadLocal呢?这个和连接池无关,我认为更多的是和程序本身相关,为了更清楚的说明,我举个例子
1 | servlet中获取一个连接.首先,servlet是线程安全的吗? |
ThreadLocal?
就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突
开启步骤
业务层**接口**为业务方法打开事务@Transactional
@Transactional是方法级别或者类级别的注解,可以开在整个接口上,也可以开在单个方法上
接口能够提高复用性,降低耦合
JdbcConfig创建事务管理器Bean@Bean
PlatformTransactionManager是Spring规定的,DataSourceTransactionManager可以动,根据具体的技术选择
要注意,事务管理器的datasource和mybatis用的datasource必须是同一个,不然
打开事务@EnableTransactionManagement
事务角色
事务配置
有些异常不会触发回滚,需要手动设置一下rollbackFor
追加日志
try finally结构,finally 记日志功能必定触发
事务传播行为控制
(1) java - Spring事务传播行为详解 - 个人文章 - SegmentFault 思否
transfer、AccountDao中所有的数据层方法、日志记录的业务方法都加了Transacitonal注解。
- transfer作为方法的调用者,是事务的管理员。
- 其他作为被调用者,是事务的协调员。
- 如果默认设置Required,管理员开事务,协调员都会加入
@Transactional(propagation = Propagation.REQUIRES_NEW)
开启事务:@Transactional
管理员肯定要开启事务,管理员默认是REQUIRED
一般不用改,某一个协调员要单开另外一个事务,那么就可以把这个协调员的事务传播机制改成REQUIRES_NEW
Propagation.REQUIRED
外围方法未开启事务的情况下Propagation.REQUIRED
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
在外围方法开启事务的情况下Propagation.REQUIRED
修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED
修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
Propagation.REQUIRES_NEW
用途:某段逻辑必须独立提交或回滚(如记录日志、发操作记录),避免日志失败导致主逻辑失败)
外围方法未开启事务的情况下Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
在外围方法开启事务的情况下Propagation.REQUIRES_NEW
修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。 并且使用不同的 Connection 连接。
事务失效
spring 事务失效的 12 种场景_spring 截获duplicatekeyexception 不抛异常-CSDN博客
- 访问权限,private,default无法生效
- 方法用final或static修饰,代理对象无法重写
- 多线程调用事务方法,两个线程获取的不是同一个连接
- 数据库或表不支持事务(MySQL的MyISAM不支持事务)
- 未开启事务或未将类纳入Spring管理
@Transactional
@Service
方法自调用
1 |
|
我们看到在事务方法 add 中,直接调用事务方法 updateStatus。从前面介绍的内容可以知道,updateStatus 方法拥有事务的能力是因为 spring aop 生成代理对象proxy,但是这种方法直接调用了 this 对象的方法,所以 updateStatus 方法不会生成事务。
由此可见,在同一个类中的方法直接内部调用,会导致事务失效。
子线程为什么不能共享父线程的事务
事务:JDBC connection(连接) MyBatis sqlSessionFactory(会话)这些都是一个意思。Spring 在将其整合后,会绑定到每个线程私有的 ThreadLocal 中。
连接池中的每个
Connection
对应不同的 TCP 四元组,都是一个独立的物理连接,物理上不同(不同的socket、不同的会话)。数据库事务是“会话级别”的,绑定在某一条连接上,每条连接有独立的事务上下文、会话状态、隔离级别,不同连接之间的事务、锁、变量 互不干扰。
连接池除了管理 Connection 对象本身,还会:记录该连接是否存活、检查该连接是否超时、检查连接是否被污染(如事务未提交、连接状态被改)、所以每条 Connection 在连接池中不仅仅是个“Connection对象”,还包含一些“元数据”状态。
为了支持并发访问,一个线程用一条连接,多个线程可以并发请求数据库(否则一条连接只能排队等待)。
在Spring框架中,
@Transactional
的事务管理默认是基于线程绑定的(使用ThreadLocal
存储事务上下文)。
1 | spring: |
子线程访问自己的ThreadLocal,发现没有存父线程的连接 → 所以子线程会重新去连接池获取一个新的连接:
- 保证多线程访问数据库时线程间互不干扰
- 保证每个线程内的事务隔离
- 防止一个线程操作数据库时,另一个线程“偷用”这个连接引发混乱(如事务状态、关闭连接)
1 |
|
- 如果像这样,新的线程有自己的 ThreadLocal,必须自己再新拿一条连接来操作数据。
总结:不要在事务里面开启子线程,应该是在子线程里开启事务。
Spring MVC
入门案例
Java实现MVC模型的web框架,灵活性强,主要进行表现层开发 Controller。
bean创建@Controller
方法级别注解 请求映射 @RequestMapping
方法级别注解 设置响应 @ResponseBody
都是方法级别注解
创建SpringMvcConfig@Configuration@ComponentScan
扫描到controller
创建servlet容器Config
ServletContainerInitConfig继承AbstractDispatcherServeltInitializer类重写对应方法。都是一次性
getServletMappings
表示接管URL的那个部分的映射createRootApplicationContext
创建Spring Framework容器并指定配置createServletApplicationContext
创建web容器并指定配置- web容器 servlet容器
工作流程
bean加载@ComponentScan.Filter
Spring避免加载springmvc的controller
导包:mybatis自动代理会返回dao接口的实现对象,可以不写,但是其他技术不一定是这样,所以为了通用性还是应该导dao
SpringConfig扫描排除含有Controller注解的类:Filter 可以更细粒度地加载bean
exclude排除
加了configuration的类,spring都会将其作为配置类,里面如果有componentScan,就会扫上。
创建容器设定配置再去返回容器,简化过程只需要指定类的字节码即可
Controller
Request
请求映射路径@RequestMapping
对于不同的controller可能会优相同方法,这时会有冲突问题,解决办法是在controller**类上加一个@RequestMapping,要和方法**的@RequestMapping注解结合一下。
名称匹配 指定请求参数名@RequestParam
用于接收GET请求中URL的查询参数,也可以接收POST请求的参数(表单)
You can use the
@RequestParam
annotation to bind Servlet request parameters (that is, query parameters or form data) to a method argument in a controller.
与mybatis类似,对于外部传进来的请求参数用map封装,因此**@RequestParam接收的是key-value形式的参数,发送get请求**只会处理URL中的参数,忽略请求体中的数据
发送post请求时,表单数据在请求体中,不过仍然是username=root这样键值对的形式存在,如果URL里有请求参数,服务端收到以后会一并加到map中,打印出来,即使方法里的参数只是一个String也能打印出来
@RequestParam XXX xxx 表示查询参数用XXX类型接
SpringMvc--@RequestBody和@RequestParam注解以及不加注解接收参数的区别_不写接收参数的注解,默认使用什么的-CSDN博客
解决mybatis不加@Param报错 org.apache.ibatis.binding.BindingException_51CTO博客_mybatis @Param
记一次SpringMVC碰到的坑 - zeng1994 - 博客园 (cnblogs.com)
关于SpringMvc使用时,不加@RequestParam注解,根据方法形参名也可以获取请求值的分析_spring 请求体不写注解-CSDN博客
和 MyBatis 一样,使用反射机制获取参数名称,JDK8以后 java.lang.reflect.Parameter 中能够获取参数相关信息,框架就是利用这个机制,不加RequestParam获取参数信息。不然就只有arg0 arg1这种形式。
- required参数 是否为必传参数,默认必传
- defaultValue 参数默认值
传递多种类型的请求参数
与mybatis类似
pojo:直接使用pojo内部的字段名称
嵌套pojo:address.字段名称
数组:直接接收字符串数组即可,请求的参数名就是数组的形参名,会把数组当成一个独立的参数
集合类型:加RequestParam注解。 因为集合属于引用类型,spring会把它当成pojo处理(造pojo然后根据字段名注入依赖),不加param注解,spring就不会像数组一样把他当成一个独立的参数。
传JSON @EnableWebMvc
导坐标 webMvc
参数在请求体里@RequestBody
@RequestBody XXX xxx 表示请求体中的数据用XXX类型接
RequestBody请求体中的数据通常是以JSON、XML等格式发送的,可以将请求体中的数据自动绑定到指定的Java对象上。
- 参数写在请求体里,用List接收json数组
- 用Pojo类接单个pojo对象json
- 用List
接收多个pojo对象的json,
一个请求,只有一个RequestBody;一个请求,可以有多个RequestParam。
Body vs Param:
日期参数格式化@DateTimeFormat(pattern=”yyyy-MM-dd”)
默认yyyy/MM/dd 其他形式不认识,需要自己手动指明formatPattern
Converter接口-将字符串参数转换成Java类型
请求里面的参数都是以字符串形式发来的,converter要根据形参类型,把字符串转成对应类型提供给方法,这就是为什么前面能把字符串12按照需求转换成int 12
@EnableWebMvc
Response
直接返回:响应的是一个页面
1 |
|
直接在方法里 return “page.jsp” ,Spring默认认为Controller的方法返回的就是一个页面,也就是说会经过渲染步骤。
返回值就是响应体:@ResponseBody
如果直接return一个字符串Spring会认为这个字符串是一个网页,响应分为响应行响应头和响应体,响应头里是放状态码之类的,只能在响应体里返回数据,加@ResponseBody注解表示返回值就是响应体。
1 |
|
响应POJO对象(JSON形式)
1 |
|
直接返回user即可,jackson帮我们做的
响应POJO对象集合(JSON数组)
1 |
|
HttpMessageConverter接口
POJO转JSON字符串
REST
Representational State Transfer
根据请求的方式区分 GET POST PUT DELETE
同一URL,请求方式不同,调用的方法也不同
RESTful:用REST风格访问资源
@RequestMapping 加 method 参数
@RequestMapping(value = "/users/{id}" ,method = RequestMethod.DELETE)
URL占位符传参 @PathVariable
value=”/users/{id}” URL中的{id}和用@PathVariable修饰的方法参数id是一致的
@RequestBody@RequestParam@PathVariable
RESTful 快速开发
类级别注解 @RequestMapping
省去所有user前缀,写一次就好。
类级别注解 @RestController
类级别的@RequestBody,表示所有类的返回值都是请求体的数据,既然@Controller和RequestBody都要写,合而为一即可。
方法级别注解 @PostMapping
1 |
RESTful 页面交互案例
RestController PostMapping GetMapping DeleteMapping PutMapping
方法参数RequestBody
Config 类 SpringMvcSupport 放行静态页面访问
@Configuration
默认SpringMvcConfig接管/后所有东西,通过ResourceHandlerRegistry 设置SpringMVC如何处理对静态资源的访问
前端ajax
SSM整合
创建工程
Config
SpringConfig
Configuration
ComponentScan
Import
MyBatisConfig & JdbcConfig
JdbcConfig:数据源 DataSource Bean
MyBatisConfig:sqlSessionFactoryBean
Bean
jdbc.properties
SpringMvcConfig
Configuration
ComponentScan
EnableWebMvc
ServletConfig
rootApplicationContext和webApplicationContext
编写后端模块
Domain
实体类,User
Dao
MyBatis Mapper自动代理,写接口,写方法结合注解
占位符对应参数的名称,这里映射实体类中的字段信息
Service
BookDao@Autowired
dao接口加repository注解(可加可不加)
RestController
参数在url中:PathVariable
参数在请求体:RequestBody
JUnit 测试 Service
Postman 测试 Controller
Spring 事务激活
JdbcConfig
里 加PlatformTransactionManager Bean, 接dataSource参数
Service
接口添加@Transactional
前后端联调
表现层数据封装模型 - 设置统一的返回结果集Result
实际开发过程中前后端约定
1 | public class Result{ |
Result.data
业务方法不同返回数据格式也不同,可能是true false这样的text,也可能是json数据,还可能是json数组,约定将数据封装到data字段中
Result.code
不同业务方法可能会返回相同的内容,返回一个true可能对应新增,修改,删除的业务方法,加一个识别码code字段区分 ,可以约定尾数是0表示失败,尾数是1表示成功:
1 | public class Code { |
Result.message
一些业务方法,本来应该返回json,没查到只能返回null,不能直接把null展示给用户看,展示的是message信息
Controller 返回值统一设定为 Result
将返回值封装到Result中,data
1 |
|
异常处理器@RestControllerAdvice
类级别注解
后端抛出的异常如果不处理,就会抛到前端页面,不美观,并且不会返回任何数据,导致数据不统一
要让WebMvcConfig扫到这个Advice类
常见异常诱因
- 框架内部抛出的异常:因使用不合规导致
- 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
- 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
- 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
- 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
处理方法:全部抛到表现层Controller —— AOP 编程,用最少量的代码实现最强大的功能,快速统一地处理异常
方法级别注解 处理具体类别的异常@ExceptionHandler
处理异常返回的结果也要封装成Result
项目异常处理方案 (捕获异常并返回Result)
异常分类
- 业务异常 BusinessException 可预期
- 发送对应消息,提醒用户规范操作
- 系统异常 SystemException
- 发送固定消息,安抚用户
- 发送特定消息给运维,提醒维护
- 记录日志
- 其他异常 Exception
- 发送固定消息,安抚用户
- 发送特定消息给开发,提醒维护(纳入预期范围)
- 记录日志
自定义异常
继承RuntimeException
加一个code属性(getter setter),重写RuntimeException的方法。异常构造的时候需要用到这些构造器,包装返回数据要用到code和message
建议放在源根的exception包下面
异常代码扩充
触发异常
拦截并处理异常 返回Result
在RestControllerAdvice下方的类中处理对应类型的异常,将异常继续封装成Result返回
放行静态资源配置
SpringMvcSupport(Config类)
加Configuration注解,继承WebMvcConfigurationSupport类,重写resourceHandler方法
配置类
ServletContainersInitializerConfig (Servlet容器配置类)
用来构建ServletContext,继承自 AbstractDispatcherServletInitializer,Spring MVC是建立在 DispatcherServlet 基础之上的,每一个请求最先访问的都是它,负责转发每一个Request请求,所以是必不可少的。
先以 AbstractDispatcherServletInitializer 为例介绍这个加载Config类的职责 具体介绍在下方
a. createRootApplicationContext
需要加载Spring IoC容器的配置类(SpringConfig),返回配置好的 Root WAC
b.createServletApplicationContext
需要加载WebMvc容器的配置类(SpringMvcConfig) 返回配置好的 Servlet WAC
c.getServletMappings
配置由此DispatcherServlet接管的URL映射路径
SpringConfig(@Configuration)
对应applicationContext.xml,配置 Root WAC
applicationContext.xml及spring-servlet.xml详解 - 长木木弓 - 博客园 (cnblogs.com)
注解开发用来替代传统的XML配置文件,因此可以透过xml与注解的映射关系来了解,@Configuration
用来替代<beans>
</beans>
在应用启动时,Spring 会自动扫描并加载所有带有 @Configuration
注解的类,根据@ComponentScan
扫描要加入的@Component
(代替<bean>
</bean>
),最终创建出对应的容器(Context)
@Import({MyBatisConfig.class,JdbcConfig.class})
MyBatisConfig
和 JdbcConfig
中方法级别注解@Bean
用来替代 <bean>
</bean>
标签(表示方法返回的对象当成Bean/Component交给Spring容器管理)虽然没有加Configuration
注解,但是会由SpringConfig
@Import
,导入的还是SpringConfig配置的容器,属于 Root WAC
SpringMvcConfig(@Configuration @EnableWebMvc)
对应spring-webmvc.xml,配置 Servlet WAC
@EnableWebMvc (Spring Framework 6.1.14 API)
WebMvcConfigurationSupport (Spring Framework 6.1.14 API) (WMCS)
@EnableWebMvc
会通过导入WMCS
完成 Spring MVC 默认配置的添加,只有一个类能拥有此注解不写
@EnableWebMvc
直接继承 WMCS 也可以实现相同效果自定义具体某项WebMvc配置:
extends WebMvcConfigurer
允许多个WebMvcConfigurer存在,但是具有一定侵入性
- 主要Config:
@EnableWebMvc
+ 继承WebMvcConfigurer
重写方法 +@ComponentScan
其他Config类 - 次要Config:继承
WebMvcConfigurer
重写方法+@Configuration确保被主要Config扫描到 - 没有暴露高级设置,如果需要高级设置 需要第二种方式直接继承 WMCS 来做更高级别的配置,此时要移除@Enable注解
SpringMvcSupport(@Configuration)
对应springMvcContext.xml,配置 Servlet WAC
- 继承WMCS,完成对SpringMVC的默认配置,重写resourceHandler方法,实现在默认配置基础上的自定义。
- 案例中的SMS是配置类,用于配置容器,被SMC扫config包扫到了,此时SMS这里的自定义配置会覆盖SMC的Enable注解
前端逻辑
查询: get请求
保存/添加:post请求
新增要弹出表单,添加成功要关闭表单并清空表单数据,不论成功与否finallyGetAll回显 ,按照识别码判别成功与否
修改:put请求
删除:delete请求
拦截器
Interceptor
Filter
filter在一定是在访问 servlet 之前,interceptor只能在servlet中, before Controller
功能类
控制表现层:controller下新建interceptor包,新建一个Interceptor类 extends HandlerInterceptor
注意preHandle返回值和@Component
SpringMvcSupport
addInterceptors 自动注入自定义拦截器
addPathPatterns 加的不是前缀,是严格的URL匹配,配/books就拦截对/books发的请求,/books/100就拦截不了
preHandle,yourService,postHandle,afterCompletion 顺序
示意图
简化开发-WebMvcConfigurer
已经和Spring接口绑定,侵入性强。
拦截方法
boolean preHandle(req,resp,handler)
req和resp是servlet的响应和请求,handler实际上是 HandlerMethod,通过 getMethod 能拿到执行的业务方法的对象(反射)
void postHandle(req,resp,handler,modelAndView)
渲染页面之前调用
void afterCompletion(req,resp,handler,exception)
能拿到原始业务方法执行过程中的异常
拦截链顺序
拦截顺序,和注册顺序有关系
如果某个pre返回false,post全部跳过,倒序执行,从最近一个pre返回true的拦截器开始执行afterCompletion
万字详解 GoF 23 种设计模式(多图、思维导图、模式对比),让你一文全面理解-CSDN博客
Spring MVC 启动流程
WebApplicationContext
WebApplicationContext(WAC)
ApplicationContext(AC) 表示 ioc 容器。WAC是普通AC的扩展,它具有Web应用程序所需的一些额外功能,比如可以获取 ServletContext并修改
1 | public interface WebApplicationContext extends ApplicationContext { |
Root WebApplicationContext(applicationContext.xml)
Root WAC
在应用启动时首先被加载,并且作为父上下文,供表示层使用,主要负责管理服务层(Service)、数据访问层(DAO)、中间件配置等非 Web 层(表示层)的 Bean
Servlet WebApplicationContext(spring-mvc.xml)
Servlet WAC
是Root WAC
的子上下文,专门用于处理表示层的 Bean 和配置。比如控制器(Controller
)、视图解析器、拦截器(Interceptor
)等- 每个
DispatcherServlet
实例会有一个独立的Servlet WAC
Parent & Child ApplicatitonContext
Root WAC 作为 所有 Servlet WAC 的 Parent,DispatherServlet在创建属于自己的ServletContext的getAttribute方法来判断是否存在Root WebApplicationContext。如果存在,则将其设置为自己的parent。这就是父子上下文(父子容器)的概念,getParentBeanFactory。
对于作用范围而言,在DispatcherServlet中可以引用由ContextLoaderListener所创建的RootWAC中的内容,而反过来不行。当Spring在执行ApplicationContext的getBean时,如果在自己context中找不到对应的bean,则会在父ApplicationContext中去找。这也解释了为什么我们可以在DispatcherServlet中获取到RootWAC中的bean。
ServletContext(web.xml)
作用
- 存储 Web 应用的全局参数(如 web.xml 中的
<context-param>
) - 提供对 Web 应用资源(如文件、配置)的访问
- 作为应用范围内的共享数据存储(通过
setAttribute()
和getAttribute()
) - 生命周期与 Web 应用一致,从服务器启动到停止
注意事项
Servlet容器(Tomcat)在启动一个web应用时,根据web.xml 会为整个应用创建一个唯一的ServletContext(SC)对象,应用内部所有的Servlet共享同一个SC。
ServletContext是Servlet与Servlet容器(Tomcat)之间直接通信的接口。
容器中的Servlet可以通过它来访问容器中的各种资源
ServletContext跟XML一样,由Attributes组成,要访问资源就要通过字符串name访问,可以通过
void setAttribute(name, object)
来将ServletContext与你的object绑定,Object getAttribute(name)
可以得到objectEnumeration<String> getInitParameterNames()
获取所有<context-param/>
参数的名称 字符串枚举String getInitParameter(name)
根据name获取指定的<context-param/>
参数值
Root WAC, Servlet WAC, ServletContext之间的关系
Root WebApplicationContext存储key为
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
,可以通过此Key访问Root WAC。WebApplicationContextUtis工具类提供了从ServletContext获取RootWAC的方法:
WebApplicationContextUtils.getWebApplicationContext(ServletContext sc)
WAC提供了获取ServletContext的抽象方法
getServletContext()
web.xml 配置 ServletContext
Tomcat创建web应用时,会构建ServletContext对象,根据web.xml中的配置把如下参数都存到ServletContext对象中,注册Listener,Servlet等
1 |
|
ContextLoaderListener - 创建 Root WAC
本质就是一个Listener,因此需要在web.xml中注册
实现了ServletContextListener接口,EventListener->ServletContextListener
继承了ContextLoader类,见名知意,是用来加载WAC的,有一个WAC参数context,所有方法都是围绕加工这个context字段进行的
WebApplicationContext initWAC(ServletContext sc)
ContextLoaer 接收一个ServletContext参数sc,调用initWAC方法返回加载好的WAC对象this.context
打印在服务器日志上 servletContext.log("Initializing Spring root WebApplicationContext");
中间调用createWAC(ServletContext sc)
返回一个ConfigurableWAC对象,将其parentContext设置为sc,
再把这个CWAC和sc传给configureAndRefreshWAC(CWAC cwac,ServletContext sc)
方法进行配置(ServletContext是根据web.xml构建的,根据key: contextConfigLocation找到RootWAC的配置文件applicationContext.xml
)
之后在sc中创建一个Attribute,使得能通过ServletContext对这个Root WAC进行访问(键值对形式)
setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
, this.context)
最终返回 this.context
作为 Root WAC
WebApplicationContext contextInitialized(ServletContextEvent sce)
ContextLoaderListener 能监听Web应用启动或关闭的事件(会修改ServletContext中的参数),触发contextInitializaed/contextDestroyed,创建或销毁Root WAC。
DispatcherServlet - 创建 Servlet WAC
本质——Servlet
Spring MVC 的核心前端控制器,用于处理所有进入的 HTTP 请求。将请求分发给适当的处理器(控制器 Controller),并在处理后将响应返回给客户端。
每一个
DispatcherServlet
都拥有自己的 Servlet WebApplicationContext,管理与 Web 层(表现层)相关的 Bean,如控制器Controller、视图解析器ViewResolver、拦截器Interceptor等。本质就是一个Servlet,在web.xml中注册,继承链
HttpServlet->HttpServletBean->FrameworkServlet
HttpServletBean有一个final
init()
[Servlet的入口方法] 其中会调用抽象方法initServletBean()
FrameServlet实现了initServletBean(): [生成Servlet WAC,设置parent和ServletContext] 最后会调用initStrategies
1 | ServletContext var10000 = this.getServletContext(); |
- 同样的,途中也会调用自己的initWAC方法:
- 调用WACUtils工具类,获得自己所在的ServletContext的 Root WAC
- 将自己的WAC转换成ConfigurableWAC,如果存在 RootWAC,则将其设置为自己的parent
- 然后configureAndRefreshWAC(cwac):设置ServletContext为sc,从其中ServletConfig中获取
<init-param>
参数的值
最后根据自己的ServletConfig获取到ServletContext,根据自己的名称设置自己的Servlet WAC在ServletContext中的Key
DispatcherServlet实现了initStrategies [生成各个功能组件,异常处理器,视图处理,请求映射]
这两个context都是在ServletContext中,属于dispatcherServlet的上下文是servletWAC,找不到的话就去rootWAC中找
Java配置ServletContext——WebApplicationInitializer

替代web.xml
ServletContainerInitializer
之前,web容器(Tomcat)会根据WEB-INF下的web.xml初始化ServletContext
Java EE Servlet 规范定义了这个接口,web容器(Tomcat)启动时根据这个初始化器做一些组件内的初始化工作。
SpringServletContainerInitializer 是Spring 对其的实现,其onStartup方法会调用 WebApplicationInitializer 的onStartup(ServletContext sc)初始化Web应用
典型 SpringMVC 应用启动流程
Tomcat 读取web.xml中
<context-param>
<listener>
然后创建一个全局共享的ServletContext- Tomcat 将
<context-param>
<listener>
转化为键值对,存到ServletContext
- Tomcat 将
Tomcat 加载Listener实例,实施监听,Listener必须实现ServletContextListener接口(比如ContextLoaderListener)
Web项目继续启动,触发Listener中的contextInitialized(ServletContexEvent event),根据ServletContext中
<context-param>
部分创建根容器,configClass是类的形式,configLocation是xml配置文件(如applicationContext.xml
)。将根上下文绑定到 ServletContext(键为ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
)创建完父容器,如果有
<filter>
会创建filter,然后读取<servlet>
用于注册DispatcherServlet(这块流程建议从init方法一步步往下看,流程还是很清晰的),因为DispatcherServlet实质是一个Servlet,所以会先执行它的init方法。这个init()方法在HttpServletBean这个类中实现,其主要工作是做一些初始化工作,将我们在web.xml中配置的参数设置到ServletContext的ServletConfig中,然后再触发FrameworkServlet的initServletBean()方法;- FrameworkServlet主要作用是初始化Spring子容器,设置其父容器,并将其放入ServletContext中;
- FrameworkServlet在调用initServletBean()的过程中同时会触发DispatcherServlet的
onRefresh()
方法,这个方法会初始化Spring MVC的各个功能组件。比如异常处理器、视图处理器、拦截器、请求映射处理HandlerMapping等
XML-free startup
用Java类的形式配置ServletContext,有一些细微差异,Spring这边实现了ServletContainerInitializer接口,注册组件的工作就交给了WebApplicationInitializer:
先根据指定的rootWacConfig配置类(SpringConfig)创建出父容器,父容器作为参数进行Listener的有参构造,最后以addListener的方式注册到ServletContext中。
Spring MVC 执行流程
核心组件
组件名称 | 核心职责 | 常见实现类 |
---|---|---|
DispatcherServlet | 前端控制器,协调所有组件处理请求 | org.springframework.web.servlet.DispatcherServlet |
HandlerMapping | 映射 URL 到处理器 | RequestMappingHandlerMapping、SimpleUrlHandlerMapping |
HandlerAdapter | 适配处理器并执行 | RequestMappingHandlerAdapter、SimpleControllerHandlerAdapter |
ViewResolver | 解析逻辑视图名到物理视图 | InternalResourceViewResolver、ThymeleafViewResolver |
Interceptor | 拦截请求并在不同阶段处理(预处理、后处理、完成处理) | HandlerInterceptor 接口实现类 |
View | 渲染模型数据为响应内容 | JSPView、ThymeleafView、JSONView |
doDispatch()
SpringMVC 的执行流程本质是 **“组件协作式” 的请求处理模式 **,通过 DispatcherServlet 作为中枢,核心方法是DispatcherServlet#doDispatch
方法,串联 HandlerMapping、HandlerAdapter、ViewResolver 等组件,实现从请求接收到响应的自动化处理。理解此流程的核心在于掌握各组件的职责及数据流转路径,这也是排查 MVC 相关问题(如请求 404、参数绑定失败)的关键思路。
步骤 1:DispatcherServlet 接收请求
- 所有请求通过 URL 映射到 DispatcherServlet(如
/api/*
),由其统一调度。
**步骤 2:HandlerMapping 获取 HandlerExecutionChain 和相应的 HandlerAdaptor **
- 常见实现类:
RequestMappingHandlerMapping
(基于注解映射)、SimpleUrlHandlerMapping
(基于 URL 路径映射)。请求/api/user/list
会匹配到@RequestMapping("/user/list")
标注的 Controller 方法。
DispatcherServlet#doDispatch(HttpServletRequest request, HttpServletResponse response)
DispatcherServlet
接收请求后,调用HandlerMapping
,将 Controller(handler) 和拦截器包装到HandlerExecutionChain
。
DispatcherServlet#getHandlerAdapter(Object handler)
获取真正的业务handler,可能是注解形式的Controller方法。
1 |
|
Spring 在运行时会把这个方法封装为 HandlerMethod
对象,这个对象包含控制器实例(即 MyController
)、方法对象(即 hello()
)、方法参数等元信息。handler instanceof HandlerMethod == true
除此之外可能会实现旧版的 Controller 接口,返回的是 ModelAndView。不知道是哪种,所以就会从 HandlerAdaptors 中找到一个支持 HandlerMethod 的 Adaptor。
步骤 3:拦截器预处理器
HandlerExecutionChain#applyPreHandle(req,resp)
- 预处理(preHandle):按注册顺序依次调用拦截器的
preHandle()
方法。在处理器执行前拦截请求,可用于权限校验、日志记录。可以实现 WebMvcConfigurer 的addInterceptors(InterceptorRegistry registry)
注册拦截器并设定拦截路径。
步骤 4:HandlerAdapter 执行处理器
HandlerAdaptor#handle(req,resp,handler)
返回值 modelandview
- 常见实现类:
RequestMappingHandlerAdapter
(处理注解控制器)、SimpleControllerHandlerAdapter
(处理传统控制器)。 - 职责:所有拦截器的
preHandle()
都返回true
时,通过HandlerAdapter
的handle()
方法执行 Handler。 - 实现类有参数解析器和和结果处理器。
- REST 风格的调用栈,从外到内:
AbstractHandlerMethodAdapter#handle()
RequestMappingHandlerAdapter#handleInternal()
RequestMappingHandlerAdapter#invokeHandlerMethod()
将 handlerMethod 包装成 invocableServletInvocableHandlerMethod#invokeAndHandle()
通过反射调用原始方法。HandlerMethodReturnValueHandler#handleReturnValue()
结果处理器。- REST:通过
mavContainer
告诉框架已经写完 response,不需要渲染。比如RequestResponseBodyMethodProcessor
这样的实现类,里面的writeWithMessageConverters()
把返回值通过xxxxMessageConverter
转化成 JSON/XML,将 JSON 直接写入HttpServletResponse
输出流。之后会返回 mv 为 null。
步骤 5:拦截器后处理器
HandlerExecutionChain#applyPostHandle(req,resp,mv)
- 后处理(postHandle):在处理器执行之后,逆序调用拦截器的
postHandle()
方法,可用于在渲染之前修改模型数据,或者可以添加响应头(CORS、token…)。
步骤 6:结果响应与异常处理(可能有渲染)
DispatcherServlet#processDispatchResult(req, resp, chain, mv, dispatchException)
步骤 6.1:异常处理,processHandlerException
DispatcherServlet 执行 handler(即 controller)期间抛出异常时,会尝试通过一组 HandlerExceptionResolver
进行异常处理,如下为REST处理异常的一个示例。
1 |
|
如果是
@ResponseBody
或@RestController
,ModelAndView
为 null,不会走渲染逻辑。(步骤 6.2:处理器返回的 ModelAndView)
- 处理器方法返回值会被封装为 ModelAndView:
- 模型(Model):存储数据(如
model.addAttribute("users", userList)
)。- 视图(View):逻辑名称(如
"user/list"
,由视图解析器转换为物理视图)。(步骤 6.3:ViewResolver 解析视图)
1
2
3
4 <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
- 逻辑视图
"user/list"
会被解析为/WEB-INF/views/user/list.jsp
。(步骤 6.4:视图渲染与响应生成)
- 视图对象(如 JSP)将模型数据渲染为 HTML 内容,写入 HttpServletResponse 的输出流。
步骤 7:拦截器完成处理器
- 所有拦截器的
afterCompletion
方法按照注册的顺序逆序执行,用于渲染后处理。
Spring Boot
入门案例
创建
创建boot模块
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案_spring3不支持jdk8-CSDN博客
SpringBoot2停止维护,SpringBoot3最低Java17
写控制器类
把controller类写好
启动app
快速启动
打包
package
之前 clean
全部设置为UTF-8参数
命令行启动 java -jar
jar执行要有入口类,boot打包需要插件才能生成可执行的入口类
boot 依赖管理
starter 起步依赖
starter-parent 依赖管理
starter-parent:定义了无数jar包的版本管理和依赖管理,减少依赖冲突。只写GA 不写V
dependencies-辅助功能
每一个dependency(以web包为例)把真正需要用到的jar包声明,去找parent要即可
spring-boot-starter-web:
spring-boot-dependencies:
替换starter的某个依赖: exclusion
依赖排除exclusion,换技术
application.yml
resources目录下配置文件加载优先级
properties>yml>yaml
自动提示功能消失—引入配置文件到boot模块中
debug>info>warn
YAML—(YAML Ain’t Markup Language)
YAML 简介
Jamel Camel
语法规则
1 | # 空格数量不限,只要前面格数一样就是同一级,不允许使用tab缩进 |
以Java方式读取yaml
单个数据—成员变量@Value(${enterprise.subject)
自动赋值
全部数据—Environment
定义一个Environment,自动装配,将配置中的属性全部遍历:
environment.getProperty("name")
pojo类映射到yaml @ConfigurationProperties
可以拿到需要的某个属性的信息(prefix),在控制器中定义成员变量自动装配,常用
自定义对象封装数据警告
多环境开发
生产环境设定
独立生产环境设定:
spring.profiles
boot2
spring.config.activate.on-profile
boot3
---
三条横线分割配置
spring.profiles.active
设置激活的环境
带参数启动boot
命令行参数临时修改配置内容
1 | # 修改启动环境为test |
参数加载优先级
maven&boot开发环境兼容—加载配置文件
maven和boot都设置了多环境,但是打包工作是maven负责,所以maven应该占主导
手动配置resources插件,覆盖parent设定
maven-resources-plugin详解 - 红尘过客2022 - 博客园 (cnblogs.com)
<resources>
(<filtering>
)
<resources>
标签其实就是maven-resources-plugin
的<resources>
配置,主要用来配置资源目录的。普通项目没有parent
,默认继承父pom.xml
:
可以看到resources
默认就是项目下的src/main/resources
,但是没开过滤filtering
,所以之前maven课程中,在配置文件中引入占位符还得重写一遍resources标签。
而boot模块默认继承的starter-parent默认的resources
标签是开了过滤的,而且资源明确包括application.yml
这种配置文件,所以即使子项目里不用手动复写resources
也能匹配到占位符:
maven-resources-plugin
(<useDefaultDelimeters>
)
<useDefaultDelimeters>
支持使用${}或@
过滤资源
boot的starter-parent默认对resources-plugin
的配置也做了自定义更改(主要就是<useDefaultDelimeters>
= false)而上文提到的父pom没有,<useDefaultDelimeters>
默认就是true
定义多环境 <profiles>
配置属性替换占位符
@
boot项目的parent为了防止spring占位符被扩展,所以只允许@
为占位符,不解析${}
。如果已经继承starter-parent,直接在配置文件中@xxx@即可。 parent不是starter-parent的解决办法
${}
非要用${},可以使用如下方法覆盖配置:
或者启用插件的<useDefaultDelimeters>
= true
多配置文件加载优先级
包外配置
假设JAR包位于file目录下,file/config/application.yml
> file/application.yml
包内配置
resources/config/application.yml
> resources/application.yml
Java项目目录结构

src/main 就是编译以后的classpath(classes),java是java源代码,resources资源文件,编译完打包都在同一个classes目录下。
src/test 是test-classes,属于测试文件,默认不会参与打包。
依赖放在包内和classes并列的lib目录
对于maven webapp骨架,main还有webapp目录,其中WEB-INF文件夹存放web.xml,打包之后web.xml classes lib并列放在WEB-INF中
- webapp也可存放静态资源,打包之后在JAR包中的第一级
JAR包内部结构
JAR (文件格式) - 维基百科,自由的百科全书 (wikipedia.org)
META-INF 整个项目的元数据,MANIFEST.MF 包含执行时的入口类等信息
BOOT-INF boot项目的jar包中 classes+lib
WEB-INF web项目war包中 classes+lib+web.xml
APK内部结构
APK作为JAR包的变种,也具有相似的结构:
框架整合
JUnit
测试类注解@SpringBootTest
SpringBoot启动类:@SpringBootApplication有加载bean的功能,会扫描当前包同层以及子包中所有的bean,加载bean(包含配置类)
SpringBootTest会自动扫描SpringBootApplication,测试类不在启动类所在包/子包中,需要指定启动类的class文件
SSM
整合 MyBatis
1.启动依赖-MyBatis,MySQL
2.pojo dao @Mapper @MapperScan
mybatis自动代理注解开发返回的对象就是实体类,所以实体类不用配置,
mybatis注解开发中@Mapper注解取代了bookMapper.xml,对mybatis声明这是一个mapper。
spring-mybatis整合中,mybatis生成mapper的代理对象会以FactoryBean交给Spring容器管理,要让mybatis知道mapper在哪里,就要加@Mapper注解
spring-mybatis整合中,不加@Mapper注解,要么配置mapperScannerConfigurer,要么加@MapperScan扫mapper包。
3.application.yml 配置数据源
SSM迁移到SpringBoot
- 配置类全部删除
- Dao加@Mapper
- Controller Service不变
- application.yml 配置端口和数据源
- 静态资源放到resources/static,静态资源可重定向(JS脚本):
访问一个web资源,如果直接访问 localhost:port
一般会请求一个主页index.html,为了能直接从地址访问资源,创建一个index.html,添加一个跳转的js脚本
1 | <script> |
@SpringBootApplication
@SpringBootApplication
是 Spring Boot 的核心注解,它是一个组合注解,整合了以下三个关键注解的功能:
@Configuration
标记当前类为配置类,允许通过@Bean
定义 Spring 容器中的组件。@EnableAutoConfiguration
启用 Spring Boot 的自动配置机制,根据项目依赖(如 JDBC、Web、Redis 等)自动配置 Spring 应用。@ComponentScan
自动扫描当前包及其子包下的组件(如@Controller
、@Service
、@Repository
等),无需手动注册。
特性 | Spring Boot (@SpringBootApplication ) |
传统 Spring |
---|---|---|
配置方式 | 自动配置 + 默认约定 | 手动 XML 或 Java Config |
组件扫描 | 自动(默认包扫描) | 需显式配置 @ComponentScan |
依赖管理 | 通过 Starter 简化 | 手动管理依赖版本 |
用法
1 |
|
- 通常放在项目的主类(含
main
方法)上。 - 启动后会自动初始化 Spring 容器、加载配置、启动内嵌服务器(如 Tomcat)。
配置
排除特定自动配置
1 |
- 例如:项目未使用数据库时,可排除数据源自动配置。
自定义扫描路径 scanBasePackages
1 |
- 默认扫描主类所在包,如需扫描其他包,可通过
scanBasePackages
指定 - 因此,默认情况下,SpringBootApplication修饰的类应该在根目录,确保所有类都能被扫到。
常见问题
- Q:为什么我的
@Component
组件没被扫描到? A:确保组件位于主类的同级或子包下,或通过scanBasePackages
显式指定路径。 - Q:如何查看生效的自动配置? A:启动时添加
--debug
参数,日志会输出所有自动配置的评估结果。
SpringBoot 启动流程
app.run()
new SpringApplication()
- 确认应用类型(SERVLET、NON、响应式)
- 加载
ApplicationContextInitializer
(包括在 spring.factories 自定义的) - 加载
ApplicationListener
- 记录主启动类(main所在类)
run(MyApp.class,args)
1 | SpringApplication.run() // 准备Environment PropertySource |
由此可见 CommandLineRunner
是在容器启动完成以后执行的。可以实现这个接口的 run()
方法来注入参数。
IoC 容器初始化: refresh()
AbstractApplicationContext#refresh()
1 | refresh() // 刷新 IOC 容器 (往里面填充) |
加载配置类(带
@Configuration
、@ComponentScan
、@Import
等注解)扫描、注册阶段
invokeBeanFactoryPostProcessors(beanFactory)
执行beanFactory后处理器- (由
ClassPathBeanDefinitionScanner
完成) - 扫描被
@ComponentScan
指定的包,找到带注解的类(如@Component
、@Service
、@Controller
) - 解析为
BeanDefinition
,使用BeanDefinitionRegistry
将其注册进beanDefinitionMap
还未创建对象。- 修改Bean定义:执行所有
BeanFactoryPostProcessor
的实现类(如PropertySourcesPlaceholderConfigurer
),允许对BeanDefinition
进行修改(例如替换占位符)。 - 提前实例化处理器:注册
BeanPostProcessor
实现类(如AutowiredAnnotationBeanPostProcessor
),这些处理器需在普通Bean之前初始化,以便后续处理其他Bean的创建。 - 初始化消息源以及事件广播器。
- 修改Bean定义:执行所有
- (由
实例化阶段(容器对于单例且非懒加载的 Bean)
AbstractAutowireCapableBeanFactory#doCreateBean()
对每个要使用的 Bean:
(主要是针对懒加载)存在性检查:Scope判断(若为单例则检查到单例缓存)以及循环依赖判断(如果当前正在创建就从单例三级缓存获取原始对象)
实例化:从
BeanDefinitionRegistry
获取BeanDefinition
,包含类名、作用域、初始化方法等元数据。检查是否存在未满足的依赖(如通过@DependsOn
指定的前置依赖,或者@Order
加载顺序)最后实例化对象(通过反射或者工厂方法调用Constructor
创建原始对象)依赖注入:
@Autowired/@Resource
递归调用getBean()
获取依赖bean,通过三级缓存(singletonFactories
、earlySingletonObjects
、singletonObjects
)提前暴露对象引用,解决setter的循环依赖。设置好属性。Aware 接口回调:如果实现了
XXXAware
接口,则通过setXXX
注入容器底层信息。如名称,类加载器等BeanPostProcessor
: 每个bean在构建的过程中,Spring都会遍历所有的BeanPostProcessor
的实现类,调用实现类中的方法,入参为构建好的bean。要实现无感的对bean的处理必须使用BeanPostProcessor
。方法(按照先后顺序) 方法所属 1. Object postProcessBeforeInitialization()
BeanPostProcessor
2. @PostConstruct
标注的方法JSR-250 规定 3. void afterPropertiesSet()
InitializingBean
4. init()
@Bean (initMethod = init)
5. Object postProcessAfterInitialization()
BeanPostProcessor
5 是 AOP 动态代理的关键阶段:Spring 在这里可能会返回代理对象替代原对象
完成容器启动:触发
ContextRefreshedEvent
,通知监听器容器已就绪。此时可以通过getBean()
获取单例 Bean,如果是懒加载或者Scope = prototype
的则会在主动调用getBean()
的时候才实例化。
SpringBoot 自动装配:自动装配框架内部的 Bean
自动装配基于自动装配类,所以要把所有的bean都用bean方法的形式注册到容器中。
包括之前讲的 服务发现、服务注册、代理工厂、BeanPostProcessor、ConfigurationProperties等。
Bean 方法不需要使用Autowired在参数上注解!!!!
介绍
自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。
- 没有 Spring Boot 的时候,我们写一个 RestFul Web 服务,还首先需要自己写 Configuration 配置类,写 Bean 方法。
- 但有了 SpringBoot,只需要引入依赖,启动 SpringBootApplication 即可。
SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的
META-INF/spring.factories
文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。
1
2
3
4
5
6 # SpringBoot 2.x 在 META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.rpc.RpcServerAutoConfiguration
# SpringBoot 3 在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.rpc.RpcServerAutoConfiguration没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,Spring Boot 中,我们直接引入一个 starter 即可。比如你想要在项目中使用 redis 的话,直接在项目中引入对应的 starter 即可。
1
2
3
4 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>引入 starter 之后,我们通过少量注解和一些简单的配置就能使用第三方组件提供的功能了。
原理浅析
机制核心 @EnableAutoConfiguration (@SpringBootApplication 的一部分)
底层通过 @Import(AutoConfigurationImportSelector.class) 加载所有自动配置类(通过 spring.factories 找到) 这个ImportSelector很重要,通过 selectImport
方法扫描获取所有符合条件的类的全限定类名,将这些类注册到 IoC 容器。核心调用路径如下:
selectImport->getAutoConfigurationEntry->getCandidateConfigurations->SpringFactoriesLoader.loadFactoryNames->loadSpringFactories
不光是这个依赖下的META-INF/spring.factories
被读取到,所有 Spring Boot Starter 下的META-INF/spring.factories
都会被读取到。后边会根据条件进行逐层筛选。
示例 创建 starter
自定义 starter | scatteredream’s blog
引入 starter-validation
@Validated注解加到类上,下面这些注解可以用到 字段、参数
@NotBlank @Email @Min(1) @Max(91)
@Pattern(regexp = “^[a-zA-Z0-9]{8,16}$”,message = “用户名只能是长度在8至16” + “之间的包含数字和大小写字母的字符串”)
1 |
|
pojo 类 + @ConfigurationProperties注解,可在 application.yml 中按照前缀配置属性。
1 | rpc.server.app-name=provider-1 |
注解 | 作用 |
---|---|
@ConditionalOnProperty |
属性Property,满足一定的条件才生效 |
@ConditionalOnMissingBean |
只有没有这个类型的 Bean 时才生效 (用户自定义实现了Bean方法,可以替换这个自动装配的) |
@ConditionalOnClass |
类路径下有某个类才生效 |
@ConditionalOnBean |
依赖的 Bean 存在才生效 |
@Primary |
多个同类的 Bean 存在时首选注入 |
1 |
|
例子:添加 spring-boot-starter-data-redis
后,可直接注入 RedisTemplate
。
特征 | Starter 模块 |
---|---|
命名 | 以 -spring-boot-starter 结尾 |
依赖 | 包含 spring-boot-autoconfigure |
自动配置 | 有 @AutoConfiguration 类,并注册到 spring.factories 或 AutoConfiguration.imports |
配置属性 | 包含 @ConfigurationProperties 类 |
功能入口 | 提供开箱即用的 Bean,无需用户手动配置。可通过 application.properties 或 @Bean 覆盖 Starter 的默认配置。 |
实现自动配置类 AutoConfiguration
按照 SpringBoot 版本将配置类的全限定名引入指定路径下。
新建 starter 模块,添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<dependencies>
<!-- 必须依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- 可选:配置注解处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 你的模块核心实现 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>rpc-server-spring-boot</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>