聊Spring框架,得先承认一件事:这个生态大得让人容易迷路。光看最近大家搜索的词——Spring AI、Spring Boot 4.x、三级缓存原理、手写Spring、Spring Security、Spring Cloud Gateway、Actuator未授权访问……一个新入门的人看到这堆东西,很容易陷入“怎么什么都叫Spring”的困惑里。我写这篇文章,就是想把这些散落的关键词串成一条线:从Spring的核心IoC容器讲起,把Bean生命周期、三级缓存、MVC和Boot的自动配置、安全网关、再到AI扩展全部理一遍,最后聊聊手写Spring和学习路线的思路。内容适合两类人:一类是刚学完基础语法、想系统搞懂Spring的初中级Java开发者,另一类是面试前突击原理、想把手里的“会用”升级成“懂为什么”的朋友。
1. 先搞懂Spring到底解决了什么:从JavaEE时代的痛点到IoC容器
1.1 控制反转:把“谁找谁”的问题彻底掉了个头
早期Java服务端开发里,Service要调DAO,就得自己new一个DAO实例;几十个类互相协作,到处是new。这时候你改一个实现类,可能牵连十几个文件。核心痛点不是“代码多”,而是对象之间的耦合全靠程序员自觉——谁忘了初始化谁,运行期就给你个空指针。
Spring的解法看起来很朴素:你只声明“我需要什么”,容器负责把“需要的东西”给你。这个“声明依赖,容器注入”的动作就是依赖注入(DI),而“控制权从代码手里交到容器手里”就是控制反转(IoC)。
举个例子,你写一个订单服务:
@Service public class OrderService { private final UserService userService; public OrderService(UserService userService) { this.userService = userService; } }代码里没有一次new。UserService从哪来?Spring容器创建OrderService时发现构造器需要UserService,就从容器里找到(或先创建)UserService,再把它塞进来。控制权反转的本质是:你不用管理依赖的创建和组装了,你只写自己那部分逻辑。
1.2 三种注入方式,我的建议和理由
依赖注入有三种常见姿势:构造器注入、Setter注入、字段注入。
| 注入方式 | 优点 | 缺点 | 我的评价 |
|---|---|---|---|
| 构造器注入 | 依赖不可变、空指针在启动期暴露 | 构造函数参数多时难看 | 官方推荐,Spring Boot自动装配大量使用 |
| Setter注入 | 可选依赖灵活 | 依赖可能被替换、漏注入难发现 | 老XML时代用得多,现在尽量不用 |
| 字段注入 | 代码最少 | 隐藏依赖、不方便测试、容易造出循环依赖 | 不推荐,别为了少写两行牺牲可测性 |
我个人强烈推荐构造器注入。Spring团队在官方文档里也是这个倾向,Spring Boot的自动配置代码里,构造器注入到处可见。字段注入最大的坑是把依赖关系隐藏了:一个大类十个@Autowired字段,你根本不知道它到底依赖什么,单元测试里你也只能靠反射硬塞。
1.3 BeanFactory与ApplicationContext:别再把它们当一回事
BeanFactory是IoC容器的底层接口,提供getBean()、containsBean()这些基础能力。ApplicationContext在它之上加了事件发布、国际化、环境配置、AOP整合等能力。你平时代码里看到的ClassPathXmlApplicationContext、AnnotationConfigApplicationContext,本质都是经过增强的BeanFactory。
这个区别很重要,因为BeanFactory创建Bean是懒加载的(调用getBean时才创建),而ApplicationContext默认启动时就完成单例Bean的实例化和初始化——这也是为什么Spring Boot启动慢、但一启动你就能直接用的原因。
2. Bean生命周期全链路拆解:从定义到销毁,每个扩展点都在干什么
面试问Bean生命周期,很多人都能背出“实例化、属性填充、初始化、销毁”这四步,但一追问“BeanPostProcessor在哪一步介入”“Aware回调又是什么时候”就卡壳。这里我把整条链路按实际源码顺序拆开。
2.1 生命周期从抽象定义开始:BeanDefinition才是源头
Spring不是直接用反射new一个对象就完事,它先要把“怎么创建这个Bean”的描述性信息存下来,这个描述就是BeanDefinition。
// 简化版,源码里是接口 class BeanDefinition { Class<?> beanClass; // 类全限定名 String scope; // singleton还是prototype boolean lazyInit; // 是否懒加载 String initMethodName; // 初始化方法 String destroyMethodName; // 销毁方法 List<?> propertyValues; // 属性值 }XML里的<bean>标签、@Bean注解、@Component扫描,最终都会被解析成BeanDefinition放进容器。这部分是很多人忽略的重点:Spring拿到的是Bean的“图纸”,不是现成的零件。
2.2 生命周期十个节点,逐个过
以单例Bean为例,完整链路大致如下:
- 实例化:通过构造器创建原始对象
- 属性填充:把依赖的其他Bean、配置项注入进来
- Aware回调:如果Bean实现了
BeanNameAware、BeanFactoryAware等接口,容器会回调对应方法把上下文信息告诉Bean - BeanPostProcessor前置处理:
postProcessBeforeInitialization() - 初始化方法调用:
@PostConstruct注解方法 →InitializingBean.afterPropertiesSet()→ 自定义init-method - BeanPostProcessor后置处理:
postProcessAfterInitialization() - (到这里对象可能被AOP代理增强,实际存进单例池的是代理对象)
- 就绪,正常使用
- 容器关闭时,
@PreDestroy注解方法 →DisposableBean.destroy()→ 自定义destroy-method
这里不少人有疑问:@PostConstruct和InitializingBean、init-method到底什么关系?答案是顺序固定:先@PostConstruct,再afterPropertiesSet,最后init-method。同样,销毁顺序是先@PreDestroy,再destroy(),最后destroy-method。
2.3 BeanPostProcessor:Spring所有高级功能的“插槽”
很多神秘高层API的本质都藏在BeanPostProcessor里。AOP代理怎么加上的?就是某个BeanPostProcessor在postProcessAfterInitialization阶段给目标对象生成代理。@Autowired怎么注入的?AutowiredAnnotationBeanPostProcessor在属性填充阶段帮你干的活。
@Component public class LogBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // 这里可以对Bean做包装、代理、埋点 return bean; } }理解了这个"插槽",你就明白为什么框架里那么多"魔法"——Spring自己也是靠这些插槽实现扩展的。以后遇到什么奇怪需求,比如给每个Controller方法加耗时日志,第一反应就应该是起一个BeanPostProcessor或者BeanFactoryPostProcessor,而不是改业务代码。
2.4 一个实战教训:别在构造器里玩花活
我有一次排查线上问题,某服务启动时偶发NullPointerException,最后定位到是个Service的构造器里调用了另一个Bean的某个方法,而那个Bean还没完成属性填充。构造器里能拿到的是“半成品”,此时属性还没注入完。如果你必须依赖别的Bean,正确的做法是用@PostConstruct或InitializingBean,因为此时所有依赖都就绪了。这条教训写进代码评审规范里能省很多事。
3. 三级缓存不是缓存:一步一步推导循环依赖的解决方案
"Spring三级缓存"是面试题里出现频率最高的词,但它其实是个名不副实的名字——这三个Map不是为了缓存性能,而是为了解决单例Bean循环依赖问题。
3.1 循环依赖是什么场景
最简单的例子:
@Service public class A { private final B b; public A(B b) { this.b = b; } } @Service public class B { private final A a; public B(A a) { this.a = a; } }创建A需要B,创建B需要A,死循环。Spring的解决方式是:先创建一个"A的半成品"暴露出去,等属性填充阶段再拿B。
3.2 三个Map各司其职
Spring容器里维护了三个Map:
// 一级缓存:最终的成品Bean,也就是大家拿到的那个 Map<String, Object> singletonObjects; // 二级缓存:提前暴露的半成品,已经实例化但还没完成属性填充和初始化 Map<String, Object> earlySingletonObjects; // 三级缓存:存储ObjectFactory,提前暴露的"工厂",可以后续生成代理 Map<String, ObjectFactory<?>> singletonFactories;创建A时,先把一个ObjectFactory放进三级缓存,然后属性填充时发现需要B,就转而创建B;B填充属性时发现需要A,此时从三级缓存拿到A的工厂,调用getObject()得到一个A的早期引用(early reference),B完成创建;再回过来A拿到B,完成自己的填充和初始化。
3.3 为什么必须是三级,二级不够吗
这是面试里最刁钻的追问。二级缓存确实能解决"提前暴露半成品"的问题——把A放进二级缓存,B直接拿就行。但AOP代理这关过不去。
如果A被@Transactional或@Async增强,Spring最终放进单例池的应该是个代理对象,不是原始对象。早期暴露时,如果只暴露原始对象,B拿到的就是未经代理的裸Bean,等A完成增强生成代理后,B里的引用还是旧的——这不就出问题了吗。
所以Spring用三级缓存装ObjectFactory,工厂内部走getEarlyBeanReference(),在暴露早期引用时就先检查需不需要代理,需要就先把代理生成出来。这就保证了所有循环依赖的对象拿到的都是最终形态的引用。
3.4 哪些情况下循环依赖照样报错
三级缓存不是万能药。下面几类循环依赖Spring明确说没办法:
- 构造器循环依赖:实例化阶段就卡死了,没有"半成品"可言
- prototype作用域的循环依赖:容器只对单例做循环依赖处理
@Transactional和@Async组合的某些场景:代理生成时机不对也会报错
所以设计上最好的做法还是尽量避免循环依赖。重构方向很简单:把A依赖B、B依赖A拆成A依赖B、B通过事件或ObjectProvider延迟获取A,或者把公共部分抽到C里让AB都依赖C。
4. 手写一个微型Spring容器:你才能彻底理解框架的骨架
老有人问我:手写Spring到底有没有用?我的回答是:如果你能亲手写一个精简版容器,框架在你眼里就不再是一坨黑魔法。下面我带你走一遍最核心的逻辑,代码很短,但五脏俱全。
4.1 扫描、注册、刷新:容器的三个动作
手写容器的第一步是扫描包路径,找到所有带@Component注解的类,注册成BeanDefinition。然后是刷新(refresh):实例化所有单例Bean,执行依赖注入。
public class MiniApplicationContext { private Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(); // 1. 扫描:解析带注解的类 public void scan(String basePackage) { // 遍历classpath下所有.class文件 // 判断是否带@MiniComponent注解 // 把类信息封装成BeanDefinition注册进beanDefinitionMap } // 2. 刷新:实例化单例 public void refresh() { for (String beanName : beanDefinitionMap.keySet()) { getBean(beanName); } } // 3. 获取Bean:从单例池拿,没有就创建 public Object getBean(String beanName) { Object bean = singletonObjects.get(beanName); if (bean == null) { bean = createBean(beanName); singletonObjects.put(beanName, bean); } return bean; } }4.2 createBean里最关键的依赖注入
创建实例以后,遍历Bean的所有字段,凡是带@MiniAutowired的,就创建(或获取)对应类型的Bean,反射塞进去。
private Object createBean(String beanName) { BeanDefinition bd = beanDefinitionMap.get(beanName); Class<?> clazz = bd.getBeanClass(); Object instance = clazz.getDeclaredConstructor().newInstance(); // 遍历字段,执行依赖注入 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(MiniAutowired.class)) { Object dependency = getBean(field.getName()); field.setAccessible(true); field.set(instance, dependency); } } // 支持Aware:让Bean知道自己叫什么名字 if (instance instanceof BeanNameAware aware) { aware.setBeanName(beanName); } return instance; }能走到这一步,说明你已经完全吃透IoC的核心:扫描注解、注册定义、反射实例化、手动注入。Spring源码里AbstractAutowireCapableBeanFactory.doCreateBean做的事比这多几百倍,但骨架子就是这套。
4.3 手写是学习方法,不是工程方案
我明确建议:手写容器练手就好,别在生产项目里自己造轮子。生产环境要的是事务、AOP、分布式协调、监控、扩展点、生态整合,这些不是几百行代码能替代的。但如果你能把手写代码和源码路径对应起来——getBean对应AbstractBeanFactory.doGetBean,refresh对应AbstractApplicationContext.refresh——面试时聊原理就有底气得多。
5. 请求进来了怎么走:Spring MVC与Spring Boot自动配置的本质
现在Spring Boot几乎成了Java Web开发的代名词,但很多人直接把两者划等号。其实Boot的核心价值不是新的Web框架,而是自动配置,它把Spring的繁复配置工程化、约定化。
5.1 DispatcherServlet:从前端控制器到HandlerMapping的完整链路
Spring MVC的核心是一个叫DispatcherServlet的前端控制器。一次请求的完整路径是:
- 请求到达
DispatcherServlet HandlerMapping根据URL找到对应的HandlerExecutionChain(包含Controller方法和拦截器)HandlerAdapter实际调用方法,把参数绑定、校验、转换都做掉- 方法返回
ModelAndView或@ResponseBody结果 - 视图解析器处理视图渲染(如果返回的是页面)
- 各种
HandlerInterceptor的afterCompletion收尾
你写的@GetMapping("/user/{id}")只是第一步的一半——Spring启动时会把所有@RequestMapping注解解析成URL到方法的映射表,请求进来后通过查表定位。
5.2 自动配置:为什么你引入一个starter就“能用”
Spring Boot最让人上瘾的特性就是:加入spring-boot-starter-web依赖,什么都不配,一个@RestController就活了。这背后是@EnableAutoConfiguration做的事。
Boot启动时会加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,里面列了一堆自动配置类。每个配置类上都有一堆条件注解:
@AutoConfiguration @ConditionalOnClass(DispatcherServlet.class) @ConditionalOnWebApplication(type = Type.SERVLET) public class DispatcherServletAutoConfiguration { // 只有当classpath下有DispatcherServlet且是Web项目时才生效 }这套机制被称作条件装配:没有DispatcherServlet类,就不配置Web;没有DataSource,就不配置JdbcTemplate。你加依赖,classpath变了,条件成立,配置自动生效。这就是"约定优于配置"的根本实现。
5.3 Actuator未授权访问:一个所有人都该知道的坑
热搜词里出现"spring boot actuator未授权访问",说明这个隐患踩的人非常多。Actuator是Boot提供的生产监控端点,能暴露/actuator/env、/actuator/heapdump等敏感信息,一旦端口对外开放又没做鉴权,等于把运行环境、内存快照、配置明文全送出去了。
工程上最低限度的防御有这三板斧:
- 关掉不必要端点:
management.endpoints.web.exposure.include=health,info - 把管理端口独立并禁止外网访问:
management.server.port=9091 - 实在要对外,必须加Spring Security或网关认证
我见过太多人图省事直接include=*,这种行为在正式环境等于裸奔。一个好习惯是写完配置顺手访问一下/actuator,摸清自己到底暴露了什么。
6. 防止框架裸奔:Spring Security、Spring Cloud Gateway与Actuator的工程化配合
6.1 Spring Security:过滤链才是核心思维
Spring Security是Spring生态里的安全认证框架,核心是一个叫FilterChain的东西。请求进入后被一串过滤器依次处理:先做认证,再做授权,最后放行到业务接口。你可以通过SecurityFilterChain配置链上每一环。
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); }初学者容易犯的错是只在方法上写@PreAuthorize,却忘了对静态资源和公开接口做放行配置,最后被过滤器链拦得莫名其妙。理解Security最好的方式是先画出一条请求穿过过滤链的路径,再逐个看拦截点。
6.2 Spring Cloud Gateway:网关层能做的不只是路由
Gateway是Spring Cloud微服务体系的入口网关,底层基于WebFlux。它最常用的能力是路由配置:
spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/api/user/** filters: - StripPrefix=1但实际工程里,网关还承担了统一鉴权、接口限流、灰度路由、日志埋点的职责。把Spring Security的鉴权逻辑上移到网关,业务服务可以变得更纯粹——只关注业务,不用关心"来的人是谁"。我用下来的经验是:网关层做粗粒度过滤(有没有token、路径是否合法),业务层做细粒度权限(这个用户能不能删这条数据),这样既不重复也不遗漏。
6.3 框架与边界:不是加得越多越好
一条非常朴素的工程原则:每个中间件、每个框架模块都有它要解决的问题边界。用Gateway是为了统一流量入口,不是为了让你的单体应用绕一圈;用Security是为了认证授权,不是拿它当业务判断的替代品。我见过有些团队把网关里的过滤器写得比业务代码还长,这种"框架滥用"比不用框架更危险——出了问题都找不到源头。
7. Spring AI来了:LLM时代框架的新扩界面和生态方向
7.1 Spring AI 2.0的核心概念:把LLM当作一个Bean注入
“Spring AI”是搜索热词里最扎眼的一个,热度上升很快。Spring官方推出它的逻辑其实很自然:过去我们集成数据库用JdbcTemplate、集成缓存用CacheManager,现在集成大模型为什么不能有个统一抽象?
Spring AI的用法有一种和IoC融为一体的感觉:
@RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient = builder.build(); } @PostMapping("/chat") public String chat(@RequestBody String message) { return chatClient.prompt(message).call().content(); } }你不在代码里关心底层是GLM、通义千问还是OpenAI,只要配置好Model的接入参数,ChatClient就能像操作JDBC一样自然地对话。这种"统一抽象"正是Spring生态的一贯风格。
7.2 AI Skill、MCP与Multi Agent:从“掉接口”到“能力编排”
搜索词里出现spring ai skill、spring ai mcp、spring ai multi agent,说明大家已经不满足于简单对话了。Spring AI 2.0里三个新方向值得关注:
- AI Skill:把函数调用工具化。你写一个普通Java方法,标注好描述,模型在需要时会自动调用这个方法,相当于把业务能力暴露给大模型。
- MCP:模型上下文协议,目标是让AI Agent能统一接入文件、数据库、浏览器等外部工具,Spring AI封装了MCP客户端实现,概念上看很高效。
- Multi Agent:把一个大任务拆给多个不同角色的Agent协作完成,Spring AI提供了编排框架支持。
这套玩法落地到项目里,典型场景就是客服工单系统:一个Agent负责语义理解,一个Skill负责查订单状态,另一个Agent负责生成答复。代码模式和写多模块业务没本质区别,但理解编排思维比记住API更持久。
7.3 Spring AI Alibaba与Admin控制台:国内落地的实际选择
热词里反复出现spring ai alibaba和spring ai alibaba admin,说明国内开发者对这个方向有多关注。阿里在Spring AI上做了自己的适配层,接通通义千问等国产模型,同时提供一个可视化的Admin控制台,可以管理模型调用、查看Token消耗、调试Agent链路。实际项目里,尤其考虑到模型合规、网络稳定和成本控制,国内云厂商的适配通常比直接连海外API更省心。
我建议的方式是:先把Spring AI的ChatClient跑通一个Demo,接入最简单的文本对话,再逐步加Memory(聊天记忆)、Skill(工具函数)、Multi Agent(任务编排)。从简单到复杂,每一层都能看到框架怎么帮你省事,出了问题也好按层排查。
7.4 Spring Boot 4.0的兼容性提醒
搜索词里还有个很具体的spring boot 4.x where to find datasourceautoconfiguration,以及spring boot 4.0.0 解决jackson的jsonmapper$builder问题。Spring Boot 4.x把一批自动配置的包路径做了调整,旧项目升级时确实会遇到一些配置类找不到、Jackson序列化方式变化的问题。经验是:升大版本前先看官方迁移指南,启动不了先看自动配置条件哪里没满足,用debug=true开启自动配置报告,比瞎猜快得多。
8. 面试与学习的正确姿势:以源码为纲,以手写为验证
8.1 高频题背后的考察意图
进入过Spring面试高频题:三级缓存原理、Bean生命周期、依赖注入、Spring中@GetMapping、事务失效场景、工厂模式在框架里的应用。说句实在话,这些题考的不是背诵,而是考察你有没有源码级的理解能力。
比如面试官问"Spring怎么解决循环依赖",他不关心你能背出三个Map叫什么,他想听到的是你能否讲清楚:单例Bean在实例化后就具备暴露条件、AOP代理如何影响早期引用、哪些边界情况框架处理不了。说白了,面试考察的是你排查问题时能不能定位到容器层面,而不是换个行为就一脸懵。
分享一下我推荐的知识地图:
| 面试主题 | 必看源码 | 核心结论 |
|---|---|---|
| Bean生命周期 | AbstractAutowireCapableBeanFactory.doCreateBean | 实例化→填充→初始化,中间穿插多个扩展点 |
| 循环依赖 | DefaultSingletonBeanRegistry.getSingleton | 三级缓存逐级找,最后一级存工厂 |
| AOP原理 | AnnotationAwareAspectJAutoProxyCreator | 本质上是一个BeanPostProcessor |
| 事务原理 | TransactionInterceptor | 通过AOP实现声明式事务,自调用会失效 |
| 自动配置 | SpringApplication.run/AutoConfiguration.imports | 条件装配驱动一切 |
8.2 一条接地气的学习路线
我看到很多新人一上来就想啃源码,结果在refresh()里转晕,几周后放弃。我的建议是三层递进:
- 会用阶段:能手写一个带Controller/Service/Mapper的Boot项目,理解注解的作用,能跑通CRUD和事务。
- 原理阶段:跟着官方文档画Bean生命周期图,调试看
getBean的调用栈,搞懂三级缓存和条件装配。这一步的目的不是记住每行代码,而是搞懂设计意图。 - 验证阶段:手写一个微型IoC容器,再用Spring的AOP做一个日志切面,最后试着回答自己之前不会的面试题。
这三层走下来,基本就从一个"注解摆渡人"变成"框架明白人"了。遇到线上诡异问题,你至少能判断“问题在容器里还是在业务里”,这本身就是很大的进步。
最后分享一个我自己的习惯:每次升级依赖,都把Spring Boot版本号和当前引用的Spring核心版本记下来。不同大版本之间包迁移、配置类路径变化很常见,别指望"升级了能跑就万事大吉"——跑之前看一遍启动日志的自动配置条件,跑起来看一眼/actuator/health和关键端点,比任何文档都更能告诉你系统真实的状态。这套"先理解再行动"的思路,从学框架到做架构,长期看都值得坚持。