Skip to content

Spring 从零到精通验收清单

这页用来验收你是否真正学懂 Spring。不是会写 @Service@Autowired@Transactional 就算掌握,而是要能解释:一个普通 Java 类为什么会变成 Bean,Bean 为什么能被注入,为什么有时拿到的是代理对象,事务为什么会失效,扩展点为什么能支撑 Spring Boot 自动配置、MyBatis Mapper、Feign Client、Dubbo 服务引用。

Spring 的学习主线是一句话:

Spring 先把类变成 BeanDefinition,再由容器按生命周期创建 Bean,通过依赖注入组装对象,通过后置处理器生成代理,通过 AOP 实现事务和横切增强,通过扩展点让框架接入容器。

最终目标

能力达标标准
零基础能懂能从“为什么不要到处 new 对象”讲到 IOC 容器
原理能讲能画出 BeanDefinition、Bean 生命周期、三级缓存、AOP、事务链路
Demo 能写能写 IOC、AOP、声明式事务、编程式事务、事件、扩展点 Demo
项目能落地能解释订单、支付、采集、资产目录系统里 Spring 怎么组织业务
故障能排查能排查注入失败、代理失效、事务不回滚、循环依赖、Bean 创建失败
面试能闭环面试页给标准回答,知识点页讲清原理和失败边界

总路线

mermaid
flowchart TD
    A["为什么需要 Spring"] --> B["BeanDefinition 元信息"]
    B --> C["BeanFactory 和 ApplicationContext"]
    C --> D["Bean 生命周期"]
    D --> E["依赖注入和自动装配"]
    E --> F["循环依赖和三级缓存"]
    F --> G["AOP 代理"]
    G --> H["声明式事务和编程式事务"]
    H --> I["事件机制和扩展点"]
    I --> J["商业场景和生产排查"]

阶段 1:为什么需要 Spring

没有 Spring 会怎样

没有 Spring 时,业务对象经常这样创建:

java
public class OrderService {
    private final OrderRepository orderRepository = new JdbcOrderRepository();
    private final PayClient payClient = new HttpPayClient();
}

短期看很直接,长期会出现:

问题后果
依赖写死测试时难替换,接入新实现要改业务类
生命周期分散初始化连接、关闭资源、缓存预热散落各处
横切逻辑重复日志、事务、权限、监控每个方法都写
配置不统一不同模块自己读配置,生产排查困难
框架难集成MyBatis、事务、定时任务、消息监听难统一管理

Spring 的做法是让业务类只声明“我依赖什么”,容器负责“怎么创建、何时创建、注入谁、何时销毁、是否代理”。

最小 IOC Demo

java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

interface PayClient {
    void pay(String orderNo);
}

class MockPayClient implements PayClient {
    public void pay(String orderNo) {
        System.out.println("pay " + orderNo);
    }
}

class OrderService {
    private final PayClient payClient;

    OrderService(PayClient payClient) {
        this.payClient = payClient;
    }

    void createOrder(String orderNo) {
        payClient.pay(orderNo);
    }
}

@Configuration
class AppConfig {
    @Bean
    PayClient payClient() {
        return new MockPayClient();
    }

    @Bean
    OrderService orderService(PayClient payClient) {
        return new OrderService(payClient);
    }
}

public class IocDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        OrderService orderService = context.getBean(OrderService.class);
        orderService.createOrder("O1001");
        context.close();
    }
}

这段代码里,OrderService 不自己 new PayClient,而是由 Spring 组装依赖。这就是 IOC 的第一层价值。

详细知识点:Spring IOC

阶段 2:BeanDefinition 是 Spring 的图纸

是什么

Spring 不会扫描到一个类就立即 new。它会先把 Bean 的信息解析成 BeanDefinition。BeanDefinition 可以理解为创建 Bean 的图纸。

BeanDefinition 里通常包含:

  • Bean 的 class。
  • Bean 名称。
  • 作用域:singleton、prototype 等。
  • 是否懒加载。
  • 构造参数。
  • 属性依赖。
  • 初始化方法。
  • 销毁方法。
  • 是否候选自动装配。

为什么需要 BeanDefinition

如果没有 BeanDefinition,Spring 就只能边扫描边创建对象,很多能力都做不了:

能力为什么依赖 BeanDefinition
条件注册创建对象前先判断要不要注册
依赖排序先知道依赖关系,再决定创建顺序
循环依赖处理容器需要知道哪些 Bean 正在创建
AOP 代理后置处理器要有机会替换最终对象
Spring Boot 自动配置自动配置本质上也是注册 BeanDefinition
MyBatis Mapper扫描接口后动态注册 Mapper 代理定义

BeanDefinition 流程

mermaid
flowchart TD
    A["读取配置类或扫描包"] --> B["识别 Component 或 Bean 方法"]
    B --> C["解析成 BeanDefinition"]
    C --> D["注册到 BeanDefinitionRegistry"]
    D --> E["BeanFactory 根据定义创建 Bean"]
    E --> F["放入单例池或按作用域返回"]

详细知识点:Bean生命周期

阶段 3:BeanFactory 和 ApplicationContext

两者关系

BeanFactory 是 Spring IOC 容器的基础接口,核心能力是获取 Bean、管理 Bean 定义和创建流程。ApplicationContext 是更完整的应用上下文,在 BeanFactory 基础上增加了国际化、事件、资源加载、环境配置等能力。

对比BeanFactoryApplicationContext
定位基础容器应用级容器
Bean 创建支持支持
事件机制基础能力弱内置事件发布和监听
资源加载较基础更完整
国际化较弱支持
常用程度框架底层业务项目常用

不这样理解会怎样

如果把 Spring 简单理解成“一个 Map”,会解释不了:

  • 为什么 Bean 创建前可以修改 BeanDefinition。
  • 为什么某些 Bean 初始化后变成代理。
  • 为什么事件监听器能收到容器事件。
  • 为什么 Spring Boot 能在启动早期加载 Environment。
  • 为什么容器关闭时能执行销毁回调。

阶段 4:Bean 生命周期全过程

Bean 生命周期是 Spring 原理中心。注入失败、循环依赖、AOP 代理、事务失效、初始化顺序,都能放到这个流程里定位。

mermaid
flowchart TD
    A["BeanDefinition"] --> B["实例化对象"]
    B --> C["属性填充和依赖注入"]
    C --> D["Aware 回调"]
    D --> E["BeanPostProcessor 前置处理"]
    E --> F["初始化方法"]
    F --> G["BeanPostProcessor 后置处理"]
    G --> H["可能返回代理对象"]
    H --> I["放入单例池"]
    I --> J["容器关闭执行销毁"]

生命周期 Demo

java
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
class LifeService implements BeanNameAware, InitializingBean, DisposableBean {
    public LifeService() {
        System.out.println("1 construct");
    }

    public void setBeanName(String name) {
        System.out.println("2 aware beanName = " + name);
    }

    public void afterPropertiesSet() {
        System.out.println("4 init");
    }

    public void destroy() {
        System.out.println("6 destroy");
    }
}

@Component
class LifeBeanPostProcessor implements BeanPostProcessor {
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof LifeService) {
            System.out.println("3 before init");
        }
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof LifeService) {
            System.out.println("5 after init");
        }
        return bean;
    }
}

常见误区

误区正确理解
构造方法里依赖都可用构造阶段属性还没填充完
初始化后一定是原对象后置处理器可能返回代理对象
prototype Bean 容器会自动销毁prototype 创建后交给调用方,Spring 不统一销毁
BeanPostProcessor 只是回调AOP、事务代理、异步、缓存都依赖它

详细知识点:Bean生命周期

阶段 5:依赖注入和自动装配

注入方式怎么选

注入方式优点风险
构造器注入依赖清晰、便于测试、可用 final依赖太多会暴露类职责过重
Setter 注入适合可选依赖对象可能处于半初始化状态
字段注入写法短隐藏依赖、测试困难、不利于不可变对象

业务 Service 推荐构造器注入:

java
import org.springframework.stereotype.Service;

@Service
public class AssetService {
    private final AssetRepository assetRepository;
    private final AuditService auditService;

    public AssetService(AssetRepository assetRepository, AuditService auditService) {
        this.assetRepository = assetRepository;
        this.auditService = auditService;
    }
}

@Autowired@Resource

注解来源默认规则
@AutowiredSpring按类型,多个候选时配合 @Qualifier@Primary
@ResourceJSR-250默认按名称,再按类型

多个实现时必须明确选择:

java
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class NotifyService {
    private final MessageSender messageSender;

    public NotifyService(@Qualifier("smsSender") MessageSender messageSender) {
        this.messageSender = messageSender;
    }
}

不明确会出现 NoUniqueBeanDefinitionException,生产启动直接失败。

详细知识点:@Autowired和@Resource区别

阶段 6:循环依赖和三级缓存

什么是循环依赖

A 依赖 B,B 又依赖 A,就是循环依赖。

mermaid
flowchart TD
    A["ServiceA"] --> B["ServiceB"]
    B --> A

Spring 只能解决部分循环依赖:单例、非构造器注入、没有特别复杂代理链路的场景。构造器循环依赖通常解决不了,因为 A 构造时必须拿到 B,B 构造时又必须拿到 A,没有任何一方能先创建出对象壳子。

三级缓存简化流程

mermaid
flowchart TD
    A["创建 A 实例"] --> B["提前暴露 A 的 ObjectFactory"]
    B --> C["填充 A 属性时需要 B"]
    C --> D["创建 B 实例"]
    D --> E["填充 B 属性时需要 A"]
    E --> F["从三级缓存拿 A 的早期引用"]
    F --> G["B 创建完成"]
    G --> H["A 继续完成初始化"]

为什么不建议依赖循环依赖

循环依赖往往说明职责边界混乱。比如采集服务依赖资产服务,资产服务又依赖采集服务,通常应该抽出编排服务、领域事件或独立查询服务。

详细知识点:循环依赖

阶段 7:AOP 代理全过程

AOP 解决什么问题

AOP 用来处理横切逻辑:事务、日志、权限、审计、监控、缓存、异步等。这些逻辑不属于核心业务,但很多方法都需要。

调用链路

mermaid
flowchart TD
    A["外部调用"] --> B["代理对象"]
    B --> C["拦截器链"]
    C --> D["事务或日志等增强"]
    D --> E["目标方法"]
    E --> F["返回或抛异常"]
    F --> G["增强收尾"]

AOP Demo

java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class CostAspect {
    @Around("execution(* com.example..*Service.*(..))")
    public Object cost(ProceedingJoinPoint point) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return point.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.println(point.getSignature().getName() + " cost " + cost + "ms");
        }
    }
}

JDK 动态代理和 CGLIB

代理方式原理限制
JDK 动态代理基于接口生成代理类目标类需要接口
CGLIB生成目标类子类final 类或 final 方法无法增强

自调用为什么失效

java
@Service
public class OrderService {
    public void outer() {
        this.inner(); // 绕过代理
    }

    @Transactional
    public void inner() {
        // 事务可能不生效
    }
}

this.inner() 调的是目标对象自己,不是代理对象,所以事务拦截器没有机会执行。

详细知识点:Spring AOP

阶段 8:声明式事务和编程式事务

声明式事务是什么

声明式事务最常见写法是 @Transactional。它底层依赖 AOP 代理和 TransactionInterceptor

java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderAppService {
    private final OrderRepository orderRepository;
    private final StockRepository stockRepository;

    public OrderAppService(OrderRepository orderRepository, StockRepository stockRepository) {
        this.orderRepository = orderRepository;
        this.stockRepository = stockRepository;
    }

    @Transactional
    public void createOrder(CreateOrderCommand command) {
        orderRepository.insert(command.toOrder());
        stockRepository.deduct(command.getSkuId(), command.getCount());
    }
}

声明式事务流程

mermaid
flowchart TD
    A["调用代理方法"] --> B["TransactionInterceptor"]
    B --> C["读取事务注解"]
    C --> D["选择事务管理器"]
    D --> E["获取或创建事务"]
    E --> F["绑定连接到当前线程"]
    F --> G["执行业务方法"]
    G --> H{"是否异常或标记回滚"}
    H -->|否| I["提交事务"]
    H -->|是| J["回滚事务"]
    I --> K["清理线程资源"]
    J --> K

编程式事务是什么

编程式事务是在代码里主动控制事务边界,常用 TransactionTemplate

java
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class ImportService {
    private final TransactionTemplate transactionTemplate;
    private final AssetRepository assetRepository;

    public ImportService(TransactionTemplate transactionTemplate, AssetRepository assetRepository) {
        this.transactionTemplate = transactionTemplate;
        this.assetRepository = assetRepository;
    }

    public void importBatch(AssetBatch batch) {
        transactionTemplate.executeWithoutResult(status -> {
            try {
                assetRepository.batchInsert(batch.getItems());
                assetRepository.updateBatchStatus(batch.getBatchNo(), "SUCCESS");
            } catch (RuntimeException ex) {
                status.setRollbackOnly();
                throw ex;
            }
        });
    }
}

声明式和编程式怎么选

场景推荐
普通 Service 写操作声明式事务
批量导入每 1000 条提交一次编程式事务
方法内只有一小段需要事务编程式事务
捕获异常后返回失败结果但仍要回滚编程式事务或手动标记回滚
失败日志要独立提交REQUIRES_NEW 或独立事务

事务为什么会失效

原因为什么
同类自调用没经过代理对象
方法不是 public代理无法按预期增强
异常被 catch代理看到方法正常返回
checked exception 未配置 rollbackFor默认不回滚受检异常
跨线程执行事务资源绑定当前线程
数据库不支持事务比如 MyISAM
事务管理器选错多数据源场景常见

详细知识点:Spring事务

阶段 9:事务传播、连接绑定和 MyBatis 加入事务

传播行为解决什么问题

传播行为解决“一个事务方法调用另一个事务方法时,内层应该加入外层、新开事务、挂起事务还是非事务执行”。

传播行为含义常见场景
REQUIRED有事务就加入,没有就新建默认,大多数业务
REQUIRES_NEW挂起外层,新开独立事务失败日志、补偿记录
NESTED基于保存点的嵌套事务局部回滚但依附外层
SUPPORTS有事务就加入,没有就非事务查询类方法
NOT_SUPPORTED挂起事务,以非事务执行不希望占用事务资源的慢操作

MyBatis 为什么能加入 Spring 事务

Spring 开启事务时,DataSourceTransactionManager 会从数据源获取连接,并把连接绑定到当前线程。MyBatis 通过 Spring 集成获取连接时,会先看当前线程是否已有绑定连接,如果有就复用。因此多个 Mapper 操作可以落在同一个物理事务里。

mermaid
flowchart TD
    A["事务代理开启事务"] --> B["DataSource 获取 Connection"]
    B --> C["Connection 绑定到 ThreadLocal"]
    C --> D["Mapper A 执行 SQL"]
    C --> E["Mapper B 执行 SQL"]
    D --> F["复用同一 Connection"]
    E --> F
    F --> G["统一 commit 或 rollback"]

为什么事务内不建议调远程接口

远程接口慢会让数据库连接和锁长时间不释放;远程调用成功但本地事务回滚,会导致跨系统不一致。商业项目更推荐本地短事务 + 状态机 + 可靠消息 + 补偿任务。

详细知识点:Spring事务商业场景训练营

阶段 10:事件机制和解耦

是什么

Spring 事件用于应用内部解耦。发布方只发布事件,不直接依赖所有处理方。

java
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

class OrderPaidEvent {
    private final String orderNo;

    OrderPaidEvent(String orderNo) {
        this.orderNo = orderNo;
    }

    public String getOrderNo() {
        return orderNo;
    }
}

@Service
class OrderPayService {
    private final ApplicationEventPublisher publisher;

    OrderPayService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void paySuccess(String orderNo) {
        publisher.publishEvent(new OrderPaidEvent(orderNo));
    }
}

@Component
class CouponListener {
    @EventListener
    public void onOrderPaid(OrderPaidEvent event) {
        System.out.println("send coupon for " + event.getOrderNo());
    }
}

注意边界

Spring 事件默认仍在应用内存里,不等于可靠消息。应用宕机、监听器失败、事务提交后回调失败,都可能导致事件丢失。核心跨系统链路要用 MQ、本地消息表、事务消息或 CDC。

阶段 11:扩展点怎么选

扩展点要按生命周期阶段选择。

扩展点阶段适合场景
BeanDefinitionRegistryPostProcessorBeanDefinition 注册阶段扫描接口并注册代理,如 Mapper、Feign
BeanFactoryPostProcessorBean 实例化前修改 BeanDefinition 属性
BeanPostProcessorBean 初始化前后生成代理、包装 Bean
FactoryBean特殊 Bean 创建暴露代理对象或复杂对象
ImportSelector配置导入根据注解决定导入哪些配置
DeferredImportSelector延迟导入Spring Boot 自动配置常用思想
ImportBeanDefinitionRegistrar编程式注册定义@EnableXxx 扫描注册组件
ApplicationListener事件监听应用内事件解耦

扩展点选错会怎样

错误用法后果
BeanFactoryPostProcessor 里 getBean()可能导致 Bean 过早创建,绕过后置处理器
BeanPostProcessor 写复杂业务容器启动变慢,所有 Bean 都受影响
FactoryBean 里做远程慢调用getBean() 卡住,启动和运行都可能慢
事件当可靠消息应用崩溃或回调失败会丢事件

详细知识点:Spring核心扩展点

阶段 12:商业场景验收

场景 1:订单创建和库存扣减

要求:

  • Controller 只做参数接收和校验。
  • Service 作为事务边界。
  • Repository/Mapper 只负责数据库。
  • 订单创建和库存扣减在同一事务内。
  • 支付通知、发券、积分不要全部塞进一个数据库事务。

验收问题:

  • 为什么事务放在 Service 层?
  • 为什么事务内不建议调远程支付接口?
  • 如果库存扣减异常被 catch,事务会怎样?

场景 2:医疗数据采集批次入库

要求:

  • 批次状态和明细入库一致。
  • 大批量数据分批提交。
  • 单批失败记录失败原因。
  • 失败日志可以独立事务提交。
  • 重试任务要幂等。

验收问题:

  • 为什么一个超大事务风险高?
  • 为什么 REQUIRES_NEW 会增加连接池压力?
  • 编程式事务如何精确控制一批数据?

场景 3:框架集成

要求:

  • MyBatis Mapper 接口可以被注入。
  • Feign Client 可以像本地 Bean 一样调用。
  • Spring Boot Starter 引入后能自动注册默认 Bean。

验收问题:

  • Mapper 接口没有实现类,为什么能注入?
  • Feign Client 为什么实际是代理对象?
  • 自动配置最终是不是还要进入 Spring Bean 创建流程?

阶段 13:生产排查

注入失败排查

mermaid
flowchart TD
    A["启动注入失败"] --> B["Bean 是否被扫描"]
    B --> C["是否有多个候选 Bean"]
    C --> D["是否缺少 Qualifier 或 Primary"]
    D --> E["是否条件注册没生效"]
    E --> F["是否构造器循环依赖"]

代理或事务失效排查

mermaid
flowchart TD
    A["事务或切面没生效"] --> B["调用是否经过 Spring Bean"]
    B --> C["是否同类自调用"]
    C --> D["方法是否 public"]
    D --> E["异常是否抛到代理层"]
    E --> F["是否跨线程"]
    F --> G["数据库和事务管理器是否正确"]

Bean 创建慢排查

常见原因:

  • 构造方法或 @PostConstruct 里调远程接口。
  • BeanPostProcessor 对所有 Bean 做重逻辑。
  • 初始化阶段加载大文件。
  • 数据源、Redis、MQ 客户端连接超时。
  • 循环依赖或条件配置导致反复尝试。

排查方法:

  • 看启动日志卡在哪个 Bean。
  • 打开 Spring 相关 debug 日志。
  • 配合 Spring Boot Actuator startup、beans、conditions。
  • 临时注释可疑初始化逻辑定位。

阶段 14:面试闭环

mermaid
flowchart TD
    A["面试题"] --> B["标准回答"]
    B --> C["知识点页讲原理"]
    C --> D["Demo 证明"]
    D --> E["商业场景落地"]
    E --> F["失败边界和排查"]
面试题必须答到的深度
Spring 核心是什么IOC、AOP、BeanDefinition、生命周期、扩展点、代理
IOC 是什么控制反转、容器创建对象、依赖注入、生命周期管理
Bean 生命周期实例化、注入、Aware、BPP、初始化、代理、销毁
循环依赖单例、三级缓存、早期引用、构造器循环不能解决
AOP 原理代理对象、拦截器链、JDK/CGLIB、自调用失效
声明式事务AOP、TransactionInterceptor、连接绑定、提交回滚
编程式事务TransactionTemplate、精确边界、分批提交、手动回滚
事务传播REQUIRED、REQUIRES_NEW、NESTED、连接池风险
事务失效自调用、非 public、异常吞掉、跨线程、数据库引擎
扩展点BDRPP、BFPP、BPP、FactoryBean、ImportSelector、事件

面试标准回答看:Spring 面试知识点

最终验收题

  1. 为什么不要在业务代码里到处 new 依赖对象?
  2. BeanDefinition 是什么?为什么 Spring 不直接创建对象?
  3. BeanFactory 和 ApplicationContext 有什么区别?
  4. Bean 生命周期完整流程是什么?
  5. BeanPostProcessor 为什么能生成 AOP 代理?
  6. 构造器注入、Setter 注入、字段注入怎么选?
  7. @Autowired@Resource 区别是什么?
  8. 循环依赖是什么?三级缓存解决了什么?
  9. 为什么构造器循环依赖解决不了?
  10. AOP 调用链路是什么?
  11. JDK 动态代理和 CGLIB 区别是什么?
  12. 同类自调用为什么导致 AOP 和事务失效?
  13. @Transactional 底层执行流程是什么?
  14. 声明式事务和编程式事务怎么选?
  15. REQUIRES_NEW 为什么可能耗尽连接池?
  16. MyBatis Mapper 为什么能加入 Spring 事务?
  17. afterCommit 为什么不能替代可靠消息?
  18. 事务内为什么不建议调远程接口?
  19. Spring 事件适合什么,不适合什么?
  20. BeanDefinitionRegistryPostProcessor 和 BeanPostProcessor 区别是什么?
  21. FactoryBean 和 BeanFactory 区别是什么?
  22. ImportSelector 和 ImportBeanDefinitionRegistrar 适合什么场景?
  23. 注入失败怎么排查?
  24. 事务不回滚怎么排查?
  25. Bean 创建慢怎么排查?

本章小结

Spring 不是一堆注解,而是一套对象管理和扩展机制。先有 BeanDefinition,再有 BeanFactory 创建对象,再通过依赖注入组装对象,再通过生命周期扩展点介入,再用 AOP 代理实现事务和横切逻辑。学懂这条链,Spring Boot 自动配置、MyBatis Mapper、Feign Client、事务失效、循环依赖、扩展点选型都会连起来。