《Spring 核心》《Spring Boot》《Spring Cloud》讲了 IoC、AOP、自动配置、微服务。本页补企业级后端绕不开的硬骨头:容器与 Bean 生命周期、AOP 代理底层、安全认证授权、OAuth2 单点登录、缓存抽象与事务传播、测试体系、接口文档与可观测,以及 Spring Cloud 的微服务治理。我会像《Java 进阶》那样,把每个点拆成"是什么 → 为什么 → 怎么用 → 踩过什么坑"。基线:Spring Boot 3.x(jakarta 命名空间)/ Spring Security 6 / Spring Cloud 2023+。
IoC/DI 容器与 Bean 生命周期、作用域
很多人用 Spring 多年,仍说不清"容器到底替我干了什么"。这一章把对象的创建、装配、存活范围、销毁彻底讲透——这是后面所有注解(@Cacheable/@Async/@Transactional)能生效的地基。
IoC 反转的是什么:控制权交给容器
没有容器时,对象自己 new 依赖、自己管生命周期;用了 Spring,你只声明"我需要什么",容器负责把它造好、组装好、按时给你。这就是"控制反转"。好处不是少写一行 new,而是依赖关系集中、可替换、可测试。
| 对比 | 自己 new | 交给容器(DI) |
|---|---|---|
| 依赖来源 | 类里硬编码 new Repo() | 构造函数声明,容器注入 |
| 替换实现 | 改源码 | 换一个 @Bean 即可 |
| 单测 | 难 mock,强耦合 | 注入 mock 对象即可测 |
| 生命周期 | 自己管 | 容器统一管(作用域/销毁) |
论为什么 Spring 推荐构造器注入
字段注入(@Autowired 直接标属性)写起来爽,但对象在构造完仍是"半残"(依赖为 null 直到容器回填),单测很难不靠容器。构造器注入让依赖在 new 时就齐活、不可变、强制非空,设计上更诚实。
容器启动:从配置到 BeanDefinition
容器启动做三件事:① 读配置(@Configuration / 组件扫描 @ComponentScan 得到一堆候选类);② 解析成 BeanDefinition(记录类、作用域、依赖、初始化方法等元信息);③ 按需实例化并走生命周期。理解"先有定义、后有实例",就知道为什么循环依赖有那么多讲究。
依赖注入的三种方式
Bean 作用域:同一个类能有几个实例
默认是 singleton(全容器一个实例,线程共享),但有的对象必须每次新建(如带状态的请求上下文)。作用域选错是隐蔽 bug 的高发区。
| 作用域 | 含义 | 典型用途 |
|---|---|---|
| singleton(默认) | 整个容器一个实例 | 无状态 Service / DAO |
| prototype | 每次获取 new 一个 | 有内部状态、非线程安全对象 |
| request | 每个 HTTP 请求一个 | 请求级上下文(Web 环境) |
| session | 每个会话一个 | 登录用户状态 |
| application | 每个 ServletContext 一个 | 全局共享配置 |
singleton Bean 在容器启动时一次性创建,此时注入的 prototype 也被"固定"了——你以为每次拿到新的,其实拿到的是同一个。要每次拿新实例,得用 ObjectProvider 或 @Lookup 让容器在运行时再给。
生命周期回调:对象生老病死都有钩子
论初始化顺序:构造 → 依赖注入 → @PostConstruct → 可用
记住这个顺序能避开很多"空指针":构造器里不能用还没注入的依赖;需要用到依赖做准备的活,放进 @PostConstruct。AOP 代理也在这里被织入——所以 @PostConstruct 里看到的 bean 可能已是代理对象。
循环依赖与三级缓存
A 依赖 B、B 又依赖 A,怎么办?Spring 用三级缓存解决 singleton 的 setter/字段循环依赖:提前暴露"半成品"对象引用,让对方先拿到。但构造器注入的循环依赖无解——两边都在等对方构造完才能注入,直接启动报错。
@Conditional 与自动配置原理
Spring Boot 的"开箱即用"靠的就是条件化装配:@ConditionalOnClass(类路径有某类才配)、@ConditionalOnMissingBean(用户没自定义才用默认)。理解它就懂了为什么引入一个 starter 自动生效、又能被你轻易覆盖。
AOP 原理(代理/JDK vs CGLIB)与实战
日志、事务、鉴权……这些"横切"在多个方法上的逻辑,不该复制粘贴到业务里。AOP(面向切面编程)用代理把横切逻辑织入。这一章先讲底层代理,再写三个真正能上生产的切面。
AOP 是什么:把"横切关注点"拎出来
业务方法只关心"算账",不关心"记日志、开事务、鉴权"。AOP 把这些通用能力抽成切面(Aspect),在指定切点(Pointcut)处织入。织入的时机叫"通知(Advice)"。
| 通知类型 | 执行时机 | 典型用途 |
|---|---|---|
@Before | 目标方法前 | 参数校验、鉴权 |
@After | 方法后(无论成败) | 资源清理 |
@AfterReturning | 正常返回后 | 结果加工、审计 |
@AfterThrowing | 抛异常后 | 异常告警 |
@Around | 包围整个调用 | 计时、事务、重试(最灵活) |
代理机制:JDK 动态代理 vs CGLIB
Spring AOP 默认用代理实现。被代理对象有接口时用 JDK 动态代理(基于接口),没接口时用 CGLIB(基于子类继承)。Spring Boot 2.0+ 默认统一用 CGLIB,更省心但代价是类必须能被继承(final 类/方法无法代理)。
论为什么同类方法内部调用,AOP 不生效
代理是"包在外面的壳"。你用 this.x() 调用同类方法,走的是"本体"不是"代理",自然绕过增强。这是 @Transactional/@Cacheable 失效最常见的原因。解决:自注入(注入自己)、或抽到另一个 Bean。
切点表达式:精确命中要增强的方法
实战一:日志/耗时切面
用 @Around 包住方法,前后分别记开始/结束、算耗时。比在每个方法里手写 long t = System.nanoTime() 优雅得多。
实战二:事务切面与 @Transactional 原理
@Transactional 本质是 AOP 帮你加的 @Around:方法前开连接、设 autoCommit=false,成功 commit、抛异常 rollback。所以 transaction 也是"代理增强",同样受"同类调用失效"约束。
① 同类方法互调(this 调用绕开代理)。② 吞了异常(catch 后没再抛,事务以为成功)。③ 非 RuntimeException 被吞(默认只回滚unchecked,受检异常不回滚)。④ private 方法无法被代理。⑤ 标注在接口方法上但用 CGLIB时行为差异。
实战三:多切面执行顺序
多个切面命中同一方法时,用 @Order 决定谁先谁后:数值小的前置先执行、后置后执行(像洋葱,外层的先包、后拆)。
Spring Security / OAuth2 / OIDC:认证授权模型
"登录""权限""单点登录"是后端永远逃不开的需求。Security 6 把整套机制收敛成一条过滤器链;OAuth2/OIDC 解决"第三方登录"和"跨系统认证"。这一章把它们从模型讲到落地。
认证 vs 授权:两个常被混为一谈的词
论认证与授权职责不同
认证(Authentication):你是谁(用户名密码、Token)。授权(Authorization):你能干什么(这个接口你能不能调)。Security 用 AuthenticationManager 处理前者、AuthorizationManager 处理后者,二者可以分离部署(资源服务器只做授权校验)。
请求进来先过认证(你是谁),再过授权(你能干啥)。整条 SecurityFilterChain 就是一串过滤器,每个负责一环(CSRF、登录、鉴权……)。
用户详情与密码编码
Security 不关心你怎么存用户,只认 UserDetailsService:给它用户名,还它一个 UserDetails(含账号、加密后的密码、权限)。密码必须哈希,用 PasswordEncoder 委托。
数据库绝不能存明文。用 BCrypt / Argon2 等自适应哈希,加盐且抗暴力;登录失败要限流 + 审计,防撞库。曾发生过的泄露事件,十有八九是"明文/弱哈希 + 拖库"。
方法级安全:在业务方法上直接声明权限
| 常用表达式 | 含义 |
|---|---|
hasRole('X') | 拥有角色 X(自动加 ROLE_ 前缀) |
hasAuthority('SCOPE_read') | 拥有某权限/作用域 |
isAuthenticated() | 已登录 |
authentication.name | 当前用户名 |
OAuth2 授权码模式:第三方登录的标准姿势
"用 GitHub 登录"背后就是授权码流:用户被导向 GitHub → 同意授权 → GitHub 带着授权码跳回你的回调 → 你后端用码换 access_token。码只在后端之间传递,前端拿不到 token,最安全。纯前端/移动端则加 PKCE 防截获。
论为什么不用"密码模式"做第三方登录
密码模式要你把用户账号密码直接交给第三方,等于把钥匙送人,早已不推荐。授权码模式让"授权"与"凭据"分离:第三方只拿到"代表用户访问某资源"的令牌,拿不到密码,用户还能随时在授权方吊销。
| 授权模式 | 场景 |
|---|---|
| 授权码 + PKCE | 有/无后端的 Web、移动端(当前推荐基线) |
| 客户端凭证 | 服务到服务,无用户(机器账号) |
| 刷新令牌 | access_token 过期后换新的 |
资源服务器与 JWT
资源服务器(真正提供 API 的服务)只做一件事:校验令牌合法性,不负责登录。JWT 把用户信息自包含进令牌,资源服务器用签名公钥本地校验,不必每次查库。
论JWT 还是不透明令牌?
JWT:自包含、资源服务器免查库、易跨服务,但难吊销(令牌在有效期内一直有效)。不透明令牌:存服务端、可随时吊销,但每次要 introspection 查一次。折中:访问令牌短期(15min)+ 刷新令牌可吊销;敏感操作二次校验。
OIDC 与刷新令牌
OAuth2 只管"授权",OIDC 在其上管"认证":授权服务器额外发一个 id_token(JWT),携带"是谁"。刷新令牌用于 access_token 过期后换新——它必须服务端可控、可吊销。
常见安全陷阱
① permitAll() 配错路径(如 /api/** 误放行管理接口),等于门户洞开。② allowedOriginPatterns("*") + allowCredentials(true) 同时开,等于任意站点都能带你的登录态调接口(CSRF 风险)。③ 静态资源误放行导致 actuator/接口文档裸奔。④ 用 .antMatchers 老 API(已废弃),新项目用 requestMatchers。
| 风险点 | 正确做法 |
|---|---|
| 放行路径过宽 | 白名单最小化,按角色细分 |
| CORS 通配 + 凭据 | 限定具体可信源,必要时用 token 而非 cookie |
| actuator 裸奔 | 生产关掉或加 IP 白名单/认证 |
| 令牌存 localStorage | 用 HttpOnly Cookie 或内存 + 短时效 |
缓存 / 异步 / @Async / 事务传播行为
性能与正确性的三块基石:缓存挡热点、异步解耦重活、事务保一致。Spring 用注解把它们声明化,但每个注解背后都有语义要搞清楚,否则"看起来对了,实际坑了"。
@Cacheable 缓存抽象:别让热点每次打库
Spring 的缓存是抽象层——你只标注解,底层接 Redis/Caffeine/内存都行。第一次查库、结果进缓存;之后再调用同名同参,直接从缓存取。
| 注解 | 行为 |
|---|---|
@Cacheable | 有就返回缓存,没有才执行并写入 |
@CachePut | 总是执行,并把结果写缓存 |
@CacheEvict | 执行后清除缓存项 |
@Caching | 组合多个缓存操作 |
缓存与数据库的一致性
论没有完美的"双写一致"
"先更新 DB 还是先删缓存"没有银弹。最常用 Cache-Aside:先更 DB,再删缓存,配合过期时间兜底与延时双删(更新后隔几百毫秒再删一次,清掉并发读回的旧值)。别指望 @CacheEvict 解决所有一致性——按业务容忍度设计,强一致场景考虑分布式锁或走数据库。
穿透:查不存在的 key 每次都打库 → 缓存空值或用布隆过滤器。击穿:热点 key 过期瞬间大量请求涌入 → 互斥锁单飞重建。雪崩:大量 key 同一时刻失效 → 加随机过期时间错峰。
@Async 异步执行:把慢活丢给其他线程
@Async 让方法在独立线程执行,调用方不阻塞。但必须配自定义线程池——默认用 SimpleAsyncTaskExecutor 每次 new 线程,高并发会炸。
① 同类方法调用:this.sendEmail() 走本体不走代理(和 @Transactional 同理)。② 返回 void 且内部抛异常:异常被吞,主线程毫无察觉;建议返回 Future 或 CompletableFuture 以便拿到异常。
事件驱动:ApplicationEvent 解耦上下游
下单成功后,短信、积分、风控都想响应。与其在方法里硬编码调用,不如发一个领域事件,各监听器自行订阅——新增需求不改主流程。
事务传播行为:方法互相调用时事务怎么算
一个事务方法调另一个事务方法,是共用一个事务,还是各算各的?由 propagation 决定。这是"写对多步业务"的关键。
| 传播行为 | 语义 | 场景 |
|---|---|---|
| REQUIRED(默认) | 有则加入,无则新建 | 绝大多数业务方法 |
| REQUIRES_NEW | 挂起外层,自己新开一个 | 独立日志/审计,不受主事务回滚影响 |
| NESTED | 外层事务内的子事务(可回滚到保存点) | 部分失败不影响整体 |
| SUPPORTS | 有事务就用,没有就以非事务跑 | 查询也可在事务中复用 |
| NOT_SUPPORTED | 挂起事务,非事务执行 | 不想被事务绑定的耗时操作 |
| NEVER | 必须在非事务中,否则报错 | 严禁事务的读取 |
论REQUIRES_NEW 不是"更安全的 REQUIRED"
它会挂起外层事务另开一个——外层回滚不会带走它,外层锁也不保护它。滥用会让数据出现"部分已提交",破坏一致性预期。只在确实需要"独立落库/隔离"时用,如审计、通知。
事务隔离级别与"读到了什么鬼"
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ_UNCOMMITTED | 可能 | 可能 | 可能 |
| READ_COMMITTED | 无 | 可能 | 可能 |
| REPEATABLE_READ(MySQL默认) | 无 | 无 | 可能 |
| SERIALIZABLE | 无 | 无 | 无 |
论隔离级别越高越慢
SERIALIZABLE 等于给数据加锁串行化,吞吐骤降。多数业务用数据库默认的 REPEATABLE_READ(MySQL)或 READ_COMMITTED(PostgreSQL)就够,只有"对账""库存扣减"等强一致点才局部提升,并用乐观锁兜底。
事务的常见坑:自调用、异常被吞、只读优化
① 同类 this 调用:代理没介入,注解形同虚设。② catch 后没抛:事务管理器收不到异常,照常提交。③ 受检异常默认不回滚:需 @Transactional(rollbackFor=Exception.class)。④ 只读查询标 @Transactional(readOnly=true):让 ORM 走只读优化、不开写事务,省开销。
测试 / OpenAPI / Actuator 健康检查
写完不等于能上线。测试保证行为不漂移,OpenAPI 让前端能调,Actuator 让运维能看。这一章把"可交付"的三件套讲明白。
测试切片:只加载需要的零件,跑得快又稳
全容器启动慢且易抖。Spring 提供"切片"注解,只装配相关组件,其余 mock 掉。
| 注解 | 加载范围 | 用途 |
|---|---|---|
@WebMvcTest | 仅 Web 层 + MVC | Controller 单测(Service 用 @MockBean) |
@DataJpaTest | 仅 JPA + 内存库 | 仓储层/查询 |
@JsonTest | 仅 JSON 序列化 | DTO 序列化 |
@SpringBootTest | 全容器 | 端到端集成 |
MockMvc 与 @MockBean:断言 HTTP 行为
论测试金字塔在 Spring 里怎么落
大量快的单测(切片)→ 少量集成测关键路径(Testcontainers 真库)→ 极少端到端。把"真数据库"放进测试,能抓到 SQL/事务/类型这些单测抓不到的 bug,又不至于每次都全容器启动拖慢 CI。
Testcontainers:用真实中间件做集成测试
覆盖率与测试分层性价比
论别盲目追 100% 覆盖率
覆盖率高 ≠ 测得好。把精力放在核心业务分支(金额计算、状态机、权限判断),getter/setter 的覆盖是虚的。用 JaCoCo 看"哪些分支没覆盖",补的是逻辑不是数字。CI 里设一个合理下限(如增量 80%),防退化。
OpenAPI / springdoc:从代码生成接口文档
手写文档必过期。springdoc-openapi 从注解和类型直接产出 OpenAPI 规格,前端随时能调。
Actuator:健康检查与指标,但别裸奔
默认 /actuator/env 会暴露配置(含可能的密钥占位)、/actuator/heapdump 能下堆转储。生产务必:只暴露 health/metrics/info、show-details: when_authorized、加网络安全组/IP 白名单,绝不让它从公网直连。
微服务 Spring Cloud:注册发现 / 配置 / 熔断限流
单体拆成多个服务后,新问题来了:服务在哪、配置怎么统一、一个挂了会不会拖垮一片。Spring Cloud 给了一套标准化答案。这一章讲清核心四件套。
为什么需要服务治理
论拆分带来的是"分布式的新麻烦"
单体里方法调用是函数跳转;微服务里是网络调用——会超时、会失败、地址会变。于是需要:注册发现(地址动态寻址)、配置中心(一处改处处生效)、熔断限流(防雪崩)、网关(统一入口)。这不是"加功能",是"为网络不可靠买单"。
服务注册发现:地址不用写死
每个服务启动时把自己注册到注册中心(Nacos/Eureka/Consul),调用方从中心发现可用实例。扩容缩容、实例挂掉,地址自动更新。
| 注册中心 | 特点 | 适用 |
|---|---|---|
| Nacos | 注册 + 配置一体,国内主流 | 新项目、阿里系生态 |
| Eureka | AP 优先,Netflix 经典 | 纯注册、简单 |
| Consul | CP、强一致、含 KV/健康检查 | 需强一致、多数据中心 |
配置中心:一处改,处处生效
把配置从代码里抽出来集中管理,支持环境隔离、动态刷新、版本回滚。改数据库连接不用重新打包部署。
论配置中心不是"配置随便改"
动态配置很香,但改错了全集群即时中招。要配权限、审批、灰度(先放一台验证)、变更审计。密钥类配置走 Vault/密钥管理,别和普通过配置混在一起明文存。
声明式调用 OpenFeign
不想手写 RestTemplate + 拼 URL?Feign 用接口 + 注解描述"我要调哪个服务的哪个方法",像调本地方法一样调远程。
Feign 默认超时要显式配(否则沿用 Ribbon/全局默认值,可能过长把线程占满)。它不自动重试非幂等写——重试"下单"会重复创建。务必区分幂等(GET/查询)与非幂等(POST 创建),并对写操作加幂等键防护。
熔断限流 Resilience4j:别让一个挂拖累一片
| 模式 | 解决什么 | 触发 |
|---|---|---|
| CircuitBreaker | 下游持续失败,快速失败防雪崩 | 失败率 / 慢调用率超阈值 |
| RateLimiter | 限制单位时间请求数 | 超过配额直接拒 |
| Bulkhead | 隔离不同下游的并发,互不挤占 | 并发数超上限排队/拒 |
| Retry | 瞬时故障自动重试 | 异常 + 退避策略 |
网关 Spring Cloud Gateway:统一入口
网关是系统的"前台":路由转发、鉴权、限流、灰度、日志都在这里收口,后端服务只管业务。
论网关不是万能胶
鉴权放网关能统一,但细粒度业务权限仍应在服务内(零信任:不轻信"网关已验过")。限流在网关做粗粒度(按路由),服务内做细粒度(按用户/接口)。网关本身要 HA——它挂了全站进不来,记得多副本 + 健康检查。
IoC 把对象创建与装配交给容器:构造器注入最稳,Bean 有 singleton/prototype 等作用域与完整生命周期钩子,循环依赖靠三级缓存但构造器注入无解,自动配置用 @Conditional 实现"有默认、可覆盖"。AOP 用 JDK/CGLIB 代理织入横切逻辑,@Transactional/@Cacheable/@Async 都是代理增强,同类 this 调用会失效。Security 6 用过滤器链统一认证授权、密码必须哈希;OAuth2 管授权、OIDC 管认证,JWT 自包含但难吊销,访问令牌短效 + 刷新令牌可吊销。缓存用 Cache-Aside + 过期兜底、警惕穿透/击穿/雪崩;事务传播以 REQUIRED/REQUIRES_NEW 为核心、隔离级别越高越慢、注意自调用与异常吞噬。测试靠切片 + Testcontainers;OpenAPI 与 Actuator 补全可观测但别裸奔。Spring Cloud 用注册发现、配置中心、OpenFeign、Resilience4j、Gateway 解决分布式寻址、动态配置、防雪崩与统一入口。
1.为什么 Spring 推荐构造器注入而不是字段注入?
查看答案
构造器注入让依赖在 new 时就齐活、不可变、强制非空,对象不会处于"半残"状态;单测可以不依赖容器直接传入 mock。字段注入依赖在构造后才被容器回填,且难测、易隐藏循环依赖。
2.@Transactional 标注在一个 Service 的私有方法上、或被同类其他方法 this 调用,会生效吗?为什么?
查看答案
都不生效。Spring 事务是靠 AOP 代理织入的,私有方法无法被代理增强;而同类 this 调用走的是"本体"而非"代理",绕过了事务拦截器,注解形同虚设。应抽成独立 Bean 或自注入来调用。
3.OAuth2 和 OIDC 的区别?JWT 做访问令牌有什么隐患,怎么缓解?
查看答案
OAuth2 解决"授权"(代表用户访问资源),OIDC 在其上解决"认证"(确认用户身份,通过 id_token 携带)。JWT 自包含、服务端不存,难以在到期前吊销(用户登出/被盗仍可用)。缓解:访问令牌短效(如 15 分钟)+ 可吊销刷新令牌;敏感操作二次校验。
4.事务传播行为 REQUIRED 和 REQUIRES_NEW 有什么本质区别?什么时候该用 REQUIRES_NEW?
查看答案
REQUIRED 有外层事务就加入、没有就新建;REQUIRES_NEW 会挂起外层事务、自己开一个独立事务,外层回滚不影响它、它也不受外层锁保护。只在需要"独立落库/隔离"时用,如审计日志、通知,滥用会破坏一致性预期。
5.为什么推荐 Testcontainers 做集成测试?缓存的"穿透/击穿/雪崩"分别怎么防?
查看答案
Testcontainers 用真实数据库/中间件容器,能抓到单测覆盖不到的 SQL、事务、类型、驱动兼容问题,比内存库更接近生产。缓存穿透:缓存空值或布隆过滤器;击穿:互斥锁单飞重建热点;雪崩:加随机过期时间错峰。
下一步往哪走
路学完 Spring 进阶之后
① 看系统设计页:限流、熔断、缓存策略在这里系统化,与 Resilience4j/Gateway 呼应。
② 看 DevOps 页:可观测三支柱、SLO、容器编排是这套后端的运维底座,Actuator 数据正喂给 Prometheus。
③ 动手搭一个:带 Security + OAuth2 + Redis 缓存 + Testcontainers + 熔断的最小可运行服务,把本章每个坑都亲手踩一遍。