上一篇《Spring 核心知识点全解析》发完之后,评论和私信里出现频率最高的一批问题,我盘点了一下,基本都集中在几个点上:循环依赖为什么要三级缓存、Spring Boot 自动配置为什么经常“改了没反应”、AOP 日志切面写好了却死活不生效、Spring MVC 参数解析背后到底发生了什么。这些问题属于典型的“面试会考、工作会踩”的内容,只看概念只能记住答案,真遇到线上故障照样懵。这一篇我就把它们串起来讲透,重点放在原理拆解和排障经验上,顺手把 Spring Security OAuth2、Spring AI、StateMachine、自定义校验这些相对零散但实战常用的模块也整理一遍。如果你正在啃 Spring 源码、准备面试,或者已经被循环依赖和代理失效折磨过,这一篇应该能帮你省不少时间。
1. 三级缓存与循环依赖:从“背概念”到真正理解设计取舍
1.1 为什么会出现循环依赖,Spring 又是怎么兜住的
很多人张口就能背出“三级缓存”是哪三级,但问一句“为什么需要第三级”就卡住了。先把问题本身拆开。
循环依赖意味着 A 依赖 B,B 依赖 A。Spring 默认管理的是单例 Bean,单例意味着容器里只有一个实例,而这个实例只有被完整创建后才会放进单例池。问题来了:创建 A 需要注入 B,创建 B 需要注入 A,如果严格按照“创建完再放池子”的规则,两边都等不到对方,创建流程会直接死锁。Spring 的解法是“提前暴露”:A 还没完成全部初始化时,先把一个早期引用暂存下来,让 B 能拿着这个引用先把自己建完,最后 A 再继续走完剩余的初始化步骤。
这里有一个很容易被忽略的前提:循环依赖不是所有作用域都能解。Spring 只能兜住“单例 + 属性注入(setter / 字段)”这个组合。构造器注入产生的循环依赖基本无解,因为构造阶段连对象实例都还没有生成,根本没有可提前暴露的东西;prototype 作用域同样无解,每次 getBean 都是新建对象,不存在暂存早期引用的动作。所以一旦项目里出现构造器注入加循环依赖的组合,启动直接报 BeanCurrentlyInCreationException,这个异常本质上是设计问题的报警器,而不是框架缺陷。
注意:很多人遇到循环依赖第一反应是改成字段注入让它跑起来,我强烈不建议这么做。字段注入只是把问题藏了起来,后续每次维护都要面对一个说不清依赖方向的对象图。
1.2 每级缓存到底存的是什么,第三级为什么必须存在
Spring 处理单例 Bean 的缓存体系包含三个 Map,理解每一级的职责比记住名字重要得多:
| 缓存级别 | 存储内容 | 作用阶段 |
|---|---|---|
| 一级缓存 singletonObjects | 完整初始化后的单例对象 | 每次 getBean 最终命中的地方 |
| 二级缓存 earlySingletonObjects | 早期暴露的对象,属性可能还未填充 | 循环依赖发生时,提前给其他 Bean 使用的引用 |
| 三级缓存 singletonFactories | ObjectFactory 工厂对象 | 用于按需生成早期对象,支持代理的判断 |
创建 A 时发现需要注入 B,容器去获取 B;创建 B 时发现需要注入 A,容器去获取 A。此时 A 不在 singletonObjects,也不在 earlySingletonObjects,但在 singletonFactories 里存在一个 ObjectFactory,于是通过这个工厂拿到 A 的早期引用,放进二级缓存,同时把三级缓存里的工厂移除。B 拿着这个早期引用完成构建并进入一级缓存,然后 A 继续填充剩余属性、执行初始化方法,最终也进入一级缓存。
为什么三级缓存要保留 ObjectFactory,而不是直接把原始对象放进二级缓存?这是整个机制里最值得品的地方。因为拿到早期引用时,A 可能还没有经历 AOP 代理的创建阶段。Spring AOP 的代理通常是在 Bean 初始化后置处理器里生成的,如果二级缓存直接放原始对象,提前暴露出去的引用就是未经代理的普通实例,后面真正生成代理时,已经持有旧引用的 B 拿到的还是原始对象,切面逻辑全部失效。三级缓存里的 ObjectFactory 可以在“被引用”的那一刻才判断是否需要生成代理,保证提前暴露出去的引用恰好是最终代理对象。这就是三级比二级多出来的价值。
1.3 实战中的处理思路与遗留问题排查
实际项目里循环依赖常出现在两个方向:业务 Service 互相调用,以及配置类与 Service 互相注入。前者多数是职责边界没划清楚,后者多半是配置类里顺手写了业务逻辑。我建议先把构造器注入想一遍能不能用:如果代码能改成构造器注入且不出现循环依赖,说明原来是可以拆解的;如果改了构造器注入立刻报错,那就老老实实梳理依赖关系。
调整方向通常有三个。第一,把相互调用的逻辑提取到第三方组件,或者引入事件机制解耦,这是最彻底的做法,比如把 A 依赖 B 的那段逻辑下沉到 B 对应的事件监听器里。第二,给其中一个依赖加 @Lazy,延迟它代理对象的初始化,让 Spring 在创建时可以先注入一个代理占位符,真正调用时才去初始化目标对象。第三,如果确实是领域对象之间的双向协作,考虑 setter 注入加 @Autowired,但这只适合少数场景,不建议成为默认方案。
另外要有一点心理准备:加了 @Transactional 或 @Async 的类,循环依赖的行为会变得更微妙。因为这两类功能都依赖代理,提前暴露引用时如果代理还没生成,拿到的引用可能在后续初始化阶段被替换,进而引发“日志切面生效但事务代理失效”这类组合问题。碰到这种场景不要困在三级缓存的字面概念里,打开 Spring 的 trace 日志观察 Bean 创建顺序,比靠猜快得多。
2. Spring Boot 自动配置:不仅知道“有”,还要学会“查”和“改”
2.1 @SpringBootApplication 到底打开了什么开关
@SpringBootApplication 是组合注解,主要由 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 组成。其中最核心的是 @EnableAutoConfiguration,它通过导入 AutoConfigurationImportSelector 加载自动配置类列表。Spring Boot 2.7 之后,自动配置类的注册列表放在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件里;2.7 之前则是 META-INF/spring.factories。理解这个机制的意义在于,自动配置不是魔法,它本质上就是一批“带着条件注解的配置类”。
每个自动配置类通常搭配一堆条件注解,比如 @ConditionalOnClass 判断类路径是否存在某个类,@ConditionalOnMissingBean 判断容器中没有某个 Bean,@ConditionalOnProperty 判断配置项是否打开。只有所有条件都满足,这个配置类中的 @Bean 才会被注册。这也是为什么 Spring Boot 能做到“开箱即用”的同时,又允许你用自己的 Bean 替换默认实现。
2.2 配置改了没反应?先查自动配置是否真的生效
“我改了 application.yml,为什么没变化”是 Spring Boot 社区第一高频问题。大部分情况下不是配置没读到,而是对应的自动配置类根本没有激活。举个最典型的例子:项目里已经存在自定义的 DataSource Bean,Spring Boot 的数据源自动配置看到 @ConditionalOnMissingBean(DataSource.class) 不满足,就不会创建默认的连接池。此时你在 yml 里修改 spring.datasource.url,当然毫无反应。
排查这类问题最直接的方法是启动时加 debug=true。控制台会打出 ConditionEvaluationReport,里面分“正向匹配”和“负向匹配”两类,每条自动配置都会给出匹配或失败的具体原因。比如你看到 DataSourceAutoConfiguration 匹配失败,原因写着 did not find bean DataSource,说明容器里没有该类型的 Bean,那么问题很可能出在主类扫描范围不对或者自动配置被排除。生产环境还可以引入 actuator,通过 /actuator/conditions 端点在线查看所有自动配置的匹配状况。
2.3 自定义 Starter:理解自动配置的最好实验
想真正掌握自动配置,自己写一个 Starter 是性价比极高的路径。思路很简单:在 META-INF 下放置自动配置文件,写一个自动配置类,类里面用 @Bean 加 @ConditionalOnMissingBean 暴露默认实现。需要注意的关键点有两个。
第一,自动配置类不能被应用主类的 @ComponentScan 扫到。如果自动配置类和业务代码放同一个包,它会被普通扫描当成普通组件提前注册,条件注解的“按需生效”就失去了意义。所以官方推荐自动配置类放在独立的包,或者通过自动配置文件注册而不是扫描。
第二,自动配置类的顺序影响很大。多个 Starter 协作时,比如连接池配置和健康检查配置有先后依赖,用 @AutoConfigureOrder 或 @AutoConfigureBefore / @AutoConfigureAfter 显式声明顺序,能避免非常难排查的启动行为问题。我实际碰到过连接池自动配置先于健康检查执行,导致健康检查拿到的是初始化未完成的链路,表现是启动偶尔报错、重启后症状消失,这类问题不看条件报告很难定位。
@AutoConfiguration @ConditionalOnClass(MessageService.class) public class MessageAutoConfiguration { @Bean @ConditionalOnMissingBean public MessageService messageService() { return new DefaultMessageService(); } }3. Spring AOP 实战:日志记录是入门题,代理失效才是真考点
3.1 一份可直接落地的日志切面写法
先给一个可以直接抄的日志切面,再讲里面的坑。假设要给 com.example.service 包及其子包下的所有业务方法打印入参与耗时:
@Aspect @Component public class LogAspect { @Around("execution(* com.example.service..*.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { String method = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); long start = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); System.out.println("[" + method + "] 耗时: " + (System.currentTimeMillis() - start) + "ms"); return result; } catch (Exception e) { System.out.println("[" + method + "] 异常: " + e.getMessage()); throw e; } } }这个切面本身没有难度,真正让无数人困惑的是:切面已经写好了,@Aspect 也加了,为什么方法执行时日志就是不打印?这背后的原因几乎都指向同一个词:代理失效。
3.2 代理不生效的四个高频场景
第一个是同类内部调用。Controller 调 service.outer(),outer() 内部执行 this.inner(),inner 方法上的切面不生效。原因是 Spring AOP 基于代理,this 指向的是原始对象而不是代理对象,内部调用根本没有经过代理入口。
第二个是 private 方法。Spring AOP 无论用 JDK 动态代理还是 CGLIB,都无法对私有方法做增强。因为私有方法不参与接口契约,CGLIB 的子类也不能重写它。在 private 方法上标注切入逻辑,结果只会是静默不生效。
第三个是 final 方法或 final 类。CGLIB 通过生成子类来实现代理,final 方法无法被重写,自然无法增强。如果类被 final 修饰,连子类都生成不了。
第四个是整个项目根本没启用切面。Spring Boot 默认自动配置会开启 AOP,但如果你手动排除或包扫描漏掉了 @Aspect,切面对象没有注册到容器,自然没有效果。判断方法很简单:在切面类的构造方法或 @PostConstruct 里打一行日志,启动时看不到这行日志,就说明切面根本没被加载。
3.3 自调用问题的最优解法:不是炫技,是拆职责
自调用问题最常见的解决方式有三种。第一种是把 inner() 拆到另一个 Service,让切面作用在跨类调用上。第二种是往原类注入 ApplicationContext,从容器里取代理对象再调用 inner()。第三种是用 AopContext.currentProxy(),但需要在配置类上加 @EnableAspectJAutoProxy(exposeProxy = true) 打开暴露开关,否则 currentProxy() 返回 null。
从可维护性角度,我强烈推荐第一种。拆方法本质上是把职责边界拆干净,既解决了代理失效,也让代码结构更好读,顺带让单元测试变得更容易。第二种和第三种可以解决眼前问题,但会留下“为什么这里要拿代理”“为什么方法内部调用要拐弯”的疑问,后续维护者理解成本很高。
@Transactional 和 @Async 也存在完全相同的自调用问题。很多人发现“同一个类里内部调用 @Transactional 方法,事务回滚没有按预期工作”,根因就在这里。事务注解依赖的也是 AOP 代理,同类内部调用绕过了代理,事务自然不生效。
3.4 切面优先级与切入点表达式的易错细节
多个切面作用于同一个方法时,执行顺序由 @Order 决定,数值越小优先级越高。比如 @Order(1) 的日志切面在外层,@Order(2) 的事务切面在内层,那么日志会先记录请求进来,事务完成后日志再记录返回结果。如果你想在日志里记录事务提交后的状态,日志切面必须在事务切面外层;如果你想记录事务回滚的异常详情,顺序调整又会带来完全不同的结果。顺序没有绝对对错,但你必须清楚自己日志的语义是什么。
切入点表达式也有很多细节。execution(* com.example.service...(..)) 的语义是“com.example.service 包及子包下所有类的所有方法”。想匹配某个方法上的自定义注解,要写 @annotation(com.example.LogAnnotation);想匹配目标类上带某个注解的所有方法,要写 @within。很多人把这两个混用,导致日志只打了一部分。表达式写完以后,建议先配一个空方法加 @Pointcut,再写个单元测试或者临时接口验证匹配范围,避免上线后才发现切面扫多了或扫少了。
4. Spring MVC 请求链路:从 URL 到方法参数的底层真相
4.1 DispatcherServlet 与三大核心组件的分工
Spring MVC 的核心是 DispatcherServlet。请求进来后,它先通过 HandlerMapping 找到能处理当前请求的 Handler,一般是一个 Controller 方法;再通过 HandlerAdapter 适配调用这个 Handler。HandlerMapping 最常用的实现是 RequestMappingHandlerMapping,它根据 @RequestMapping 注解的 URL、HTTP 方法、Headers、Params 等条件建立映射。HandlerAdapter 中最核心的是 RequestMappingHandlerAdapter,它负责方法参数解析、方法调用和返回值处理。
排查问题时,分清楚请求在哪个阶段失败很关键。404 多数发生在 HandlerMapping 阶段,也就是压根没找到匹配的映射;405 通常也是这个阶段,找到了 Handler 但 HTTP 方法不匹配;而 400 或 500 往往是 HandlerAdapter 阶段参数解析或方法执行失败。看到异常类型先判断阶段,再去看具体堆栈,排查效率会高很多。
4.2 参数解析器:@RequestBody 背后发生了什么
每个方法参数都对应一个 HandlerMethodArgumentResolver。@RequestBody 参数由 RequestResponseBodyMethodProcessor 处理,核心逻辑是读取请求体,通过 HttpMessageConverter 将 JSON 反序列化为目标对象。Spring Boot 默认引入 Jackson,所以常见场景都不需要额外配置转换器。
但这个机制里有一个高频踩坑点:前端用 application/json 提交,后端却用 @RequestParam 接收,结果拿不到数据。@RequestParam 从 query string 或表单体取值,不负责解析 JSON body,两者是两套完全不同的解析通道。另外,如果你自定义了 HttpMessageConverter 并手动注册,顺序很重要。Spring 会按顺序遍历转换器,第一个能处理当前 Content-Type 的转换器被选中。多个自定义转换器时顺序不对,会出现“明明写了转换逻辑,但反序列化还是用的默认 Jackson”的诡异现象。
如果你希望某个参数类型不需要加 @RequestBody 就能自动解析,可以实现 HandlerMethodArgumentResolver。比如从 header 中取某个字段构造参数对象,在 supportsParameter 中判断参数类型,在 resolveArgument 中从 request 取数据构建对象。实现之后通过 WebMvcConfigurer 的 addArgumentResolvers 注册进去,Spring 就会在解析该类型参数时调用你的逻辑。
4.3 返回值包装与全局异常处理的细节
Controller 方法返回对象并标记 @ResponseBody 时,返回值由 RequestResponseBodyMethodProcessor 处理。它通过 HttpMessageConverter 将对象序列化为 JSON。很多项目要求统一返回 Result ,如果每个接口手动包裹,容易漏且维护成本高。你可以用 @RestControllerAdvice 加 ResponseBodyAdvice 统一处理返回值。
使用 ResponseBodyAdvice 时注意几个细节。第一,supports 方法里要判断是否需要包装,如果接口返回类型本身已经是 Result,要跳过,否则会产生嵌套包装。第二,String 类型需要特殊处理,因为 String 默认走 StringHttpMessageConverter,它和 JSON 转换器的 content-type 不同,处理不当会出现“包装好了但响应变成了 text/plain 且中文乱码”的问题。第三,ResponseEntity 这类特殊返回值也要过滤,避免影响已经手动控制响应状态的接口。
异常处理同理。@ExceptionHandler 加 @RestControllerAdvice 是标准做法,可以捕获异常并返回统一格式。每个 @ExceptionHandler 方法可以声明接收异常实例参数,也可以注入 HttpServletRequest,但每个方法只能捕获一个异常类型,多个类型可以写数组。如果你希望所有未捕获异常也返回 JSON 而非默认错误页,需要再加一个兜底方法捕获 Exception.class,或者在 Web 层配置自定义错误处理逻辑。
4.4 请求链路上最容易忽略的编码与精度问题
这部分单独拿出来说,因为真实项目里出现的频率非常高。
中文乱码问题一般都集中在 Content-Type 的 charset。Spring 默认 StringHttpMessageConverter 编码是 ISO-8859-1,Spring Boot 中通过 server.servlet.encoding 配置 UTF-8 可以解决大部分场景。但如果你自定义了消息转换器或手动 write 字符串,就需要显式设置字符集。
BigDecimal 精度问题也很常见。JSON 序列化时如果不做处理,前端拿到的数字可能是 0.1 而不是 0.10,或者某个大数被转成科学计数法。可以在字段上用 @JsonSerialize 指定序列化器,也可以在全局 Jackson 配置里统一处理 BigDecimal。
日期格式化是另一类高频问题。Spring Boot 默认日期格式不一定符合前端约定,用 @JsonFormat 或者在 application.yml 中配置全局日期格式都能解决。关键是你要知道优先级:@JsonFormat 注解的优先级高于全局配置,全局配置又高于默认值,别配置完全局发现个别字段不合预期又找不到原因。
5. Spring 新生态模块:OAuth2、Spring AI、StateMachine 与自定义校验的实战要点
5.1 Spring Security OAuth2 Authorization Server 的配置要点
Spring 官方把 OAuth2 授权服务器独立成了单独的项目 spring-security-oauth2-authorization-server,和资源服务器配置分开。授权服务器核心是注册客户端(RegisteredClient),配置令牌端点、授权码端点、JWK 签名等。开发中最常见的需求是“自定义用户信息”,包括往令牌中追加自定义 claims,做法是注册 OAuth2TokenCustomizer 类型的 Bean,在生成令牌时修改 JwtClaimsSet。如果需要完全自定义认证逻辑,可以考虑实现 OAuth2AuthenticationProvider 并替换默认 Provider。
授权服务器的安全细节容易被忽略。client_secret 在生产环境一定要加密存储,不能明文写在配置里或者个人本地仓库提交出去。令牌端点可以加上登录失败次数限制,避免暴力破解。资源服务器配置相对简单,引入 spring-boot-starter-oauth2-resource-server,指定 jwk-set-uri 即可。但是要注意资源服务器和授权服务器如果部署在同一个项目里,路径匹配经常会冲突,请求放行规则要明确。实际项目里最稳的方案是网关统一做认证,微服务内的资源服务器只做 token 校验,业务服务内部不要再写一遍复杂的认证逻辑。
5.2 Spring AI 与 Agent 相关集成的现状
Spring AI 是 Spring 官方推出的 AI 应用集成框架,目前支持 OpenAI、Azure OpenAI、Ollama 等模型。集成方式非常模板化:引入 spring-ai-starter,配置 api-key 和模型名称,然后注入 ChatClient 或者 ChatModel 调用模型接口。它的价值在于把底层 HTTP 调用、请求构造、响应解析抽象出来,让开发者不必关心各家模型 API 的差异,写代码的手感接近操作 RestTemplate。
从项目落地角度看,Spring AI 的版本迭代非常快,很多类名和方法在 0.x 版本里频繁调整,示例代码很容易失效。使用时要先确认项目所用的 Spring Boot 主版本和 Spring AI 版本,再去看对应版本的官方示例,不能直接复制最新文档里的代码。另外,Spring AI 目前更适合做原型验证和轻量集成,如果你的 AI 调用链路涉及复杂提示词编排、长期记忆、多轮对话,还是需要在前端或应用层增加状态管理,不要把业务状态堆在模型调用模板里。
5.3 Spring StateMachine:状态流转的正确设计与持久化教训
Spring StateMachine 提供了状态机模型,适合订单状态、审批流等场景。核心概念是 State、Event、Transition、Action。配置方式通过 Builder 定义状态集合、初始状态、事件触发关系,以及进入某个状态或执行某个 Transition 时触发的 Action。相比在业务代码里写一堆 if-else 判断当前状态能否执行某个操作,状态机能把状态迁移的合法路径集中在可读的配置中,维护成本低很多。
实际开发中,状态机的坑主要在持久化。Spring StateMachine 默认状态保存在内存里,进程重启会丢失。要持久化,需要在业务表中保存状态字段,每次操作前从数据库加载状态并构建状态机,然后在状态变更后回写状态。高并发场景下要注意状态机实例的并发更新问题,通常需要数据库乐观锁或者分布式锁。另外,状态变更之后的业务副作用,比如发送消息、记录审计日志,建议放在状态机外部的事件监听里,而不是写在 Action 内部。Action 内部做副作用容易遇到状态机重试或重复执行时副作用也重复触发的问题。
5.4 自定义 Validator:@Valid 如何真正生效
Spring 的 Bean Validation 基于 Hibernate Validator 实现。@Valid 之所以能触发校验,前提是方法参数上标注了 @Valid 或 @Validated,同时参数解析器内部会调用 Validator。如果你想自定义校验注解,核心是创建一个注解并实现 ConstraintValidator 接口。注解定义中需要指定 @Constraint(validatedBy = SomeValidator.class),并提供 message、groups、payload 属性。实现类里 initialize 方法获取注解属性,isValid 方法返回校验结果。
自定义校验器的坑点在于它只对“经过 Spring MVC 参数解析的请求参数”生效。如果你在 Service 内部直接 new 对象再调用校验,@Valid 不会起作用。Session 级别的校验也需要注意,有些项目把 @Valid 用在 Controller 方法上,却期望它对 Service 方法内部的参数也生效,结果当然是无效的。正确的做法是在需要校验的方法参数上加上 @Valid 或 @Validated,并且确保这个方法本身是经过 Spring 代理处理的,否则约束不会触发。
6. Spring Cloud 微服务:选型、快速上手与一线避坑清单
6.1 微服务要解决的问题是什么
很多人上来就上 Spring Cloud,其实先想清楚微服务要解决的是什么:独立部署、独立扩展、故障隔离、技术异构。如果你的系统只有两三个业务模块,单体架构完全够用,强行拆分只会把简单问题复杂化,部署成本、运维成本、链路排查成本全部上升。
快速上手的路径建议按这四件套走:服务注册与发现(Nacos)、配置中心(Nacos Config)、客户端负载均衡与声明式调用(OpenFeign + Spring Cloud LoadBalancer)、网关(Spring Cloud Gateway)。这四个组件的组合就能搭建一套可运行的微服务骨架,之后再逐步加入分布式事务、消息队列、链路追踪等更重的东西。
6.2 服务注册发现与配置中心的核心注意点
Nacos 是目前国内用得最多的选择,它同时承担注册中心和配置中心,比 Eureka + Spring Cloud Config 的组合少维护一套组件。使用 Nacos 时最需要注意的是 namespace、group、dataId 的三层命名逻辑。namespace 做环境隔离,比如 dev、test、prod;group 做业务分组;dataId 通常与服务名和 profile 对应。之前很多朋友反馈“配置改了没生效”,最后查出来要么是 client 没配置监听,要么是 dataId 和代码中 spring.config.import 指定的配置不一致。
配置中心的核心价值是让配置具备生命周期:动态修改、版本回滚、实时推送。但要注意敏感配置不要以明文形式放在配置中心,数据库密码、密钥建议加密存储或者结合配置中心自带的加密插件。启动时如果配置中心连不上,Spring Cloud 不会直接崩溃,但会有大量重试日志,生产环境建议把配置中心地址放到启动参数或 bootstrap 配置里,避免硬编码导致迁移困难。
6.3 网关与流量治理的正确姿势
Spring Cloud Gateway 基于 WebFlux,是响应式模型,天然适合高并发网关场景。常用能力包括路由、断言、过滤器、限流、鉴权。写过滤器时要注意 GlobalFilter 与 GatewayFilter 的区别:GlobalFilter 对所有路由生效,GatewayFilter 只对指定路由生效。限流可以用内置的 RequestRateLimiter,也可以结合 Sentinel 规则管理。
实际经验是网关层尽量保持轻量,不要写太重业务逻辑,否则网关会变成新的性能瓶颈。另一个容易忽略的问题是认证逻辑放网关后,微服务之间的内部调用不走网关,如果内部调用也需要鉴权,要么在服务间传递 token 并各自校验,要么用服务间白名单的方式简化处理。网关层不要做同步阻塞操作,WebFlux 的线程模型对阻塞非常敏感,一个阻塞调用可能拖垮整个网关。
6.4 快速上手的路线与避坑清单
如果你准备学习 Spring Cloud 并快速落地,建议直接基于 Spring Boot 3.x 和 Spring Cloud 2023.x 开始,不要再去看老版本的 Ribbon 配置,因为负载均衡已经被 Spring Cloud LoadBalancer 取代。学习路径可以这样规划:先写三个 demo 服务,一个网关、两个 provider,把注册发现、OpenFeign 调用、配置中心串起来,再逐步加入 Sentinel 或 Resilience4j 做熔断限流。
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| 网关里调用远程接口导致性能下降 | 阻塞调用占满了 WebFlux 工作线程 | 用 WebClient 替代 RestTemplate,避免同步阻塞 |
| OpenFeign 调用超时 | 默认 connectTimeout/readTimeout 可能很小 | 同时配置 connectTimeout 和 readTimeout |
| 服务名带下划线启动异常 | 某些中间件对下划线支持不佳 | 服务命名一律使用中划线 |
| 出问题找不到调用链 | 没有接入链路追踪 | 第一时间接入 Sleuth + Zipkin 或 Micrometer Tracing |
写到这里,Spring 的知识点仍然列不完。我个人做这些梳理的出发点,不是让你背会多少概念,而是希望每个知识点都能落到“遇到问题怎么排查、怎么修”的层面。三级缓存的第三级为什么必须存在、AOP 为什么自调用失效、自动配置为什么改了没生效,这些想清楚之后,Spring 就不再是一堆注解的堆砌,而是一条可以预测、可以推理的链路。这也是源码阅读最值得投入的方向。如果这一篇能帮你在下一次面试或者排障的时候,从“背过答案”变成“知道为什么”,那它就是值得的。后续我还会把 Spring 测试、性能调优、应用部署这几个方向单独拿出来写,有需要的朋友可以留意后续更新。