Spring全家桶

Spring Framework

Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。

Spring官网列出的Spring的6个特征:

  • 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。
  • 数据访问:事务,DAO支持,JDBC,ORM,编组XML。
  • Web支持:Spring MVC和Spring WebFlux Web框架。
  • 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言:Kotlin,Groovy,动态语言。

下图对应的是Spring 4.x的版本,目前最新的5.x版本中Web模块的Portlet组件已经被废弃掉,同时增加了用于异步响应式处理的WebFlux组件。

  • Spring Core:基础,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。
  • Spring Aspects:该模块为与AspectJ的集成提供支持。
  • Spring AOP:提供面向切面的编程实现。
  • Spring JDBC:Java数据库连接。
  • Spring JMS:Java消息服务。
  • Spring ORM:用于支持Hibernate等ORM工具。
  • Spring Web:为创建Web应用程序提供支持。

IoC(Inversion of Control,控制反转)

1. 核心思想:从"主动索取"到"被动接受"

在传统开发中,如果 A 类依赖 B 类,A 必须亲自 new B()。这意味着 A 必须知道 B 的构造细节,两者产生了硬编码耦合

  • 控制反转 (IoC) :将对象的创建、组装、生命周期管理的权利 移交给第三方容器(Spring)。

  • 依赖注入 (DI) :IoC 的具体实现手段。容器在运行时动态地将依赖对象注入到组件中。

底层哲学:好莱坞原则(Don't call us, we'll call you)。你只需要声明"我需要什么",至于什么时候给、怎么给,由容器说了算。

2. 核心模型

要实现 IoC,Spring 抽象出了两个极其关键的模型:

A. BeanDefinition

Spring 不会直接操作 .class 文件,而是先将其扫描并解析成 BeanDefinition 对象。

  • 它记录了 Bean 的所有元数据:类名、作用域(Scope)、是否懒加载、构造函数参数、属性值、初始化方法名等。

B. BeanFactory 与 ApplicationContext

  • BeanFactory :这是 IoC 的底层引擎(根接口),负责最基础的 Bean 实例化和依赖注入,采用延迟加载

  • ApplicationContext :是 BeanFactory 的子接口,增加了国际化、事件广播、资源加载等高级功能,并在启动时预加载所有单例 Bean。

BeanFactory 与 ApplicationContext 的底层区别

特性 BeanFactory ApplicationContext
加载时机 懒加载 。只有在调用 getBean 时才创建 Bean。 预加载。启动时就创建所有非懒加载的单例 Bean。
扩展功能 仅提供基础的 IoC/DI 支持。 继承了 BeanFactory,提供国际化、AOP 集成、事件传播。
底层实现 它是 IoC 的底层引擎,轻量级。 内部持有 DefaultListableBeanFactory 的引用,功能更全面。

ApplicationContext提供了一种解析文本消息的方法,一种加载文件资源(如图像)的通用方法,它们可以将事件发布到注册为侦听器的bean。

底层原理

Spring IoC 的核心本质可以概括为:将配置元数据(XML/注解)转化为内部数据结构(BeanDefinition),再通过反射机制和设计模式(工厂)驱动对象的全生命周期。

1. 核心基石:BeanDefinition (元数据建模)

在 Spring 内部,你写的每一个 class 并不是直接丢进 Map 里的。Spring 首先会将类信息"抽象化"为一个 BeanDefinition 对象。

  • 它存了什么? 类的全路径、是否单例、是否懒加载、依赖的对象、初始化/销毁方法、构造函数参数等。

  • 为什么要这一层? 它是解耦的关键。有了 BeanDefinition,Spring 就不再依赖具体的类,而是依赖这套"说明书"。

2. 核心引擎:DefaultListableBeanFactory

如果你去翻源码,你会发现 ApplicationContext 只是个"空壳",它内部真正干活的对象是 DefaultListableBeanFactory

它内部维护了几个核心集合(数据结构):

  • beanDefinitionMap : 一个 ConcurrentHashMap存放所有的 BeanDefinition

  • singletonObjects: 一级缓存,存放已经创建好的、完整的单例 Bean。

  • beanDefinitionNames : 一个 List,记录 Bean 注册的顺序。

3. 运行过程

IoC 的运行过程可以高度概括为三个阶段:解析、注册、实例化

第一阶段:解析与注册 (Loading & Registration)

  1. Resource 定位 :通过 ResourceLoader 找到 XML 或扫描 @Component 路径。

  2. 解析 :BeanDefinitionReader 读取资源,将其解析为 BeanDefinition

  3. 注册 :调用 BeanDefinitionRegistry.registerBeanDefinition,把"说明书"存入 beanDefinitionMap 中。

第二阶段:预处理 (BeanFactoryPostProcessor)

在 Bean 还没实例化之前,Spring 允许你修改这些"说明书"。

  • 典型案例PropertyPlaceholderConfigurer。它会扫描 BeanDefinition,把里面的 ${db.url} 占位符替换成真实的配置文件内容。

第三阶段:实例化与依赖注入 (The Lifecycle)

当调用 getBean() 时,触发以下流程:

  1. 实例化 (Instantiation)

    • 默认使用 CGLIBJDK 反射 调用构造函数创建原始对象。
  2. 属性填充 (Populate Bean)

    • 这一步处理 DI (依赖注入) 。Spring 会递归地调用 getBean 来获取依赖项,并通过反射 设置字段值。
  3. 初始化 (Initialization)

    • Aware 接口回调 :注入 BeanNameBeanFactory 等环境信息。

    • BeanPostProcessor (前置) :执行 postProcessBeforeInitialization

    • 自定义 Init 方法 :执行 @PostConstructInitializingBean.afterPropertiesSet

    • BeanPostProcessor (后置)AOP 动态代理通常就在这一步发生 ,它会返回一个代理后的对象。

Bean的生命周期

大概的流程模型是这样:

  1. 实例化 (Constructor)

  2. 属性填充 (DI)

  3. Aware 接口回调

  4. BeanPostProcessor 前置处理 (处理 @PostConstruct)

  5. 初始化 (InitializingBean / init-method)

  6. BeanPostProcessor 后置处理 (AOP 的切入点)

  7. 销毁 (DisposableBean / destroy-method)

第一阶段:实例化前后的扩展 (Instantiation)

这是对象从"说明书"(BeanDefinition)变成"内存对象"的过程。

  1. InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()

    • 作用:在调用构造函数之前触发。

    • 高级技巧 :这里是短路点。如果你在这里返回了一个代理对象,Spring 将不再走后续流程,直接进入初始化后的阶段。

  2. 实例化(Bean 的构造)

    • 通过反射调用构造函数。
  3. MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition()

    • 作用 :处理 @Autowired@Value 等注解的元数据采集。
  4. InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()

    • 作用 :实例化后,属性填充前。如果返回 false,则跳过后续属性填充流程。
第二阶段:属性赋值与依赖注入 (Populate)
  1. InstantiationAwareBeanPostProcessor.postProcessProperties()

    • 核心逻辑 :真正处理 @Autowired@Resource 等注解,完成依赖注入
  2. 属性填充

    • 如果是 XML 配置,这里会执行具体的 Setter 注入。
第三阶段:初始化阶段 (Initialization) ------ 核心扩展点

这是程序员最常干预的阶段。

1. Aware 接口回调

Spring 会按顺序调用各种 Aware 接口,让 Bean 感知到容器的存在:

  • BeanNameAware:获取 Bean 的名称。

  • BeanFactoryAware:获取当前 BeanFactory。

  • ApplicationContextAware:获取整个应用上下文。

2. BeanPostProcessor.postProcessBeforeInitialization()

  • 关键点 :处理 @PostConstruct 注解。它由 CommonAnnotationBeanPostProcessor 实现。

3. 初始化

  • InitializingBean.afterPropertiesSet():实现该接口自定义逻辑。

  • Custom init-method :在 XML 或 @Bean(initMethod = "...") 中定义的初始化方法。

4. BeanPostProcessor.postProcessAfterInitialization() ------ AOP 的发生地

  • 核心原理 :这是生命周期中最强大的扩展点。Spring 的 AOP 代理对象通常就在这里生成。

  • 注意:这里返回的对象会替换掉原始的 Bean,最终存入单例池。

第四阶段:使用与销毁 (Usage & Destruction)
  1. Bean 准备就绪

    • 此时 Bean 已经放入单例池,可以被业务代码使用了。
  2. 销毁前扩展

    • @PreDestroy:在销毁动作开始前执行。
  3. 销毁回调

    • DisposableBean.destroy():实现该接口。

    • Custom destroy-method:自定义的销毁方法。

Bean的作用域

在 Spring 中,作用域(Scope) 决定了容器如何创建 Bean 实例,以及这个实例在什么时候、什么范围内被共享。

Spring 核心作用域(最常用)

这是所有 Spring 应用(包括非 Web 应用)都通用的作用域。

Singleton(单例 - 默认)

  • 定义在整个 Spring IoC 容器中,该 Bean 只有一个实例。

  • 加载时机 :默认在容器启动、预初始化时创建。

  • 线程安全不是线程安全的 。因为多个线程共享同一个实例,如果 Bean 内部有成员变量(有状态),并发修改会导致数据混乱。

  • 场景:无状态的服务类(Service)、数据访问类(DAO)、工具类(Utils)。

Prototype(多例/原型)

  • 定义 :每次通过 getBean() 获取时,容器都会创建一个新的实例。

  • 加载时机延迟加载,只有在用到时才创建

  • 生命周期Spring 不管"售后" 。容器负责创建并初始化实例交给客户端,但之后不再管理它的生命周期,也不会调用其 destroy 销毁方法。

  • 场景:有状态的 Bean、非线程安全的对象。

Web 相关作用域

这些作用域仅在 Web 环境下(如 Spring MVC)有效。

作用域 范围 生命周期描述
Request HTTP 请求 每次 HTTP 请求创建一个实例,请求结束,Bean 销毁。
Session HTTP 会话 同一个 Session 共享一个实例,Session 过期,Bean 销毁。
Application ServletContext 整个 Web 应用生命周期内只有一个实例(类似全局单例)。
WebSocket WebSocket 会话 在一次完整的 WebSocket 会话中共享一个实例。
作用域失效问题(Scope Breakdown)

如果在一个单例(Singleton)Bean里 注入了一个多例(Prototype)Bean,会发生什么?

想象你有两个类:

  • ServiceA (Singleton):单例,全公司只有一台复印机。

  • TaskB (Prototype):多例,每次复印需要的纸张。

java 复制代码
@Component
@Scope("singleton")
public class ServiceA {
    @Autowired
    private TaskB taskB; // 注入多例 Bean

    public void doWork() {
        System.out.println("ServiceA 使用了: " + taskB);
    }
}

@Component
@Scope("prototype")
public class TaskB {
    // 每次创建都会有一个新的对象地址
}

预期的结果: 每次调用 serviceA.doWork(),打印出的 taskB 地址都不一样(因为taskB是多例)。

真实的结果: 无论调用多少次,打印出的 taskB 永远是同一个对象( 这个多例 Bean 表现得像单例一样**)**。

问题的根源在于 "依赖注入只发生一次"

  1. 容器启动 :Spring 扫描到 ServiceA 是单例,开始创建它。

  2. 寻找依赖Spring 发现 ServiceA 依赖 TaskB

  3. 创建依赖Spring 去容器里找 TaskB,发现是 Prototype,于是 "嘭" 地一声创建了一个全新的 TaskB 实例。

  4. 完成注入 :Spring 把这个全新的 TaskB 塞进了 ServiceA 的成员变量里。

  5. 结束ServiceA 组装完成,进入单例池。

关键点: 因为 ServiceA 是单例,它只会被组装一次 。一旦组装完成,它肚子里的那个 TaskB 就被"焊死"了。以后你再怎么调用 ServiceA,它都不会再去向 Spring 容器要新的 TaskB

如何解决?

如果你确实需要每次都拿到新的多例 Bean,有三种常见的方法:

@Lookup 注解(推荐)

这是最优雅的方式。Spring 会动态拦截这个方法的调用,每次都去容器里重新找一次 TaskB。Spring 会动态生成子类重写该方法,确保每次调用都返回新实例。

放弃注入,手动获取 :每次用的时候调用 applicationContext.getBean()(不推荐,侵入性强)。

使用 ScopedProxy:通过 AOP 代理,在每次访问该属性时,由代理对象去容器里找一个新的目标实例。

Bean的线程安全问题

Spring 框架本身并没有对 Bean 进行任何多线程封装,也没有提供线程安全的保证。

Bean 是否线程安全,完全取决于 Bean 的作用域(Scope) 以及 它是"有状态"还是"无状态"的。

Singleton(单例)------ 有风险

由于单例 Bean 在整个容器中只有一个实例,所有的线程都会共享这个对象。

  • 如果这个 Bean 是**无状态(Stateless)**的(如典型的 Service、DAO、Controller),它是逻辑上"线程安全"的。

  • 如果这个 Bean 是有状态(Stateful)的(即内部有可以被修改的成员变量),那它就是线程不安全的。

Prototype(多例)------ 相对安全

每次获取都会创建一个新实例,不涉及线程间的共享,因此不存在竞态条件。但如果这个多例 Bean 被注入到了一个单例 Bean 中,或者被传递给了其他线程,依然需要开发者自己保证安全

"有状态" vs "无状态"

这是判断线程安全最务实的标准:

  • 无状态(Stateless)类中没有成员变量,或者成员变量只 读(如通过 IoC 注入的其他 Service)。

    • 例子 :大多数 UserServiceUserMapper

    • 结论安全

  • 有状态(Stateful)类中定义了可以被改变的成员变量 (如 private int count;private List list;),用于存储中间业务数据。

    • 例子:一个用来统计访问次数的单例 Controller。

    • 结论不安全

为什么 Controller 和 Service 通常不需要考虑线程安全?

在典型的 Spring Boot 场景中,虽然 Controller 是单例,但我们几乎不在其中定义"业务相关的成员变量"。

  1. 局部变量 :所有业务逻辑的数据都存在于方法内部的局部变量中。每个线程调用方法时,局部变量会存放在各自线程的虚拟机栈中,彼此隔离。

  2. 只读注入 :注入的 MapperService 也是无状态单例,这种"只读"链条是安全的。

如果你必须在单例 Bean 中存储某种"状态",请参考以下资深工程师的解决方案:

方案 A:使用 ThreadLocal (最推荐)

ThreadLocal 为每个线程提供了一个独立的变量副本。这在处理"当前登录用户"、"数据库连接"或"事务上下文"时非常有用。

  • 原理:将变量绑定到当前线程,实现线程隔离。
方案 B:使用并发容器或原子类

如果确实需要跨线程累计数据,使用 AtomicIntegerConcurrentHashMap 等。

方案 C:改变作用域

将 Scope 设置为 prototyperequestsession。但要注意之前讨论过的"作用域失效"注入问题。

方案 D:避免成员变量

尽量将状态通过方法参数传递,或者封装到专门的 Request DTO 对象中,随方法栈生灭。

循环依赖

简单来说,循环依赖就是:A 对象的创建依赖于 B,而 B 对象的创建又依赖于 A。

循环依赖的两种形态

在 Spring 中,循环依赖通常表现为以下两种形式:

A. 直接循环依赖(双向依赖)

这是最常见的场景:Bean A 注入了 Bean B,同时 Bean B 也注入了 Bean A。

A <---> B

B. 间接循环依赖(环形依赖)

涉及三个或更多的 Bean 形成一个闭环。

A ---> B ---> C ---> A

要理解为什么循环依赖棘手,我们需要回到我们之前聊过的 Bean 生命周期

  1. Spring 尝试创建 Bean A

  2. 实例化 A:在堆里开辟内存,拿到 A 的原始引用(此时 A 还是个空壳)。

  3. 属性填充 A:Spring 发现 A 需要 B,于是去容器里找 B。

  4. 创建 Bean B:容器里没有 B,开始创建 B。

  5. 实例化 B:拿到 B 的原始引用。

  6. 属性填充 B:Spring 发现 B 需要 A,于是又去容器里找 A。

  7. 死锁点 :此时 A 还没创建完(还在第 3 步),如果没有任何机制,Spring 就会在这里无限套娃,最终抛出 BeanCurrentlyInCreationException

循环依赖的分类与 Spring 的处理能力
依赖类型 作用域 是否能解决 理由
Setter 注入 Singleton Spring 利用三级缓存,通过提前暴露"半成品"对象来打破循环。
构造器注入 Singleton 实例化需要调用构造函数,构造函数需要参数。如果参数还没实例化,连"半成品"都生不出来。
Setter 注入 Prototype 多例 Bean 不入缓存,Spring 无法找到之前创建过的半成品。
如何规避循环依赖?

虽然 Spring 的单例池能帮我们解决大部分 Setter 循环依赖,但从架构设计 的角度来看,循环依赖通常意味着组件职责划分不清

  • 业务重构:尝试提取出第三个 Bean C,让 A 和 B 都依赖 C,从而将环拆开。

  • 延迟加载 (@Lazy) :在其中一个注入点加上 @Lazy。Spring 会先注入一个代理对象(Proxy),等真正用到时再从容器获取。

  • 使用 Setter 注入代替构造器注入:虽然不推荐,但确实能让 Spring 的缓存机制生效。

Spring三层缓存

在源码中,它们的定义如下:

缓存级别 变量名 存储的内容 作用
第一级缓存 singletonObjects 完整的 Bean 存放已经完全初始化好的单例 Bean 。我们平时 getBean 拿到的就是它。
第二级缓存 earlySingletonObjects 半成品 Bean 存放已经实例化(new 了),但还没填充属性的 Bean。用于解决重复提取
第三级缓存 singletonFactories Bean 工厂 存放 ObjectFactory。用于在需要时提前创建代理对象 (AOP)

假设 ServiceAServiceB 互相依赖。我们看 Spring 是如何运作的:

  1. 实例化 A :Spring 通过反射 new 出 A 的原始对象。

  2. 加入三级缓存 :Spring 把一个能获取 A 的"工厂"丢进 singletonFactories。

  3. 属性填充 A :发现需要 B,于是去 getBean(B)

  4. 实例化 Bnew 出 B 的原始对象。

  5. 加入三级缓存 :把 B 的工厂丢进 singletonFactories。

  6. 属性填充 B:发现需要 A。

  7. 惊险时刻 (查询 A)

    • 查一级缓存:没有。

    • 查二级缓存:没有。

    • 查三级缓存:找到了 A 的工厂!

  8. 执行工厂 (获取 A) :通过 A 的工厂拿到 A 的引用。如果 A 需要 AOP,这里拿到的就是 A 的代理对象。

  9. 转移缓存 :把 A 从三级缓存移到二级缓存

  10. 完成 B :B 顺利注入了 A,完成初始化,进入一级缓存

  11. 回溯 A :A 拿到 B 的引用,也完成初始化,进入一级缓存

为什么一定要有"第三级"?

如果我只用两级缓存(即只有原始对象和完整对象),行不行?

如果不涉及 AOP,两级缓存确实够了。但如果要处理 AOP,必须有第三级。

为什么?

  • Bean 的原则 :Spring 默认是在 Bean 完全初始化后的最后一步(BeanPostProcessor)才创建代理对象的。

  • 循环依赖的矛盾:如果发生了循环依赖,A 必须在还没初始化完时,就得把自己的代理对象给 B。

  • 解决方案 :第三级缓存不直接存对象,而是存一个工厂

    • 如果没有循环依赖,这个工厂永远不会被触发,代理对象依然在最后一步创建。

    • 如果发生了循环依赖,B 通过这个工厂提前触发 AOP 逻辑,拿到 A 的代理对象,从而保证了 B 注入的是增强后的 A,而不是原始的 A。

结论 :三级缓存的本质是为了延迟处理 AOP。它保证了即便在循环依赖这种极端情况下,AOP 的代理逻辑依然能正确介入。

DI(Dependency Injection,依赖注入)

如果说 IoC(控制反转)是一套哲学思想 ,那么 DI 就是这套思想的具体技术实现。在 Spring 生态中,DI 是让整个应用"活"起来的血液。

底层实现原理

DI 的运行高度依赖 Java 的反射(Reflection)机制和 元数据(Metadata)

A. 元数据解析

Spring 启动时,通过扫描注解(如 @Autowired)或读取 XML,将依赖关系记录在 BeanDefinition 中。

B. 依赖查找(Dependency Resolution)

当 Spring 创建 Bean A 时,它会检查 A 的 BeanDefinition

  1. 发现 A 依赖 B。

  2. 去单例池(一级缓存)中查找 B。

  3. 如果没找到,则递归触发 B 的创建流程。

C. 注入操作(Injection Logic)

一旦依赖对象准备好,Spring 会通过以下底层方式完成注入:

  • 反射字段赋值 :利用 Field.set(Object obj, Object value) 强制设置私有属性。

  • 反射方法调用 :利用 Method.invoke(Object obj, Object... args) 调用 Setter 方法。

  • 反射构造调用 :利用 Constructor.newInstance(Object... initargs) 创建实例。

三种主要的 DI 实现方式

方式 代码示例 优点 缺点
构造器注入 public A(B b) { this.b = b; } 官方推荐 。保证依赖不为空,支持 final 字段,方便单测。 参数过多时构造函数显得臃肿。
Setter 注入 public void setB(B b) { this.b = b; } 允许可选依赖,可以在对象创建后重新注入。 无法保证对象在使用前已完全初始化。
字段注入 @Autowired private B b; 代码最简洁,可读性高。 不推荐 。强耦合容器,无法使用 final,隐藏了类的真实依赖。

@Autowired和@Resource的区别

特性 @Autowired @Resource
来源 Spring 框架 (org.springframework...) Java 标准 (JSR-250, javaxjakarta)
默认注入方式 按类型 (By Type) 按名称 (By Name)
支持的注入位置 构造器、字段、Setter 方法、配置方法 字段、Setter 方法(不支持构造器
是否支持可选 支持 (required = false) 不支持
多实现处理 配合 @Qualifier 使用 通过 name 属性指定
@Autowired 的逻辑流转
  1. 按类型查找 :去容器里找类型匹配的 Bean。

  2. 唯一性检查

    • 如果只找到一个,直接注入。

    • 如果找到多个,则触发按名称查找(找变量名和 Bean ID 一致的)。

    • 如果没有匹配名称,抛出异常(除非设置了 required = false)。

@Resource 的逻辑流转
  1. 按名称查找 :默认取变量名 作为 name 去容器找。

  2. 按类型回退

    • 如果指定了 name 且没找到,直接报错。

    • 如果没有指定 name 且按名称没找到,则回退到按类型查找

来源与生态
  • @Autowired 是 Spring 特有的。如果你的项目未来要脱离 Spring(虽然概率极低),这些注解会失效。

  • @Resource 是 Java 定义的标准规范(JSR-250)。它具有更好的通用性,理论上在其他支持 CDI 的容器中也能运行。

注入位置的差异
  • @Autowired 支持构造器注入 。这是 Spring 目前官方推荐的方式,因为构造器注入能保证依赖不为空,且方便编写单元测试。

  • @Resource 不支持构造器注入 。如果你坚持使用构造器注入,就只能用 @Autowired 或者 @Inject(另一个 Java 标准注解)。

处理多实现类(Bean 冲突)

当你一个接口有多个实现类(比如 SmsServiceEmailService 继承自 MessageService)时:

  • 使用 @Autowired

    java 复制代码
    @Autowired
    @Qualifier("smsService") // 必须显式配合 @Qualifier
    private MessageService messageService;
  • 使用 @Resource

    java 复制代码
    @Resource(name = "smsService") // 语义更清晰,一个注解搞定
    private MessageService messageService;

@Component和@Bean的区别

特性 @Component @Bean
作用位置 类(Class)级别 方法(Method)级别
注册方式 配合**组件扫描(Component Scan)**自动注册 配置类中通过方法调用手动注册
适用范围 适用于你自己编写的源代码类 适用于第三方库的类,或需要复杂逻辑创建的对象
灵活性 较低(类上打注解即可) 极高(可以在方法内写逻辑、设参数)
侵入性 强(需要在业务类上加注解) 无(业务类可以是一个纯净的 POJO)
Lite 模式 vs Full 模式
  • Full 模式(@Configuration + @Bean) :Spring 会使用 CGLIB 对配置类进行代理。这样当你在一个 @Bean 方法中调用另一个 @Bean 方法时,Spring 会拦截该调用,直接从容器中找现有的单例,确保单例一致性

  • Lite 模式(仅在普通类中使用 @Bean) :Spring 不会生成 CGLIB 代理。如果你在一个方法里调用另一个方法,它会像普通 Java 调用一样直接 new 一个新对象,这可能会破坏 Bean 的单例属性。

在实际的企业级项目开发中,我们遵循以下原则:

  1. 项目内部业务类 :优先使用 @Component / @Service。这样代码整洁,符合"约定大于配置"。

  2. 第三方依赖、中间件配置、跨模块共享组件 :必须使用 @Configuration + @Bean。这便于集中管理,且能利用 @Bean 提供的 initMethoddestroyMethod 属性。

  3. 需要解耦时 :如果你不希望业务代码里出现任何 Spring 的注解(保持 POJO 纯净),请使用 @Bean

AOP(Aspect-Oriented Programming,面向切面编程)

动态代理

在深入探讨 Spring AOP 时,JDK 动态代理CGLIB 是绕不开的两座大山。

简单来说:JDK 动态代理是基于"接口"的,而 CGLIB 是基于"继承"的。

JDK 动态代理:基于接口的"协议"代理

JDK 动态代理利用了 Java 的反射机制。它要求目标类必须实现至少一个接口

实现原理

  1. 生成代理类 :在运行时,JVM 会在内存中动态生成一个名为 Proxy(或类似名称)的类。

  2. 继承关系 :这个生成的代理类会继承 java.lang.reflect.Proxy实现目标类的所有接口

  3. 分发请求 :当你调用代理对象的方法时,它统一转发给 InvocationHandler.invoke() ,在该方法内部通过反射调用目标对象的真实方法

CGLIB 代理:基于继承的"暴力"重写

CGLIB (Code Generation Library) 是一个强大的高性能字节码生成库 。它不需要接口,而是通过创建目标类的子类来实现代理。

实现原理

  1. 字节码生成 :CGLIB 底层使用了 ASM 框架,直接操作字节码。

  2. 继承关系 :它会动态生成一个目标类的子类

  3. 方法拦截 :子类会重写(Override)父类的所有非 final 方法。当你调用方法时,它会通过 MethodInterceptor 进行拦截。

  4. FastClass 机制CGLIB 不使用反射 ,而是为代理类和目标类各生成一个 FastClass,通过索引直接调用方法,效率高于反射。

Spring 到底用哪一个?

这是面试的高频坑点。Spring AOP 的决策逻辑如下:

  1. 如果目标对象实现了接口 ,默认采用 JDK 动态代理

  2. 如果目标对象没有实现接口 ,采用 CGLIB

  3. 强制切换 :你可以通过配置 proxy-target-class="true" 强制 Spring 全部使用 CGLIB。

注意:Spring Boot 2.x 之后,为了减少由于"接口缺失"导致的注入失败,Spring Boot 默认将 spring.aop.proxy-target-class 设为了 true,即默认倾向于使用 CGLIB

AOP

为什么需要 AOP?(痛点分析)

如果说 IoC 是为了解耦对象 ,那么 AOP 就是为了解耦逻辑 。,AOP 并不是要取代 OOP(面向对象编程),而是作为 OOP 的一种补充,用来处理那些散落在系统各处、与业务主逻辑无关的"横向关注点"。

在没有 AOP 的时代,如果我们想给所有的 Service 方法加上日志记录、权限校验或事务控制,那么就会发生:

  • 代码重复 (Code Scattering):同样的逻辑出现在几十个甚至上百个方法中。

  • 侵入性强 (Code Tangling):核心业务逻辑被淹没在系统级服务代码中,维护困难。

设计思想:横向切分 (Cross-cutting Concerns)

AOP 的核心思想是 "将系统中的非业务代码抽离出来,形成独立的切面"

  • OOP(纵向):封装业务实体,通过继承和多态建立纵向的层次结构。

  • AOP(横向):像手术刀一样,横向切入系统执行流,在不修改原始代码的前提下,动态地注入逻辑。

为了描述这个"切入"过程,Spring 定义了一套术语:

  1. JoinPoint(连接点) :程序执行过程中的某个点,在 Spring AOP 中,这通常指一个方法执行

  2. Pointcut(切点) :一个表达式,用来定义哪些方法需要被增强(解决"在哪干"的问题)。

  3. Advice(通知):增强的代码逻辑(解决"干什么"和"什么时候干"的问题,如 Before, After, Around)。

  4. Aspect(切面)切点 + 通知。一个完整的模块,既定义了逻辑,也定义了位置。

  5. Target(目标对象):被代理的原始业务对象。

底层实现原理:代理机制 + 拦截器链

Spring AOP 的底层实现可以分为两个关键阶段:代理生成链式调用

A. 代理的生成

正如我们之前讨论的,Spring 会根据目标对象是否实现接口,决定使用 JDK 动态代理 还是 CGLIB 。在容器启动时,AnnotationAwareAspectJAutoProxyCreator(一个 BeanPostProcessor)会扫描所有的 Aspect,并为匹配的 Target 生成一个代理对象存入容器。

B. 拦截器链与递归调用 (The Chain)

当代理对象的方法被调用时,它并不会直接跳转到通知代码,而是维护了一个 MethodInterceptor 拦截器链

  • Spring 将所有的 Advice 包装成 MethodInterceptor

  • 核心类 ReflectiveMethodInvocation 负责控制链条的执行。

  • 它通过一个索引 currentInterceptorIndex 递归地调用 proceed() 方法。

AOP 的典型使用场景
  • 声明式事务管理 (@Transactional) :这是 AOP 最成功的应用,通过 TransactionInterceptor 实现。

  • 日志记录:记录接口的入参、出参、执行耗时,用于排查问题。

  • 权限校验 (Spring Security):在进入 Controller 前验证用户的权限标记。

  • 缓存处理 :通过 @Cacheable 注解,先查 Redis,命中则直接返回,不命中则执行方法。

  • 幂等性检查:通过自定义注解防止表单重复提交。

  • 异常统一处理:捕获特定切面下的异常,转化为统一的响应格式。

Spring AOP vs AspectJ
Spring AOP :基于动态代理 。仅支持方法级别的连接点,是在运行时生成的代理对象。优点是易用,不需要特殊的编译器。
  • AspectJ :基于静态织入 (Weaving) 。支持构造函数、字段访问 等所有点。它在编译期、类加载期通过修改字节码来实现。优点是性能更高、功能更全,但门槛也更高。
切点(Pointcut)、通知(Advice)与通知链的执行顺序是什么

切点与通知的关系:Where vs When & What

  • 切点 (Pointcut) :定义了"在哪里"增强。它是一个表达式,本质上是在众多的连接点(Join Points)中进行过滤。

  • 通知 (Advice) :定义了"什么时候 "干"什么事"。

  • 通知链 (Advice Chain) :当一个方法被多个通知增强时,Spring 会将这些通知封装成一个个 MethodInterceptor,并组织成一个有序的链条。

单个切面(Aspect)内通知的执行顺序

在 Spring 5.2.7 之后,为了与 AspectJ 的原生标准保持一致,执行顺序进行了微调。这是目前主流的执行路径:

正常流程:

  1. @Around (环绕通知前部分代码)

  2. @Before (前置通知)

  3. Target Method (目标方法执行)

  4. @AfterReturning (返回通知)

  5. @After (后置通知,类似 finally)

  6. @Around (环绕通知后部分代码)

异常流程:

  1. @Around (前部分)

  2. @Before

  3. Target Method (抛出异常)

  4. @AfterThrowing (异常通知)

  5. @After (后置通知)

  6. @Around (后部分,通常在 catch 块之后或不执行,取决于环绕通知内部的 try-catch 逻辑)

多个切面(Multiple Aspects)的执行顺序

当同一个方法被 Aspect AAspect B 同时拦截时,顺序由 优先级(Order) 决定。

  • 如何指定顺序

    • 实现 Ordered 接口。

    • 使用 @Order 注解。

  • 规则数值越小,优先级越高,越先进入,越后退出。(类似于"剥洋葱"模型)

执行顺序:

  1. Aspect A (@Order(1)) -> 进入(Before / Around 开始)

  2. Aspect B (@Order(2)) -> 进入(Before / Around 开始)

  3. Target Method 执行

  4. Aspect B -> 退出(After / AfterReturning / Around 结束)

  5. Aspect A -> 退出(After / AfterReturning / Around 结束)

核心原理:通知链的递归调用

Spring AOP 并没有使用简单的 for 循环来执行这些通知,而是使用了 "责任链模式 + 递归"

  1. Spring 将所有通知包装成 MethodInterceptor 。Spring 并不直接执行你的 @Before@After。在 AOP 代理生成后,Spring 会把所有通知(Advice)都适配成 MethodInterceptor 接口。】

    1. BeforeAdviceAdapter :把 @Before 包装成一个拦截器,逻辑是:先执行增强逻辑,再执行 mi.proceed()

    2. AfterReturningAdviceAdapter :把 @AfterReturning 包装成拦截器,逻辑是:先执行 mi.proceed(),拿到结果后再执行增强逻辑

  2. 核心执行器是 ReflectiveMethodInvocation。内部有两个核心属性:

    1. List<MethodInterceptor> interceptors一个有序的拦截器列表(即通知链)。

    2. int currentInterceptorIndex :一个指针,记录当前执行到第几个拦截器 ,初始值为 -1

  3. 每个拦截器都会调用 mi.proceed()。当我们调用代理对象的方法时,实际上是在调用 mi.proceed()

    • 如果还有下一个拦截器,就继续递归。

    • 如果没有了,说明已经到了链条末尾,执行目标方法(invokeJoinpoint)。

  4. 随着递归的"回溯" ,后续的 AfterAround 后半部分逻辑会被依次执行。

  • 职责链 :每个拦截器只负责自己的那部分逻辑,且知道如何交给下一个(调用 proceed())。

  • 递归 :利用了 Java 方法调用的 栈(Stack) 特性。

    • 往下走(压栈) :实现 @Before 的语义。

    • 往回走(弹栈/回溯) :实现 @After 的语义。

这套设计的精妙之处在于:它让"环绕"和"后置"通知能够极其方便地捕获到目标方法的返回值或异常,因为它们就在同一个方法的调用上下文里。

为什么必须用递归,而不用 for 循环?

这是高级工程师必须看透的一点:因为 @Around@After 逻辑需要等待"后序结果"。

  • for 循环的局限 :它是一维的,只能按顺序走完。它很难实现"先执行 A 的前半部分,再执行 B 的前半部分,再执行业务,最后执行 B 的后半部分和 A 的后半部分"

  • 递归的优势 :递归自带 "压栈""出栈"

    • 压栈过程 :处理所有 @Before@Around 的前半段。

    • 出栈过程 :处理所有 @AfterReturning@After@Around 的后半段。

这完美契合了"洋葱模型"或者"套娃模型"。

设计模式

Spring 框架之所以被公认为 Java 领域的工业级艺术品,正是因为它几乎集成了 23 种设计模式 中的精髓。

1. 工厂模式 (Factory Pattern)

这是 Spring 最基础的模式。Spring 并不直接通过 new 创建对象,而是通过工厂来管理。

  • 实用例子BeanFactoryApplicationContext

  • 解构

    • BeanFactory 是一个底层工厂,它根据 BeanDefinition(配置说明书)来生产 Bean。

    • 优点:使用者不需要关心对象是怎么创建出来的、用了哪个构造函数,实现了对象创建逻辑与业务逻辑的彻底分离。

2. 单例模式 (Singleton Pattern)

Spring 中的 Bean 默认都是单例的,但注意:Spring 的单例和 GOF 提到的传统单例有所不同。

  • 实用例子Bean Scopes (默认作用域)

  • 解构

    • 传统单例是每个 ClassLoader 只有一个实例,而 Spring 单例是每个 IoC 容器中只有一个实例。

    • Spring 通过 ConcurrentHashMap 实现了一个"单例池",确保在容器运行期间,相同的 Bean 只被实例化一次。

    • 优点:极大地减少了频繁创建和销毁对象带来的内存开销和 GC 压力。

3. 代理模式 (Proxy Pattern) ------ AOP 的灵魂

这是 Spring 中最强大的模式,没有它就没有声明式事务。

  • 实用例子@Transactional 事务管理

  • 解构

    • 当你给一个方法加上 @Transactional 时,Spring 并不是直接运行你的代码,而是为你生成一个代理对象

    • 代理对象会在方法开始前开启事务,结束后提交事务,报错时回滚事务。

    • 实现:JDK 动态代理(基于接口)或 CGLIB(基于继承)。

4. 模板方法模式 (Template Method Pattern)

Spring 极大地减少了 JDBC 或 JMS 等底层操作的样板代码(Boilerplate Code),靠的就是这个模式。

  • 实用例子JdbcTemplateRestTemplate

  • 解构

    • 父类定义了算法的骨架 (如:获取连接 -> 开启事务 -> 执行 SQL -> 关闭连接)。

    • 具体的 "执行 SQL" 这一步留给开发者通过回调函数(Callback)去实现。

    • 优点:开发者只需要关心核心业务 SQL,所有繁琐的资源关闭和异常处理都由框架统一负责。

5. 策略模式 (Strategy Pattern)

Spring 处理资源加载或实例化时,经常会根据不同情况选择不同的算法。

  • 实用例子Resource 接口

  • 解构

    • 当你访问资源时,Spring 定义了 Resource 接口,但具体实现有很多:ClassPathResourceFileSystemResourceUrlResource

    • Spring 会根据你的路径前缀(classpath:file:)自动选择最合适的策略去加载资源。

    • 优点:统一了资源访问方式,且具备极强的扩展性。

6. 观察者模式 (Observer Pattern) ------ 异步解耦

Spring 的事件驱动模型是典型的观察者模式实现。

  • 实用例子ApplicationEventApplicationListener

  • 解构

    • 当某个动作发生时(如容器启动完成 ContextRefreshedEvent),容器会向所有注册的监听器广播这个事件。

    • 优点:实现了解耦。比如你可以在用户注册成功后,发布一个"用户注册事件",由短信模块和邮件模块分别监听并执行,而注册代码本身不需要知道这些模块的存在。

7. 适配器模式 (Adapter Pattern)

在 Spring MVC 中,适配器模式让 DispatcherServlet 能够调用各种不同类型的处理器。

  • 实用例子HandlerAdapter

  • 解构

    • Spring MVC 的 Controller 可以是实现接口的类,也可以是标注了 @RequestMapping 的普通方法。

    • DispatcherServlet 不需要知道这些 Controller 的具体结构,它只调用 HandlerAdapter适配器负责把请求转换成 Controller 能听懂的话。

    • 优点:让系统能够兼容各种不同形式的处理器,提高了框架的灵活性。

8. 责任链模式(Chain of Responsibility)

将多个处理器(Handler)连接成一条链,请求沿着链条传递,每个处理器都有机会处理该请求或将其传递给下一个处理器。

Spring AOP:拦截器链 (MethodInterceptor)

Spring AOP 的底层核心------通知(Advice)的执行,就是标准的责任链模式实现。

  • 核心组件ReflectiveMethodInvocation

  • 工作机制 : Spring 将一个方法上的所有通知(Before, After, Around 等)都封装成 MethodInterceptor,并放入一个 List 中。

  • 执行逻辑 : 它维护了一个指针 currentInterceptorIndex。当调用 proceed() 方法时,指针后移,获取下一个拦截器。每个拦截器内部又会调用 invocation.proceed(),从而形成递归调用。这种设计允许通知在目标方法执行前后"环绕"执行。

Spring MVC

Spring MVC是一个基于 Servlet API 构建的原始 Web 框架,采用了经典的 MVC(Model-View-Controller) 设计模式。Spring MVC下我们一般把后端项目分为Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台页面)。

核心设计思想:前端控制器模式 (Front Controller)

在没有 Spring MVC 的原生 Servlet 开发时代,我们需要在 web.xml 中配置成百上千个 Servlet 来对应不同的 URL。这导致了配置臃肿、代码重复且难以维护。

Spring MVC 引入了 DispatcherServlet 作为统一的入口。

  • 集中处理:所有的 HTTP 请求都会先经过这个"大管家"。

  • 逻辑分发:由大管家根据配置,将请求分发给具体的业务处理器(Controller)。

  • 解耦:将请求接收、参数解析、业务逻辑执行、视图渲染等流程彻底解耦。

Spring MVC 的核心组件(六大金刚)

要理解 Spring MVC 的底层,必须掌握这六个核心组件:

组件名称 核心职责
DispatcherServlet 前端控制器。负责统一接收请求、协调各组件并分发响应。
HandlerMapping 处理器映射器 。负责根据 URL 找到对应的 Handler(控制器方法)。
HandlerAdapter 处理器适配器 。负责调用具体的 Handler,解决不同形态 Controller 的执行问题。
Handler (Controller) 业务处理器。由开发者编写,处理核心业务逻辑。
ModelAndView 数据与视图模型。封装了业务执行后的数据(Model)和要跳转的页面路径(View)。
ViewResolver 视图解析器。负责将逻辑视图名(如 "index")解析为真实的物理视图(如 "/WEB-INF/jsp/index.jsp")。

请求处理全流程

当一个请求打到服务器,Spring MVC 会经历以下步骤:

  1. 请求到达 :用户向服务器发送 HTTP 请求,请求首先被 DispatcherServlet(前端控制器)拦截。

  2. 寻找处理器 :DispatcherServlet 询问 HandlerMapping,映射器根据请求的 URL、方法等信息,寻找匹配的处理器。

  3. 返回执行链 :HandlerMapping 返回一个 HandlerExecutionChain 对象。这个对象包含了具体的 Handler (控制器方法)以及该方法关联的所有 Interceptors(拦截器)。

  4. 获取适配器 :DispatcherServlet 根据 Handler 的类型,向 HandlerAdapter(处理器适配器)发起询问,确认哪个适配器可以执行该处理器。

  5. 拦截器前置处理 :在真正执行 Controller 之前,按顺序执行拦截器的 preHandle() 方法。如果返回 false,请求在此中断。

  6. 执行逻辑 :适配器调用真正的 Controller 方法。

  7. 返回结果 :控制器方法处理完业务逻辑后,返回一个 ModelAndView 对象(包含数据模型和逻辑视图名)。

  8. 拦截器后置处理 :执行拦截器 postHandle 方法。此时可以对模型数据或视图进行最后的加工。

  9. 视图解析 :DispatcherServlet 将逻辑视图名(如 "user_list")交给 ViewResolver ,解析成真实的 View 对象(如 JSP、Thymeleaf 模板)。

  10. 渲染数据:View 对象将 Model 数据填充到模板中(如 JSP、Thymeleaf)。

  11. 响应返回:DispatcherServlet 将最终生成的 HTML 或 JSON 返回给客户端。

提示 :在现代的 RESTful 开发中,我们通常使用 @RestController:

  • 不再返回 ModelAndView:Controller 直接返回 Java 对象(POJO、List 等)。

  • HttpMessageConverter 介入 :跳过视图解析和渲染阶段。Spring 会调用 HttpMessageConverter(如 Jackson 库)直接将对象转为 JSONXML 字符串,并写入 HTTP Response 的 Body 中。

常用注解

控制器定义注解

  • @Controller:标识一个类为 Spring MVC 的控制器。通常用于传统的 Web 开发,配合视图解析器返回 HTML 页面。

  • @RestController最常用 。它是 @Controller@ResponseBody 的组合。标志着该控制器下的所有方法默认都会将返回值直接写入 HTTP 响应体(通常是 JSON),非常适合前后端分离的架构。

请求映射注解 (Routing)

现在推荐使用具体的语义化注解,而不是通用的 @RequestMapping

  • @GetMapping:用于查询操作。

  • @PostMapping:用于新增操作。

  • @PutMapping:用于更新操作(覆盖式更新)。

  • @DeleteMapping:用于删除操作。

  • @PatchMapping:用于部分更新。

  • @RequestMapping :通常定义在类级别,用于指定基础路径(Base Path),实现请求地址的层级管理。

数据绑定与入参处理 (Data Binding)

这是处理 HTTP 请求参数的核心:

  • @PathVariable :处理 RESTful 风格的路径变量。例如 /users/{id} 中的 {id}

  • @RequestParam :处理 Query String(查询字符串)或表单参数。支持设置 defaultValuerequired

  • @RequestBody极其重要 。将 HTTP 请求体中的 JSON 字符串反序列化为 Java 对象 。它底层依赖 HttpMessageConverter(如 Jackson)。

  • @RequestHeader :用于获取请求头信息(如 Authorization, User-Agent)。

  • @CookieValue:用于直接获取 Cookie 中的值。

  • @ModelAttribute:用于从 Model 中获取数据或将请求参数绑定到一个复杂的对象上(多用于 Form 表单提交)。

响应处理注解

  • @ResponseBody :将方法返回值自动转为 JSON/XML 写入响应体。如果使用了 @RestController,则无需手动加在方法上。

  • @ResponseStatus :用于指定方法执行成功后返回的 HTTP 状态码(如 201 Created204 No Content)。

Spring Data & Transaction ( 数据持久化 )

事务

Spring 事务是 Spring 框架中一项核心功能,它通过简洁的抽象,让开发者能轻松管理数据库操作的一致性。Spring事务的本质是对数据库事务的抽象和封装 。真正的数据库事务提交和回滚是通过数据库的binlogredo log 实现的。

实现原理

Spring 事务的实现原理核心在于通过 AOP(面向切面编程)动态代理技术,将复杂的事务管理逻辑透明地织入业务方法中,从而简化开发。

  1. 代理对象的创建 :当你在一个类或方法上使用 @Transactional注解 时,Spring 在容器启动阶段(Bean 初始化时)会为这个 Bean 创建一个代理对象​ 。这个代理对象并非原始的 Bean 实例,而是一个增强了事务管理逻辑的包装器。

  2. 方法拦截 :当你的代码调用被 @Transactional注解的方法时,实际上调用的是代理对象 的方法 。代理对象会拦截这次调用,并将控制权交给一个名为 TransactionInterceptor 的关键组件 。

  3. 事务管理器的调度TransactionInterceptor会解析 @Transactional注解中定义的属性(如传播行为、隔离级别等),然后调用 PlatformTransactionManager (平台事务管理器)来执行具体的事务操作,如开启、提交、回滚 。

Spring 事务抽象出三个核心接口,各司其职,共同完成了事务的管理 :

核心组件 职责说明
**PlatformTransactionManager**​ (事务管理器) 事务管理的**"总指挥"**。它定义了事务的基本操作:获取事务状态、提交、回滚。Spring 为不同数据访问技术(JDBC, JPA, Hibernate 等)提供了相应的实现 。
**TransactionDefinition**​ (事务定义) 定义了事务的**"属性规则"** ,包括传播行为、隔离级别、超时时间、是否只读等。@Transactional注解的属性就是其具体体现 。
**TransactionStatus**​ (事务状态) 代表了事务在运行过程中的**"当前状态"**,如是否是新事务、是否已被标记为回滚、是否有保存点等。事务管理器根据状态进行操作 。

事务能够正确运作的关键在于事务上下文与当前线程的绑定

  1. 事务拦截与执行流程 :当我们调用被代理的业务方法时,调用首先会被 TransactionInterceptor ​ 拦截。其核心方法 invokeWithinTransaction会按以下步骤执行 :

    • 解析事务属性 :从 @Transactional注解中提取出传播行为、隔离级别等配置信息。

    • 获取事务 :根据传播行为(例如 PROPAGATION_REQUIRED表示如果当前没有事务就创建一个,有则加入),通过 PlatformTransactionManager获取或创建事务,并得到一个 TransactionStatus对象。

    • 绑定资源 :最关键的一步是获取与当前事务关联的数据库连接(Connection),并将其通过 ThreadLocal 绑定到当前线程 。这意味着在同一个事务的上下文中,后续的所有数据库操作都会获取到同一个 Connection,从而保证了数据操作的原子性

    • 执行业务逻辑:在事务上下文中调用原始目标方法。

    • 提交/回滚:如果方法正常执行完毕,则提交事务;如果抛出了异常,则根据规则(默认只回滚运行时异常和 Error)决定是否回滚事务。

    • 清理资源 :最后,解除 ThreadLocal中绑定的资源。

  2. ThreadLocal的核心作用 :Spring 使用 ThreadLocal来确保一个事务内的所有数据库操作都能获取到同一个 Connection ,这是实现事务的基石 。TransactionSynchronizationManager是这个机制的管理者,它内部维护了多个 ThreadLocal变量,用来存储当前线程的事务资源(如 DataSource、Connection)和同步状态。

    • Spring事务管理的核心挑战在于,如何让一个事务(通常开始于Service层的一个方法)内部调用的所有数据库操作(可能跨越多个DAO方法)共享同一个数据库连接。ThreadLocal通过以下流程完美解决了这个问题

      • 事务拦截 :当执行到被 @Transactional注解标记的方法时,Spring的AOP代理(通常是 TransactionInterceptor)会拦截该方法调用。

      • 资源绑定 :事务管理器(如 DataSourceTransactionManager)会从事务相关的数据源获取一个数据库连接,并通过 TransactionSynchronizationManager将这个连接绑定到当前线程的 ThreadLocal变量中。

      • 连接传递 :在该事务后续执行过程中,任何需要数据库连接的地方(例如,通过 JdbcTemplate执行SQL),Spring都会自动从当前线程的 ThreadLocal中获取之前绑定的那个连接,而不是新建一个。

      • 事务结束与清理 :当事务方法执行完毕(成功提交或异常回滚)后,事务管理器会提交或回滚事务,并最关键的一步 :将连接从 ThreadLocal中解绑并释放回连接池。这确保了资源被正确清理,防止内存泄漏。

隔离级别

Spring 框架定义了五种标准的事务隔离级别,用于控制并发事务之间的可见性,平衡数据一致性和系统性能。

隔离级别 核心特性 能解决的并发问题 性能影响 典型应用场景
**READ_UNCOMMITTED**​ (读未提交) 允许读取其他事务未提交的更改。 并发性能最高,但数据一致性风险最大。 对数据一致性要求极低的场景,如非关键的统计估算。
**READ_COMMITTED**​ (读已提交) 只允许读取其他事务已提交的更改。 防止脏读。 平衡点,性能较好 绝大多数Web应用的默认选择,如用户会话更新。
**REPEATABLE_READ**​ (可重复读) 保证在同一事务中,多次读取同一数据的结果一致。 防止脏读、不可重复读。 并发性能有所下降。 需要事务内数据视图稳定的场景,如报表生成。
**SERIALIZABLE**​ (串行化) 最高隔离级别,事务完全串行执行。 防止脏读、不可重复读、幻读。 并发性能最低,可能导致大量锁等待和超时。 对数据一致性有严苛要求的核心业务,如金融交易。
**DEFAULT**​ (默认) 使用所连接数据库的默认隔离级别 取决于数据库 取决于数据库 追求配置简便或希望行为与数据库默认设置一致时。

选择策略

  • 优先从 READ_COMMITTED开始:它在避免脏读和保持性能之间取得了良好平衡,是许多数据库(如Oracle、PostgreSQL)和应用的默认选择。

  • 需要时升级到 REPEATABLE_READ:当业务逻辑要求在一个事务内多次读取的数据必须绝对一致时(例如,先查询后依据结果更新),可考虑此级别。

  • 极端情况才使用 SERIALIZABLE:由于其严重的性能损耗,仅在对数据一致性有零容忍要求且其他方法无法满足时使用。

  • 了解数据库差异 :不同数据库对隔离级别的实现有差异。例如,MySQL的REPEATABLE_READ通过MVCC机制在一定程度上缓解了幻读,而其他数据库可能不同。选择DEFAULT时,务必清楚当前数据库的默认行为。

当你在 Spring 的 @Transactional注解中显式设置了与 MySQL 当前会话不同的隔离级别时,Spring 会在事务启动前向数据库发送一条设置指令。

底层发生的事

  1. 当方法被调用时,Spring 的事务拦截器会先获取一个数据库连接。

  2. 随后,它会通过 JDBC 在该连接上执行一条类似 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED的 SQL 语句,临时修改了当前数据库会话的隔离级别

  3. 然后才执行业务 SQL。

  4. 方法执行完毕后,事务结束,这个连接被归还到连接池,其隔离级别设置可能会被重置(取决于连接池配置)。

因此,在此事务的生命周期内,生效的是 Spring 所设置的 READ_COMMITTED 级别,覆盖了 MySQL 实例的默认 REPEATABLE READ 设置 。

在生产环境中,建议在 Spring 代码中显式地指定所需的隔离级别 ,而不是依赖 DEFAULT。这能使代码的意图更清晰,避免因数据库默认配置的不同(例如,从 MySQL 迁移到 PostgreSQL)而引入难以察觉的行为变化 。

传播行为

Spring 事务传播行为定义了当多个事务方法相互调用时,事务应该如何传播和交互。这解决了业务方法相互调用时,是应该共用同一个事务,还是开启新事务,或者干脆不用事务等核心问题。

支持当前事务的情况:

1. REQUIRED(必需)

这是 Spring 默认的传播行为,适用于绝大多数场景。

  • 工作方式如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务

  • 场景示例 :一个下单方法 placeOrder()调用了扣减库存 deductStock()和创建订单 createOrder()等方法。这些方法通常应该在同一事务中,要么全部成功,要么全部失败。将它们都设置为 REQUIRED可以保证这一点。

  • 核心价值:保证多个关联操作的事务统一性。

2. SUPPORTS(支持)

行为较为随和,对事务的存在与否没有强制要求。

  • 工作方式如果当前存在事务,则加入;如果当前没有事务,则以非事务方式执行

  • 场景示例 :一个查询方法 getUserInfo()。如果它在某个更新事务中被调用(例如先查询后更新),则加入事务以保证数据一致性;如果被单独调用,则无需开启事务,提升性能。

  • 核心价值:灵活适应调用方的事务状态,特别适用于查询操作。

3. MANDATORY(强制)

要求必须在已有事务中运行,否则会抛出异常。

  • 工作方式强制要求当前必须存在事务,否则抛出 IllegalTransactionStateException异常。

  • 场景示例 :一个资金结算方法 settleAccount()。此操作至关重要,绝不能在没有事务保护的情况下执行。使用 MANDATORY可以确保它只能被其他事务方法调用,避免误用。

  • 核心价值:用于强制保证某些关键操作的事务安全性。

不支持当前事务的情况:

4. REQUIRES_NEW(新建)

会启动一个全新的、独立的事务,并挂起当前事务(如果存在)。

  • 工作方式无论当前是否存在事务,都会创建一个新事务。如果当前有事务,则将其挂起。

  • 场景示例 :日志记录。即使在核心业务事务(如下单)中需要记录操作日志,也希望日志记录能成功保存,即使后续业务失败回滚。将日志方法设置为 REQUIRES_NEW,可使日志事务独立提交,不受主事务影响。

  • 核心价值:实现事务的完全独立,避免内外事务相互干扰。

5. NOT_SUPPORTED(不支持)

明确不在事务中执行,并挂起当前事务。

  • 工作方式以非事务方式执行。如果当前存在事务,则将其挂起,待方法执行完毕后再恢复。

  • 场景示例:执行一些与核心业务数据一致性无关,但可能比较耗时的操作,例如发送短信/邮件通知。这样可以避免长时间占用数据库连接,影响主事务性能。

  • 核心价值:让特定操作脱离事务环境,提高效率或避免不必要的资源锁定。

6. NEVER(从不)

强制要求不能在事务中执行。

  • 工作方式强制要求当前必须没有事务,否则抛出异常。

  • 场景示例:一些数据校验方法。你可能希望这些校验是纯粹的内存操作,快速且不涉及数据库事务,以确保校验逻辑的绝对清晰。

  • 核心价值 :强制保证方法在非事务环境下运行,是 MANDATORY的反向操作。

7. NESTED(嵌套)

这是一个特殊且强大的传播行为,它在一个已存在的事务中创建一个"嵌套事务"。

  • 工作方式 :如果当前存在事务,则在嵌套事务内执行(基于数据库的保存点,Savepoint 机制)。如果当前没有事务,则其行为同 REQUIRED。

  • 场景示例:处理一个包含多个子项的订单。你可以在主事务中保存订单主信息,然后在一个嵌套事务中处理每个子项。如果某个子项处理失败,你可以回滚到保存点,只撤销该子项的操作,而不影响已保存的订单主信息和之前成功的子项,然后可以选择重试或跳过该子项,继续处理后续子项。

  • 核心价值允许部分回滚,提供了更精细的事务控制粒度。需要注意的是,此行为依赖于底层数据库对保存点的支持。

事务失效

1. 内部调用(自调用问题)

这是最常见的失效原因。在同一个类中,一个无事务的方法调用另一个有 @Transactional 注解的方法。

  • 失效原因 :Spring 事务是基于 AOP 代理 的。当你通过 this.method() 调用时,走的是目标对象原生的方法,绕过了代理对象,事务增强逻辑根本没机会执行。

  • 解决方案

    • 将方法拆分到不同的 Service 类中。

    • 通过 AopContext.currentProxy() 获取当前代理对象调用。

    • 注入自己(自我注入,注意循环依赖)。

2. 访问权限问题(非 public 方法)

如果你@Transactional 注解写在 private、protected 或 default 作用域的方法上,事务会失效。

  • 失效原因Spring 的 TransactionInterceptor 在拦截方法前会检查方法的修饰符。出于设计考虑,它只处理 public 方法。此外,如果使用 JDK 动态代理,它只能代理接口定义的方法。

  • 解决方案 :确保注解方法是 public 的。

3. 异常被"吃掉"了(Try-Catch 处理不当)

在 Service 层手动 try-catch 了异常,且没有在 catch 块中手动抛出。

  • 失效原因 :Spring 事务感知的信号是 异常 。如果异常在方法内部被捕获且消化了,代理对象认为方法执行成功,依然会提交事务

  • 解决方案

    • catch 块中重新抛出 RuntimeException

    • 手动调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()

4. 异常类型匹配错误

Spring 默认只在捕获到 RuntimeExceptionError 时回滚。

  • 失效原因 :如果你抛出的是 Checked Exception (受检异常,如 IOExceptionSQLException),事务默认不会回滚

  • 解决方案 :显式指定回滚类型:@Transactional(rollbackFor = Exception.class)

5. 数据库引擎不支持事务

这是一个基础设施层面的问题。

  • 失效原因 :如果你的 MySQL 表使用了 MyISAM 存储引擎,它是没有事务日志的,Spring 无论怎么发 rollback 命令,数据库都不会有反应。

  • 解决方案 :将表引擎修改为 InnoDB

6. Bean 没有被 Spring 管理

如果你通过 new 关键字手动创建了 Service 实例,而不是从 Spring 容器中获取。

  • 失效原因 :Spring 只能拦截容器内的 Bean。手动 new 的对象不具备代理特性,事务自然失效。

  • 解决方案 :使用 @Autowired@Resource 注入 Bean。

7. 多线程调用

在事务方法内部开启新线程执行异步任务

  • 失效原因 :Spring 的事务信息(连接、状态等)是存储在 ThreadLocal 中的。新开启的线程无法获取主线程的事务上下文,它们不在同一个连接里。

  • 解决方案:异步任务通常需要独立的事务控制,或者通过消息队列解耦。

8. 错误的传播行为(Propagation)

配置了不支持事务的传播属性。

  • 失效原因 :如果设置了 Propagation.NOT_SUPPORTED(挂起当前事务,以非事务运行)或 Propagation.NEVER(如果存在事务则抛异常),则不会有事务保障。

  • 解决方案 :根据业务选择正确的传播行为(通常默认 REQUIRED 即可)。

失效类型 检查点 核心原理
代理失效 是否是 this.xxx() 调用?是否是 private AOP 代理增强失效
异常失效 是否 try-catch 没抛出?异常类型是否对? 信号捕获失败
环境失效 数据库引擎是否是 InnoDB?Bean 是否受管? 基础设施不支持
范围失效 是否在多线程中使用事务? ThreadLocal 变量隔离

Spring Boot

为什么要使用Spring Boot?

1. 告别配置地狱:起步依赖 (Starter POMs)

在没有 Spring Boot 之前,如果你想搭建一个 Web 项目,你需要手动管理几十个 Jar 包的版本冲突。

  • 痛点 :版本不一致导致的 NoSuchMethodError 是家常便饭。

  • Spring Boot 的解法 :它引入了 Starter 概念。你只需要引入 spring-boot-starter-web,它就会自动帮你把 Spring MVC、Jackson、Tomcat 等所有必要的依赖打包进来,并保证它们的版本是兼容的

2. 约定优于配置 (Convention over Configuration)

这是 Spring Boot 的灵魂。

  • 传统 Spring :你需要手动配置 DispatcherServlet、视图解析器、事务管理器、组件扫描路径......即使每个项目的配置都大同小异。

  • Spring Boot 的解法 :它通过 自动配置 (Auto-configuration) 机制,在项目启动时扫描 Classpath。如果你引入了 MySQL 驱动,它就自动帮你配好 DataSource;如果你引入了 Redis,它就自动配好 RedisTemplate

  • 关键点 :它提供的是默认配置。如果你有特殊需求,随时可以覆盖。

3. 内嵌服务器:让部署变得简单 (Embedded Server)

  • 传统方式:你需要安装 Tomcat,打包成 WAR 包,拷贝到 Tomcat 的 webapps 目录下。

  • Spring Boot 方式 :它直接将 Tomcat(或 Jetty、Undertow)内嵌 在 Jar 包里。

  • 优势 :你的应用就是一个独立的、可执行的 Jar 包。通过 java -jar app.jar 即可启动,这为 Docker 容器化微服务云原生部署 扫清了最后的障碍。

4. 生产级特性:监控与管理 (Actuator)

Spring Boot 不仅仅关注开发过程,还关注运维阶段。

  • Actuator 组件 :只需一行配置,就能暴露 /health(健康检查)、/metrics(性能指标)、/env(环境变量)等端点。

  • 价值:这让 Spring Boot 应用能完美对接 Prometheus、Grafana 等现代监控系统,实现了真正的"生产就绪"。

约定优于配置

"约定优于配置"(Convention over Configuration,简称 CoC )的出现,本质上是为了减少开发人员需要做出的决定数量,从而在不失灵活性的前提下,极大地提高开发效率。

在 Java EE 早期(如 EJB 时代)或 Spring 3.x 以前,配置信息的膨胀让项目变得极其臃肿:

  1. 配置地狱:配置文件比业务代码还长。

  2. 重复劳动:每个项目都要配数据库连接、配视图解析器、配组件扫描,而这些配置在 90% 的场景下是一模一样的。

  3. 学习曲线陡峭:新手不得不先掌握复杂的配置逻辑,才能写出第一个 "Hello World"。

核心定义:什么是"约定"?

简单来说:"如果你没有指定配置,那么我们就按照默认的套路来;如果你指定了,就以你的为准。"

  • 传统开发模式:每一步都需要显式告诉框架该怎么做(Explicit Configuration)。

  • 约定优于配置:框架预设了一套"标准答案"。如果你的需求符合标准,你一句话都不用说,框架自动帮你接好线。

Spring Boot 是如何落地这一思想的?

Spring Boot 是 CoC 思想的集大成者,主要体现在以下三个层面:

A. 标准的项目结构 (Maven/Gradle)

Spring Boot 约定了源代码必须放在 src/main/java,资源文件必须放在 src/main/resources。只要你遵守这个约定,构建工具就能自动找到编译路径,无需手动配置。

B. 自动配置 (Auto-Configuration) ------ 最核心的实现

Spring Boot 会根据你 Classpath 下引入的 Jar 包,自动推断你的意图。

  • 例子 :你在 pom.xml 里引入了 spring-boot-starter-data-jpa

  • 约定 :Spring Boot 认为你肯定要连数据库。它会自动检测路径下有没有 MySQL 驱动,如果有,它就自动为你配置一个 DataSourceEntityManager 和事务管理器。你只需要在 application.properties 里填上地址就行。

C. 条件装配机制 (@Conditional)

底层原理是大量的 @ConditionalOnClass@ConditionalOnMissingBean 注解。

  • 逻辑 :只有当你没有手动定义某个 Bean 时,Spring Boot 的默认 Bean 才会生效。这完美契合了"约定为主,自定义覆盖"的逻辑。

自动装配

在 Spring Boot 中,自动装配(Auto-Configuration) 是实现"约定优于配置"的核心技术。它能根据类路径(Classpath)下的 Jar 包、定义的 Bean 以及各种属性设置,自动推断并配置 Spring 容器所需的 Bean。

1. @SpringBootApplication为应用启动类上的一个复合注解,它主要由三个关键注解组成:

  1. @SpringBootConfiguration :本质就是 @Configuration,标志这是一个配置类。

  2. @ComponentScan:启用组件扫描,默认扫描当前包及其子包。

  3. @EnableAutoConfiguration这是自动装配的真正开启者。

@EnableAutoConfiguration利用了 Spring 的 @Import 机制,导入了 AutoConfigurationImportSelector 类。这个类的职责是:在容器启动时,决定哪些配置类应该被加载到容器中。

  1. Spring Boot 会扫描类路径下的特定文件来寻找候选配置类。
  • 旧版本(2.7 以前) :读取 META-INF/spring.factories 文件 中以 EnableAutoConfiguration 为 Key 的类。

  • 新版本(2.7 及 3.x) :读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。

这些文件中列出了成百上千个配置类(如 DataSourceAutoConfigurationRedisAutoConfiguration)。

  1. Spring 并不会加载文件里列出的所有类,而是通过 @Conditional 及其派生注解进行筛选:
  • @ConditionalOnClass :类路径下存在某个类时(比如存在 Druid.class),才加载该配置。

  • @ConditionalOnMissingBean :容器中没有用户自定义的同名 Bean 时,才加载默认 Bean(这是实现"自定义覆盖约定"的关键)。

  • @ConditionalOnProperty:配置文件中存在某个属性且值为特定值时,才开启配置。

启动流程

流程始于 SpringApplication.run(Application.class, args)这行代码。

第一阶段:初始化阶段

当你调用 new SpringApplication(...) 时,Spring 并没有立即启动:

  • 判定 Web 应用类型 : Spring 通过检查 Classpath 下是否存在特定的类(如 DispatcherServletDispatcherHandler)判定是 REACTIVE(WebFlux)、SERVLET(MVC)还是 NONE(普通 Java 应用)。

  • 加载初始化器 (ApplicationContextInitializer) : 利用 SPI 机制 (从 META-INF/spring.factoriesspring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)读取所有的初始化器。

  • 加载监听器 (ApplicationListener): 同样通过 SPI 机制,加载所有感兴趣的事件监听器(如日志系统初始化监听器)。

  • 推断主类 (Main Class) : 通过堆栈追踪(Stack Trace)找到那个包含 main 方法的类

@SpringBootApplication启动类上的这个注解是一个复合注解,它是以下三个注解的集合体 :

  • @Configuration:标记该类是一个配置类,其内部可以使用 @Bean注解定义 Bean。
  • @ComponentScan:自动扫描当前包及其子包下的组件(如 @Component, @Service, @Controller),并将它们注册为 Bean。
  • @EnableAutoConfiguration:这是 Spring Boot 自动配置的开关,是整个框架"约定优于配置"理念的核心 。

第二阶段:执行 run() 方法

1. 启动监听器与环境构建
  • 启动计时器 (StopWatch):记录启动耗时。

  • 获取运行监听器 (getRunListeners) :获取 SpringApplicationRunListeners。它负责在启动的各个节点发布事件。

  • 准备环境 (prepareEnvironment)

    • 创建一个 ConfigurableEnvironment

    • 配置属性源 (configurePropertySources) :加载命令行参数、YAML、Properties、环境变量等。

    • 发布环境就绪事件触发一系列监听器(如日志系统的初始化)。

2. 容器创建与预处理
  • 创建上下文 (createApplicationContext) :根据推断的 Web 类型,使用策略模式 创建对应的容器。

    • 例如:AnnotationConfigServletWebServerApplicationContext
  • 容器预处理 (prepareContext)

    • 关联环境 :将刚才准备好的 Environment 塞进容器。

    • 应用初始化器 (applyInitializers) :依次调用构造阶段加载的 ApplicationContextInitializer

    • 单例注入 :将 args 参数和 Banner 注册为单例 Bean。

    • 加载 Bean 定义 :最重要的动作------将启动类(PrimarySource)加载到容器中,为后续的注解扫描(@ComponentScan)做准备。

第三阶段:刷新容器

这是最硬核的部分。refreshContext 底层调用的是 Spring 核心方法 AbstractApplicationContext.refresh()

1. 激活工厂后置处理器 (invokeBeanFactoryPostProcessors)

这是 自动装配 (Auto-configuration) 发生的时刻。

  • ConfigurationClassPostProcessor :它会解析 @Configuration 注解,执行 @ComponentScan,并读取 spring.factories 中的自动配置类。

  • 条件评估 :此时会触发 @ConditionalOnClass 等逻辑,决定哪些 Bean 应该进入候选名单。

2. 初始化内嵌 Web 容器 (onRefresh)

这是 Spring Boot 与传统 Spring 的最大区别。

  • 在 Web 环境下,onRefresh 会触发 createWebServer()

  • 根据配置(如 spring-boot-starter-tomcat),它会实例化 TomcatJettyUndertow

3. 完成单例 Bean 的实例化 (finishBeanFactoryInitialization)
  • Spring 会冻结 Bean 定义,开始根据依赖关系实例化所有的单例 Bean。

  • 此时发生 DI(依赖注入)BeanPostProcessor 拦截、以及 AOP 代理 的生成。

4. 发布就绪与运行 Runners
  • 发布完成事件:容器完全启动。

  • 执行 Runner遍历执行所有的 CommandLineRunner 和 ApplicationRunner。这通常用于执行系统的"预热"逻辑(如加载缓存、检查配置)。

如何自定义编写一个starter?

Starter(起步依赖) 是 Spring Boot 的核心"开箱即用"机制。它是一组预先打包好的 Maven 或 Gradle 依赖描述符,旨在简化开发者的依赖配置。

在没有 Starter 之前,如果你想开发一个 Web 项目,你可能需要手动在 pom.xml 中引入 Spring MVC、Tomcat、Jackson、Logback 等十几个依赖,还得小心翼翼地处理它们之间的版本冲突

从技术实现上看,一个 Starter 主要包含两部分:

  1. 依赖清单(Dependency List) :它利用了 Maven 的传递依赖 特性。你只需引入一个 spring-boot-starter-web,它就会自动拉取 Web 开发所需的所有 Jar 包。

  2. 自动配置逻辑(Auto-configuration):这是 Starter 的灵魂。它包含了一组特殊的代码,当检测到类路径(Classpath)下存在特定的类时,会自动在 Spring 容器中创建并配置对应的 Bean。

starter 解决了什么问题?

  • 版本管理地狱 :Spring Boot 通过 spring-boot-dependencies (BOM) 统一管理了所有 Starter 的版本。你不再需要为每个 Jar 包手动写 <version>,从而避免了版本不兼容引发的 NoSuchMethodError

  • 配置冗余 :它遵循"约定优于配置"。例如,只要你引入了 spring-boot-starter-jdbc,它就假设你需要数据库连接,并自动为你寻找数据源配置。

  • 快速起步:开发者只需关注业务功能(如:我要做 Web、我要用 Redis、我要连 JPA),通过引入对应的"全家桶"即可秒级搭建环境。

常见的核心 Starter 举例

Starter 名称 应用场景 包含的核心技术
spring-boot-starter-web 构建 Web 应用(含 RESTful) Spring MVC, Tomcat, Jackson
spring-boot-starter-test 单元测试与集成测试 JUnit, AssertJ, Mockito
spring-boot-starter-data-jpa 数据库持久化(JPA) Hibernate, Spring Data JPA, JDBC
spring-boot-starter-aop 面向切面编程 Spring AOP, AspectJ
spring-boot-starter-actuator 应用监控与管理 健康检查、度量指标、审计

当你引入一个 Starter 时,背后的执行逻辑如下:

  1. 引入依赖:Maven 将 Starter 及其下挂的所有 Jar 包下载到类路径下。

  2. 触发扫描 :Spring Boot 启动时,扫描 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。

  3. 条件装配 :配置类通过 @ConditionalOnClass 等注解检查类路径。如果发现了 Starter 带来的特定类(如 RedisTemplate.class),则激活该配置。

  4. 注入 Bean :将配置好的 Bean 注入 IoC 容器,供你直接 @Autowired 使用

1. 项目命名与依赖配置

首先,创建一个 Maven 项目。根据官方约定,自定义 Starter 的命名格式建议为 {name}-spring-boot-starter

核心依赖: 你需要引入 spring-boot-autoconfigure,这是实现自动配置逻辑的基础。

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 编写业务功能类 (The Feature)

这是你想要"启动"的核心功能。比如我们写一个简单的 WeatherService,根据配置返回天气信息。

java 复制代码
public class WeatherService {
    private String unit; // 摄氏度或华氏度

    public WeatherService(String unit) {
        this.unit = unit;
    }

    public String getSchema() {
        return "Current unit is: " + unit;
    }
}

3. 定义配置属性类 (Properties)

利用 @ConfigurationPropertiesapplication.yml 中的配置映射到 Java 对象中。这体现了"外部化配置"的思想(将散落在配置文件中的设置,集中、类型安全地映射到一个 Java 对象中,方便在程序里使用。)。

java 复制代码
// 请自动将配置文件中所有以 weather为前缀的属性,找出来并绑定到这个类的字段上
@ConfigurationProperties(prefix = "weather")
public class WeatherProperties {
    private String unit = "Celsius"; // 默认值

    public String getUnit() { return unit; }
    public void setUnit(String unit) { this.unit = unit; }
}
  • 使用 application.properties文件时

    XML 复制代码
    # 设置温度单位为华氏度
    weather.unit=Fahrenheit
  • 使用 application.yml文件时(更常用,层次更清晰)

    XML 复制代码
    weather:
      unit: Fahrenheit

4. 编写自动配置类 (Auto-Configuration)

这个类决定了你的 Bean 在什么情况下会被创建 。这里需要大量使用 @Conditional 系列注解来实现"条件装配"。

java 复制代码
@Configuration
@EnableConfigurationProperties(WeatherProperties.class) // 启用属性绑定
@ConditionalOnClass(WeatherService.class) // 当类路径下存在这个类时才触发
public class WeatherAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean // 只有当用户没有自定义 WeatherService 时,才创建默认的
    @ConditionalOnProperty(prefix = "weather", name = "enabled", havingValue = "true") // 只有配置了 enabled=true 才开启
    public WeatherService weatherService(WeatherProperties properties) {
        return new WeatherService(properties.getUnit());
    }
}

5. 注册自动配置 (SPI 发现)

Spring Boot 不会自动扫描你 Jar 包里的 @Configuration。你必须通过 SPI (Service Provider Interface) 机制明确告诉它。

  • 路径 :在 src/main/resources/ 下创建目录 META-INF/spring/

  • 文件名 :创建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports

  • 内容:填入自动配置类的全限定名。

常用注释

1. 核心启动与配置类

这类注解定义了 Spring Boot 应用的入口和基础行为。

  • @SpringBootApplication: 这是最核心的复合注解。它包含了:

    • @SpringBootConfiguration:标识为配置类。

    • @EnableAutoConfiguration:开启自动装配魔法。

    • @ComponentScan:默认扫描当前包及其子包下的 Bean。

  • @Configuration :标识一个类为配置类,内部通常包含 @Bean 方法。

  • @Bean:手动声明一个 Bean 实例,交给 IoC 容器管理。

2. 条件装配类

这些注解是实现"按需加载"的关键,通常出现在自动配置类(Auto-configuration)或自定义 Starter 中。

注解 触发条件
@ConditionalOnClass 当类路径(Classpath)下存在指定的类时生效。
@ConditionalOnMissingBean 当容器中没有指定类型的 Bean 时生效(常用于提供默认实现)。
@ConditionalOnProperty 当配置文件(yml/properties)中存在特定属性且符合值时生效。
@ConditionalOnBean 当容器中存在指定的 Bean 时生效。
@ConditionalOnExpression 基于 SpEL 表达式的逻辑判断。

3. 配置属性绑定类

Spring Boot 强调"外部化配置 ",以下注解负责将配置文件与 Java 对象解耦。

  • @ConfigurationProperties最推荐。将配置文件中指定前缀(prefix)的属性批量绑定到一个 POJO 类上。支持松散绑定(Relaxed Binding)和嵌套属性。

  • @EnableConfigurationProperties : 在配置类上开启对指定 @ConfigurationProperties 类的支持。

  • @Value: 用于注入单个属性值。不支持松散绑定,通常用于注入简单的配置项或环境变量。

4. Web 与集成开发常用类

虽然部分注解属于 Spring MVC,但在 Spring Boot 项目中几乎是必用的。

  • @RestController@Controller + @ResponseBody 的结合体,返回 JSON 数据。

  • @RequestMapping 家族

    • @GetMapping@PostMapping@PutMapping@DeleteMapping
  • @RestControllerAdvice:全局异常处理和数据增强。

  • @RequestBody:接收并解析 HTTP 请求体中的 JSON 数据。

  • @PathVariable:获取 RESTful URL 中的路径参数。

将yml中的配置项注入到类中

在Spring Boot中,将YAML配置文件中的设置应用到代码里主要有三种核心方法。

方法 核心注解 适用场景 关键特点
类型安全的批量绑定 @ConfigurationProperties 注入一组具有层级结构的配置(如数据库、第三方服务配置) 支持复杂数据类型(List, Map, 嵌套对象)、类型安全、松散绑定、校验
单个配置项注入 @Value 注入少量独立的配置项 简单灵活,支持SpEL表达式和默认值,但不易管理大量相关配置
编程式动态获取 Environment接口 需要在运行时根据条件动态读取配置 编程方式灵活,可在非Bean组件中使用,但需手动处理类型转换

@ConfigurationProperties

这是Spring Boot推荐的方式,尤其适合将一组相关的配置(如整个数据库连接信息)一次性映射到一个JavaBean中,从而实现类型安全的配置管理 。

1. 创建配置类

首先,你需要创建一个普通的Java类,并使用 @ConfigurationProperties 注解,通过其 prefix属性指定配置在YAML文件中的共同前缀 。

XML 复制代码
# application.yml
app:
  name: "MyApplication"
  server:
    host: "192.168.1.100"
    port: 8080
  services:
    - "user-service"
    - "order-service"
java 复制代码
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;

@Component // 将该类声明为Spring管理的组件(Bean)
@ConfigurationProperties(prefix = "app") // 绑定所有以'app'开头的配置
public class AppConfig {
    private String name;
    private Server server; // 嵌套对象
    private List<String> services; // 集合

    // 静态内部类,对应YAML中的`server`嵌套结构
    public static class Server {
        private String host;
        private int port;
        // 必须提供getter和setter方法
        public String getHost() { return host; }
        public void setHost(String host) { this.host = host; }
        public int getPort() { return port; }
        public void setPort(int port) { this.port = port; }
    }

    // 必须为所有需要绑定的字段提供getter和setter方法
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Server getServer() { return server; }
    public void setServer(Server server) { this.server = server; }
    public List<String> getServices() { return services; }
    public void setServices(List<String> services) { this.services = services; }
}

2. 启用配置绑定

为了让Spring识别并处理 @ConfigurationProperties注解,你需要通过以下两种方式之一启用它:

  • 在配置类上添加 @Component等注解,让组件扫描能发现它(如上例所示)。

  • 在一个 @Configuration类上使用 @EnableConfigurationProperties(YourConfigClass.class)进行显式启用 。

3. 在业务类中注入使用

配置类本身已经是一个Spring Bean,你可以像注入其他服务一样在Controller或Service中使用它

其他配置注入方式

使用 @Value注解

适用于注入单个、零散的配置值 。

java 复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MyService {
    // 直接注入单个值
    @Value("${app.name}")
    private String appName;

    // 可以设置默认值,当`app.description`不存在时使用冒号后的值
    @Value("${app.description:Default Description}")
    private String appDescription;
}

使用 Environment接口

提供了一种编程式的、更灵活的方式来获取属性,尤其适合在非Bean组件中或需要动态获取配置的场景 。

java 复制代码
import org.springframework.core.env.Environment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyComponent {
    @Autowired
    private Environment environment;

    public void someMethod() {
        // 通过getProperty方法获取配置
        String appName = environment.getProperty("app.name");
        Integer serverPort = environment.getProperty("app.server.port", Integer.class); // 指定类型
        String someValue = environment.getProperty("some.key", "Default Value"); // 带默认值
    }
}

定时任务

在 Spring Boot 中编写定时任务非常方便,它提供了一套清晰的注解和配置方式。

1. 启用定时任务支持

在主启动类上添加 @EnableScheduling注解,这是告诉 Spring Boot 开启定时任务功能的开关。

java 复制代码
@SpringBootApplication
@EnableScheduling // 核心注解,启用定时任务功能
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

2. 创建定时任务类并编写方法

在一个被 Spring 管理的 Bean(例如添加了 @Component注解的类)中,编写你的任务方法,并在方法上使用 @Scheduled注解来定义执行规则。

java 复制代码
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component // 将类声明为Spring管理的组件
public class MyScheduledTasks {

    // 此方法每5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void taskWithFixedRate() {
        System.out.println("固定频率任务执行,当前时间:" + System.currentTimeMillis());
        // 在这里写入你的业务逻辑,比如清理临时文件、发送心跳包等
    }
}

@Scheduled注解提供了多种参数来满足不同的调度需求:

调度策略 关键字 / 示例 核心特点与适用场景
固定频率 @Scheduled(fixedRate = 5000) 从上一次开始 时间计算,每隔5秒执行一次。适合执行时间短且稳定的任务。
固定延迟 @Scheduled(fixedDelay = 5000) 从上一次结束 时间计算,延迟5秒后再执行下一次。适合执行时间不确定的任务,避免重叠。
Cron表达式 @Scheduled(cron = "0 0 12 * * ?") 功能最强大,可实现复杂时间规则(如每天中午12点执行)。适合需要精确日历控制的任务。
初始延迟 @Scheduled(initialDelay = 10000, fixedRate = 5000) 常与上述策略结合,让应用启动后先延迟10秒 ,再开始按固定频率执行。适合避免应用启动时资源竞争。

Cron 表达式

Cron 表达式由6-7个字段组成,格式为:秒 分 时 日 月 周 年(可选)

  • *表示任意值。
  • ?用于不关心的字段(通常用于日和周的冲突避免)。
  • -表示范围,如 10-20。
  • /表示间隔,如 0/5表示从0秒开始,每5秒。

常用示例

  • 0 0/5 * * * ?:每5分钟执行一次。

  • 0 0 9-18 * * MON-FRI:工作日早9点到晚6点整点执行。

  • 0 0 10 L * ?:每月最后一天上午10点执行。

SpringBoot如何自定义线程池并且应用到实际的场景

配置自定义线程池

创建配置类是定义线程池的第一步,以下是两种主流方式:

1. 基础配置方式

直接使用 @Bean注解创建 ThreadPoolTaskExecutor实例,这是最常用的方法 。

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync // 关键注解:启用异步支持
public class ThreadPoolConfig {

    @Bean(name = "customTaskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数,默认为1
        executor.setCorePoolSize(10);
        // 最大线程数,默认为Integer.MAX_VALUE
        executor.setMaxPoolSize(20);
        // 队列容量,默认为Integer.MAX_VALUE
        executor.setQueueCapacity(100);
        // 非核心线程空闲存活时间(秒)
        executor.setKeepAliveSeconds(60);
        // 线程名称前缀,便于日志追踪
        executor.setThreadNamePrefix("CustomExecutor-");
        // 拒绝策略:当池和队列都满时,由调用者线程直接执行任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 关闭前的等待时间(秒)
        executor.setAwaitTerminationSeconds(60);
        // 初始化
        executor.initialize();
        return executor;
    }
}
2. 基于配置文件的动态配置

通过 application.yml管理参数,提升灵活性

XML 复制代码
# application.yml
task:
  pool:
    corePoolSize: 10
    maxPoolSize: 20
    keepAliveSeconds: 60
    queueCapacity: 100
java 复制代码
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "task.pool") // 绑定配置
public class TaskThreadPoolConfig {
    private int corePoolSize;
    private int maxPoolSize;
    private int keepAliveSeconds;
    private int queueCapacity;
}

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Autowired
    private TaskThreadPoolConfig config;

    @Bean(name = "customTaskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 使用配置文件中的值
        executor.setCorePoolSize(config.getCorePoolSize());
        executor.setMaxPoolSize(config.getMaxPoolSize());
        executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
        executor.setQueueCapacity(config.getQueueCapacity());
        executor.setThreadNamePrefix("CustomExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

线程池的应用

配置好线程池后,可以通过以下方式在业务代码中使用。

1. 结合 @Async注解实现异步执行

在方法上添加 @Async并指定线程池名称,该方法便会异步执行 。

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    // 指定使用自定义的线程池
    @Async("customTaskExecutor")
    public void processOrder(Order order) {
        // 模拟耗时操作,如订单处理、日志记录等
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("订单处理完成,线程: " + Thread.currentThread().getName());
    }
}
@Async

在 Spring 框架中,@Async注解是一个强大的工具,它能让你轻松地将方法调用变为异步执行,从而提升应用的响应速度和并发处理能力。

@Async的核心价值在于实现 "异步调用 "。这意味着,当你调用一个标记了 @Async的方法时,该方法不会在调用者的线程中立即执行,而是由 Spring 将其提交到一个独立的线程池中运行。调用者的线程在发起调用后立即返回,不会被这个耗时任务阻塞。

工作原理:AOP 与动态代理

@Async的功能并非由注解本身实现,而是依赖于 Spring 强大的 AOP 和动态代理机制

  1. 启用与扫描 :当你在配置类或启动类上添加 @EnableAsync注解后,Spring 会开启异步功能支持。

  2. 代理创建 :Spring 容器在初始化 Bean 时,如果发现某个 Bean 的类或方法上标有 @Async,就会通过一个后置处理器为其创建一个代理对象。这个代理对象包装了原始的 Bean。

  3. 调用拦截 :当你的代码(例如 Controller)通过 Spring 容器注入并调用这个 Bean 的方法时,实际上调用的是代理对象 的方法。

  4. 异步执行 :代理对象的方法被调用时,会被一个特殊的拦截器(如 AsyncExecutionInterceptor )捕获。该拦截器不会直接执行你的业务方法,而是从注解中解析出需要使用的线程池,然后将你的业务方法封装为一个 Callable任务,提交给线程池去执行 。提交完成后,拦截器立即返回,从而使调用者线程得以继续。

常见"坑"与最佳实践

  1. 自调用失效在同一个类中,一个方法A直接调用另一个被 @Async标记的方法B,异步是无效的 。因为自调用绕过了 Spring 的代理对象,直接调用了原始方法。解决方案是将异步方法抽取到另一个 Service 中。

  2. 方法必须为 public@Async注解只能应用于 public方法上,对 private, protected方法无效。

  3. 异常处理 :异步方法内部的异常默认不会 传播到调用者。如果方法返回 Future,异常会在调用 future.get()时抛出。对于返回 void的方法,需要实现 AsyncUncaughtExceptionHandler接口来全局处理未捕获的异常。

  4. 避免循环依赖 :如果两个 Service 相互注入,并且都包含 @Async方法,可能在启动时因代理创建顺序问题导致循环依赖。建议使用构造函数注入来避免。

2. 编程式使用 CompletableFuture

对于需要处理返回结果或组合多个异步任务的场景,CompletableFuture更合适 。

java 复制代码
@Service
public class DataProcessService {

    @Autowired
    private ThreadPoolTaskExecutor customTaskExecutor; // 直接注入线程池

    public CompletableFuture<String> processDataAsync(String data) {
        // 使用 supplyAsync 并指定线程池
        return CompletableFuture.supplyAsync(() -> {
            // 模拟数据处理
            return "处理后的数据: " + data.toUpperCase();
        }, customTaskExecutor);
    }
}

// 在调用处,可以等待所有任务完成
@GetMapping("/process-batch")
public String processBatch() {
    List<String> dataList = Arrays.asList("data1", "data2", "data3");
    CompletableFuture<?>[] futures = dataList.stream()
        .map(dataProcessService::processDataAsync)
        .toArray(CompletableFuture[]::new);
    
    // 等待所有任务完成
    CompletableFuture.allOf(futures).join();
    return "批量处理完成";
}

异常处理

整个异常处理流程的起点在 Spring MVC 的核心------DispatcherServlet 。当 Controller中的方法抛出异常时,会被 DispatcherServlet的 doDispatch方法捕获 。随后,它进入异常解决流程,核心目标是找到一个能够处理该异常的组件 。

  1. 局部优先:@ExceptionHandler

    首先,Spring 会查找抛出异常的那个 Controller内部是否定义了用 @ExceptionHandler注解的方法如果找到了匹配的异常类型,就由这个局部处理器方法直接处理并生成响应 。这是最精确、优先级最高的处理方式 。

    1. @ExceptionHandler注解的源码非常简单,其主要作用是指定该方法可以处理的异常类型(Class<? extends Throwable>[] value())。这些异常类型就是后续匹配的关键。

    2. ExceptionHandlerMethodResolver :这个类是幕后功臣。它在应用启动时(例如,当某个Controller类被加载时),会扫描该类中所有被 @ExceptionHandler标记的方法。它会解析出每个方法能处理的异常类型 ,并建立一个内部的 映射表(Map) ,其键(Key)是异常类型,值(Value)是对应的 Method对象。这个映射关系是后续快速匹配的基础。

    3. 当异常发生时,Spring并不是简单地按顺序尝试所有方法。它会使用一个叫做 ExceptionDepthComparator的比较器 ,从所有可能匹配的异常类型中(包括异常本身及其所有父类),找出与抛出异常继承关系最直接、最具体 的那个类型。这就保证了 NullPointerException会优先由处理 NullPointerException的方法处理,而不是由处理其父类 RuntimeException的方法处理。

  2. 全局兜底:@ControllerAdvice

    如果 Controller内部没有合适的处理器,Spring 会转而查找由 @ControllerAdvice注解的全局异常处理类 。这些类中定义的 @ExceptionHandler方法能够处理所有 Controller抛出的指定异常,实现了异常的集中管理 。其背后的核心技术是 ExceptionHandlerExceptionResolver,它负责在应用启动时扫描和缓存这些处理方法,并在异常发生时执行精确的匹配逻辑 。

    1. Spring在容器启动时,会通过 ControllerAdviceBean类扫描所有被 @ControllerAdvice注解的类 ,并将它们收集起来。随后,ExceptionHandlerExceptionResolver会为每一个这样的类创建一个 ExceptionHandlerMethodResolver实例,用于解析其内部的 @ExceptionHandler方法,并将这些解析器缓存起来,形成全局的异常处理能力。

    2. @ControllerAdvice的强大之处在于它可以通过注解属性(如 basePackages, assignableTypes, annotations)来限制其生效的Controller范围。例如,@ControllerAdvice(basePackages = "com.example.api")只会处理指定包下Controller的异常。这是通过一个 HandlerTypePredicate组件在匹配阶段进行判断的。

    3. 当存在多个 @ControllerAdvice类时,可以通过 @Order注解或实现 Ordered接口来定义它们的优先级。数值越小,优先级越高。这在处理模块化应用的异常时非常有用。

    4. 推荐使用 @RestControllerAdvice :对于现代RESTful应用,直接使用 @RestControllerAdvice代替 @ControllerAdvice更为方便,因为它本身组合了 @ControllerAdvice@ResponseBody注解,可以确保你的 @ExceptionHandler方法返回值直接序列化为JSON/XML,而无需额外注解

Spring Boot 的默认兜底机制

当异常未被任何 @ExceptionHandler(无论是局部的还是全局的)捕获时,Spring Boot 的默认机制才会登场,这是它相较于 Spring MVC 提供的一大便利。

  1. 统一入口:ErrorPageCustomizer

    Spring Boot 通过自动配置类 ErrorMvcAutoConfiguration注册了一个 ErrorPageCustomizer 。这个组件的作用是:当容器(如 Tomcat)监测到异常发生时,自动将请求转发到配置的 /error路径 。这个路径默认为 /error,但可以通过 server.error.path配置项自定义 。

  2. 请求处理:BasicErrorController

    请求被转发到 /error后,由 BasicErrorController 处理。它的一个精妙之处在于能自动适配客户端类型​ :

    • 对于浏览器 ,它的 errorHtml()方法会被调用,返回一个 HTML 错误页面。

    • 对于机器客户端 (如 Postman、移动应用),它的 error()方法会被调用,返回 JSON 格式的错误信息。

  3. 数据与视图解析

    • 错误属性:DefaultErrorAttributes

      无论返回 HTML 还是 JSON,BasicErrorController都会使用 DefaultErrorAttributes工具来从请求中获取详细的错误信息,如状态码、异常信息、请求路径等,并将其封装在一个 Map中供视图或响应体使用 。

    • 视图解析:DefaultErrorViewResolver

      当需要返回 HTML 页面时,会使用 DefaultErrorViewResolver 。它会按照一套清晰的优先级规则去解析自定义错误视图 :

      1. 精确匹配:templates/error/404.html

      2. 模糊匹配:templates/error/4xx.html

      3. 静态资源:static/error/404.html

      4. 如果以上都未找到,最终会使用内置的 "Whitelabel" 错误页面。

Spring Cloud

微服务

单体架构(Monolith):最初的美好与必然的崩塌

每个伟大的系统几乎都始于单体。在项目初期,单体是完美的:开发快、部署简单、测试容易、性能损耗极低(全是内存调用,没有网络开销)。

单体的弊端:

随着业务膨胀,单体逐渐变成了"大泥球":

  • 发布流水线拥堵:哪怕改一行代码,也要重新编译部署整个几百MB甚至数GB的 Jar 包。一个人的错误会让全公司的发布停摆。

  • 扩展性差:如果系统里只有"图片处理"模块耗内存,你必须水平扩展整个单体应用,极度浪费资源。

  • 技术债累积:想把旧的 Struts2 换成 Spring Boot?对不起,全局重构,没人敢动。

  • 故障蔓延(Blast Radius):一个模块的内存泄漏或死循环,会拖垮整台服务器,导致全站崩溃。

微服务的本质是 "分而治之" 。它将单体拆分为多个独立自治的服务,每个服务运行在自己的进程中。但这带来了一个致命的问题:原本简单的内存调用,变成了不可靠的网络调用。 为了解决这些"网络带来的混乱",微服务组件才应运而生。每个组件的出现,都是为了填补分布式系统的一个坑。

Spring Cloud 并不是一个具体的、单一的框架,而是一个全家桶(Umbrella Project) 。它集合了一系列子项目,旨在为开发者提供一套快速构建分布式系统(即微服务)的工具集。在微服务架构中,你会遇到很多单体架构不曾有的麻烦(比如:服务怎么找、配置怎么改、挂了怎么办)。Spring Cloud 的出现,就是为了屏蔽分布式环境下的复杂性,提供一套标准化的解决方案。

Spring Boot 负责"从 0 到 1"创建一个应用,而 Spring Cloud 负责"从 1 到 N"协调这些应用如何像一个整体一样工作。

五大核心组件(架构视角)

一个完整的微服务系统,通常需要这五类功能的支撑。目前市面上最流行的是 Spring Cloud Alibaba 体系,下表展示了功能与对应组件的映射:

核心功能 解决什么问题? 主流实现 (Alibaba/原生) 早期实现 (Netflix/停更)
服务发现与注册 自动记录和查找服务的 IP 和端口。 Nacos / Consul Eureka
集中配置中心 统一管理所有服务的配置文件。 Nacos / Spring Cloud Config Config
智能路由网关 系统的统一入口,负责鉴权、限流、转发。 Spring Cloud Gateway Zuul
熔断与降级 防止某个服务挂掉导致全站雪崩。 Sentinel / Resilience4j Hystrix
负载均衡 将请求平均分配到多个服务实例上。 LoadBalancer Ribbon
  1. 注册中心:服务 A 和 B 启动后,把自己的地址报给 Nacos。

  2. 配置中心:服务从 Nacos 实时读取自己的数据库连接等配置。

  3. 网关:用户的请求打到 Gateway,Gateway 问 Nacos :"服务 A 在哪?",然后转发过去。

  4. 容错处理:如果服务 B 响应慢,Sentinel 立即介入,防止请求堆积。

注册中心(Registry Center)

1. 核心作用:它解决了什么问题?

注册中心的核心职责是实现服务实例的自动化管理,具体解决了三个痛点:

  • 解耦 IP 与服务名 :调用者(Consumer)不再需要记住成百上千个具体的 IP 地址,只需要记住服务逻辑名(如 order-service)。

  • 动态扩缩容:当流量高峰期你新增了 10 台机器,注册中心会自动把这 10 台机器的 IP 告知调用者,无需人工修改配置。

  • 健康检查(熔断的前提):如果某台机器宕机,注册中心通过心跳机制发现它"失联"了,会立即将其从通讯录中剔除,防止请求打到死节点上。

2. 运行机制:服务的一生

注册中心的工作流程可以概括为以下四个关键环节:

  1. 服务注册(Service Register)服务提供者在启动时 ,将**自己的服务名、IP、端口、元数据(版本、权重等)**上报给注册中心。

  2. 服务续约(Service Renew / Heartbeat)提供者每隔几秒(默认通常 30s)发一次"心跳"给注册中心,证明自己还活着。

  3. 服务发现(Service Discovery)消费者在需要调用接口时,去注册中心拉取(Fetch)一份最新的提供者名单,并缓存在本地。

  4. 服务剔除(Service Evict)如果注册中心长时间没收到某实例的心跳,就会认为它挂了,将其从注册表中抹除,并通知所有消费者更新缓存。

3. 底层原理:CAP 定理的权衡

注册中心作为分布式系统,必须在 CAP(一致性、可用性、分区容错性) 中做取舍。目前主流注册中心分为两大派系:

AP 架构(可用性优先)------ 代表:Eureka、Nacos(默认)

  • 理念 :在分布式环境下,即便数据短时间不一致(比如某个 IP 已经挂了但还没被剔除),也比整个注册中心瘫痪强。

  • 特点:每个节点都是平等的,即使只剩一个节点也能提供服务。适合服务规模大、对可用性要求极高的场景。

CP 架构(一致性优先)------ 代表:Zookeeper、Consul、Nacos(通过配置)

  • 理念注册中心里的数据必须是绝对正确的。如果发生网络分区导致无法选出 Leader,整个注册中心宁可拒绝服务。

  • 特点 :利用 RaftZAB 协议保证强一致性。适合对数据准确性要求极高的场景。

在服务发现领域,AP 通常优于 CP。因为即使拿到了一个稍微过时的 IP,也可以通过客户端负载均衡和重试机制解决;但如果注册中心挂了,整个微服务集群都会瞬间"失联"。

特性 Eureka Zookeeper Nacos (推荐) Consul
开发者/背景 Netflix Apache Alibaba HashiCorp
CAP 模式 AP CP AP/CP 可切换 CP
一致性协议 HTTP 最终一致性 ZAB Raft / Distro Raft
健康检查 TTL (心跳) TCP 连接 (临时节点) TCP/HTTP/MySQL HTTP/TCP/gRPC
功能丰富度 仅注册中心 通用分布式协调 注册中心 + 配置中心 注册中心 + 配置中心
现状 维护状态,不再更新 成熟稳定 国内主流,生态活跃 强大,支持 Service Mesh

配置中心(Configuration Center)

实现配置的集中化管理、动态更新、环境隔离、版本控制与安全审计

1. 核心作用:为什么需要它?

在单体时代,配置通常写在 application.yml 里。但在微服务架构下,这种做法会引发以下问题:

  • 配置散乱:几百个微服务,每个都有自己的配置文件,修改一个公共参数(如数据库连接池大小)需要手动改几百次。

  • 无法热更新:传统的配置修改必须重启服务才能生效。在生产环境下,重启意味着业务中断或风险。

  • 环境不隔离:开发、测试、生产环境的配置容易混淆,手动切换极易出错。

  • 缺乏安全性与审计:谁在什么时候改了什么配置?能不能回滚?明文密码是否泄露?单体配置无法回答这些问题。

2. 核心机制:它是如何工作的?

配置中心的运作遵循"一处修改,到处生效 "的原则,通常采用 C/S(客户端/服务器)架构

  1. 管理后台(Dashboard):运维或开发在 UI 界面修改配置并保存。

  2. 持久化存储:配置中心 Server 将变更存入数据库(如 MySQL)或版本库(如 Git)。

  3. 配置下发/拉取:Server 通知客户端(微服务)有更新,或者客户端定期询问 Server。

  4. 本地缓存与刷新 :客户端获取新配置后,更新本地缓存,并通过 Spring 的容器机制 (如 @RefreshScope)重新注入到 Bean 中。

3. 底层原理:推(Push)还是拉(Pull)?

这是配置中心设计的核心技术点。目前主流框架通常采用 "长轮询(Long Polling)" 机制,兼顾了实时性和系统压力。

  • 长轮询机制(以 Nacos 为例)

    • 客户端发起请求询问:"我的配置变了吗?"

    • Server 不会立即返回,而是把请求"挂起"一段时间(如 30s)。

    • 如果在挂起期间配置变了 :Server 立即返回新内容。

    • 如果到期了配置还没变 :Server 返回空,客户端立即再次发起请求。

    • 优点:比简单的"拉"更实时,比纯粹的"推"更可靠(不会因为网络闪断导致丢失通知)。

  • 动态刷新原理 : 在 Spring 生态中,当配置中心通知变更时,Spring 会发布一个 EnvironmentChangeEvent。标注了 @RefreshScope 的 Bean 会被销毁并重新创建,从而加载最新的配置值,实现无感更新

特性 Apollo (阿波罗) Nacos (推荐) Spring Cloud Config
开发者/背景 携程 (Ctrip) 阿里巴巴 Spring 官方
配置持久化 MySQL MySQL / Raft Git / SVN / 本地文件
热更新支持 极佳 (毫秒级) 极佳 (长轮询) 需配合 Bus (消息总线)
权限/审核 非常严谨(多级审核) 较简单 基本没有 UI,需自研
部署难度 较高(组件多) 非常简单(一个包) 中等
选型建议 大型企业级,对权限要求极严。 中小型及主流项目,简单好用。 极简主义,深度依赖 Git。

典型使用场景

  • 数据库/中间件切换:在不停机的情况下,将流量从主库切换到从库,或修改 Redis 集群地址。

  • 业务开关(Feature Toggle):通过一个布尔值动态开启或关闭某个新功能(如:大促期间关闭不重要的非核心业务逻辑)。

  • 灰度发布控制:动态调整网关的权重配置,实现 10% 的用户访问新版本。

  • 日志级别动态调整 :线上出 Bug 时,临时将日志级别从 INFO 改为 DEBUG,排查完再改回来,无需重启。

网关(API Gateway)

统一接入、安全防护、流量治理、业务解耦。

核心作用:为什么它不可或缺?

在没有网关的时代,客户端(手机、网页)需要直接和几十个微服务通信。这简直是灾难:

  • 鉴权混乱:难道每个微服务都要写一遍登录校验逻辑吗?

  • 跨域控制:每个服务都要配置一遍 CORS,维护成本极高。

  • 安全风险:微服务的 IP 直接暴露在公网,等于裸奔。

  • 客户端复杂:客户端需要维护大量的服务端地址,一旦服务拆分或合并,客户端就得跟着改代码。

三大核心概念

在 Spring Cloud Gateway 中,理解了这三个词,你就理解了它的工作逻辑:

  1. 路由(Route):网关的基本模块。它由 ID、目标 URI、一组谓词和一组过滤器组成。

  2. 谓词(Predicate)"匹配规则" 。它是 Java 8 的 java.util.function.Predicate。简单说,就是判断当前的请求是否符合这个路由。比如:路径是不是以 /order/ 开头?

  3. 过滤器(Filter)"加工环节"。在请求发送到下游服务之前(pre)或之后(post)对请求进行修改。比如:增加一个请求头,或者统计耗时。

为什么 Spring Cloud Gateway 淘汰了经典的 Zuul 1.x?

Zuul 1.x 的局限(Servlet 模式)

Zuul 1.x 基于 Servlet 2.5,采用的是阻塞式 I/O 。每一个请求都会占用一个线程,直到请求结束。在高并发场景下,线程池很快就会被耗尽,导致系统瘫痪。

Gateway 的底层(Reactive 模式)

Spring Cloud Gateway 是基于 Spring 5、Project Reactor 和 Netty 构建的。

  • 非阻塞/事件驱动 :它运行在 Netty 之上,不需要为每个请求分配一个线程。一个少量的线程池就能处理数万个并发连接。

  • 性能:在高并发、高长连接(如 WebSocket)场景下,性能远超 Zuul 1.x。

4. 核心工作流程

  1. 请求到达DispatcherHandler 接收请求。

  2. 查找路由RoutePredicateHandlerMapping 根据 Predicate 找到匹配的路由。

  3. 过滤链执行FilteringWebHandler 获取该路由的 Filter 链。

  4. 转发与响应:请求经过 Pre 过滤器 -> 转发到后端服务 -> 获取响应 -> 经过 Post 过滤器 -> 返回客户端。

特性 Spring Cloud Gateway Netflix Zuul 2.x Nginx / OpenResty
底层基础 Netty / Project Reactor Netty C / Lua
编程模型 响应式 (Reactive) 响应式 (Reactive) 事件驱动
动态路由 支持较好 支持 需要 Reload 或 Lua
开发难度 Java 开发者上手极快 较复杂 需要熟悉 Nginx/Lua
适用场景 Java 微服务生态首选 Netflix 体系 高性能静态资源/边缘负载

典型使用场景

  • 统一鉴权:在网关层校验 JWT 或 Session,合法的请求才下发到业务模块。

  • 流量监控 & 限流:利用 Redis + Lua 脚本实现令牌桶或漏桶算法,保护下游服务不被压垮。

  • 黑白名单:封禁恶意爬虫的 IP。

  • 灰度发布:通过自定义 Predicate,将 10% 的流量引导至新版本的 Service B。

  • 协议转换:外部是 HTTP 请求,网关内部转为 Dubbo 或 gRPC 调用。

熔断(Circuit Breaker)和降级(Fallback)

核心作用:为什么要当"保险丝"?

在微服务环境中,请求往往需要跨越多个服务(A -> B -> C)。如果服务 C 响应极慢或宕机,会导致服务 B 的线程大量积压,进而拖垮服务 A,最终引发全站崩溃,这就是所谓的**"雪崩效应(Cascading Failure)"**。

  • 熔断的作用 :当检测到下游服务异常(超时、报错率高)时,及时切断调用鏈,直接返回错误,保护调用方不被拖死。

  • 降级的作用 :当主逻辑走不通时(熔断、限流或报错),提供一个"备选方案"(如返回默认值、缓存数据或友好提示),保证业务流程不断掉,只是功能有所损耗。

核心机制:熔断器的"三态"转换

熔断器的设计灵感完全来自于物理电表箱。它有三种标准状态,通过状态机进行切换:

A. 关闭状态 (Closed)

  • 逻辑:这是正常状态。请求可以自由通过。

  • 动作:熔断器会统计请求的成功率和错误率。

  • 切换 :当错误率达到设定的阈值(如最近 10 次请求有 50% 失败),熔断器会跳到"开启"状态。

B. 开启状态 (Open)

  • 逻辑:这是保护状态。所有请求直接被拦截,不再去调下游服务。

  • 动作 :直接执行 Fallback(降级逻辑)

  • 切换 :开启一段时间(冷却时间,如 5s)后,会自动切换到"半开"状态。

C. 半开状态 (Half-Open)

  • 逻辑 :这是尝试状态。允许少量 请求通过,去测试下游服务是否恢复。

  • 切换

    • 如果请求全部成功,说明下游好了,回到"关闭"状态。

    • 如果请求依然失败 ,说明下游没好,滚回"开启"状态,继续冷静。

目前主流框架(如 Sentinel 或 Resilience4j)通常采用以下两种维度进行判定:

  1. 慢调用比例:如果请求响应时间超过 X 毫秒的比例达到阈值,触发熔断。这在解决"拖慢系统"的问题上非常有效。

  2. 异常比例/异常数:如果请求报错的比例(如 1 分钟内失败率 > 50%)达到阈值,触发熔断。

底层支撑:通常使用滑动时间窗口(Sliding Window)算法。它将时间切成小格,动态统计最近一段时间内的请求数据,保证统计的时效性和准确性。

特性 Hystrix (已停更) Sentinel (推荐) Resilience4j
背景 Netflix 阿里巴巴 Spring 官方推荐
隔离策略 线程池隔离 / 信号量隔离 信号量隔离 (性能更高) 信号量隔离
熔断判定 基于异常比例 异常比例/异常数/慢调用 基于异常比例/慢调用
控制台 较弱 (Hystrix Dashboard) 非常强大 (实时监控/动态规则) 无原生控制台
动态规则 不支持(需重启或复杂配置) 支持 (配合 Nacos 实时下发) 支持

典型使用场景

  • 外部接口依赖:调用第三方支付、地图或物流接口。这些接口不可控,必须加熔断,防止对方宕机拖死我方系统。

  • 核心与非核心业务剥离:在大促(如双 11)期间,手动或自动降级掉"评价"、"猜你喜欢"等非核心逻辑,把资源留给"下单"、"支付"。

  • 缓存失效补偿:当从 Redis 读不到数据或 Redis 挂了时,降级逻辑可以去读一个备份的本地缓存或内存数据。

负载均衡(Load Balancing)

1. 核心作用:它解决了什么问题?

它是为了解决分布式系统的三大生存问题:

  • 高可用性(High Availability):如果某个实例挂了,负载均衡器能瞬间感知并绕过它,保证业务不中断。

  • 水平扩展(Scalability):当流量翻倍时,你只需多开几台机器,负载均衡器会自动把新流量分过去。

  • 低延迟与高性能:通过将请求分摊,避免单个节点由于 CPU 或内存过载而导致的

2. 核心机制:服务器端 vs 客户端

这是理解 Spring Cloud 负载均衡的关键。负载均衡主要分为两大流派:

A. 服务器端负载均衡 (Server-side LB)

  • 硬件代表:F5。

  • 软件代表:Nginx、LVS、阿里云 SLB。

  • 机制客户端只知道网关或负载均衡器的地址 。负载均衡器接收请求后,根据内部配置转发给后端的某台机器。

  • 特点:客户端是"盲目"的,它不知道后面到底有多少台机器。

B. 客户端负载均衡 (Client-side LB)

  • 代表组件Spring Cloud LoadBalancer (取代了已停更的 Netflix Ribbon)。

  • 机制客户端(调用者)自己去注册中心(如 Nacos)拉取一份完整的服务清单,然后自己在本地根据算法选出一台机器进行调用。

  • 特点:调用者"心中有数",减少了一次额外的网络跳转(不需要经过中间代理),性能更高,灵活性更强。

负载均衡器的大脑就是它的算法。常见的算法包括:

算法名称 逻辑描述 适用场景
轮询 (Round Robin) 像发牌一样,按顺序给每个实例发一个请求。 实例硬件配置基本一致。
随机 (Random) 随便抓一个。 简单场景,概率上基本平均。
加权 (Weight) 给性能好的机器多发点,给老旧机器少发点。 异构服务器(新老机器混搭)。
最小连接数 (Least Connections) 谁现在手里的活儿最少就给谁。 请求处理耗时差异较大的场景。
源地址哈希 (Source IP Hash) 同一个 IP 的请求永远发到同一台机器。 需要实现 Session 保持 或本地缓存。
一致性哈希 (Consistent Hash) 即使节点增减,也能最大限度保证请求分布的稳定性。 分布式缓存、有状态服务。

3. 底层原理:它是如何运作的?

在 Spring Cloud 环境下,负载均衡的运作通常包含以下三个关键步骤:

  1. 服务列表获取 :通过与注册中心通信,实时维护一份 ServiceList

  2. 健康检查(Ping):持续监测列表中的实例是否还活着。

  3. 选择策略执行 :在发起 RestTemplateFeign 调用时,通过拦截器(Interceptor)拦截请求,修改 URL(将服务名替换为具体 IP),然后发送。

Spring Cloud LoadBalancer 是基于 Reactive 响应式编程构建的,相比于 Ribbon 的阻塞式逻辑,它更适配 Spring 5+ 的异步环境。

典型使用场景

  • 网关接入层:Nginx 接收外部公网流量,分发给多个网关集群节点。

  • 微服务间调用:Service A 通过 Feign 调用 Service B,在 A 内部执行负载均衡逻辑。

  • 有状态连接:在 WebSocket 场景下,利用 IP Hash 确保长连接不中断。

  • 灰度/蓝绿发布 :通过自定义负载均衡策略,将特定的用户流量(如 Header 里带 version=v2 的)定向到新版本实例。

动态代理是如何将接口转化为 HTTP 请求的?

1. 启动阶段

当你开启**@EnableFeignClients** 时,Spring Boot 会启动扫描机制。

  1. 解析元数据FeignClientRegistrar 会找到所有标注了 @FeignClient 的接口,并解析其属性(服务名、URL、配置等)。

  2. 注册 BeanDefinition :Spring 并不会直接实例化这个接口,而是注册一个 FeignClientFactoryBean

  3. 创建 JDK 动态代理 :当你真正 @Autowired 这个接口时,FactoryBean 会通过 Feign.builder() 构造出一个 JDK 动态代理对象 (即 Proxy.newProxyInstance)。

2. InvocationHandler 的分发

当你调用接口方法(如 userClient.getUser(1))时,由于它是个代理对象,请求会被拦截并转发给 FeignInvocationHandler

  • 内部结构 :这个 Handler 内部持有一个 Map<Method, MethodHandler>

  • 路由寻址 :它会根据你调用的 Method,从 Map 中找到对应的 SynchronousMethodHandler 。每一个方法都有自己专属的处理器,里面记录了该方法的注解信息。

3. 转化过程:将方法"翻译"成模板

这是最关键的一步。SynchronousMethodHandler 会通过以下步骤完成转化:

  1. RequestTemplate 生成 :利用 Contract(契约)解析器。它会读取方法上的 @GetMapping、@PathVariable、@RequestBody 等注解。

  2. 参数填充 :它将你传入的实参(如 id=1)填入 URL 模板中。

    • GET /users/{id} \\rightarrow GET /users/1
  3. 构造 Request 对象 :最终生成一个包含 URL、Method、Header、Body 的****Request 对象。

4. 运行时执行:从逻辑请求到物理请求

有了 Request 对象后,Feign 会进入最后的发射阶段:

  • 负载均衡 (Load Balancer) :如果你的 URL 是服务名(如 http://user-service/),Feign 会拦截请求,询问 LoadBalancer(如 Spring Cloud LoadBalancer),根据策略选出一个真实的物理 IP。

  • 执行客户端 (Client)

    • 默认是 HttpURLConnection(性能较差,不推荐)。

    • 可以配置为 OkHttpApache HttpClient

  • 编解码 (Encoder/Decoder)

    • Encoder:将 Java 对象转为 JSON(通过 Jackson)。

    • Decoder:将 HTTP 响应的 JSON 转回 Java 对象。

当网络出现抖动时,Feign 是如何配合"重试机制 (Retryer)"和"超时配置"来保证请求的可靠性的?

网络抖动(Jitter)是微服务架构中的"幽灵",它不一定会让服务直接宕机,但会通过延迟波动和偶发性丢包,把系统的脆弱性放大。

1. 超时配置:划定容忍的底线

在进行网络调用时,Feign 主要受两个超时参数控制。如果这两个参数设置不当,网络抖动就会演变成大面积的线程积压。

  • ConnectTimeout(连接超时) :指从建立 TCP 连接到连接成功的最大等待时间。在网络抖动时,如果握手包丢失,这个设置能防止线程死等。

  • ReadTimeout(读取超时) :指连接建立后,从服务器读取到可用数据的最大等待时间。这是应对"慢接口"和"网络丢包"的关键。

默认情况下,Feign 的超时时间非常慷慨(甚至可能是无穷大,取决于底层 HttpClient 的实现)。在生产环境,必须显式设置。 建议 ReadTimeout 设为下游接口平均响应时间的 2~3 倍。

2. 重试机制 (Retryer):给予第二次机会

当 Feign 发现请求失败时(触发了 RetryableException ),它不会立即放弃,而是询问 Retryer:我还能再试一次吗?

Feign 默认重试策略的工作逻辑:

  1. 捕获异常 :只有特定的异常(如超时、连接拒绝)会被封装成 RetryableException

  2. 计算间隔Retryer 会根据当前重试次数,计算下一次重试需要等待的时间。

  3. 判断限制 :检查是否达到了最大重试次数(maxAttempts)。

Feign 内部重试 vs Spring Cloud 重试

作为高级开发,你必须分清这两套重试机制。这是最容易出错的地方:

  1. Feign 原生重试 :由 feign.Retryer 实现。默认是关闭 的(NEVER_RETRY)。

  2. Spring Cloud 负载均衡重试 :这是通过 Spring Cloud LoadBalancer(或早期的 Ribbon)实现的。

在现代 Spring Cloud 环境中,我们通常禁用 Feign 的原生重试,而使用 Spring Cloud LoadBalancer 的重试机制。

  • 原因:LoadBalancer 的重试更高级,它可以在请求失败后,**换一台服务器实例(Next Server)**进行重试,这能有效避开由于单台机器故障导致的持续失败。

Spring Security

Spring Security是一套由 Servlet Filter 组成的强大过滤器链,配合 AOP 实现的方法拦截体系。

过滤器链(Security Filter Chain)

Servlet 规范定义了 Filter,但 Servlet 容器(如 Tomcat)并不认识 Spring 容器里的 Bean。为了让 Spring 定义的安全逻辑生效,Spring 设计了一套"套娃"机制:

  1. DelegatingFilterProxy :这是一个标准的 Servlet 过滤器,部署在 Tomcat 中。它不干活,只负责找人。它会去 Spring 容器里找一个名为 springSecurityFilterChain 的 Bean。

  2. FilterChainProxy :这就是那个被找的人,它是 Spring Security 的总司令部 。它持有并管理着一个或多个 SecurityFilterChain

当我们说"过滤器链"时,通常指的是 DefaultSecurityFilterChain。它内部维护了一个 List<Filter>。在一个标准的 Web 安全配置中,过滤器通常按以下顺序排列:

顺序 过滤器名称 职责 (Responsibility)
1 ChannelProcessingFilter 检查协议(HTTP vs HTTPS)。
2 WebAsyncManagerIntegrationFilter 整合异步请求上下文。
3 SecurityContextPersistenceFilter 极其重要。在请求开始时从 Session 加载上下文,结束时存回 Session。
4 HeaderWriterFilter 向响应中添加安全头部(如 X-Frame-Options)。
5 LogoutFilter 处理登出请求。
6 UsernamePasswordAuthenticationFilter 认证核心。处理表单登录。
7 DefaultLoginPageGeneratingFilter 生成默认登录页面。
8 ExceptionTranslationFilter 异常翻译官。捕获后续滤器抛出的安全异常,决定是去登录还是报 403。
9 FilterSecurityInterceptor 授权终点。最终判定用户是否有权访问该 URL。

过滤器链的执行并不是简单的 for 循环,而是基于 职责链模式(Chain of Responsibility) 的递归调用。

java 复制代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    if (currentFilterIndex < filters.size()) {
        Filter filter = filters.get(currentFilterIndex++);
        // 过滤器处理完自己的逻辑后,必须调用 chain.doFilter 才能继续往下走
        filter.doFilter(request, response, this);
    } else {
        // 所有过滤器都走完了,进入真正的 Controller(Servlet)
        targetServlet.service(request, response);
    }
}
  • 前进(Pre-processing):请求从第 1 个过滤器一直走到第 N 个,每一步都在做检查或提取信息。

  • 回溯(Post-processing):Controller 执行完后,响应会逆序经过每个过滤器。这让过滤器有机会修改响应头或处理异常。

这种机制带来了极高的灵活性解耦性

  • 按需插拔 :如果你想搞 JWT 认证,只需要写一个 JwtAuthenticationFilter 插入到链中即可。

  • 短路机制 :如果第 6 个过滤器发现用户没登录,它可以直接返回响应,而不调用 chain.doFilter(),请求根本打不到 Controller,保护了业务系统。

  • 职责单一:每个过滤器只管一件事(有的管 Session,有的管密码,有的管 CSRF),代码极其清晰。

认证(Authentication)授权(Authorization)

在任何应用中,安全主要归结为两个最基本的问题:

  1. 认证(Authentication):你是谁?(用户名/密码、验证码、指纹、三方登录)。

  2. 授权(Authorization):你能干什么?(普通用户只能看,管理员能删)。

一、 Authentication(认证)内部流程

认证流程的核心是将用户提供的凭证(如用户名密码)转化为一个已认证的身份对象

1. 核心组件
  • Authentication:认证令牌,包含用户信息、权限列表、是否已认证的状态。

  • AuthenticationManager :认证入口,通常实现类是 ProviderManager

  • AuthenticationProvider:具体的认证逻辑实现(如处理用户名密码、短信验证码、OAuth2 等)。

  • UserDetailsService:负责从数据库或内存加载用户信息。

  • PasswordEncoder:负责密码的加密与匹配。

2. 执行步骤
  1. 请求拦截 :用户提交登录请求,被 UsernamePasswordAuthenticationFilter 拦截。

  2. 创建未认证令牌 :过滤器从请求中提取用户名和密码,封装成一个 UsernamePasswordAuthenticationToken(此时 isAuthenticated = false)。

  3. 提交认证 :过滤器将该令牌交给 AuthenticationManager

  4. 委派 ProviderProviderManager 遍历其持有的 AuthenticationProvider 列表,找到能处理该令牌的 Provider(如 DaoAuthenticationProvider)。

  5. 加载用户信息 :Provider 调用 UserDetailsService.loadUserByUsername() 获取数据库中的用户信息(UserDetails)。

  6. 校验凭证 :Provider 使用 PasswordEncoder 比对用户输入的密码与数据库中的加密密码。

  7. 认证成功 :校验通过后,Provider 会创建一个新的、已认证的 Authentication 对象,其中包含用户的权限信息(Authorities),并将 isAuthenticated 设为 true

  8. 存储上下文 :认证信息被返回给过滤器,过滤器将其放入 SecurityContextHolder 中(底层是 ThreadLocal),供后续流程使用。


二、 Authorization(授权)内部流程

授权流程发生在认证之后,当用户尝试访问受保护的资源(如 URL 或方法)时触发。

1. 核心组件
  • SecurityContextHolder:授权时从中获取已认证的身份信息。

  • FilterSecurityInterceptor:负责 Web 资源授权的拦截器。

  • MethodSecurityInterceptor:负责方法级别授权的拦截器。

  • AccessDecisionManager:访问决策管理器,决定是否放行。

  • AccessDecisionVoter:投票器,根据用户权限和资源要求进行投票。

2. 执行步骤
  1. 资源拦截 :用户访问 URL(如 /admin/delete),被 FilterSecurityInterceptor 拦截。

  2. 获取权限要求 :拦截器根据配置(如 .hasRole("ADMIN"))获取访问该资源所需的权限信息(ConfigAttribute)。

  3. 提取当前身份 :从 SecurityContextHolder 中获取当前登录用户的 Authentication 对象。

  4. 发起决策请求 :拦截器将"当前用户信息"、"请求资源"、"资源权限要求"三者交给 AccessDecisionManager

  5. 内部投票AccessDecisionManager 轮询其持有的 AccessDecisionVoter 列表。每个投票器根据自己的逻辑(如角色匹配、表达式计算)投出:ACCESS_GRANTED(赞成)、ACCESS_DENIED(反对)或 ACCESS_ABSTAIN(弃权)。

  6. 统计结果

    • 一票通过制(默认):只要有一个赞成,就放行。

    • 一票否决制:只要有一个反对,就拒绝。

    • 多数服从制:赞成票多于反对票则放行。

  7. 最终处理

    • 通过:请求继续向后执行(进入 Controller)。

    • 拒绝 :抛出 AccessDeniedException,由异常处理过滤器(ExceptionTranslationFilter)处理,通常返回 403 错误。

结合 OAuth2 和 JWT 实现"单点登录(SSO)"和"无状态认证"

在微服务架构中,传统的 Session 模式由于需要服务器端存储且难以跨域共享,已经不再适用。

Spring Security + OAuth2 + JWT 的组合,实质上是将"身份验证"与"资源保护"进行了解耦。OAuth2 提供了协议标准 ,JWT 提供了数据载体 ,而 Spring Security 则负责逻辑拦截。

1. 核心架构角色

在微服务环境中,这套体系通常分为三个关键角色:

  • 认证服务器 (Authorization Server)负责用户的登录校验、发放 JWT 令牌。它是整个系统的"护照办"。

  • 资源服务器 (Resource Server)各个具体的微服务它们不负责登录,只负责校验 JWT 是否合法,并根据其中的权限信息决定是否放行。

  • 网关 (API Gateway) :**系统的统一入口。**它通常负责"令牌转发 (Token Relay)"和初步的鉴权。

2. "单点登录 (SSO)" 的实现逻辑

SSO 的核心是:用户只需要在认证服务器登录一次,就能获得访问所有微服务的通行证。

内部流程:

  1. 引导登录:用户访问任一子服务或网关,Spring Security 发现无权访问,将其重定向到认证服务器。

  2. 统一认证 :用户在认证服务器输入账号密码。此时,认证服务器会建立自己的 Session (通常是基于 Cookie 的)。

  3. 发放令牌 :认证成功后,根据 OAuth2 协议(通常是 Authorization Code 模式),认证服务器生成一个 JWT 返回给客户端。

  4. 后续访问 :当用户访问第二个子服务时,再次被重定向到认证服务器。由于认证服务器已有该用户的 Session,它会直接静默发放新的令牌,用户感知不到第二次登录。

3. "无状态认证 (Stateless)" 的实现原理

微服务的横向扩展要求服务必须是无状态 的。JWT(JSON Web Token) 是实现这一点的绝佳工具。

为什么是无状态的?

  • 自包含性 :JWT 内部包含了用户信息(如 user_id, user_name)和权限列表(authorities)。

  • 自验证性 :JWT 使用非对称加密算法(如 RSA)。

    • 认证服务器 使用私钥对令牌签名。

    • 各微服务 只需持有公钥,即可在本地校验令牌的完整性和合法性。

底层逻辑 :微服务在接收到请求时,Spring Security 的拦截器会提取 Header 中的 JWT,利用本地公钥解密校验。由于不需要查数据库,也不需要查 Session,这个过程是完全无状态且高性能的。

  • 令牌刷新 (Refresh Token) :JWT 通常过期时间较短(如 30 分钟)。为了不让用户频繁登录,需要使用 OAuth2 的 refresh_token 机制来动态置换新的 JWT。

  • 令牌撤回 (Revocation):JWT 一旦发出,除非过期,否则无法撤回。

    • 高级方案在 Redis 中维护一个"黑名单",记录已注销或被踢出的令牌 ID(jti)。微服务校验 JWT 时需额外查一次 Redis。
  • 敏感信息:不要在 JWT 的 Payload 里存放密码等敏感信息,因为 Base64 编码是可以通过工具轻松解密的。

在微服务节点内部,Spring Security 做了以下工作:

  1. 配置资源服务器 :通过 oauth2ResourceServer() 将自己定义为资源服务器。

  2. JWT 转换 (Converter) :将解密后的 JWT Claims 转化为 Spring Security 认识的 GrantedAuthority 权限对象。

  3. 上下文传播在请求执行期间,将用户信息存入 SecurityContextHolder,让业务代码可以像单体应用一样方便地获取当前用户。

服务 A 调用服务 B 时,令牌如何传递?

  • Feign 拦截器 :我们通常会编写一个 RequestInterceptor 。它的任务是从当前线程的 SecurityContext 中取出 JWT,并将其塞入 Feign 调用的 Header 中

  • 网关透传Spring Cloud Gateway 会通过 TokenRelayGatewayFilterFactory 自动将前端传来的 Token 转发给下游服务,确保认证信息在链路中不丢失。

相关推荐
Halo_tjn2 小时前
基于封装的专项 知识点
java·前端·python·算法
Fleshy数模3 小时前
从数据获取到突破限制:Python爬虫进阶实战全攻略
java·开发语言
像少年啦飞驰点、3 小时前
零基础入门 Spring Boot:从“Hello World”到可上线的 Web 应用全闭环指南
java·spring boot·web开发·编程入门·后端开发
苍煜3 小时前
万字详解Maven打包策略:从基础插件到多模块实战
java·maven
有来技术3 小时前
Spring Boot 4 + Vue3 企业级多租户 SaaS:从共享 Schema 架构到商业化套餐设计
java·vue.js·spring boot·后端
东东5163 小时前
xxx医患档案管理系统
java·spring boot·vue·毕业设计·智慧城市
一个响当当的名号4 小时前
lectrue9 索引并发控制
java·开发语言·数据库
进阶小白猿4 小时前
Java技术八股学习Day30
java·开发语言·学习
hhy_smile5 小时前
Class in Python
java·前端·python