Spring 框架基础知识

什么是Spring框架?

Spring模块?

最主要的七大模块:

  • Spring Core:提供了框架的基本组成部分,包括控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)功能。
  • Spring Beans:提供了BeanFactory,是工厂模式的一个经典实现,Spring将管理对象称为Bean。
  • Spring Context:构建于 core 封装包基础上的 context 封装包,提供了一种框架式的对象访问方法
  • Spring JDBC:提供了一个JDBC的抽象层,消除了烦琐的JDBC编码和数据库厂商特有的错误代码解析,用于简化JDBC。
  • Spring AOP:提供了面向切面的编程实现,让你可以自定义拦截器、切点等。
  • Spring Web:提供了针对 Web 开发的集成特性,例如文件上传,利用 servlet listeners 进行 ioc容器初始化和针对 Web 的 ApplicationContext。
  • Spring Test:主要为测试提供支持的,支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。

什么是框架式的对象访问方式?就是Spring框架管理**对象依赖(DI)**。
不直接去 new 进行强依赖,通过 IoC 容器实现松散的耦合。

Spring Bean

1. Bean 基础概念

什么是 Bean?

在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。Bean 是由 Spring Ioc 容器实例化、装配和管理的对象。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——BeanDefinition 对象。

BeanDefinition 是 Spring 中定义 Bean 的配置元信息接口,它包含:

  • Bean 类名
  • Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等
  • 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies)
  • 配置设置,如 Bean属性(Properties)

什么是元数据和元信息?简单来说,Spring IoC容器需要知道如何创建和管理Bean,这些”如何”的信息就是元信息或元数据(例如自动注入的方式,工厂方法和销毁方法的确认等)。BeanDefinition就是Spring框架用来存储这些元信息的对象,它包含了Spring容器创建Bean实例所需的所有配置信息。

当我们通过XML、注解或Java配置类定义Bean时,Spring会将这些定义转换为BeanDefinition对象,然后IoC容器根据这些BeanDefinition来实例化、配置和组装Bean。

与此同时,Bean对象也有两种设计模式呈现:

  • 单例对象: singleton

总结:单例对象的生命周期和容器相同,

  • 多例对象:prototype

使用对象时,只要是在使用过程中就一直活着;

死亡:当对象长时间不用且没有其它对象引用时,由java的垃圾回收机制回收

Bean 生命周期是什么?

Bean 的生命周期指的是从创建到销毁的整个过程,主要分为以下几个阶段:

  1. 实例化: Spring 容器创建 Bean 的实例
  2. 属性赋值: 设置 Bean 的属性和依赖
  3. 执行Aware:加buff,为后续的Aware方法做准备
  4. 初始化前: 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
  5. 初始化
    • 调用 InitializingBean 的 afterPropertiesSet 方法
    • 调用配置的 init-method 方法
  6. 初始化后: 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
  7. 使用: Bean 可以在应用中使用
  8. 销毁前: 调用 DestructionAwareBeanPostProcessor 的方法
  9. 销毁
    • 调用 DisposableBean 的 destroy 方法
    • 调用配置的 destroy-method 方法

总结一下:

  • 首先是实例化、属性赋值、初始化、销毁这 4 个大阶段;
  • 再是初始化的具体操作,有 Aware 接口的依赖注入、BeanPostProcessor 在初始化前后的处理以及 InitializingBean 和 init-method 的初始化操作;
  • 销毁的具体操作,有注册相关销毁回调接口,最后通过DisposableBean 和 destory-method 进行销毁。

2. Bean 工厂与容器

什么是FactoryBean?

FactoryBean 是 Spring 框架中的一个特殊接口,它允许开发者自定义 Bean 的创建逻辑。当一个 Bean 的创建过程比较复杂,不适合通过普通方式创建时,可以实现 FactoryBean 接口来控制 Bean 的实例化过程。当你需要一个 FactoryBean 创建的 Bean 时,Spring 容器会调用其 getObject() 方法获取实际的 Bean 实例。

FactoryBean 接口定义了三个方法:

1
2
3
4
5
public interface FactoryBean<T> {
T getObject() throws Exception; // 返回由工厂创建的对象实例
Class<?> getObjectType(); // 返回对象的类型
boolean isSingleton(); // 返回由工厂创建的对象是否是单例
}

isSingleton():该方法用来判断由 FactoryBean 创建的 bean 是否为单例模式。在 Spring 框架中,单例模式的 bean 会被容器重用,而多例模式(原型模式)每次都会创建一个新的实例。默认情况下,isSingleton()方法返回 true,表示缺省创建的是单例bean.

如果一个对象实现了这接口,那它就成为一种特殊的 Bean,注册到 IOC 容器之后,如果调用 getBean 获取得到的其实是 FactoryBean#getObject()方法返回的结果。

BeanFactory 和 FactoryBean区别是什么?

这两个概念经常被混淆,但它们的作用完全不同:

BeanFactory:

  • 是 Spring IoC 容器的基本接口
  • 负责管理、装配和提供 Bean
  • 是 Spring 容器的核心,定义了容器的基本功能
  • 例如:ApplicationContext 是 BeanFactory 的子接口

FactoryBean:

  • 是创建 Bean 的一种方式
  • 是一个能产生其他 Bean 实例的特殊 Bean
  • 主要用于封装复杂的 Bean 创建逻辑
  • 当你从容器中获取 FactoryBean 时,得到的是它所创建的对象,而不是 FactoryBean 本身(除非使用 & 前缀)

简单说:BeanFactory 是装 Bean 的容器,而 FactoryBean 是一个能产生 Bean 的工厂 Bean。

FactoryBean创建自定义的Bean。创建好后的Bean对象同样交由BeanFactory管理,但是要用get方法调用。

BeanFactory和ApplicationContext的关系是什么?

BeanFactory 和 ApplicationContext 是 Spring 框架中两个核心的容器接口,它们的关系是:

BeanFactory:

  • 是 Spring 框架最基础的核心接口
  • 提供了基本的 IoC 容器功能
  • 采用延迟加载策略(懒加载)
  • 相对轻量级

ApplicationContext:

  • 扩展了 BeanFactory 接口
  • 提供了更多企业级功能,如国际化支持、事件发布、资源加载等
  • 默认采用即时加载策略(非懒加载)
  • 集成了更多的 Spring 功能模块

ApplicationContext 是 BeanFactory 的子接口,它增强了 BeanFactory 的功能,并且是我们在实际应用中更常用的容器类型。可以理解为 ApplicationContext 是一个更高级的容器,它包含 BeanFactory 的所有功能,并且还提供了更多的扩展功能。

3. Bean 的使用与作用域

Bean的作用域有哪些?

Spring 框架支持以下几种 Bean 作用域:

  1. singleton (单例): 默认作用域,每个容器中只有一个 Bean 实例
  2. prototype (原型): 每次请求获取 Bean 时,都会创建一个新的实例
  3. request: 每个 HTTP 请求都有自己的 Bean 实例,仅在 Web 应用中有效
  4. session: 在一个 HTTP Session 中,一个 Bean 定义对应一个实例,仅在 Web 应用中有效
  5. application: 在一个 ServletContext 中,一个 Bean 定义对应一个实例,仅在 Web 应用中有效
  6. websocket: 在一个 WebSocket 中,一个 Bean 定义对应一个实例,仅在 Web 应用中有效

可以通过 @Scope 注解或 XML 配置来指定 Bean 的作用域:

1
2
3
@Component
@Scope("prototype")
public class MyBean { ... }

Bean 是线程安全的吗?

Bean 的线程安全性取决于其作用域和状态:

  1. singleton 作用域:
    • 默认情况下不是线程安全的
    • 如果 Bean 有状态(包含可变的成员变量),多线程访问可能导致并发问题
    • 无状态的 Bean(没有可变的成员变量)通常是线程安全的
  2. prototype 作用域:
    • 每次请求都创建新实例,不存在多线程共享问题
    • 但每个实例本身仍需考虑其内部的线程安全性

如果线程之间不会对 Bean 的成员执行除查询以外的其它操作,则这个Bean 就是无状态的,是线程安全的。否则就是有状态的。

保证 Bean 线程安全的方法:

  • 使用 ThreadLocal 存储状态
  • 使用同步机制(synchronized、Lock等)
  • 使用线程安全的集合和对象
  • 使用原型作用域(如果适用)
  • 设计无状态的 Bean

4. Bean 的声明与注解

将一个类声明为 Spring 的 Bean 的注解有哪些?

在 Spring 中,可以通过多种注解将类声明为 Bean:

  1. @Component: 通用的组件注解,表示该类是一个 Spring 组件
  2. @Service: 用于标记服务层组件
  3. @Repository: 用于标记数据访问层组件,还启用了与持久层相关的异常转换
  4. @Controller: 用于标记控制器组件(如 MVC 控制器)
  5. @RestController: 结合了 @Controller 和 @ResponseBody,用于 RESTful Web 服务
  6. @Configuration: 标记配置类,通常与 @Bean 方法一起使用
  7. @Bean: 在配置类中的方法上使用,表示该方法返回一个 Bean 实例

这些注解大多数都是 @Component 的特化形式,添加了特定的语义,但在容器中的角色基本相同。

注入 Bean 的方式有哪些?

@Autowired底层的实现原理是什么?

  1. 注解处理: Spring 容器启动时,会扫描带有 @Autowired 注解的字段、方法和构造函数
  2. AutowiredAnnotationBeanPostProcessor: 这是处理 @Autowired 注解的主要类,它实现了 BeanPostProcessor 接口
  3. 依赖解析过程:
    • 根据类型(Type)查找匹配的 Bean
    • 如果找到多个匹配项,尝试使用 @Qualifier 或 bean 名称进行筛选
    • 如果仍有多个匹配项,且 @Autowired 的 required 属性为 true(默认值),则抛出异常
  4. 注入时机:
    • 对于字段和方法:在 Bean 实例化后,属性填充阶段进行注入
    • 对于构造函数:在 Bean 实例化阶段进行注入

简单点其实只要说,是通过依赖注入/反射实现的就可以啦~

说说@Autowired和@Resource注解的区别?

@Autowired 和 @Resource 都用于依赖注入,但有以下区别:

@Autowired (Spring 提供):

  • 默认按类型注入 (byType)
  • 若有多个相同类型的 Bean,需要结合 @Qualifier 指定名称
  • 支持在构造方法、普通方法、字段、参数等位置使用
  • 有 required 属性,可以配置是否必须注入成功
  • 是 Spring 框架特有的注解

@Resource (JavaEE/Jakarta EE 标准):

  • 默认按名称注入 (byName),当找不到名称匹配时才按类型注入
  • 可以通过 name 属性指定 Bean 名称
  • 主要用于字段和 setter 方法
  • 没有 required 类似的属性
  • 是 Java 标准注解,不依赖于 Spring 框架

选择建议:

  • 如果项目重度依赖 Spring,推荐使用 @Autowired
  • 如果考虑代码可移植性,推荐使用 @Resource
  • 如果需要更精确的控制注入方式,@Autowired + @Qualifier 提供了更细粒度的控制

以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// UserRepository接口
public interface UserRepository {
User findById(Long id);
}

// UserRepository的两个实现类
@Repository
public class MySqlUserRepository implements UserRepository {
@Override
public User findById(Long id) {
// MySQL实现
return new User(id, "MySQL用户");
}
}

@Repository
public class MongoUserRepository implements UserRepository {
@Override
public User findById(Long id) {
// MongoDB实现
return new User(id, "MongoDB用户");
}
}

默认情况下,@Autowired会按类型注入,但有两个UserRepository实现会报错。

1
2
3
4
5
6
7
8
9
10
11
12
   // 解决方法1:使用@Qualifier指定bean名称
@Autowired
@Qualifier("mySqlUserRepository") // 明确指定使用哪个实现
private UserRepository sqlRepository;

// @Resource默认按名称注入,所以变量名必须匹配bean名称
@Resource
private UserRepository mySqlUserRepository; // 注入名为"mySqlUserRepository"的bean

// 也可以显式指定name属性
@Resource(name = "mongoUserRepository")
private UserRepository mongoRepo;

5. Bean 的高级特性

什么是三级缓存?

三级缓存是 Spring 解决循环依赖问题的机制,指的是 Spring 中用于存储 Bean 的三个 Map:

  1. 一级缓存 (singletonObjects):
    • 存放完全初始化好的单例 Bean
    • 可以直接使用的 Bean 对象
  2. 二级缓存 (earlySingletonObjects):
    • 存放原始的 Bean 对象(尚未填充属性)
    • 用于解决循环依赖时的临时访问
  3. 三级缓存 (singletonFactories):
    • 存放 Bean 工厂对象
    • 主要作用是提前暴露创建中的 Bean,用于解决循环依赖问题
    • 可以产生原始对象的代理对象

当发生循环依赖时,Spring 的处理流程:

  1. 尝试从一级缓存获取对象,若不存在则继续
  2. 尝试从二级缓存获取对象,若不存在则继续
  3. 从三级缓存获取对象工厂,调用工厂方法创建对象并放入二级缓存
  4. 完成对象的初始化后,将对象放入一级缓存,并从二、三级缓存移除

为什么需要三级缓存?

三级缓存的设计主要解决以下问题:

  1. 解决循环依赖:
    • 在单例 Bean 的情况下,通过提前暴露创建中的 Bean 引用,打破循环
    • 允许 Bean A 引用尚未完全初始化的 Bean B
  2. 支持 AOP 代理:
    • 三级缓存通过 ObjectFactory 可以在需要时生成代理对象
    • 确保即使在循环依赖的情况下,最终注入的也是代理对象而非原始对象
  3. 性能优化:
    • 避免不必要的代理对象创建
    • 只有在真正需要时才创建代理对象

如果仅仅是为了解决循环依赖问题,理论上二级缓存就足够了。三级缓存的引入主要是为了处理 AOP 代理的情况,确保在循环依赖时注入的对象是最终形态的对象(可能是原始对象或其代理)。

Spring如何解决循环依赖问题?

什么是循环依赖:

Spring 主要通过以下机制解决循环依赖问题:

三级缓存机制:

  • 循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖。
  • 一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象。
  • 二级缓存:缓存早期的bean对象(生命周期还没走完)。
  • 三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的。

  • 对象实例化后(即构造函数完成后)、属性填充前,将对象工厂放入三级缓存

  • 允许其他 Bean 引用尚未完全初始化的 Bean

处理流程:

  • A 创建过程中依赖 B
  • B 创建过程中又依赖 A
  • 此时 A 尚未完全初始化,但已经实例化
  • Spring 从三级缓存中获取 A 的早期引用给 B
  • B 完成初始化并注入到 A
  • A 完成初始化

如果A对象是代理对象的话,此时就需要用到三级缓存了:

然而,若是循环依赖发生在实例化之前,即构造函数循环依赖应该如何解决?

答案是使用@lazy懒加载注解:

Spring 中可以出现两个 ID 相同的 bean 吗,如果不行会在什么时候报错?

Spring 容器中不允许存在两个 ID 相同的 Bean。如果出现这种情况,会在以下时机报错:

  1. 容器启动阶段:
    • 在加载 Bean 定义时,如果发现已存在同名的 Bean 定义,会抛出异常
    • 异常类型通常是 BeanDefinitionOverrideExceptionBeanDefinitionStoreException
  2. Bean 注册时:
    • 在向容器注册 Bean 时,如果发现同名 Bean 已存在且不允许覆盖,会报错
  3. 配置冲突场景:
    • 多个 XML 配置文件中定义了相同 ID 的 Bean
    • 配置类中定义的 Bean 与 XML 或其他配置类中的 Bean ID 冲突
    • 组件扫描时发现与已有 Bean 名称冲突

错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl1();
}

@Bean
public MyService myService() { // 编译错误:相同方法名
return new MyServiceImpl2();
}
}

可以通过设置 spring.main.allow-bean-definition-overriding=true 允许 Bean 覆盖,但这通常不是推荐的做法。

6. Spring 配置与内部 Bean

Spring 提供了哪些配置方式?

Spring 框架提供了多种配置方式,可以根据项目需求灵活选择:

  1. XML 配置:

    • 传统方式,使用 XML 文件定义 Bean 及其依赖关系
    • 例如:applicationContext.xml
    1
    2
    3
    <bean id="myService" class="com.example.MyServiceImpl">
    <property name="dao" ref="myDao"/>
    </bean>
  2. 注解配置:

    • 使用注解标记组件和依赖关系
    • 主要注解:@Component, @Service, @Repository, @Controller, @Autowired
    • 需要在 XML 中启用组件扫描或使用 @ComponentScan
  3. Java 配置:

    • 使用 Java 代码进行配置,无需 XML
    • 主要通过 @Configuration 和 @Bean 注解;@Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间的依赖关系。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class StudentConfig {
@Bean
public StudentBean myStudent() {
return new StudentBean();
}

@Bean
public CourseBean myCourse() {
// 直接调用myStudent()方法来引用StudentBean
// Spring保证这里不会创建新的StudentBean实例
return new CourseBean(myStudent());
}
}

在这个例子中,myCourse()方法调用了myStudent()方法来获取StudentBean实例。Spring保证:

  1. myStudent()方法只会被执行一次
  2. myCourse()获得的是容器中已存在的StudentBean实例
  3. 自动建立了CourseBean依赖于StudentBean的关系

这种机制使得在Java配置中定义Bean之间的依赖关系变得非常简单和直观。

现代 Spring 应用通常倾向于使用注解配置和 Java 配置,特别是在 Spring Boot 项目中。

什么是 Spring 的内部 bean?

这个是属于xml属性配置里的知识点,看下就好,毕竟正常人怎么会去写xml呢😅

内部 Bean (Inner Bean) 是指定义在另一个 Bean 内部的 Bean,它仅存在于外部 Bean 的上下文中,不能被其他 Bean 引用。主要特点:

  1. 定义方式: 定义在 <property><constructor-arg> 元素内部
  2. 作用域:
    • 内部 Bean 的作用域始终与外部 Bean 相同
    • 即使内部 Bean 声明为 prototype,也只会被实例化一次(一般都是作为prototype)
  3. 命名:
    • 内部 Bean 不需要 ID 或 name
    • 如果指定了,Spring 容器也会忽略它
  4. 示例:

Spring IOC

1.IOC概述

什么是IoC(控制反转)?

传统编程中,如果你需要使用某个对象,你需要自己创建它:

1
2
3
4
5
6
7
8
// 传统方式
Class A {
public void doSomething() {
// 我需要B,所以我自己创建B
B b = new B();
b.work();
}
}

这就像你去旅游时,你自己研究酒店,自己联系,自己负责预订过程。

而在IoC模式下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// IoC方式
Class A {
private B b; // 我需要B

// 但B是"送上门来的",我不负责创建
public A(B b) {
this.b = b;
}

public void doSomething() {
b.work();
}
}

这就像酒店预订平台(如booking.com):你只需告诉平台你的需求(日期、位置、价格范围),平台负责给你找合适的酒店并安排预订。控制权从你手中转移到了平台手中,这就是”控制反转”。

什么是依赖?

“依赖”指的是一个类需要使用另一个类。在上面的例子中:

  • 类A依赖于类B,因为A要调用B的方法才能完成工作
  • 就像你依赖酒店提供住宿服务才能完成旅行

什么是依赖注入?

“依赖注入”是实现IoC的一种方式,指的是:依赖的对象不由类自己创建,而是由外部提供(注入)。

有三种常见的注入方式:

构造函数注入:通过构造函数提供依赖

1
2
3
public A(B b) {
this.b = b;
}

setter方法注入:通过setter方法提供依赖

1
2
3
public void setB(B b) {
this.b = b;
}

字段注入:在Spring中通过@Autowired等注解直接注入字段

1
2
@Autowired
private B b;

IoC和DI的关系总结

简单来说:

  • IoC是一种思想:不再由调用者创建被调用者,而是由第三方(容器)负责创建和维护
  • DI是这种思想的具体实现方法:通过”注入”的方式将依赖提供给需要的对象

这里我们可以以构造函数为例作为对比区分有无IOC的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 传统方式(没有IoC)
class A {
private B b;

public A() {
// A自己控制B的创建
this.b = new B();
}
}

// 构造函数注入(有IoC)
@Configuration
class A {
@Bean
private B b;

@Bean
public A(B b) {
// A不再控制B的创建,B是从外部传入的
// Spring会自动传入B的实例
this.b = b;
}
}

益处:

  1. 代码更简洁(不需要写创建对象的代码)
  2. 更容易测试(可以注入测试替身)
  3. 更容易替换实现(只需更换注入的对象,不需要修改代码)
  4. 更好的分离关注点(每个类只关注自己的功能)

2.什么是IOC?解决了什么问题?怎么理解控制反转,依赖注入?

IoC 即控制反转(Inversion of Control,缩写为 IoC)。IoC 又称为依赖倒置原则(设计模式六大原则之一),它的要点在于: 程序要依赖于抽象接口,不要依赖于具体实现。它的作用就是用于降低代码间的耦合度。

依赖倒置原则(Dependency Inversion Principle,DIP):

高层模块不应依赖于低层模块,二者都应依赖于抽象;抽象不应依赖于细节,细节应依赖于抽象。

什么被”控制”了?

“控制”指的是对象创建和对象之间关系的控制权

什么被”反转”了?

“反转”指的是控制权的流向发生了反转:

  • 传统方式:控制权在应用程序代码手中;
  • IoC方式:控制权从应用程序代码转移到了外部容器。

Spring IOC 是 Spring 框架的核心,它实现了一种基于容器的对象管理机制。在 Spring IoC 中,控制权由应用程序代码转移到了 Spring 框架中,Spring 框架负责创建对象、管理对象之间的依赖关系、调用对象的方法等操作,应用程序只需要声明需要使用的对象和依赖关系,无需自己负责对象的创建和管理,从而实现了控制反转。

实际开发中的过程就是:

1.通过 @Controller、@Component 等注解声明需要使用的对象。

2.通过 @Resource、@Autowired 等注解声明依赖关系。

个人理解:被动获取叫“反转”,也就是对象声明的时候只标记引用关系,只声明没有实例化,没有new,真正到使用的时候通过注入的方式去组合。

在 Spring IOC 中,容器负责创建和管理对象,接着根据配置文件或者注解中的信息,自动创建和管理对象之间的依赖关系,然后将这些对象注入到应用程序中。应用程序只需要声明需要使用的对象和依赖关系,通过注入的方式获取这些对象,从而避免了硬编码和耦合性的问题。


IoC 的实现方式有两种:

  • 依赖注入(Dependency Injection,简称 DI):不通过 new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造器、函数参数等方式传递(或注入)给类使用。
  • 依赖查找(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象(如getBean方法)。

Spring IoC 是 IoC 的一种实现。DI 是 Spring loC 的主要实现原则。

3.谈谈Spring IOC的实现原理?

Spring loC 的实现原理可以分为两个步骤:

1)扫描和解析配置文件或注解信息,将其转换为内部的对象定义和依赖关系;

2)根据对象定义和依赖关系,使用反射机制动态创建和初始化对象,并将对象注入到需要使用它们的地方。

具体实现过程如下:

  1. 读取配置文件或解析注解信息,将其转换为内部的对象定义和依赖关系。在 Spring 中,可以使用XML 文件或注解来配置对象和依赖关系。Spring 通过解析配置文件或注解信息,将其转换为内部的对象定义和依赖关系(BeanDefinition)放到容器(BeanFactory)中。对象定义包括对象的类型、属性、构造函数等信息,依赖关系包括对象之间的依赖关系、依赖注入方式等信息。
  2. 实例化bean对象:Spring 会根据对象定义的类型和构造函数信息,使用反射机制来创建对象。
  3. 设置属性:实例化后的仍然是一个原生的状态,并没有进行依赖注入。这一步Spring会根据BeanDefinition中的信息进行属性填充,依赖注入。
  4. 调用Aware接口:Spring会检测该对象是否实现了xxxAware接口,如果有会在这里执行完成。Aware主要是能获取到Spring容器中的一些资源,然后可以供后续步骤,例如初始化阶段使用
  5. BeanPostProcessor前置处理:postProcessBeforeInitialzation方法。上述几个步骤后,bean对象已经被正确构造,但如果想要对象被初始化前再进行一些自定义的处理,就可以通过BeanPostProcessor接囗的该方法来实现。
  6. 初始化阶段:该阶段Spring首先会看是否是实现了InitializingBean接口的afterPropertiesSet方法以及是否有自定义的init-method等,如果有会进行调用执行。
  7. BeanPostProcessor后置处理:postProcessAfterInitialzation方法。当前正在初始化的bean对象会被传递进来,我们就可以对这个bean作任何处理,与前面前置处理相对的,这个函数会在InitialzationBean完成后执行,因此称为后置处理。
  8. bean初始化完成可以被使用了。

总结一下,就是扫描读取->实例化->注入->Aware加buff->自定义(前)->初始化buff验证->自定义(后)。

Spring AOP

1.讲一下你对AOP的理解,底层实现原理?

什么是AOP?

AOP(Aspect-Oriented Programming),即面向切面编程,它与 00P( Object-Oriented Programming,面向对象编程)相辅相成,提供了与 OOP 不同的抽象软件结构的视角。

在 OOP 中,我们以类(class)作为我们的基本单元,而 AOP 中的基本单元是 Aspect(切面)。

具体是含义可以理解为:通过代理的方式,在调用想要的对象方法时候,进行拦截处理,执行切入的逻辑,然后再调用真正的方法实现

例如,你实现了一个A对象,里面有 addUser 方法,此时你需要记录该方法的调用次数。 那么你就可以搞个代理对象,这个代理对象也提供了 addUser 方法,最终你调用的是代理对象的 addUser ,在这个代理对象内部填充记录调用次数的逻辑,最终的效果就类似下面代码:

1
2
3
4
5
6
7
8
9
class A代理{
A a;// 被代理的 A
void addUser(User user){
count();// 计数
a.addUser(user);
}
}
最终使用的是:
A代理.addUser(user);

这就叫做面向切面编程,当然具体的代理的代码不是像上面这样写死的,而是动态切入。

2.AOP概念集

3.代理分类

代理大体上可以分为: 动态代理静态代理

  • 动态代理,即在运行时将切面的逻辑进去,按照上面的逻辑就是你实现A类,然后定义要代理的切入点和切面的实现,程序会自动在运行时生成类似上面的代理类。例如JDK代理和CGlib代理都是动态代理,区别我们会在之后讲到。
  • 静态代理,在编译时或者类加载时进行切面的织入,典型的 AspectJ就是静态代理。

AspectJ

Spring AOP 属于运行时增强,而 AspectJ是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ基于字节码操作(Bytecode Manipulation)。Spring AOP 已经集成了 AspectJ,AspectJ应该算的上是 Java 生态系统中最完整的 AOP 框架了。Aspect]J相比于Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ,它比 Spring AOP快很多。

AspectJ通知类型有哪些?

  • Before(前置通知):目标对象的方法调用之前触发

  • After (后置通知):目标对象的方法调用之后触发

  • AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发

  • AfterThrowing(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。

  • Around (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法

动态代理

​ Java动态代理是Java中一种重要的代理模式,它允许在运行时动态地生成代理类和对象,无需编写静态代理类。在Java中,动态代理可以通过Java自带的两种方式实现:基于接口的动态代理(JDK 动态代理)和基于类的动态代理(CGLIB 动态代理)。

1.基于接口的动态代理(JDK 动态代理)

​ 基于接口的动态代理是Java官方提供的一种动态代理实现方式。在这种实现方式中,代理类必须实现一个或多个接口,然后在运行时动态创建代理对象。JDK中提供了一个Proxy类和一个InvocationHandler接口来实现基于接口的动态代理。

​ 首先,需要定义一个实现InvocationHandler接口的代理类,该类实现了代理类的逻辑。这个类中有一个invoke方法,这个方法在代理类的方法被调用时被执行。在运行时通过Proxy类的静态方法newProxyInstance生成代理类对象。这个方法需要三个参数:ClassLoader、代理类需要实现的接口数组和InvocationHandler实现类的实例。当通过代理类对象调用方法时,这个方法首先被转发到InvocationHandler的invoke方法中。在invoke方法中,可以根据代理类方法的不同来执行不同的逻辑,包括调用被代理对象的方法和执行其他的逻辑。最终,代理类的方法被执行完毕,返回结果。

2.基于类的动态代理(CGLIB 动态代理)

​ 基于类的动态代理是通过字节码生成技术实现的。在这种实现方式中,代理类不需要实现接口,而是通过继承一个已有的类来实现代理功能。在Java中,可以通过CGLIB库实现基于类的动态代理。CGLIB(Code Generation Library)是一个高性能的代码生成库,它可以在运行时动态生成字节码来实现类的增强功能。通过CGLIB库,可以直接在运行时创建目标对象的子类,从而实现基于类的动态代理。基于类的动态代理相比于基于接口的动态代理,可以代理那些没有实现任何接口的类,更加灵活。但是它的实现原理比较复杂,我们记住它需要在运行时动态生成字节码,会带来一定的性能开销,这一点就行。

4.两个动态代理的区别?

JDK 代理和 CGLib 代理都是 Spring 默认支持的代理模式,它们的区别如下:

  • 代理对象:JDK 代理只支持面向接口代理,而 CGLib 代理除了接口外,也可以面向普通的类进行代理。
  • 实现原理:JDK 代理是生成接口的匿名实现类,而 CGLib 则还可以生成目标类的子类。
  • 拦截方法:JDK 代理只支持拦截接口中的公共抽象方法,而 CGLib 支持拦截任何非私有的实例方法。

当然,被 final 修饰的除外。Cglib 不能对声明为 final 的方法进行代理。

  • 内部调用支持:JDK 代理不支持代理内部调用,而 CGLib 支持,理由同上一点。

在默认情况下,Spring 会优先使用 JDK 代理(由于性能开销的原因),不过如目标类没有实现一个公共接口,那就会基于 CGLib 进行代理。此外,还有一种特殊情况,那就是基于 @configuration 的配置类,在 Full 模式下,总是固定使用 CGLib代理。

要了解@Configuration的特殊性,这里我们就需要清楚Full和Lite模式了。请先看图:

这个注解有一个属性proxyBeanMethods。这个属性的值默认是true,会通过Cglib对这个配置类进行增强,增强后这个配置类就显得比较“重”,因此叫Full模式。如果我们将这个属性设置成false的话,这个配置类产生的Bean就是原始的对象,比较“轻量级”,叫Lite模式。因此,Full模式和Lite模式最本质的区别是:配置类本身的Bean对象会不会被Cglib增强。

自Spring5.2(对应Spring Boot 2.2.0)开始,内置的几乎所有的@Configuration配置类都被修改为了@Configuration(proxyBeanMethods = false),以此来降低启动时间,为Cloud Native继续做准备。

优点

  • 运行时不再需要给对应类生成CGLIB子类,提高了运行性能,降低了启动时间
  • 可以把该配置类当作一个普通类使用喽:也就是说@Bean方法 可以是private、可以是final

缺点

  • 不能声明@Bean之间的依赖,也就是说不能通过方法调用来依赖其它Bean

5.AOP的应用场景有哪些?

AOP主要用于解决横切关注点(Cross-Cutting Concerns),即跨越多个模块的通用功能,以下为典型场景:

  1. 日志记录(Logging)
  • 作用:统一记录方法调用、参数、返回值或异常。
  • 示例:通过@Around通知记录方法执行时间。
  1. 事务管理(Transaction Management)
  • 作用:自动开启、提交或回滚事务。
  • 示例:Spring的@Transactional注解基于AOP实现。
  1. 安全控制(Security)
  • 作用:校验用户权限或角色。
  • 示例:在方法执行前检查用户是否有访问权限(@Before通知)。
  1. 性能监控(Performance Monitoring)
  • 作用:统计方法耗时或系统资源使用情况。
  • 示例:使用@Around通知计算方法执行时间并上报监控系统。
  1. 缓存管理(Caching)
  • 作用:自动缓存方法结果,避免重复计算。
  • 示例:通过@Around通知实现“缓存命中则直接返回,否则执行方法并缓存结果”。
  1. 异常处理(Exception Handling)
  • 作用:统一处理异常,避免重复的try-catch代码。
  • 示例:使用@AfterThrowing通知捕获特定异常并发送告警。
  1. 参数校验(Validation)
  • 作用:统一校验方法参数合法性。
  • 示例:通过@Before通知检查参数是否符合规则(如非空、范围等)。

6.多个切面如何进行顺序控制

1.通常使用@Order 注解直接定义切面顺序

  • 作用:直接标注在切面类上,通过数值指定优先级。

  • 规则数值越小,优先级越高,切面越先执行。

  • 示例

    1
    2
    3
    4
    5
    6
    @Order(3)
    @Component
    @Aspect
    public class LoggingAspect {
    // 切面逻辑
    }

    此时,该切面的优先级值为3,若另一个切面标注@Order(1),则后者会先执行。

2.实现Ordered 接口

  • 作用:通过实现Ordered接口并重写getOrder()方法,动态定义优先级。

  • 规则返回值越小,优先级越高,切面越先执行。

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    @Component
    @Aspect
    public class LoggingAspect implements Ordered {
    @Override
    public int getOrder() {
    return 1; // 优先级为1,即优先级最高
    }
    }

注意事项:

  1. 优先级覆盖

    • 如果同时使用@Order注解和Ordered接口,**Ordered接口的getOrder()方法返回值会覆盖@Order注解的值**。

    • 例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Order(3)
      @Component
      @Aspect
      public class LoggingAspect implements Ordered {
      @Override
      public int getOrder() {
      return 1; // 实际优先级为1,而非3
      }
      }
  2. 相同优先级问题

    • 若多个切面的优先级值相同,执行顺序可能不确定(取决于Bean的加载顺序)。
    • 可通过显式指定Bean名称或进一步细化优先级值来避免。
  3. 通知类型的执行顺序

    • 在同一切面中,不同通知类型(如@Before@After)的执行顺序遵循Spring的默认规则:
      • @Around@Before → 目标方法 → @Around@After@AfterReturning/@AfterThrowing
    • 不同切面之间的顺序由优先级值统一控制。

总结:

  • 推荐使用 @Order 注解:适合静态定义优先级,代码简洁。
  • 使用 Ordered 接口:适合需要动态计算优先级的场景(例如根据配置调整)。
  • 避免混合使用:若同时使用,需注意Ordered接口会覆盖@Order注解的值。

Spring 事务

1. 什么是事务?

定义:事务是数据库操作的最小工作单元,保证一组操作要么全部成功,要么全部失败。

示例场景

1
2
3
4
5
// 银行转账操作(伪代码)  
public void transfer(Account from, Account to, int amount) {
from.balance -= amount;
to.balance += amount;
}
  • 问题:如果from.balance扣款成功,但to.balance加款失败,数据将不一致。
  • 解决:用事务包裹这两个操作,确保原子性。

2. 事务的ACID特性

特性 说明
原子性 事务内的操作要么全部成功,要么全部回滚
一致性 事务执行前后,数据状态符合业务规则(如总金额不变)
隔离性 并发事务之间互不干扰
持久性 事务提交后,数据永久保存(即使系统崩溃)

3. 为什么需要Spring事务?

传统JDBC事务的痛点

1
2
3
4
5
6
7
8
9
10
Connection conn = dataSource.getConnection();  
try {
conn.setAutoCommit(false); // 开启事务
// 执行SQL操作...
conn.commit(); // 提交
} catch (Exception e) {
conn.rollback(); // 回滚
} finally {
conn.close();
}
  • 缺点:代码冗余、需手动管理连接和异常。

Spring事务的优势

  • 通过声明式事务(如@Transactional)自动管理事务生命周期。
  • 支持多种数据源和事务管理器(如JDBC、Hibernate、JPA)。

4.Spring事务核心原理

这里我们讲解声明式事务的实现原理:

流程示意图

1
[调用者] --> [代理对象] --> [事务管理器] --> [真实对象]  
  • 关键步骤
    1. Spring为被@Transactional标记的类生成代理对象。
    2. 调用代理对象的方法时,先开启事务(beginTransaction())。
    3. 执行真实方法,成功则提交事务(commit()),失败则回滚(rollback())。

5.@Transactional注解详解

1. 基本用法

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service  
public class OrderService {
@Autowired
private OrderRepository orderRepository;

@Transactional(
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED,
rollbackFor = Exception.class
)
public void placeOrder(Order order) {
orderRepository.save(order);
// 其他业务逻辑...
}
}

2. 参数说明

参数 作用 默认值
isolation 事务隔离级别 数据库默认隔离级别
propagation 事务传播行为 REQUIRED
timeout 事务超时时间(秒) -1(不超时)
rollbackFor 触发回滚的异常类型 RuntimeException

3. 常见错误

错误1:非public方法使用@Transactional

1
2
3
4
@Transactional  
private void internalUpdate() { // 事务失效!
// ...
}

错误2:异常被捕获未抛出

1
2
3
4
5
6
7
8
@Transactional  
public void updateData() {
try {
jdbcTemplate.update("...");
} catch (DataAccessException e) {
// 未抛出异常,事务不会回滚!
}
}

6.事务失效场景与传播行为

1. 事务失效场景总结

场景 原因 解决方案
非public方法 Spring AOP无法代理私有方法 改为public方法
自调用 类内部方法调用不走代理 通过AopContext.currentProxy()调用
异常被捕获 未触发回滚条件 在catch块中抛出异常
异常抛出类型错误 / 修正
数据库引擎不支持 如MyISAM引擎 改用InnoDB引擎
事务传播行为使用错误 权限出错 改为正确权限

2. 事务传播行为详解

传播行为 说明
REQUIRED 默认行为,存在事务则加入,否则新建事务
REQUIRES_NEW 新建独立事务,挂起当前事务(两个事务独立提交/回滚)
SUPPORTS 有事务则加入,没有则以非事务执行
NOT_SUPPORTED 以非事务执行,挂起当前事务
NEVER 以非事务执行,如果存在事务则抛出异常
MANDATORY 必须在事务中运行,否则抛出异常
NESTED 嵌套事务,外层事务回滚会导致内层回滚,内层回滚不影响外层(需数据库支持)

代码示例:REQUIRES_NEW

1
2
3
4
5
6
7
@Service  
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog() {
// 记录日志,独立于主事务
}
}

3.事务隔离行为

什么是脏读、幻读、不可重复读?

1. 脏读(Dirty Read)

场景:你看到同事正在修改一份共享文档,但还没保存,你直接复制了他未保存的内容。
结果:如果同事最后放弃修改,你复制的就是错误数据。

技术解释
事务A修改了数据但未提交,事务B读取了这份未提交的“脏数据”。如果事务A最终回滚,事务B得到的就是无效数据。

解决: 读已提交、可重复读、串行化

2. 不可重复读(Non-repeatable Read)

场景:你查看天气预报,显示今天晴天;5分钟后刷新页面,发现变成了雨天。
结果:同一数据在同一事务中被多次读取,结果不一致。

技术解释
事务A第一次读取数据后,事务B修改并提交了这份数据。事务A再次读取时,发现数据变了。

解决: 可重复读、串行化

3. 幻读(Phantom Read)

场景:你统计公司员工人数是100人,这时HR新增了1人并提交。你再次统计,发现变成了101人。
结果:同一事务中,多次查询符合条件的数据行数不一致,仿佛出现了“幻觉”。

技术解释
事务A根据条件查询一批数据(如age > 30),事务B新增或删除了符合条件的数据并提交。事务A再次查询时,发现多出或少了数据行。

解决: 串行化

在TransactionDefinition接口中定义了五个表示隔离级别的常量:

  • ISOLATION_DEFAULT:使用后端数据库默认的隔离界别,MySQL 默认可重复读,Oracle 默认读已提交。
  • ISOLATION_READ_UNCOMMITTED:读未提交,最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • ISOLATION_READ_COMMITTED:读已提交,允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • ISOLATION REPEATABLE READ:可重复读,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • ISOLATION_SERIALIZABLE:串行化,最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

7.事务案例

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service  
public class BankService {
@Autowired
private JdbcTemplate jdbcTemplate;

@Transactional(rollbackFor = Exception.class)
public void transfer(String fromId, String toId, int amount) {
// 扣款
jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE id = ?", amount, fromId);
// 模拟异常
if (amount < 0) throw new RuntimeException("金额非法");
// 加款
jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE id = ?", amount, toId);
}
}

测试步骤

  1. 正常转账:transfer("A", "B", 100) → 数据一致。
  2. 非法金额:transfer("A", "B", -100) → 事务回滚,数据不变。

Spring常用注解

  1. 你用过哪些重要的 Spring 注解
    常用的 Spring 注解包括:

    • @Autowired:自动注入依赖。
    • @Component:标记类为 Spring 组件,用于自动扫描。
    • @Service@Repository@Controller:分别用于服务层、数据访问层和表现层。
    • @Configuration:定义配置类,通常与 @Bean 搭配使用。
    • @Bean:显式声明 Bean。
    • @RequestMapping@GetMapping:映射 HTTP 请求到控制器方法。
    • @RestController:组合 @Controller@ResponseBody,用于 RESTful API。
    • @Qualifier:解决自动装配时的歧义性。
    • @Value:注入配置文件中的属性值。
  2. 如何在 Spring 中启动注解装配
    在 XML 配置中启用:

    1
    2
    <context:component-scan base-package="com.example" />
    <context:annotation-config />

    在 Java 配置类中启用:

    1
    2
    3
    4
    @Configuration
    @ComponentScan("com.example")
    @EnableAutoConfiguration
    public class AppConfig { ... }
  3. @Configuration 和 @Component 有什么区别?

    • @Configuration:标记类为配置类,其中的 @Bean 方法会被代理以确保单例,适合显式定义 Bean。
    • @Component:通用注解,标记类为组件,由 Spring 自动扫描并注册为 Bean,用于隐式装配。
  4. @Component, @Controller, @Repository, @Service 有何区别?

    • @Component:通用组件。
    • @Controller:标记为 MVC 控制器,处理 HTTP 请求。
    • @Service:标记业务逻辑层组件。
    • @Repository:标记数据访问层组件,支持异常转换(如将 JDBC 异常转换为 Spring 统一异常)。
  5. @Component 和 @Bean 有什么区别?

    • @Component:作用于类,通过组件扫描自动注册 Bean。
    • @Bean:作用于方法(通常在 @Configuration 类中),显式定义 Bean,适合第三方库的集成。
  6. @Required 注解有什么用?
    @Required 用于标记必须注入的依赖(Setter 方法),若未注入会抛出异常。现已被 @Autowired(required=true) 取代。

  7. @Autowired 注解有什么用?
    自动按类型注入依赖,可作用于构造器、字段或方法。若有多个候选 Bean,需配合 @Qualifier 指定名称。

  8. @Qualifier 注解有什么用?
    当存在多个相同类型的 Bean 时,通过 @Qualifier("beanName") 指定具体注入的 Bean 名称。

  9. @RequestMapping 注解有什么用?
    映射 HTTP 请求到控制器方法,可指定 URL、请求方法(GET/POST 等)、请求头等属性。

  10. @RequestMapping 和 @GetMapping 的不同之处?

    • @RequestMapping(method=RequestMethod.GET):需显式定义请求方法。
    • @GetMapping:是 @RequestMapping 的简写,仅处理 GET 请求。
  11. @Controller 注解有什么用?
    标记类为 Spring MVC 控制器,处理 HTTP 请求,通常结合 @RequestMapping 定义路由。

  12. @RestController 和 @Controller 有什么区别?

    • @Controller:返回视图名称(如 JSP)。
    • @RestController:组合 @Controller@ResponseBody,直接返回数据(如 JSON/XML)。
  13. @RequestParam 和 @PathVariable 的区别?

    • @RequestParam:从 URL 查询参数中取值(如 /user?name=Alice)。
    • @PathVariable:从 URL 路径模板中取值(如 /user/{id})。
  14. 返回 JSON 格式使用什么注解?
    使用 @ResponseBody 标记方法返回值,或在类上使用 @RestController(自动为所有方法添加 @ResponseBody)。

Spring MVC

MVC是什么?MVC设计模式的好处有哪些

Spring MVC是一个基于]ava的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把**模型(model)-视图(view)-控制器(controller)**分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。

流程如下:

1.用户通过View 页面向服务端提出请求,可以是表单请求、超链接请求、AJAX 请求等;

2.服务端 Controler 控制器接收到请求后对请求进行解析,找到相应的Model,对用户请求进行处理Model 处理;

3.将处理结果再交给 Controller(控制器其实只是起到了承上启下的作用);

4.根据处理结果找到要作为向客户端发回的响应View 页面,页面经染后发送给客户端。

MVC设计模式的好处:

  • 分层设计,实现了业务系统各个组件之间的解耦,有利于业务系统的可扩展性,可维护性。
  • 有利于系统的并行开发,提升开发效率。

了解SpringMVC的处理流程吗

SpringMVC分为两个版本:早期ModelAndView版本和现在的前后端分离版本。以下分别介绍:

早期JSP版本

  1. DispatcherServlet 处理浏览器发起的请求。

  2. DispatcherServlet 根据用户或默认的配置使用 HandlerMapping 查找可处理请求的处理器。

  3. DispatcherServlet 拿到 HandlerMapping 返回的处理器链 HandlerExecutionChain。整个处理器链包含拦截器和处理。

  4. DispatcherServlet 将处理器适配为 HandlerAdapter。

  5. DispatcherServlet 使用拦截器进行请求前置处理。

  6. DispatcherServlet 使用处理器进行请求处理。

  7. DispatcherServlet 使用拦截器进行请求后置处理。

  8. DispatcherServlet 从拦截器或处理器中提取到模型及视图 ModelAndView。

  9. DispatcherServlet 使用视图解析器 ViewResolver 解析视图出视图 View。

  10. DispatcherServlet 渲染视图,响应请求。

当前版本

① 用户发送出请求到前端控制器DispatcherServlet

② DispatcherServlet收到请求调用HandlerMapping(处理器映射器)

③ HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet

④DispatcherServlet调用HandlerAdapter(处理器适配器)

⑤ HandlerAdapter经过适配调用具体的处理器(Handler/Controller)

⑥方法上添加了@ResponseBody

⑦通过HttpMessageConverter来返回结果转换为JSON并响应

HandlerMapping,HandlerAdapter介绍下?

  • DispatcherServlet:前置控制器,是整个流程控制的核心,控制其他组件的执行,进行统一调度,降低组件之间的耦合性,相当于总指挥。

  • Handler:处理器,完成具体的业务逻辑,相当于 Servlet 或 Action。

  • HandlerMapping:DispatcherServylet 接收到请求之后,通过 HandlerMapping 将不同的请求映射到不同的Handler.

    本质是一个装载路径和类名/方法名的键值对,返回给前端控制器HandlerExecutionChain和拦截器。

  • HandlerInterceptor:处理器拦截器,是一个接口,如果需要完成一些拦截处理,可以实现该接口。

  • HandlerExecutionChain:处理器执行链,包括两部分内容:Handler 和 HandlerInterceptor(系统会有一个默认HandlerInterceptor,如果需要额外设置拦截,可以添加拦截器)。

  • HandlerAdapter:处理器适配器,Handler 执行业务方法之前,需要进行一系列的操作,包括表单数据的验证、数据类型的转换、将表单数据封装到 JavaBean 等,这些操作都是由 HandlerApater 来完成,开发者只需将注意力集中业务逻辑的处理上,DispatcherServlet 通过 HandlerAdapter 执行不同的 Handler。

    这里黑马的解释比较通俗:可以对多元的参数和返回值进行处理。比如说,参数/返回值是User user的情况下,就需要适配器使得方法能够正确地接收参数。

  • ModelAndView:装载了模型数据和视图信息,作为 Handler 的处理结果,返回给 DispatcherServlet.

  • ViewResolver:视图解析器,DispatcheServlet 通过它将罗辑视图解析为物理视图,最终将清染结果响应给安户端。

SpringMVC常用注解

  • @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。

  • @RequestBody:注解实现接收http请求的json数据,将json反序列化为java对象。

  • @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。

  • @Controller:控制器的注解,不能用别的注解代替。

对于@RequestBody,这里可以举一个例子:

首先创建实体类;

1
2
3
4
5
6
7
8
@lombok	//使用lombok进行getter和setter方法的调用
public class User {
private String name;
private String email;

// 必须包含无参构造函数(JSON 反序列化需要)
public User() {}
}

接着创建Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

@PostMapping("/addUser")
public String addUser(@RequestBody User user) {
// 这里可以处理接收到的 user 对象
return "User added: " + user.getName() + ", Email: " + user.getEmail();
}
}

这样,当前端请求这样发送来时:

1
2
3
4
{
"name": "John Doe",
"email": "john@example.com"
}

收到的回复就是:

1
User added: John Doe, Email: john@example.com

全局异常处理、自定义异常

全局异常处理流程如下:

SpringMVC提供了两种全局异常捕获和处理的实现方式:

第一种:自定义类并实现 HandlerExceptionResolver 接口 并实现 resolveException 方法进行处理全局异常

第二种:通过SpringMVC提供的特定注解(**@ControllerAdvice + @ExceptionHandler**)方式来进行处理全局异常

这里用@ControllerAdvice + @ExceptionHandle做示例:

测试的controller层:

测试结果如下:

http://localhost:8080/hi/1

http://localhost:8080/hi/2

http://localhost:8080/hi/3

这里也用到了AOP的思想:

  • 动态代理:Spring 通过动态代理技术将 @ControllerAdvice 的逻辑织入到请求处理流程中
  • 执行时机:异常处理发生在控制器方法执行之后,响应返回给客户端之前

另外还想说一下,我一直想把拦截器和异常处理器作类比,但很可惜这是错误的:

  • 拦截器
    • 必定触发(除非被前置拦截)
    • 执行顺序:preHandleControllerpostHandleafterCompletion
  • 异常处理器
    • 仅在 Controller 或拦截器抛出异常时触发
    • 若异常在 preHandle 阶段抛出,异常处理器不会生效
能力 拦截器 异常处理器
修改请求/响应内容
阻止 Controller 执行
获取异常对象
直接返回业务错误响应

其实应该说,除了两者都在Spring MVC的控制层附近工作以外,几乎就没有什么相同点了😅

拦截器和过滤器的区别是什么?

Interceptor

拦截器必须实现 org.springframework.web.servlet 包的 HandlerInterceptor。此接口定义了三种方法:

  • preHandle:在执行实际处理程序之前调用。
  • postHandle:在执行完实际程序之后调用。
  • afterCompletion:在完成请求后调用。

拦截器的典型使用场景如下:

  • 日志记录:可用于记录请求日志,便于信息监控和信息统计;
  • 权限检查:可用于用户登录状态的检查;
  • 统一安全处理:可用于统一的安全效验或参数的加密/解密等.

以下是一个简单的拦截器demo:

1.创建拦截器类:

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
40
41
42
43
44
@Component
public class DemoInterceptor implements HandlerInterceptor {

private static final Logger log = LoggerFactory.getLogger(DemoInterceptor.class);

// 请求到达 Controller 前执行
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
log.info("🚀 [PreHandle] 请求路径: {}", request.getRequestURI());

// 示例:简单鉴权逻辑
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "缺少有效凭证");
return false; // 拦截请求
}

return true; // 放行请求
}

// Controller 执行后,视图渲染前执行
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
log.info("📦 [PostHandle] 响应状态: {}", response.getStatus());
}

// 请求完成后执行(视图渲染完成)
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
if (ex != null) {
log.error("❌ [AfterCompletion] 请求异常: {}", ex.getMessage());
} else {
log.info("✅ [AfterCompletion] 请求完成");
}
}
}

2.注册拦截器类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

@Autowired
private DemoInterceptor demoInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor)
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/public/**"); // 排除路径
}
}

3.创建Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class TestController {

@GetMapping("/hello")
public String hello() {
return "Hello from protected API!";
}

@GetMapping("/public/data")
public String publicData() {
return "Public data (不需要拦截)";
}
}

Filter

要使用Filter,必须实现javax.servlet.Filter接口:

1
2
3
4
5
6
7
8
9
public interface Filter {
//web应用加载进容器,Filter对象创建之后,执行init方法初始化,用于加载资源,只执行一次。
public default void init(FilterConfig filterConfig) throws ServletException {}
//每次请求或响应被拦截时执行,可执行多次。
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
//web应用移除容器,服务器被正常关闭,则执行destroy方法,用于释放资源,只执行一次。
public default void destroy() {}
}
  • init和destroy是default方法,实现类可以不用实现。
  • doFilter必须实现,也就是说,作为一个过滤器,doFilter必须要定义。
  • doFlilter方法中传进来的FilterChain对象用来调用下一个过滤器。

区别

一、实现原理不同

  • 过滤器的实现基于回调函数
  • 拦截器基于Java的反射机制【动态代理】实现。

二、使用范围不同

  • 过滤器是Servlet的规范,需要实现javax.servlet.Filter接口,Filter使用需要依赖于Tomcat等容器。
  • 拦截器是Spring组件,定义在org.springframework.web.servlet包下,由Spring容器管理【又有更加丰富的生缪那个周期处理方法,细粒度,且能够使用Spring中的资源】,不依赖Tomcat等容器。

三、触发时机不同

这一段在HandlerInterceptor类的注释上可以发现,两者的触发时机是不同的:

  • 过滤器:对请求在进入后Servlet之前或之后进行处理。
  • 拦截器:对请求在handler【Controller】前后进行处理。

四、执行顺序不同

同时配置了过滤器和拦截器的情形:

1
2
3
4
5
6
7
8
9
10
11
MyFilter1 前
MyFilter2 前
MyInterceptor1 在Controller前执行
MyInterceptor2 在Controller前执行
controller方法执行...
MyInterceptor2 Controller之后,视图渲染之前
MyInterceptor1 Controller之后,视图渲染之前
MyInterceptor2 视图渲染完成之后执行
MyInterceptor1 视图渲染完成之后执行
MyFilter2 后
MyFilter1 后
  • 过滤器的顺序

每一次都将chain对象传入,达到最后接口回调的效果:

  • 拦截器的顺序

preHandle1 -> preHande2 -> 【Controller】 -> postHandle2 -> postHandle1 -> afterCompletion2 -> afterComplention1 preHandle按照注册顺序,后两个与注册顺序相反。

  • 一个拦截器的preHandle为false,则之后的所有拦截器都不会执行。
  • 一个拦截器的preHandle为true,则这个拦截器的triggerAfterCompletion一定会执行。
  • 只有所有的拦截器preHandler都为true,也就是正常执行,postHandle才会执行。

五、使用的场景不同

因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;

而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。

SpringBoot

自动装配

自定义 starter

常用注解

Spring SPI

模式注解

Mybatis

执行流程

延迟加载

二级缓存

sql 注入

Mybatis 设计模式