接手过一个老项目,Spring 3.2 时代的那种,光 XML 配置就铺了满满几百行。改一个 Bean 作用域,要在applicationContext.xml里翻半天,加一个 Controller 得先写<bean>声明,再配 URL 映射,稍微没对齐缩进,启动直接抛异常。那会儿还没用上 SpringBoot,每次搭环境都在重复劳动,心里只有一个想法:这东西能不能有点“默认值”?
后来切到 SpringBoot,第一次体会到什么叫“零配置启动”。依赖一加,main 方法一跑,服务就起来了。那种反差让我花了很长一段时间去琢磨——Spring 到 SpringBoot,表面上看到的是一堆注解替代了 XML,但真正进化的其实是整个框架的设计哲学。这篇文章我不打算做功能清单式的罗列,而是想把这段进化路径拆开,从配置、启动、依赖管理、自动装配到迁移实操,一件件说清楚。尤其是“为什么 SpringBoot 要这么设计”和“迁移时你会踩到哪些坑”,这两块是最容易被忽略的。
1. Spring 的“黄金时代”与那些让人头秃的 XML
1.1 从 EJB 的沉重到 Spring 的轻量,机制上解决了什么
要说 Spring 当年为什么能火,得先回到它的对手——EJB。那时候写一个业务对象,要继承特定的类、实现特定的接口,部署要经过一套复杂的容器流程,开发效率很低。Spring 的核心思路是:把对象的创建和对象之间的依赖关系交给容器管理,业务代码里不 new,不查字典,只声明“我需要什么”,容器负责“给什么”。
这个机制就是 IoC(控制反转)。配合 AOP(面向切面编程),像事务、日志、权限这类横切逻辑,终于不用在每个方法里手写重复代码了。这些设计在当年是颠覆性的,所以 Spring 很快就成了 Java 开发的事实标准。
但这套标准有个绕不开的问题:容器要管的东西越来越多,配置就跟着越来越重。早期的 Spring 没有一个好的“自动模式”,所有 Bean 都要显式声明。哪怕你只需要一个 JdbcTemplate,你也得把数据源、连接池、事务管理器一个个用<bean>标签描述清楚。不描述,容器就真的不知道,因为当时的设计哲学就是“全部显式,绝不含糊”。
1.2 一个真实项目里的配置膨胀体验
我维护过的一个旧项目,applicationContext.xml加上spring-mvc.xml加上mybatis-config.xml,三个文件加起来接近两千行。其中有大量重复性极强的配置块,比如每个 Service 都要注册一个<bean>,每个 Controller 都要在 MVC 配置文件里加一个<context:component-scan>的细化排除规则。
最难受的是环境切换。开发、测试、生产,三套数据源配置要做三个 properties 文件,然后用一个<context:property-placeholder>去加载,加载顺序稍微不对,变量就被覆盖,连接串直接读错。有一回排查一个数据库连不上的问题,最后发现是<context:component-scan>把某个错误的@Configuration类扫了进来,导致数据源 Bean 被二次初始化。这种问题在 XML 时代非常典型——配置越多,隐式覆盖和冲突的可能性就越大。
1.3 配置痛点的集中归纳
我后来总结过,Spring 时代的问题其实集中在四个方面:
- 配置密度过高:每加一个类,可能要同时改 Java 代码、XML 声明、装配关系,改动成本是翻倍的。
- 启动过程黑盒且缓慢:XML 解析、Bean 实例化、依赖注入全在启动时串行完成,老项目启动超过 30 秒是常事,而且报错信息很难定位到具体哪一行配置。
- 依赖管理靠手工:没有统一的依赖入口,Spring 框架自身的版本和各类扩展包的版本要自己匹配,稍有不慎就是
NoSuchMethodError。 - 嵌入与部署方式笨重:传统方式必须打成 WAR 包丢进外部 Tomcat,本地开发要先装环境、配环境变量,团队新人光“跑起来”就要折腾半天。
这些问题不是 Spring 没能力解决,而是它的核心定位是“轻量级容器”,XML 是当时唯一通用、可校验的配置载体。但 XML 本身的门槛和繁琐度,恰恰成了开发者最大的隐性成本。现在回头看,SpringBoot 的出现正是针对这些痛点的一次彻底重构,而不是简单的版本升级。
2. SpringBoot 的“反叛”:从“配置驱动”走向“约定驱动”
2.1 约定优于配置,这句话具体怎么落地
SpringBoot 最核心的思想是“约定优于配置”。这话听起来像口号,落到代码层面其实非常具体:
- 约定一:类路径上存在某个类,就默认启用对应的功能。比如你在依赖里加了
spring-boot-starter-web,框架看到类路径里有DispatcherServlet,就会自动为你配置 SpringMVC 的整套环境。 - 约定二:资源目录有固定的结构。
main/resources下的application.yml、static目录、templates目录,都有默认语义,不需要再手动声明。 - 约定三:内嵌容器是默认值。主类一启动,Tomcat 或 Jetty 直接在当前进程内运行,不再需要外部部署。
这三条约定共同指向一个问题:“最典型的场景,框架帮你搞定;特殊的场景,你才需要显式参与。”举例来说,绝大多数 Web 项目需要 JSON 序列化、静态资源映射、错误页、拦截器注册表,这些在 SpringBoot 中都是“只要你引入了 starter,默认就有”,不再需要到处写配置。
我自己的理解是:这不是为了少写几行配置而做的简化,而是把“业务开发中 80% 的重复劳动”压缩成框架内部的默认行为,让开发者把精力留在 20% 真正需要自定义的部分。所以 SpringBoot 学习曲线的关键点,恰恰不是怎么用它的 API,而是搞清楚它究竟替你做完了哪些事。
2.2 自动配置的底层机制设计与条件注解体系
自动配置是 SpringBoot 的灵魂,它看起来有点黑魔法,但原理拆开并不玄乎。核心链路是这样的:
SpringBoot 启动时,会扫描依赖中所有META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(老版本是spring.factories),拿到候选的自动配置类列表。这些配置类通常带有@AutoConfiguration注解,内部再用@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等条件注解做判断,满足条件才会真正装配对应的 Bean。
用生活类比来解释的话,自动配置就像一个“按需配料的套餐菜单”:你点了一份牛排套餐,厨房会把牛排、刀叉、调料一次性配好。但如果你自带了一瓶特殊酱汁(对应@ConditionalOnMissingBean),厨房就不会再给你强制加默认酱。你会发现,真正的灵活性就藏在这类条件的取舍里。
手动模拟一个最简单的自动配置过程,可以这么写:
@AutoConfiguration @ConditionalOnClass(name = "javax.servlet.http.HttpServlet") public class MyWebAutoConfiguration { @Bean @ConditionalOnMissingBean public MyFilter myFilter() { return new MyFilter(); } }这段代码的意思是:如果类路径下有 Servlet 相关类,并且当前容器里没有MyFilter这个 Bean,就创建一个。这个模式在 SpringBoot 内部反复使用,几乎所有的spring-boot-autoconfigure模块都是靠这样的条件注解组合来工作的。
2.3 Starter 机制为什么能“一个依赖管全家”
Spring 时代加一个功能,你需要自己找框架、核对版本号、逐个引入依赖,再手动写配置。SpringBoot 用 Starter 彻底解决了这一步。spring-boot-starter-web本身其实是一个空的引导依赖,它并不包含业务代码,而是通过 Maven 或 Gradle 的依赖传递,把spring-webmvc、spring-boot-starter-tomcat、jackson-databind等一系列配套依赖一次性带进来。
这种设计就像一个“配置清单”:你不用关心清单内部有哪些项,只要声明“我要开发 Web 项目”,框架就替你选好了一整套兼容的依赖版本组合。而且 Starter 通常会和自动配置类配合——引入依赖的同时,自动配置逻辑也会被加载,所以常常出现“加一个依赖,功能就自动好了”的效果。
在实际项目中,Starter 带来的好处不仅仅是方便,更重要的是依赖版本一致性的保证。Spring 时代框架版本冲突是家常便饭,SpringBoot 的 BOM(Bill of Materials)对常用库做了经过验证的版本组合,直接规避了无谓的版本适配问题。
3. 手写一个“迷你 SpringBoot”,拆解自动装配与启动流程
3.1 SpringApplication.run() 的启动链路设定
想真正理解 SpringBoot 的进化,建议做一次逆向拆解。SpringApplication.run()这个方法看似简单,实际在内部依次完成了:推断应用类型、加载自动配置类、创建ApplicationContext、执行所有BeanDefinitionRegistryPostProcessor、刷新容器、启动内嵌 Web 服务器、发布ApplicationReadyEvent。
这些行为里,最重要的其实是“加载自动配置类”。如果把这个过程做成一个迷你模拟,会有非常直观的“原来如此”体验。
3.2 手写自动配置发现的模拟代码
我写过一个最简化的模拟版本,用来给团队新人讲原理,核心就三个类:一个@AutoConfiguration注解,一个AutoConfigurationRegistry,一个main方法。
// 1. 定义一个自动配置注解 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface AutoConfiguration { // 仅用于标识这是一个自动配置类 } // 2. 读取所有自动配置类,检查是否包含标记注解 public class AutoConfigurationRegistry { private final List<Class<?>> autoConfigClasses = new ArrayList<>(); public void register(Class<?> configClass) { if (configClass.isAnnotationPresent(AutoConfiguration.class)) { autoConfigClasses.add(configClass); System.out.println("[AutoConfigurationRegistry] 注册自动配置类: " + configClass.getName()); } } } // 3. 模拟启动流程 public class MiniSpringBootApplication { public static void main(String[] args) throws Exception { AutoConfigurationRegistry registry = new AutoConfigurationRegistry(); // 模拟扫描依赖中的自动配置类 Class<?>[] candidates = { MyWebAutoConfiguration.class, RegistryReader.class }; for (Class<?> candidate : candidates) { registry.register(candidate); } } }这个模拟虽然简单,但揭示了一个重要事实:自动配置不是“代码写死了某个功能”,而是“框架扫描到候选类,再根据当前条件决定要不要干活”。条件装配的判断在实际框架中会复杂很多,但设计思想是一致的。理解了这个,你看spring-boot-autoconfigure里任何一个源码文件,都不会再觉得神秘。
3.3 启动速度差异背后体现的进化思路
有段时间我把项目从 Spring 4 迁到了 SpringBoot 2.3,同一台机器、同一个业务模块,启动时间从 35 秒左右降到了 12 秒左右。当然这个对比不完全公平,因为 Spring 项目里有大量的显式BeanDefinitionParser解析过程,SpringBoot 做了一定的懒加载优化。
实际上,SpringBoot 的启动优化并不只是“快”,更关键的是“可控”。spring.main.lazy-initialization可以开启全局懒加载,@Lazy可以做定点延迟,再加上SpringApplication提供了ApplicationListener和ApplicationRunner等扩展点,你可以在启动过程中的任何阶段插入自定义逻辑。这种“启动链路完全透明、可介入”的设计,才是它比 Spring XML 时代更先进的本质。
4. 迁移实战:从 Spring 到 SpringBoot 最容易踩的坑
4.1 依赖坐标重写与版本冲突处理
迁移的第一步,是把原有的零散依赖换成 Starter。最常见的问题在于,旧的项目可能已经引用了spring-web、spring-webmvc、spring-tx、spring-orm等模块,而 SpringBoot 的spring-boot-starter-web会把这些依赖重复带入。虽然 Maven 会对同一坐标的不同版本做“就近优先”处理,但版本冲突时经常出现异常。
我的处理习惯是三步走:
- 把原来所有
spring-*依赖尽量删掉,只保留非 Spring 体系内的第三方库。 - 引入
spring-boot-starter-parent作为统一的 parent 管理版本(如果是多模块项目,可以在dependencyManagement里引入spring-boot-dependencies)。 - 对必须保留的第三方库,用
exclusion排除掉和 Starter 冲突的传递依赖。
举一个具体例子,如果你的项目里原本用了 Apache HttpClient,直接引入spring-boot-starter-web并不会带它,但你原来可能为了兼容版本书写了<dependency>,这时候就建议把多余的传递给排除掉,只保留你自己的坐标。原则上,让 SpringBoot 的 BOM 统一管理框架版本,不要自己再手动指定 Spring 相关依赖的 version。
4.2 web.xml 中的配置如何迁移到 SpringBoot
老项目里那些熟悉的web.xml配置,迁移时几乎可以整块丢弃,但要做对应的逻辑替换:
ContextLoaderListener和DispatcherServlet的声明,由 SpringBoot 的DispatcherServletAutoConfiguration自动完成,不需要手动配置。- 原有的
<servlet-mapping>、<filter>、<listener>声明,迁移到 SpringBoot 里通常有两种实现方式:一种是注册FilterRegistrationBean、ServletRegistrationBean,另一种是实现WebServerFactoryCustomizer接口。 - 如果项目里有自定义的
WebMvcConfigurerAdapter,要改成实现WebMvcConfigurer接口,并且注意 SpringBoot 2.6 之后对静态资源映射规则做了调整,需要显式加入spring.mvc.static-path-pattern或使用WebMvcConfigurer.addResourceHandlers配置。
拦截器这块最容易出问题。Spring 时代你用<mvc:interceptors>声明,SpringBoot 里你得手动在WebMvcConfigurer里注册。我刚迁移时就踩过一个坑:原来写在 XML 里的拦截器只作用于特定 URL 模式,改成 Java 配置后,因为顺序不对,拦截器把静态资源也拦截了,导致首页加载异常。
4.3 配置文件迁移对照与约定了哪些默认命名
以前写 properties,现在写 YAML 是多数人的选择。对象嵌套关系更直观,还能用缩进表达数组结构。但迁移过程最容易忽略的是配置项的默认命名差异。我整理过一张频繁用到的对照表:
| Spring XML / Properties 中的配置 | SpringBoot 对应配置 |
|---|---|
context:property-placeholder | spring.config.import(或直接放 application.yml) |
<mvc:resources> | spring.web.resources.static-locations |
<bean id="dataSource" ...> | spring.datasource.url/username/password |
<tx:annotation-driven> | spring.tx.enable(通常默认开启,无需额外配置) |
mybatis.mapper-locations(mybatis-spring 配置) | mybatis.mapper-locations(配合 starter 自动识别) |
log4j.properties | logging.level+logback-spring.xml |
这里特别提醒一点:SpringBoot 的自动配置是“有条件的默认”。比如你不设置spring.datasource.url,框架会用内嵌的 H2(如果存在)创建一个嵌入式数据源;如果你根本不想要数据源,得加上@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)来排除。这种“默认但不强制”的行为,刚接触时会觉得很不适应,但它确实是约定优于配置的直接体现。
4.4 本地运行与生产监控的迁移困境
Spring 时代本地跑项目,要装 Tomcat,再配置数据源、日志目录;SpringBoot 时代,mvn spring-boot:run或者直接执行主类的main方法,服务就起来了,这对新成员非常友好。我在多个团队推广过这个流程,原来入职要花一整天搭环境,后来基本半小时内能跑通。
迁移过程中有一个经常被忽略的点:SpringBoot 应用启动后,最开始的短时间里很容易出现“假死”状态。原因通常是自动配置加载了太多不必要的组件,例如引入了数据源 Starter,但项目暂时还没连数据库。建议分阶段排查:先关掉DataSourceAutoConfiguration,再关掉RedisAutoConfiguration,逐个定位是哪个自动配置拖慢了启动。这个调试思路,比对着日志盲猜高效很多。
5. 进化的本质:从“写配置的人”变为“选配置的人”
5.1 开发范式的转换
从 Spring 到 SpringBoot,表面看是配置写法的变化,但往深一层看,这是开发范式的一次跃迁。Spring 时代,我们的注意力大量被“容器装配”占据——对象什么时候创建、依赖什么时候注入、AOP 切面怎么织入。SpringBoot 时代,这些底层细节被框架“接管”了,开发者真正要做的是:选择合适的能力模块,理解默认行为,在需要的时候做局部定制。
把这种变化用到实际工作中,你会发现自己写代码的视角变了。以前是“声明每一棵树的位置”,现在是“决定种哪片森林”。具体到每天的工作里,意味着你在pom.xml里认真地挑选每一个 Starter,在application.yml里谨慎地微调每一项参数,而不是在几百行 XML 里寻找漏掉的某个ref。
5.2 SpringBoot 对 Java Web 生态的整体影响
自动配置和 Starter 机制看起来只是工程上的便利,但从生态层面看,它催生了 Spring Cloud、Spring Data、Spring Security 等一整套“约定优先”的组件体系。以前搭建一个微服务需要手动整合 RPC、注册中心、负载均衡,各模块的配置风格还不统一;现在不同组件的使用方式在 SpringBoot 的统一治理下趋同——依赖引入采用 Starter,配置集中在 YAML,行为扩展通过注解和自动配置。
也就是说,SpringBoot 不只是简化了 Spring,它更给整个 Java 生态定义了一种“插件式集成”的通用范式。现在你去看任何一个 Spring 官方或社区项目,基本都是“引入 Starter、配置少量参数、实现自定义接口”三步走。
5.3 给进阶者的个人建议
我见过不少刚学 SpringBoot 的同学,上来就直接背注解、写 Controller,结果遇到自动配置不生效或 Bean 冲突时完全懵了。我的建议是:即便你不打算再写老式 XML 项目,也应该花一点时间手工配置一次原生的 Spring——手动声明一次 Bean、手动配一次事务管理器、手动写一遍 SpringMVC 的视图解析器。经历一遍“从繁琐到自动化”的过程,你才会真正理解 SpringBoot 到底为你省掉了什么,也才具备在问题出现时逆向排查的能力。
另外,如果你已经能熟练使用 SpringBoot,可以试着去读一下spring-boot-autoconfigure里你常用模块的源码,比如WebMvcAutoConfiguration、DataSourceAutoConfiguration。读到条件注解判断的那几行,你会对“进化”的实质——框架能力的默认化与边界条件化——有更深刻的体感。
我自己的体会是:从 Spring 到 SpringBoot,与其说是技术的更新换代,不如说是一次开发心智的升级。Spring 给了你掌控容器的能力,SpringBoot 则给了你选择容器行为的能力。前者是“发明工具”,后者是“定义工具的默认行为”,这两件事的难度和思维方式完全不同。
最后再分享一个小技巧,我至今仍会在新项目里保留一个习惯:核心的自动配置类不直接暴露给团队,而是通过自定义 Starter 统一封装。这样业务代码只见规则,不见实现细节,后续升级框架或者切换组件时,影响面被控制在最小的封装边界内。这个思路,其实也是从 SpringBoot 的进化逻辑里延伸出来的。