Spring

Spring

Review: Source Codes Analysis

Bean 生命周期

IoC 容器初始化

循环依赖

SpringMVC 启动流程执行流程

SpringBoot 启动流程

SpringBoot 自动装配

Spring IoC

IoC: Inverse of Control 原先调用服务或者DAO的需要自行new出来对象,硬编码,耦合程度高,Spring的Container能够接管对象的创建工作(实际上就是管理Bean) 并且能够根据对象Bean之间的关系进行依赖注入,创建A对象的同时会把B对象创建起来,也就是DI(Dependency Injection)

管理方式:配置文件xml IoC容器的获取:Spring提供接口

把业务接口的实现类交给Spring管理,遇到接口类,Spring就会自动去找Bean中是否有接口的实现类。

image-20241020194848409

BookDao是接口,实现类为BookDaoImpl,Impl交给Spring管理

DI:依赖注入,依赖用方法传参的方式传入

image-20241019114405108

property name 是成员变量的名字

ref 是要引用的bean id/name

IoC 配置

bean 管理

image-20241019222443537

image-20241019115106810

name 别名

ATTRIBUTE

bean name = “s1 s2 s3” alias

ref可以使用name也可以使用id

getBean

image-20241019144037398

scope 作用范围

ATTRIBUTE

Spring默认创建单例bean,scope=”singleton” prototype为多例。

  • 适合复用的才作为bean交给IoC容器管理
    • 表现层,业务层,DAO层,工具层
  • 不适合复用的对象
    • 封装的实体域对象

bean 创建方式

构造方法

无参构造器,如果使用构造器进行依赖注入,则走的是有参构造

使用静态工厂实例化Bean
  • 工厂的静态方法factoryMethod(return new Bean),不造工厂,调用工厂的静态方法造Bean ATTRIBUTE: factory-method

image-20241019160109618

使用工厂实例化Bean
  • 先造工厂bean再调用工厂的实例方法(return newBean) 造bean
FactoryBean 工厂bean
  • 第三方自定义工厂Bean类实现FactoryBean接口,重写方法image-20241019160946566

    • getObject 工厂类的returnNewBean方法
    • getObjectType return Bean.class bean的 字节码
    • isSingleton 单例
  • xml 配置image-20241019161457148

  • 主要用于第三方框架和Spring框架对接,他们创建的对象要配置一些参数,这时就需要一个FactoryBean,工厂bean会提供set对象参数的方法,返回的就是配好参的对象,可以省去手动配参的麻烦

bean 生命周期

Review: 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)之后调用的方法

生命周期示意图

image-20241019163321664

DI 依赖注入

注入给bean的属性赋值

image-20241019222530413

image-20241019163720566

注入多个bean,填写多个property

方式

setter注入
  1. setter 引用其他的bean property ref = 其他bean的名称 ATTRIBUTE
  2. setter 注入基本数据类型和简单值 property value = 值 ATTRIBUTE
  3. property name实际上是根据setter方法 setUserDao 去掉set首字母小写 userDao得到的
  4. 先无参构造创建bean,再用setter注入依赖
构造器注入

针对有参构造器,必须显式声明有参构造器

ATTRIBUTE <constructor-arg name>

  1. 引用其他bean name是构造器形参名,耦合度高,参数先后顺序固定不能变image-20241019173838816

  2. 基本数据类型和简单值image-20241019174107702

  3. 耦合度高解决方案:参数适配

    • <constructor-arg name>改成type,解决参数名的高耦合,但是type相同的参数会混淆

    • 改成index,index表示参数的位置

  4. 直接有参构造创建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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<property name="myArray">
<array>
<value>1</value>
<value>3</value>
<value>5</value>
</array>
</property>

<property name="myList">
<list>
<value>this</value>
<value>that</value>
<value>where</value>
</list>
</property>

<property name="mySet">
<set>
<value>this</value>
<value>that</value>
<value>where</value>
</set>
</property>

<property name="myMap">
<map>
<entry key="A" value="1"/>
<entry key="B" value="2"/>
<entry key="C" value="3"/>
<map>
</property>

<property name="myProperties">
<props>
<prop key="A">1</prop>
<prop key="B">2</prop>
<prop key="C">3</prop>
</props>
</property>

管理第三方Bean

别人写的对象,创建bean,类型是什么?你要配哪些参数?

image-20241019191451764

加载.properties XML Namespace

创建context命名空间

image-20241019192317628

image-20241019220634669

classpath:*.properties 当前模块下所有的配置文件

容器 ctx

创建容器方式

image-20241019221415451

getBean()

image-20241019221407031

image-20241019221914707

image-20241019222158026

image-20241019221938964

立即加载(饿汉),lazy-init 延迟加载 (懒汉)

注解开发

Bean 的定义

定义bean@Component

image-20241019223116345

image-20241019223156078

加上对应的bean的id ,不加就要加载字节码class

纯注解开发@Configuration @ComponentScan

image-20241019224153002

获取ctx: ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class)

默认xml配置文件只给了beans的命名空间,context还得另外自己加,纯注解开发需要定义一个SpringConfig类,常用的配置都有,不用手动去加命名空间 XML out!

和 @Component 的区别是,能进行代理拦截,保证bean是单例的。

Bean 的管理

作用范围 @Scope

image-20241019225307356

生命周期 @PostConstruct @PreDestroy

Review-Spring-Bean-生命周期一览

探究Spring Boot中@PostConstruct注解的使用场景-腾讯云开发者社区-腾讯云 (tencent.com)

Spring 框架中 @PostConstruct 注解详解-腾讯云开发者社区-腾讯云 (tencent.com)

image-20241019225254913

Instantiate(Constructor)> @Autowired > @PostConstruct

依赖注入完成,被显式调用之前

Using @PostConstruct and @PreDestroy :: Spring Framework

DI 自动装配

自动装配@Autowired

在需要注入依赖的一个属性,与配置文件autowire Attribute of Bean不同,注解Autowired不依赖于setter和有参构造器,直接暴力反射访问private属性,创建对象并注入依赖。

image-20241020011345128

按名称匹配@Qualifier

autowired默认按类型装 配,同一类型多个实现,用Qualifier指定具体bean名称,不加Qualifier就按一定规则选择

image-20241020011356460

先名称匹配,再按照类型匹配@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
  1. @Autowired 注解注入依赖容器,单元测试不如setter和构造器方便。
  2. 无法实现不可变性:final 属性不支持 @Autowired 注解。
  3. Autowired默认按照type,如果有两个相同type的bean,就会报错,不如 @Resource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// setter........
@Autowired(required = false)
public void setUserService(UserService userService) {
this.userService = userService;
}

// constructor..........
@Component
public class MyController {

private final UserService userService;

@Autowired
public MyController(UserService userService) {
this.userService = userService;
}
}

简单类型注入依赖@Value

需要注入的属性上 写value

image-20241020011954765

导入配置文件@PropertySource

image-20241020011844984

管理第三方Bean

Config配置类中bean的创建@Bean

与@Component不同 这个是方法级别的注解,方法返回的对象将由Spring容器管理

image-20241020012511399

@Bean声明的Bean名称?

spring boot中通过注解@Bean声明的bean的名称是什么?_springboot 声明bean的名称-CSDN博客

不指定name属性,bean名称为方法名
指定name属性,bean名称为name
导入其他Config类到核心配置 @Import

不建议直接把其他的配置写到SpringConfig里面,分出去然后import

image-20241020012755726

DI 依赖注入

最简单的方法:自己new个对象出来,手动配参数,丢给spring (其实xml就是把手动配参的过程从业务代码中解耦出来)

简单类型依赖注入:成员变量@Value

image-20241020013246624

引用类型依赖注入:方法形参

image-20241020013406409

XML vs. Annotation

image-20241020013703651

整合第三方框架

Spring & MyBatis

使用纯注解方式Spring整合MyBatis_spring整合mybatis基于注解-CSDN博客

Spring整合Mybatis(注解方式完整过程,摒弃MyBatis配置文件)_springboot启动去除mybatis-CSDN博客

image-20241020123544343

image-20241020123756444

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的方式。

image-20241020130218463

factorybean中提供了很多设置E参数的方法,最终返回的是一个设置好参数的E,思想还是一样的,只不过套了一层工厂的皮,封装进去很多固定的参数set方法,一般这个E需要xml进行配置(跟真正的业务代码解耦),工厂Bean就取代了xml,直接给你返回一个配置好的对象。

需要传参就直接在写上方法参数即可,spring自动匹配

image-20241020145504890

MyBatisConfig - MapperScannerConfigurer

image-20241020145526730

DAO没有实现类了,在原始接口上加Component、Repository给ioc容器标识一下,不标也行。

这个mapperScannerConfigurer是mybatis和spring集成的部分,扫描指定mapper所在的包,mapper生成代理对象,通过factoryBean方式交给Spring容器,所以重点不是让spring知道dao的实现类在哪,重点是要让mybatis知道mapper位置

JdbcConfig - 创建DataSource的Bean交给Spring管理

Spring & JUnit

导入spring-test 在Testimage-20241020191443245

在test.java中测试。

@RunWith @ContextConfiguration

@Autowired @Test

image-20241020145826024

需要引用类型参数直接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)
image-20250612222012377

BeanFactoryPostProcessor: BeanFactory 后处理器

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

image-20250612223522046

BeanPostProcessor

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

after: 代理对象

image-20250612224408027

Aware

image-20250612223852103

Bean 生命周期

  1. Bean 实例化(仅为构造出对象)

    • 触发条件:①容器启动 ②首次请求 Bean 时 getBean() 或者依赖注入
    • 方式:通过构造函数或工厂方法创建 Bean 的实例。
    • 异常:若依赖无法解析或构造函数抛出异常,Bean 创建失败。
  2. 属性赋值 Populate Properties

    • 依赖注入:通过 @Autowired@Resource、XML 配置等方式注入属性。
    • 处理 @Value:解析并注入 SpEL 表达式或占位符的值。
  3. Aware 接口回调 与 BeanPostProcessor 前置处理、初始化、后置处理。

  4. 就绪状态

    • Bean 完全初始化,可被应用程序使用。
    • Singleton Bean 会被缓存,后续请求直接获取。
    • Prototype Bean 每次请求创建新实例(无后续销毁步骤)。
  5. Bean 对象销毁回调

    • @PreDestroy JSR-250

    • destroy()-> DisposableBean

    • close() @Bean (destroyMethod = close)

  6. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class A {
private B b;
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}

使用@Lazy注解

@Lazy注解可以延迟Bean的实例化,从而避免循环依赖的问题。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Lazy
public class A {
@Autowired
private B b;
}
@Component
@Lazy
public class B {
@Autowired
private A a;
}

使用 setter 注入

使用setter方法注入也可以解决循环依赖的问题,但要注意可能出现的空指针异常。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class A {
private B b;
@Autowired
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
@Autowired
public void setA(A a) {
this.a = a;
}
}

获取单例对象:三级缓存机制

  1. 优先查询一级缓存(singletonObjects
    • 一级缓存也叫单例池,存储的是完全初始化的单例 Bean(例如已注入所有依赖且完成代理增强的对象)。
    • 作用:直接获取可用 Bean,避免重复创建。
    • 优先级最高:如果找到直接返回,不触发后续缓存查询。
  2. 未找到则查询二级缓存(earlySingletonObjects
    • 二级缓存存储的是已实例化但未完成初始化的 Bean(半成品)。
    • 作用:在循环依赖中临时暴露早期引用(例如 A 依赖 B 时,B 可能正在创建中,需引用 A 的半成品)。
    • 注意:若二级缓存中存在目标 Bean,则直接返回,但此时 Bean 可能尚未完成属性注入或代理。
  3. 最后查询三级缓存(singletonFactories
    • 三级缓存是 beanName 到 对象工厂(ObjectFactory)的映射,对象工厂是个函数式接口,这个接口用于动态生成 Bean 的早期引用或代理对象。
    • 触发条件:仅当一、二级缓存均未找到时,调用工厂生成 Bean 实例,之后将其提升至二级缓存。
    • 关键作用:支持 AOP 代理的延迟生成(例如解决代理对象的循环依赖)。

二级缓存可以解决问题

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 快速从一二级缓存检查已有的单例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 加锁,从三级缓存创建单例对象
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}

流程正式开始——

DefaultListableBeanFactory#preInstantiateSingletons() 用于对解析到的 beanNames 一一进行 getBean(beanName)

AbstractBeanFactory#doGetBean(name···)

  1. getSingleton(name,true) 获取不到再往下走
  2. 类似双亲委派,找 parent,parent 找不到再自行寻找
  3. 自行寻找:(dependsOn) 然后 getSingleton(name, singletonFactory)
  4. getSingleton(name, singletonFactory): 先一级缓存找,找不到就真正开始创建工作
    • 首先将其加到 CreationSet 表明其正在创建。
    • singletonFactory实际上就是一个ObjectFactory,这个函数式接口实现方法通过createBean(name···)获取对象。
    • 创建完成将其从 CreationSet 中移除,保证其只存在于一级缓存单例池中。最后返回创建好的 bean

AbstractAutowireCapableBeanFactory#doCreateBean(name,mbd,args)

  1. createBeanInstance():创建出裸对象,此时未注入依赖。(实际上返回的是 BeanWrapper,有更完善的功能,本质还是对象实例)

    • 当且仅当单例+允许循环依赖+这个bean在 CreationSet 中,才加三级缓存addSingletonFactory,一定要保证一级和二级缓存里面没有,然后把ObjectFactory加到三级缓存里面。所以单例 bean 加入了三级缓存:lambda:getEarlyBeanReference()返回创建的裸/代理对象 singletonObject
  2. populateBean():进行字段、方法注入。做一些 postProcessAfterInstantiation 实例化之后初始化之前的工作。然后就是 autowireByName/Name,本质上就是通过 getBean(name···)获取实例。

    • 依赖注入就是这里遇到的问题,如果代理对象出现循环依赖,那么其生成应该是初始化之后,所以此阶段断然不能提供出代理对象,因此加入三级缓存提前暴露出一个引用,。
  3. initializeBean()

    • 调用 BeanPostProcessor#postProcessBeforeInitialization

    • 调用初始化方法。(PostConstruct-initMethod-afterPropertiesSet)

    • 调用 BeanPostProcessor#postProcessAfterInitialization一般到这里才生成代理对象)。

  4. 完成上述工作之后,如果当前是存在于三级缓存,则调用下方的 getSingleton(name,true) : true 代表允许早期引用(主要解决循环依赖)

代理:AbstractAutoProxyCreator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这个方法是用于三级缓存生成对象的时候将bean放到earlyBeanReferences里面,
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
this.earlyBeanReferences.put(cacheKey, bean);
return this.wrapIfNecessary(bean, beanName, cacheKey);
}
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
if (this.earlyBeanReferences.remove(cacheKey) != bean) {
// wrap 包装成代理的核心方法
return this.wrapIfNecessary(bean, beanName, cacheKey);
}
}

return bean;
}

无法解决的循环引用

如果是 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
  • 功能的集合->通知类

image-20241020152910091

连接点包含切入点

Spring中进行AOP编程

导入坐标

image-20241020154236445

MyAdvice

定义通知(功能增强点)

定义切入点 @Pointcut

image-20241020154309848

private void 空参

绑定通知与切入点

image-20241020154407660

Spring接管此类 @Component

定义AOP @Aspect

image-20241020154529656

SpringConfig @EnableAspectAutoProxy

image-20241020154505110

AOP

工作流程

image-20241020155857766

**基于动态代理**:

  • 匹配失败,new原始对象;

    image-20241020160228368

    image-20241020160243146

  • 匹配成功,new出来的是原始对象的代理对象;

    image-20241020160302140

    image-20241020160341452

用获取到的bean执行方法:如果是代理的bean,根据通知和切入点进行方法执行。

切入点表达式

切入点:要对其进行增强的方法

切入点表达式:对切入点的描述方式

image-20241020161523488

execution(public User com.itheima.service.UserService.findById(int))

public exception 可省略

参数必须有

通配符使用

image-20241020161833474

  • ..和*的区别 *用于精准匹配到某个位置

image-20241020163432072

1
2
3
4
5
6
7
8
9
10
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
@Pointcut("execution(void com.itheima.dao.impl.BookDaoImpl.update())")
@Pointcut("execution(* com.itheima.dao.impl.BookDaoImpl.update(*))")
@Pointcut("execution(void com.*.*.*.update())")
@Pointcut("execution(* *..*(..))")
@Pointcut("execution(* *..*e(..))")
@Pointcut("execution(void com..*())")
@Pointcut("execution(* com.itheima.*.*Service.find*(..))")
//执行com.itheima包下的任意包下的名称以Service结尾的类或接口中的save方法,参数任意,返回值任意

通知类型

前置@Before

后置@After

环绕@Around

image-20241020170038080

  • ProceedingJoinPoint

  • 原始方法在环绕方法中执行,用pjp.proceed()执行原始方法,不出现就能隔离原始方法,(权限校验)

  • pjp能接原始方法的返回值,类型为Object,强转后可以在给他返回去,思想和动态代理里的案例比较像:利用反射invoke调用可以拿到返回值,注意修改通知方法的返回值为Object。没返回值也可以

  • 强制抛Throwable

得到返回值之后@AfterReturning

和after区别:after只要方法结束即可,不管是得到返回值正常结束还是抛异常。AfterReturning需要得到返回值正常结束才能

抛出异常之后@AfterThrowing

案例:JUnit 测量业务层接口执行效率

JUnit 测试服务类就private服务出来,@Autowired

下面写test具体方法。详见JUnit单元测试篇(Java SE)

image-20241020171154617

获取方法签名 (执行信息) getSignature()

image-20241020171252962

通知获取数据

JoinPoint & ProceedingJoinPoint

作为通知的参数,如果出现,必须在第一个参数的位置上,PJP是JP的子类

Object[] getArgs(): 获取原始方法的参数

Object proceed() :环绕 PJP专用,调用原始方法同时返回这个方法的返回值

AOP获取原始方法调用参数

image-20241020173959692

这样可以对原始参数进行处理,可以增加程序健壮性。

Object proceed(Object[] args) 可以把处理以后的参数传给原始方法。

案例:网盘提取码去空格

image-20241020180157823

args本身是Object数组,拿进来需要转成字符串toString getArgs 然后遍历参数数组,对每个字符串参数trim,再把处理以后的传给proceed

AOP获取返回值

  1. 环绕 pjp proceed
  2. AfterReturning 注解的returning要和形参名字相同

image-20241020175037570

AOP接收异常

  1. 环绕 不要往出抛Throwable 内部try-catch

  2. AfterThrowing 注解的throwing和形参名字相同

![image-20241020174307577](https://pub-9e727eae11e040a4aa2b1feedc2608d2.r2.dev/PicGo/屏幕截图 2024-10-20 174257.jpg)

环绕通知模拟其他四种通知

前置 最后调用proceed
后置 try catch finally 在finally里写
AfterReturning Object接住proceed的返回值
AfterThrowing 不要往出抛Throwable,try catch Throwable

代理对象的实质—this调用实例方法失效

AOP的核心,是从调用对象的方法时生成代理对象—PROXY,this指向真正的目标对象—TARGET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface IService{
void foo();
void zoo();
}
@Service
class ServiceImpl implements IService{


@Override
void foo(){
System.println("fooStart...");
zoo();//this指针调用实例方法
System.println("fooFinish...");
}
@Transactional
void zoo(){
save(a);//假设是将a保存到数据库
int i = 1/0;
save(b);//假设是将b保存到数据库
}
}
//
class Test{
@Autowired
private IService service;

public static void main(String[] args){
service.foo();//从外部调用内部的事务方法
}
}

事务的实现基于spring aop,如果直接用this,则会使用target对象进行方法调用,

Proxying Mechanisms :: Spring Framework

Understanding the Spring Framework’s Declarative Transaction Implementation :: Spring Framework

如果是在类内部开启的事务,就需要CGLIB动态代理,实现的基础是方法拦截器,环绕通知,

直接调用方法

image-20241104161707487

通过代理调用方法

image-20241104161648309

也就是说,如果要让代理生效,首先就是要获取代理对象,而通过@Transactional注解的方法zoo(),如果从外部调用,只要获得了代理对象的引用,事务功能就是生效的,因此直接在main中只要获取了代理对象的引用调用service.zoo()方法是没有问题的。

而在目标对象内部,this就是目标对象本身,肯定不会走代理,因此如果实在

解决方案—在类的内部获取代理对象

AopContext.currentProxy()

1
2
3
4
5
synchronized (UserHolder.getUser()) {
//开启事务需要获取当前的代理对象
IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}

@Autowired 注入服务对象本身

获取service对象即可,这样就能在类内部的上下文中获取proxy代理都象的引用

1
2
3
4
5
6
7
8
@Autowired
private ServiceImpl service;

synchronized (UserHolder.getUser()) {
//开启事务需要获取当前的代理对象
return service.createVoucherOrder(voucherId);
}
//也可以新开一个Impl2类,把方法移植进去,注入Impl2对象,思路一样

AOP:动态代理对象的生成时机

【spring系列】spring的AOP是在哪个阶段创建的动态代理对象,spring bean的生命周期中在什么阶段创建的aop动态代理对象,很多人会说第一种,其实还有一种情况也会进行aop_spring的aop代理对象什么时候创建-CSDN博客

关于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
2
3
4
5
6
7
8
9
10
11
SqlSession session = sqlSessionFactory.openSession();
try {
int affected_rows = session.insert("com.kvn.mapper.UserMapper.insert", user);
} catch (Exception e) {
// 捕获到异常,将操作回滚
session.rollback();
}
// 正常执行,提交事务
session.commit();
session.close();

Spring联合MyBatis事务管理

SpringManagedTransaction 打通了 MyBatis 的事务管理、连接管理 和 spring-tx 的 事务管理、连接管理,使得 MyBatis 与 Spring 可以使用统一的方式来管理连接的生命周期 和 事务处理。

  1. 原生的MyBatis 使用的是JdbcTransaction实现类

  2. 在一个非 @Transactional 标记的方法中执行 sql 命令,则事务的管理会通过 SpringManagedTransaction 来执行。

  3. 在一个 @Transactional 标记的事务方法中执行 sql 命令,则 SpringManagedTransactioncommit()/rollback() 方法不会执行任何动作,而事务的管理会走 Spring AOP 事务管理,即通过 org.springframework.transaction.interceptor.TransactionInterceptor 来进行拦截处理。

image-20241020215834772

  1. SqlSessionInterceptor 保证了 MyBatis 的 SqlSession 在执行 sql 时使用的连接与 Spring 事务管理操作使用的连接是同一个连接。具体就是通过 Spring 的事务同步器 TransactionSynchronizationManager 来保证的。
  2. 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
2
3
synchronized(connection){
//tx......
}

线程如何获取锁对象?为了保证一个线程所有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
2
3
4
5
6
7
8
9
servlet中获取一个连接.首先,servlet是线程安全的吗?

class MyServlet extends HttpServlet{
private Connection conn;
}
ok,遗憾的告诉你,这个conn并不是安全的,所有请求这个servlet的连接,使用的都是一个Connection,这个就是致命的了.多个人使用同一个连接,算上延迟啥的,天知道数据会成什么样.
因此我们要保证Connection对每个请求都是唯一的.这个时候就可以用到ThreadLocal了,保证每个线程都有自己的连接.
改为 private ThreadLocal<Connection> ct = new ThreadLocal<Connnection>();
然后从连接池获取Connection,set到ct中,再get就行了,至于得到的是哪个Connection就是连接池的问题了,你也管不到.

ThreadLocal?

就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突

开启步骤

业务层**接口**为业务方法打开事务@Transactional

image-20241020204651727

  • @Transactional是方法级别或者类级别的注解,可以开在整个接口上,也可以开在单个方法上

  • 接口能够提高复用性,降低耦合

JdbcConfig创建事务管理器Bean@Bean

image-20241020204933459

PlatformTransactionManager是Spring规定的,DataSourceTransactionManager可以动,根据具体的技术选择

要注意,事务管理器的datasource和mybatis用的datasource必须是同一个,不然

打开事务@EnableTransactionManagement

image-20241020205233232

事务角色

image-20241020205625500

image-20241020205648701

事务配置

image-20241020222103484

有些异常不会触发回滚,需要手动设置一下rollbackFor

追加日志

try finally结构,finally 记日志功能必定触发

image-20241020222628555

事务传播行为控制

(1) java - Spring事务传播行为详解 - 个人文章 - SegmentFault 思否

image-20241020222917510

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserService {

@Autowired
private UserMapper userMapper;


public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}

@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}

我们看到在事务方法 add 中,直接调用事务方法 updateStatus。从前面介绍的内容可以知道,updateStatus 方法拥有事务的能力是因为 spring aop 生成代理对象proxy,但是这种方法直接调用了 this 对象的方法,所以 updateStatus 方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

子线程为什么不能共享父线程的事务

img

事务:JDBC connection(连接) MyBatis sqlSessionFactory(会话)这些都是一个意思。Spring 在将其整合后,会绑定到每个线程私有的 ThreadLocal 中。

  • 连接池中的每个 Connection 对应不同的 TCP 四元组,都是一个独立的物理连接,物理上不同(不同的socket、不同的会话)。

  • 数据库事务是“会话级别”的,绑定在某一条连接上,每条连接有独立的事务上下文、会话状态、隔离级别,不同连接之间的事务、锁、变量 互不干扰

  • 连接池除了管理 Connection 对象本身,还会:记录该连接是否存活、检查该连接是否超时、检查连接是否被污染(如事务未提交、连接状态被改)、所以每条 Connection 在连接池中不仅仅是个“Connection对象”,还包含一些“元数据”状态。

  • 为了支持并发访问,一个线程用一条连接,多个线程可以并发请求数据库(否则一条连接只能排队等待)。

  • 在Spring框架中,@Transactional的事务管理默认是基于线程绑定的(使用ThreadLocal存储事务上下文)。

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: root

子线程访问自己的ThreadLocal,发现没有存父线程的连接 → 所以子线程会重新去连接池获取一个新的连接

  • 保证多线程访问数据库时线程间互不干扰
  • 保证每个线程内的事务隔离
  • 防止一个线程操作数据库时,另一个线程“偷用”这个连接引发混乱(如事务状态、关闭连接)
1
2
3
4
5
6
7
@Transactional
public void parentMethod() {
saveData1();
new Thread(() -> {
saveData2();
}).start();
}
  • 如果像这样,新的线程有自己的 ThreadLocal,必须自己再新拿一条连接来操作数据。

总结:不要在事务里面开启子线程,应该是在子线程里开启事务。

Spring MVC

入门案例

Java实现MVC模型的web框架,灵活性强,主要进行表现层开发 Controller。

bean创建@Controller

方法级别注解 请求映射 @RequestMapping

方法级别注解 设置响应 @ResponseBody

image-20241021161644841

都是方法级别注解

创建SpringMvcConfig@Configuration@ComponentScan

扫描到controller

创建servlet容器Config

ServletContainerInitConfig继承AbstractDispatcherServeltInitializer类重写对应方法。都是一次性

image-20241021161341011

  • getServletMappings 表示接管URL的那个部分的映射
  • createRootApplicationContext 创建Spring Framework容器并指定配置
  • createServletApplicationContext 创建web容器并指定配置
  • web容器 servlet容器

工作流程

image-20241021162133487

bean加载@ComponentScan.Filter

Spring避免加载springmvc的controller

image-20241021162700082

导包:mybatis自动代理会返回dao接口的实现对象,可以不写,但是其他技术不一定是这样,所以为了通用性还是应该导dao

image-20241021164147565

SpringConfig扫描排除含有Controller注解的类:Filter 可以更细粒度地加载bean

exclude排除

image-20241021164311077

加了configuration的类,spring都会将其作为配置类,里面如果有componentScan,就会扫上。

image-20241021164930504

创建容器设定配置再去返回容器,简化过程只需要指定类的字节码即可

image-20241021165128160

Controller

Request

请求映射路径@RequestMapping

对于不同的controller可能会优相同方法,这时会有冲突问题,解决办法是在controller**上加一个@RequestMapping,要和方法**的@RequestMapping注解结合一下。

image-20241021172423291

名称匹配 指定请求参数名@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也能打印出来

image-20241021220731172image-20241021220755961

@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.字段名称

  • 数组:直接接收字符串数组即可,请求的参数名就是数组的形参名,会把数组当成一个独立的参数

    image-20241021192646729

  • 集合类型:加RequestParam注解。 因为集合属于引用类型,spring会把它当成pojo处理(造pojo然后根据字段名注入依赖),不加param注解,spring就不会像数组一样把他当成一个独立的参数。

传JSON @EnableWebMvc

导坐标 webMvc

image-20241021193842326

参数在请求体里@RequestBody

@RequestBody XXX xxx 表示请求体中的数据用XXX类型接

RequestBody请求体中的数据通常是以JSON、XML等格式发送的,可以将请求体中的数据自动绑定到指定的Java对象上。

  1. 参数写在请求体里,用List接收json数组

image-20241021194047781

  1. 用Pojo类接单个pojo对象json

image-20241021194303198

image-20241021194445000

  1. 用List接收多个pojo对象的json,

image-20241021194340592

一个请求,只有一个RequestBody;一个请求,可以有多个RequestParam。

Body vs Param:

image-20241021222507281

日期参数格式化@DateTimeFormat(pattern=”yyyy-MM-dd”)

image-20241021223057976

默认yyyy/MM/dd 其他形式不认识,需要自己手动指明formatPattern

Converter接口-将字符串参数转换成Java类型

请求里面的参数都是以字符串形式发来的,converter要根据形参类型,把字符串转成对应类型提供给方法,这就是为什么前面能把字符串12按照需求转换成int 12

image-20241021223621200

@EnableWebMvc

Response

直接返回:响应的是一个页面

1
2
3
4
@RequestMapping("/toPage")
public String toJumpPage(){
return "page.jsp";
}

直接在方法里 return “page.jsp” ,Spring默认认为Controller的方法返回的就是一个页面,也就是说会经过渲染步骤。

返回值就是响应体:@ResponseBody

image-20241021225227708

如果直接return一个字符串Spring会认为这个字符串是一个网页,响应分为响应行响应头和响应体,响应头里是放状态码之类的,只能在响应体里返回数据,加@ResponseBody注解表示返回值就是响应体。

1
2
3
4
5
6
@RequestMapping("/toText")
@ResponseBody
public String toText(){
return "page.jsp";
}
//表示返回 page.jsp 这个字符串

响应POJO对象(JSON形式)

1
2
3
4
5
6
7
8
9
@RequestMapping("/toJsonPOJO")
@ResponseBody
public User toJsonPOJO(){
System.out.println("返回json对象数据");
User user = new User();
user.setName("itcast");
user.setAge(15);
return user;
}

直接返回user即可,jackson帮我们做的

响应POJO对象集合(JSON数组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("/toJsonList")
@ResponseBody
public List<User> toJsonList(){
System.out.println("返回json集合数据");
User user1 = new User();
user1.setName("传智播客");
user1.setAge(15);

User user2 = new User();
user2.setName("黑马程序员");
user2.setAge(12);

List<User> userList = new ArrayList<User>();
userList.add(user1);
userList.add(user2);

return userList;
}
HttpMessageConverter接口

POJO转JSON字符串

image-20241021225843796

image-20241021225823024

REST

Representational State Transfer

image-20241021230252993

根据请求的方式区分 GET POST PUT DELETE

同一URL,请求方式不同,调用的方法也不同

image-20241021230150675

RESTful:用REST风格访问资源

@RequestMapping 加 method 参数

@RequestMapping(value = "/users/{id}" ,method = RequestMethod.DELETE)

image-20241021231918974

image-20241021231836548

URL占位符传参 @PathVariable

value=”/users/{id}” URL中的{id}和用@PathVariable修饰的方法参数id是一致的

image-20241021231846312

@RequestBody@RequestParam@PathVariable

image-20241021232010983

RESTful 快速开发

类级别注解 @RequestMapping

省去所有user前缀,写一次就好。

类级别注解 @RestController

类级别的@RequestBody,表示所有类的返回值都是请求体的数据,既然@Controller和RequestBody都要写,合而为一即可。

方法级别注解 @PostMapping
1
2
3
@RequestMapping(value = "/{id}" ,method = RequestMethod.DELETE)
@GetMapping
@PostMapping("/{id}")

image-20241021232955549

RESTful 页面交互案例

RestController PostMapping GetMapping DeleteMapping PutMapping

方法参数RequestBody

image-20241021235556399

Config 类 SpringMvcSupport 放行静态页面访问

@Configuration

默认SpringMvcConfig接管/后所有东西,通过ResourceHandlerRegistry 设置SpringMVC如何处理对静态资源的访问

image-20241021235616215

前端ajax

image-20241021235623670

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自动代理,写接口,写方法结合注解

image-20241022135643205

占位符对应参数的名称,这里映射实体类中的字段信息

image-20241022135845990

Service

BookDao@Autowired

dao接口加repository注解(可加可不加)

RestController

参数在url中:PathVariable

参数在请求体:RequestBody

JUnit 测试 Service

Postman 测试 Controller

Spring 事务激活

JdbcConfig 里 加PlatformTransactionManager Bean, 接dataSource参数

Service接口添加@Transactional

前后端联调

表现层数据封装模型 - 设置统一的返回结果集Result

实际开发过程中前后端约定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Result{
private Object data;
private Integer code;
private String message;
//下方提供若干构造方法,有参无参
public Result() {
}

public Result(Integer code,Object data) {
this.data = data;
this.code = code;
}

public Result(Integer code, Object data, String msg) {
this.data = data;
this.code = code;
this.msg = msg;
}
//.....getter setter
}

image-20241022152253512

Result.data

业务方法不同返回数据格式也不同,可能是true false这样的text,也可能是json数据,还可能是json数组,约定将数据封装到data字段中

image-20241022144637267

Result.code

不同业务方法可能会返回相同的内容,返回一个true可能对应新增,修改,删除的业务方法,加一个识别码code字段区分 ,可以约定尾数是0表示失败,尾数是1表示成功:

image-20241022145108783

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Code {
public static final Integer SAVE_OK = 20011;
public static final Integer DELETE_OK = 20021;
public static final Integer UPDATE_OK = 20031;
public static final Integer GET_OK = 20041;

public static final Integer SAVE_ERR = 20010;
public static final Integer DELETE_ERR = 20020;
public static final Integer UPDATE_ERR = 20030;
public static final Integer GET_ERR = 20040;
}
//这里Result的构造器识别的是Integer code
/*
Enum枚举:CodeEnum是一个类,类内部有一字段code(Integer)
*/

image-20241022152238169

Result.message

一些业务方法,本来应该返回json,没查到只能返回null,不能直接把null展示给用户看,展示的是message信息


Controller 返回值统一设定为 Result

将返回值封装到Result中,data

1
2
3
4
5
6
7
8
@GetMapping
public Result getAll() {
List<Book> bookList = bookService.getAll();
Integer code = bookList != null ? Code.GET_OK : Code.GET_ERR;
String msg = bookList != null ? "" : "数据查询失败,请重试!";
return new Result(code,bookList,msg);
}
//data字段是否为null?

异常处理器@RestControllerAdvice

  • 类级别注解

  • 后端抛出的异常如果不处理,就会抛到前端页面,不美观,并且不会返回任何数据,导致数据不统一

  • 要让WebMvcConfig扫到这个Advice类

常见异常诱因
  • 框架内部抛出的异常:因使用不合规导致
  • 数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
  • 业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
  • 表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
  • 工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)

处理方法:全部抛到表现层Controller —— AOP 编程,用最少量的代码实现最强大的功能,快速统一地处理异常

方法级别注解 处理具体类别的异常@ExceptionHandler

image-20241022153703868

处理异常返回的结果也要封装成Result

项目异常处理方案 (捕获异常并返回Result)

异常分类

  • 业务异常 BusinessException 可预期
    • 发送对应消息,提醒用户规范操作
  • 系统异常 SystemException
    • 发送固定消息,安抚用户
    • 发送特定消息给运维,提醒维护
    • 记录日志
  • 其他异常 Exception
    • 发送固定消息,安抚用户
    • 发送特定消息给开发,提醒维护(纳入预期范围)
    • 记录日志

自定义异常

image-20241022154704744

继承RuntimeException

image-20241022155606802

加一个code属性(getter setter),重写RuntimeException的方法。异常构造的时候需要用到这些构造器,包装返回数据要用到code和message

建议放在源根的exception包下面

异常代码扩充

image-20241022160311854

触发异常

image-20241022160332391

拦截并处理异常 返回Result

在RestControllerAdvice下方的类中处理对应类型的异常,将异常继续封装成Result返回

image-20241022160546843

放行静态资源配置

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

image-20241022205635235

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})

MyBatisConfigJdbcConfig中方法级别注解@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存在,但是具有一定侵入性

image-20241023210351697

  • 主要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回显 ,按照识别码判别成功与否

image-20241023190220059

修改:put请求

image-20241023191646261

image-20241023191751771

删除:delete请求

image-20241023191526268

image-20241023191952545

拦截器

Interceptor

image-20241023193138396

Filter

image-20241023193414214

filter在一定是在访问 servlet 之前,interceptor只能在servlet中, before Controller

功能类

控制表现层:controller下新建interceptor包,新建一个Interceptor类 extends HandlerInterceptor

注意preHandle返回值和@Component

image-20241023210048371

SpringMvcSupport

image-20241023205518460

addInterceptors 自动注入自定义拦截器

addPathPatterns 加的不是前缀,是严格的URL匹配,配/books就拦截对/books发的请求,/books/100就拦截不了

image-20241023205715345

preHandle,yourService,postHandle,afterCompletion 顺序

示意图

image-20241023210636045

简化开发-WebMvcConfigurer

image-20241023210528299

已经和Spring接口绑定,侵入性强。

拦截方法

boolean preHandle(req,resp,handler) req和resp是servlet的响应和请求,handler实际上是 HandlerMethod,通过 getMethod 能拿到执行的业务方法的对象(反射)

void postHandle(req,resp,handler,modelAndView) 渲染页面之前调用

void afterCompletion(req,resp,handler,exception) 能拿到原始业务方法执行过程中的异常

拦截链顺序

image-20241023211911820

拦截顺序,和注册顺序有关系

image-20241023212439810

如果某个pre返回false,post全部跳过,倒序执行,从最近一个pre返回true的拦截器开始执行afterCompletion

image-20241023212527269

万字详解 GoF 23 种设计模式(多图、思维导图、模式对比),让你一文全面理解-CSDN博客

Spring MVC 启动流程

WebApplicationContext

image-20241022175254007

WebApplicationContext(WAC)

ApplicationContext(AC) 表示 ioc 容器。WAC是普通AC的扩展,它具有Web应用程序所需的一些额外功能,比如可以获取 ServletContext并修改

1
2
3
4
5
6
7
8
9
10
11
12
public interface WebApplicationContext extends ApplicationContext {
String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
String SCOPE_REQUEST = "request";
String SCOPE_SESSION = "session";
String SCOPE_APPLICATION = "application";
String SERVLET_CONTEXT_BEAN_NAME = "servletContext";
String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";
String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";

@Nullable
ServletContext getServletContext();
}

Root WebApplicationContext(applicationContext.xml)

Root WAC在应用启动时首先被加载,并且作为父上下文,供表示层使用,主要负责管理服务层(Service)、数据访问层(DAO)、中间件配置等非 Web 层(表示层)的 Bean

Servlet WebApplicationContext(spring-mvc.xml)

  • Servlet WACRoot 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。

image-20241023154504980

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)可以得到object

  • Enumeration<String> getInitParameterNames() 获取所有 <context-param/> 参数的名称 字符串枚举

  • String getInitParameter(name) 根据name获取指定的 <context-param/> 参数值

image-20241023013135254

Root WAC, Servlet WAC, ServletContext之间的关系

image-20241022230909096

  • Root WebApplicationContext存储key为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,可以通过此Key访问Root WAC。

  • WebApplicationContextUtis工具类提供了从ServletContext获取RootWAC的方法:

    • WebApplicationContextUtils.getWebApplicationContext(ServletContext sc)
  • WAC提供了获取ServletContext的抽象方法 getServletContext()

image-20250613113913619

web.xml 配置 ServletContext

Tomcat创建web应用时,会构建ServletContext对象,根据web.xml中的配置把如下参数都存到ServletContext对象中,注册Listener,Servlet等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>  

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

<!—ServletContext自有的init 参数-->
<context-param>
<!—创建Root WAC所需要的配置文件路径-->
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/applicationContext.xml</param-value>
</context-param>

<!—注册ContextLoaderListener-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!—注册DispatcherServlet ServletConfig-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!—init Servlet所需参数-->
<init-param>
<!—创建Servlet WAC所需参数-->
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/spring-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<!—指定某个servlet的URL映射路径-->
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

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
2
3
4
ServletContext var10000 = this.getServletContext();
String var10001 = this.getClass().getSimpleName();
var10000.log("Initializing Spring " + var10001 + " '" + this.getServletName() + "'");
//记录日志
  • 同样的,途中也会调用自己的initWAC方法:
    • 调用WACUtils工具类,获得自己所在的ServletContext的 Root WAC
    • 将自己的WAC转换成ConfigurableWAC,如果存在 RootWAC,则将其设置为自己的parent
    • 然后configureAndRefreshWAC(cwac):设置ServletContext为sc,从其中ServletConfig中获取 <init-param> 参数的值

image-20241023013013442

  • 最后根据自己的ServletConfig获取到ServletContext,根据自己的名称设置自己的Servlet WAC在ServletContext中的Key

  • DispatcherServlet实现了initStrategies [生成各个功能组件,异常处理器,视图处理,请求映射]

mvc-context-hierarchy

这两个context都是在ServletContext中,属于dispatcherServlet的上下文是servletWAC,找不到的话就去rootWAC中找

Java配置ServletContext——WebApplicationInitializer

![屏幕截图 2024-10-23 133904](https://pub-9e727eae11e040a4aa2b1feedc2608d2.r2.dev/PicGo/屏幕截图 2024-10-23 133904.png)

替代web.xml

ServletContainerInitializer

之前,web容器(Tomcat)会根据WEB-INF下的web.xml初始化ServletContext

Java EE Servlet 规范定义了这个接口,web容器(Tomcat)启动时根据这个初始化器做一些组件内的初始化工作。

SpringServletContainerInitializer 是Spring 对其的实现,其onStartup方法会调用 WebApplicationInitializer 的onStartup(ServletContext sc)初始化Web应用

典型 SpringMVC 应用启动流程

Spring MVC启动流程

  • Tomcat 读取web.xml中 <context-param> <listener> 然后创建一个全局共享的ServletContext

    • Tomcat 将<context-param> <listener>转化为键值对,存到ServletContext
  • 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()的过程中同时会触发DispatcherServletonRefresh()方法,这个方法会初始化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
2
3
4
5
6
@RestController
public class MyController {
@GetMapping("/hello")
public String hello() { return "hi"; }
}

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时,通过HandlerAdapterhandle()方法执行 Handler。
  • 实现类有参数解析器和和结果处理器。
  • REST 风格的调用栈,从外到内:
    1. AbstractHandlerMethodAdapter#handle()
    2. RequestMappingHandlerAdapter#handleInternal()
    3. RequestMappingHandlerAdapter#invokeHandlerMethod() 将 handlerMethod 包装成 invocable
    4. ServletInvocableHandlerMethod#invokeAndHandle()通过反射调用原始方法。
    5. HandlerMethodReturnValueHandler#handleReturnValue() 结果处理器。
    6. 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
2
3
4
5
6
7
8
9
10
@Slf4j
@RestControllerAdvice
public class WebExceptionAdvice {

@ExceptionHandler(RuntimeException.class)
public Result handleRuntimeException(RuntimeException e) {
log.error(e.toString(), e);
return Result.fail("服务器内部异常");
}
}

如果是 @ResponseBody@RestControllerModelAndView 为 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

image-20241025190302057

写控制器类

把controller类写好

image-20241025194037167

启动app

image-20241025194052846

快速启动

打包

package 之前 clean 全部设置为UTF-8参数

命令行启动 java -jar

image-20241025194844475

image-20241025194925821

jar执行要有入口类,boot打包需要插件才能生成可执行的入口类

boot 依赖管理

image-20241025195226537

starter 起步依赖

starter-parent 依赖管理

starter-parent:定义了无数jar包的版本管理和依赖管理,减少依赖冲突。只写GA 不写V

dependencies-辅助功能

每一个dependency(以web包为例)把真正需要用到的jar包声明,去找parent要即可

spring-boot-starter-web:

image-20241025201404198

spring-boot-dependencies:

image-20241025201443556

image-20241025201805886

替换starter的某个依赖: exclusion

依赖排除exclusion,换技术

image-20241025202142653

application.yml

resources目录下配置文件加载优先级

image-20241025204356892

properties>yml>yaml

自动提示功能消失—引入配置文件到boot模块中

image-20241025204437530

image-20241025204503008

debug>info>warn

YAML—(YAML Ain’t Markup Language)

YAML 简介

Jamel Camel

image-20241025204800590

语法规则

image-20241025204945594

1
2
3
4
5
6
7
# 空格数量不限,只要前面格数一样就是同一级,不允许使用tab缩进
enterprise:
name: John
likes:
- Java
- Python
- C++

image-20241025205033642

以Java方式读取yaml

单个数据—成员变量@Value(${enterprise.subject)

自动赋值

image-20241025205805383

全部数据—Environment

定义一个Environment,自动装配,将配置中的属性全部遍历:

environment.getProperty("name")

image-20241025210024893

pojo类映射到yaml @ConfigurationProperties

可以拿到需要的某个属性的信息(prefix),在控制器中定义成员变量自动装配,常用

image-20241025210710013

自定义对象封装数据警告

image-20241025211036999

多环境开发

生产环境设定

独立生产环境设定

spring.profiles boot2

spring.config.activate.on-profile boot3

--- 三条横线分割配置

spring.profiles.active 设置激活的环境

image-20241025214421983

带参数启动boot

命令行参数临时修改配置内容
1
2
3
4
5
# 修改启动环境为test
java -jar boot_1.0_SNAPSHOT.jar --spring.profiles.active=test
# 修改服务器端口号为88
java -jar boot_1.0_SNAPSHOT.jar --server.port=88

参数加载优先级

image-20241025215406246

maven&boot开发环境兼容—加载配置文件

maven和boot都设置了多环境,但是打包工作是maven负责,所以maven应该占主导

手动配置resources插件,覆盖parent设定

maven-resources-plugin详解 - 红尘过客2022 - 博客园 (cnblogs.com)

<resources> (<filtering>)

image-20241026013908079

<resources>标签其实就是maven-resources-plugin<resources>配置,主要用来配置资源目录的。普通项目没有parent,默认继承父pom.xml:

image-20241026024547372

可以看到resources默认就是项目下的src/main/resources,但是没开过滤filtering,所以之前maven课程中,在配置文件中引入占位符还得重写一遍resources标签。

而boot模块默认继承的starter-parent默认的resources标签是开了过滤的,而且资源明确包括application.yml这种配置文件,所以即使子项目里不用手动复写resources也能匹配到占位符:

image-20241026025346547

maven-resources-plugin (<useDefaultDelimeters> )

<useDefaultDelimeters> 支持使用${}或@过滤资源

image-20241026021624316

boot的starter-parent默认对resources-plugin的配置也做了自定义更改(主要就是<useDefaultDelimeters> = false)而上文提到的父pom没有,<useDefaultDelimeters>默认就是true

image-20241026025510839

定义多环境 <profiles>

image-20241026005328800

配置属性替换占位符

@

boot项目的parent为了防止spring占位符被扩展,所以只允许@为占位符,不解析${}。如果已经继承starter-parent,直接在配置文件中@xxx@即可。 parent不是starter-parent的解决办法

image-20241026021230152

${}

非要用${},可以使用如下方法覆盖配置:

image-20241026081744618

或者启用插件的<useDefaultDelimeters> = true

image-20241026005308461

image-20241026005348094

多配置文件加载优先级

包外配置

假设JAR包位于file目录下,file/config/application.yml > file/application.yml

包内配置

resources/config/application.yml > resources/application.yml

Java项目目录结构

image-20241026140333629
  • 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

image-20241026143753352

image-20241026143816570

APK内部结构

APK作为JAR包的变种,也具有相似的结构:

image-20241026144246642

框架整合

JUnit

image-20241026151902777

image-20241026152538674

测试类注解@SpringBootTest

image-20241026152650043

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 配置数据源

image-20241026163010546

SSM迁移到SpringBoot

  1. 配置类全部删除
  2. Dao加@Mapper
  3. Controller Service不变
  4. application.yml 配置端口和数据源
  5. 静态资源放到resources/static,静态资源可重定向(JS脚本):

访问一个web资源,如果直接访问 localhost:port 一般会请求一个主页index.html,为了能直接从地址访问资源,创建一个index.html,添加一个跳转的js脚本

1
2
3
<script>
document.location.href="pages/books.html";
</script>

@SpringBootApplication

@SpringBootApplication 是 Spring Boot 的核心注解,它是一个组合注解,整合了以下三个关键注解的功能:

  1. @Configuration 标记当前类为配置类,允许通过 @Bean 定义 Spring 容器中的组件。
  2. @EnableAutoConfiguration 启用 Spring Boot 的自动配置机制,根据项目依赖(如 JDBC、Web、Redis 等)自动配置 Spring 应用。
  3. @ComponentScan 自动扫描当前包及其子包下的组件(如 @Controller@Service@Repository 等),无需手动注册。
特性 Spring Boot (@SpringBootApplication) 传统 Spring
配置方式 自动配置 + 默认约定 手动 XML 或 Java Config
组件扫描 自动(默认包扫描) 需显式配置 @ComponentScan
依赖管理 通过 Starter 简化 手动管理依赖版本

用法

1
2
3
4
5
6
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args); // 启动 Spring Boot 应用
}
}
  • 通常放在项目的主类(含 main 方法)上。
  • 启动后会自动初始化 Spring 容器、加载配置、启动内嵌服务器(如 Tomcat)。

配置

排除特定自动配置

1
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
  • 例如:项目未使用数据库时,可排除数据源自动配置。

自定义扫描路径 scanBasePackages

1
@SpringBootApplication(scanBasePackages = "com.example")
  • 默认扫描主类所在包,如需扫描其他包,可通过 scanBasePackages 指定
  • 因此,默认情况下,SpringBootApplication修饰的类应该在根目录,确保所有类都能被扫到。

常见问题

  • Q:为什么我的 @Component 组件没被扫描到? A:确保组件位于主类的同级或子包下,或通过 scanBasePackages 显式指定路径。
  • Q:如何查看生效的自动配置? A:启动时添加 --debug 参数,日志会输出所有自动配置的评估结果。

SpringBoot 启动流程

app.run()

  1. new SpringApplication()
    • 确认应用类型(SERVLET、NON、响应式)
    • 加载 ApplicationContextInitializer(包括在 spring.factories 自定义的)
    • 加载 ApplicationListener
    • 记录主启动类(main所在类)
  2. run(MyApp.class,args)
1
2
3
4
5
6
7
8
SpringApplication.run() // 准备Environment PropertySource
├── 创建 SpringApplication
├── prepareEnvironment
├── createApplicationContext // 创建 context
├── prepareContext() // 设置Environment beanFactory后处理器 main类beanDefinition
├── refresh() // 刷新 IOC 容器 (往里面填充bean)
├── CommandLineRunner.run(String... args) ApplicationRunner(String... args)
└── ApplicationReady

由此可见 CommandLineRunner是在容器启动完成以后执行的。可以实现这个接口的 run() 方法来注入参数。

IoC 容器初始化: refresh()

AbstractApplicationContext#refresh()

1
2
3
4
5
6
7
8
9
10
11
refresh() // 刷新 IOC 容器 (往里面填充)
├── prepareRefresh()
├── prepareBeanFactory(beanFactory) //1.给beanFactory设置基本属性(类加载器和enviroment)
├── postProcessBeanFactory(beanFactory)//2.beanFactory后处理器
├── invokeBeanFactoryPostProcessors(beanFactory) //3.扫描并注册beanDefinition
├── registerBeanPostProcessors() // 4.注册 bean 后处理器
├── initMessageSource(); initEventMulticaster() // 5.事件源的注册
├── onRefresh() // 6.模板方法, 针对特定的context实现去执行特定逻辑,比如启动tomcat
├── registerListeners() // 7.event listener 绑定和注册
├── finishBeanFactoryInitialization() // 8.实例化所有非懒加载单例 Bean
└── finishRefresh() //9.发布ContextRefreshedEvent, 清除一些缓存
  1. 加载配置类(带 @Configuration@ComponentScan@Import 等注解)

  2. 扫描、注册阶段

    invokeBeanFactoryPostProcessors(beanFactory) 执行beanFactory后处理器

    • (由 ClassPathBeanDefinitionScanner 完成)
    • 扫描被 @ComponentScan 指定的包,找到带注解的类(如 @Component@Service@Controller
    • 解析为 BeanDefinition,使用 BeanDefinitionRegistry 将其注册进beanDefinitionMap还未创建对象。
      • 修改Bean定义:执行所有 BeanFactoryPostProcessor 的实现类(如 PropertySourcesPlaceholderConfigurer),允许对 BeanDefinition 进行修改(例如替换占位符)。
      • 提前实例化处理器:注册 BeanPostProcessor 实现类(如 AutowiredAnnotationBeanPostProcessor),这些处理器需在普通Bean之前初始化,以便后续处理其他Bean的创建。
      • 初始化消息源以及事件广播器。
  3. 实例化阶段(容器对于单例且非懒加载的 Bean)

    AbstractAutowireCapableBeanFactory#doCreateBean()

    对每个要使用的 Bean:

    • (主要是针对懒加载)存在性检查:Scope判断(若为单例则检查到单例缓存)以及循环依赖判断(如果当前正在创建就从单例三级缓存获取原始对象)

    • 实例化:从 BeanDefinitionRegistry 获取 BeanDefinition,包含类名、作用域、初始化方法等元数据。检查是否存在未满足的依赖(如通过@DependsOn指定的前置依赖,或者@Order加载顺序)最后实例化对象(通过反射或者工厂方法调用Constructor创建原始对象)

    • 依赖注入@Autowired/@Resource 递归调用getBean()获取依赖bean,通过三级缓存(singletonFactoriesearlySingletonObjectssingletonObjects)提前暴露对象引用,解决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 在这里可能会返回代理对象替代原对象

  4. 完成容器启动:触发 ContextRefreshedEvent,通知监听器容器已就绪。此时可以通过 getBean() 获取单例 Bean,如果是懒加载或者Scope = prototype的则会在主动调用 getBean() 的时候才实例化。

SpringBoot 自动装配:自动装配框架内部的 Bean

自动装配基于自动装配类,所以要把所有的bean都用bean方法的形式注册到容器中。

包括之前讲的 服务发现、服务注册、代理工厂、BeanPostProcessor、ConfigurationProperties等。

Bean 方法不需要使用Autowired在参数上注解!!!!

介绍

SpringBoot 自动装配原理详解

自动装配可以简单理解为:通过注解或者一些简单的配置就能在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Validated
@Data
@ConfigurationProperties(prefix = "rpc.server")
public class RpcServerProperties {
private String address;private Integer port;private String appName;
@Pattern(regexp = "zookeeper|nacos", message = "必须是 nacos或者zookeeper")
private String registry;
private String transport;private String registryAddr;
public RpcServerProperties() throws UnknownHostException {
this.address = InetAddress.getLocalHost().getHostAddress();
this.port = 8080;
this.appName = "provider-1";
this.registry = "zookeeper";
this.transport = "netty";
this.registryAddr = "127.0.0.1:2181";
}
}

pojo 类 + @ConfigurationProperties注解,可在 application.yml 中按照前缀配置属性。

1
2
3
4
5
6
7
rpc.server.app-name=provider-1
rpc.server.port=9991
rpc.server.registry=zookeeper
rpc.server.registry-addr=39.108.66.202:2181
rpc.server.transport=netty
# 设置指定包下的日志显示级别 INFO/DEBUG/WARNING/OFF
logging.level.com.wxy.rpc=info
注解 作用
@ConditionalOnProperty 属性Property,满足一定的条件才生效
@ConditionalOnMissingBean 只有没有这个类型的 Bean 时才生效 (用户自定义实现了Bean方法,可以替换这个自动装配的)
@ConditionalOnClass 类路径下有某个类才生效
@ConditionalOnBean 依赖的 Bean 存在才生效
@Primary 多个同类的 Bean 存在时首选注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
@EnableConfigurationProperties(RpcServerProperties.class) // 绑定 pojo 作为 properties
public class RpcServerAutoConfiguration {

@Autowired
RpcServerProperties properties;

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "rpc.server", name = "registry", havingValue = "zookeeper", matchIfMissing = true)// property 的 registry 字段的 value = zookeeper 才生效,如果配置项不存在也会生效
public ServiceRegistry serviceRegistry() {
if(properties.get)

return new ZookeeperServiceRegistry(properties.getRegistryAddr());
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "rpc.server", name = "registry", havingValue = "nacos")
public ServiceRegistry nacosServiceRegistry() {
return new NacosServiceRegistry(properties.getRegistryAddr());
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnBean({ServiceRegistry.class, RpcServer.class})
public RpcServerBeanPostProcessor rpcServerBeanPostProcessor(
@Autowired ServiceRegistry serviceRegistry,
@Autowired RpcServer rpcServer,
@Autowired RpcServerProperties properties)
{
return new RpcServerBeanPostProcessor(serviceRegistry, rpcServer, properties);
}
}

例子:添加 spring-boot-starter-data-redis 后,可直接注入 RedisTemplate

特征 Starter 模块
命名 -spring-boot-starter 结尾
依赖 包含 spring-boot-autoconfigure
自动配置 @AutoConfiguration 类,并注册到 spring.factoriesAutoConfiguration.imports
配置属性 包含 @ConfigurationProperties
功能入口 提供开箱即用的 Bean,无需用户手动配置。可通过 application.properties@Bean 覆盖 Starter 的默认配置。
  1. 实现自动配置类 AutoConfiguration

  2. 按照 SpringBoot 版本将配置类的全限定名引入指定路径下。

  3. 新建 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>