
1. 前言这个依赖到底解决了什么问题三年前我第一次用 Spring Boot 时最让我震惊的不是“内嵌Tomcat”也不是“约定优于配置”而是**起步依赖Starter**这个东西。当时我在维护一个老项目pom.xml 里躺着将近四十个依赖坐标其中有一半是为了“把一个 Web 服务跑起来”而凑出来的比如 spring-webmvc、jackson-databind、hibernate-validator、spring-boot-starter-tomcat 之类——每加一个新模块你都得自己手工去对齐版本号minio 的客户端拉进来一测发现它默认带的 okhttp 版本和项目里另一个库冲突光排除依赖就花了半天。后来切到 Spring Boot 3.x引入一个 spring-boot-starter-web其他全自动搞定。同一个工程pom 从四十多个坐标瘦身到十几个启动项目配一次就通。这个体验差距就是“起步依赖”设计价值的直观体现。这篇文章我想把 Spring Boot 3.x 的起步依赖机制完整拆一遍重点回答几个问题Starter 到底是什么它的自动配置原理是什么为什么引入一个依赖就够了而不会出现“一堆 jar 不知道干嘛”的失控局面顺带会给出一个实际项目从零搭建的完整过程以及我这些年踩过的版本冲突、自动配置失效之类的坑。适合谁看刚学 Spring Boot 3.x 的新人能帮你把“引入依赖”这件事的底层逻辑吃透已经写了几年项目但一直没空研究 Starter 原理的老开发这篇也能帮你把知识体系补完整。2. Starter 到底是什么从命名规范到自动配置2.1 命名里的学问官方 Starter 与第三方 StarterSpring Boot 的起步依赖本质上就是一个普通 Maven 依赖坐标但它不是孤立的它会通过 Maven 的传递依赖transitive dependency机制把你需要的全部 jar 一次性带进来同时配套一个自动配置类让你引入依赖之后框架自动帮你把组件创建好、参数配好。你做的只有一件事把坐标写进构建文件。这里有一个非常值得注意的细节是命名的约定。官方规范是这样的spring-boot-starter-*这样的命名代表这是 Spring 官方维护的起步依赖比如spring-boot-starter-web、spring-boot-starter-data-redis。而第三方的起步依赖通常在中间带上自己的技术名比如 MyBatis 的mybatis-spring-boot-starter或者当当网 ShardingSphere 的sharding-jdbc-spring-boot-starter。换句话说只要看到*-spring-boot-starter这个模式你就知道这是一个为 Spring Boot 自动配置专门设计的集成库。这不是一个硬性要求但是社区事实上形成的一个默契。识别这个命名的意义在于你在选型时一眼就能判断哪些库是原生支持 Spring Boot 的哪些库需要你自己写一堆Configuration把它桥接进来。2.2 为什么官方文档把 Starter 称为“一站式依赖解决方案”我一直觉得要理解 Starter关键是先理解“传递依赖”和“自动配置”这两件事是怎么配合的。先看传递依赖。一个spring-boot-starter-data-redis光是它直接声明的传递依赖就包含 spring-data-redis、jedis 或 lettuceSpring Boot 3.x 默认是 lettuce-core、commons-pool2 等这些依赖坐标的版本全部由 Spring Boot 的依赖管理 BOMBill of Materials物料清单统一锁定。也就是说你引入一个坐标得到的不只是这一个 jar而是一整套经过版本互测的 jar 组合。这个“经过互测”非常关键因为 Spring Boot 并不是把你需要的 jar 随便堆在一起它是在自己版本发布之前就把这些组件的兼容性跑过一遍的。再看自动配置。每个官方或规范化的第三方Starter它的 jar 包META-INF目录下都会有一个名为org.springframework.boot.autoconfigure.AutoConfiguration.imports的文件如果是在 Spring Boot 2.x是spring.factories文件文件里罗列了若干个自动配置类的全限定名。当你启动 Spring Boot 应用时EnableAutoConfiguration注解会触发一个加载流程——它扫描所有 jar 里的这个 imports 文件把这些自动配置类全部装载进 Spring 容器。注意这里有一个聪明之处装载不代表生效。自动配置类内部几乎都有ConditionalOnClass、ConditionalOnMissingBean等条件注解只有满足条件才会真正创建 Bean。比如你引入了spring-boot-starter-web类里检测到Servlet、DispatcherServlet这些类存在才去自动创建DispatcherServlet、HandlerMapping等一整套 Web MVC 核心组件。同样如果项目里既有 starter-data-redis又有你手工定义的一个RedisTemplateBean那么条件注解ConditionalOnMissingBean就会失效自动配置的RedisTemplate不会创建你自定义的 Bean 会优先生效。这样一套机制跑下来最终的效果就是引入依赖就自动拥有了一个配置好的组件不需要写Configuration不需要自己声明 Bean。你只需要在application.yml写几个属性键比如spring.datasource.url配置文件就会把值读出来填充到自动创建的组件里。提示Spring Boot 3.x 基于 Spring Framework 6它的自动配置加载机制已经从spring.factories迁移到了AutoConfiguration.imports。如果你把 Spring Boot 2.x 的老笔记复制到 3.x 项目里发现自定义自动配置类不生效大概率就是文件路径不对。这个细节值得记下来。3. 为什么一个依赖就够了单点依赖背后的连锁反应3.1 Maven 传递依赖一个坐标带出三十个 jar我在开头提到过一个只有最简单 Web 功能的项目。如果不用 Starter你得手动写这样的 POMdependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version6.1.5/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.17.0/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version10.1.19/version /dependency !-- 还要 hibernate-validator、spring-aop、spring-expression、jakarta.servlet-api ... 一共要十几个 -- /dependencies且不说版本号你得一个一个去查即使查到了Spring 6.1 搭配 Jackson 2.16 大概率没问题但搭配 Tomcat 10.0 还是 10.1哪两个版本的组合在本地测试才不炸这些都是靠社区版本兼容矩阵解决的不是你一个人现查能查明白的。我见过太多同事把时间浪费在这件事上。现在用官方有 Web 起步依赖POM 是这样的dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency没有写version因为 Spring Boot 父 POM 或 BOM 已经把版本锁好了。我在实际项目里用mvn dependency:tree打过一次spring-boot-starter-web这一个坐标最终解析出来的 jar 数量是 31 个其中就包含 spring-webmvc、jackson-databind、tomcat-embed-core、hibernate-validator、spring-web、spring-beans、spring-core 等所有你手写要写半天的东西。“一个依赖”在 Maven 层面的实现原理就是传递依赖。Maven 会把该坐标的 POM 里声明的所有dependency自动带进你的项目。也就是说一个 Starter 的 POM 本身也是一个普通的“聚合描述文件”它不包含多少代码它的核心价值是明确地把一个技术场景所需要的 jar 清单固化下来。3.2 版本锁定的魔力BOM 与父 POM 的分工“一个依赖就够了”这句话能成立还有一个隐藏大前提版本是谁定的如果你什么都不加只用 starter-web但你也没继承spring-boot-starter-parent父 POM那这个坐标的版本你还是得自己写。在实际项目中有两种常见做法它们的原理是一样的就是通过依赖管理dependencyManagement统一版本方式一继承 starter-parentparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version relativePath/ /parent一旦继承了它你的子模块里所有官方的spring-boot-starter-*都不需要写version因为父 POM 里已经通过dependencyManagement把版本号全部声明好了。这是绝大多数 Spring Boot 项目的标配做法。方式二引入 BOM不继承父 POM如果你不想继承 starter-parent比如你的公司内部已经有一个统一父 POM可以考虑在dependencyManagement里单独导入 BOMdependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.3.4/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement两种方式的底层都一样spring-boot-dependenciesBOM里面维护了 Spring Boot 所有支持组件版本号的约束列表。它的版本表信息非常全比如 Spring Data 的版本、Netty 的版本、Jackson 的版本全都对齐了是经过 Spring Boot 官方发布前测试的。我做项目时比较推荐方式二尤其是微服务多模块项目因为继承 pin 死了父 POM。BOM 导入像一个“约束模板”你可以同时导入多个 BOM如果再引入一个第三方 BOM注意把自定义的放前面保证优先覆盖。这个会在后面的常见问题里细讲。3.3 自动配置的“条件化加载”聪明地懒加载前面说到了装载不等于生效这里展开讲。Spring Boot 的自动配置类里大量使用条件注解这些注解是 Starter 不产生副作用的关键。核心的条件注解有这些注解作用ConditionalOnClass当类路径下存在指定类时配置才生效ConditionalOnMissingBean当容器中不存在指定 Bean 时才创建这个 BeanConditionalOnProperty当某个配置属性存在且满足值时配置才生效ConditionalOnWebApplication当前应用是 Web 应用时配置生效ConditionalOnExpression满足 SpEL 表达式时生效拿 spring-boot-starter-data-redis 来说它的自动配置类RedisAutoConfiguration上面标注了ConditionalOnClass(RedisOperations.class)。假如你的项目没用 Redis而只是依赖里不小心带进来一个包含 Redis 客户端的 jar这个自动配置类也不会激活因为它检测不到RedisOperations类。这样就不会出现“我没想用 Redis结果项目启动时报 redis 连接失败”之类的诡异现象。顺着这个思路你就能理解为什么引入 starter-web 之后你写 RESTAPI 就能直接用了却不需要自己配置DispatcherServlet因为WebMvcAutoConfiguration这个自动配置类在类路径检测到DispatcherServlet类后会通过一系列Bean方法把核心组件定义好。而如果你自己想覆盖某个组件只需要在Configuration类里手动声明一个同名同类型的 BeanConditionalOnMissingBean就会让你定义的优先生效。这个设计解决的痛点是什么是“配置文件的复杂度问题”。之前在 Spring 4 时代如果你要跑一个 Spring MVC 项目要配置 web.xml、要写EnableWebMvc、要配视图解析器、配 Jackson 序列化器全部手工来。Starter 把这些问题全部吞进 auto configuration造成的差距就是配置工作量从半天降到 5 分钟。4. 核心实操用项目验证“一个依赖就够了”4.1 前置准备我本机环境是 JDK 17Spring Boot 3.x 要求 Java 17 以上、Maven 3.9、IDEA 2024.1。你如果想跟着跑一遍最好保持一致至少 Java 版本要在 17 及以上不然 Spring Boot 3.x 是不能跑的。如果你用的是 IntelliJ IDEA 社区版没有 Spring Initializr 按钮也别急可以直接去 start.spring.io 生成项目压缩包或者更简单就手工创建一个 Maven 项目把 pom 写进去效果一样。我的建议是今天这段至少值得打开 IDEA 跑一次因为只有亲眼看到 31 个 jar 被传递依赖带进来你才能真正感受到“一个依赖”的分量。4.2 创建项目核心 POM 长成什么样我用 IDEA 自带工具创建了一个空的 Maven 项目修改后的 pom.xml 如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version relativePath/ /parent groupIdcom.example/groupId artifactIdstarter-demo/artifactId version0.0.1-SNAPSHOT/version properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project然后写一个最简单的启动类package com.example.starterdemo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; SpringBootApplication RestController public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } GetMapping(/hello) public String hello() { return Hello, Starter!; } }启动main方法控制台打出Tomcat started on port 8080然后访问http://localhost:8080/hello能够看到返回值。到这里一个最简单 REST 接口已经跑通了。这个过程中我们没有写任何DispatcherServlet配置、没有写任何EnableWebMvc甚至没有手动指定 Tomcat 端口。全部由spring-boot-starter-web的自动配置完成。4.3 眼见为实在依赖树里“解剖”一个 Starter项目跑通之后在 IDEA 右侧 Maven 工具窗口中执行命令mvn dependency:tree -Dverbose输出的内容里你会看到类似这样的结构[INFO] com.example:starter-demo:jar:0.0.1-SNAPSHOT [INFO] \- org.springframework.boot:spring-boot-starter-web:jar:3.3.4:compile [INFO] - org.springframework.boot:spring-boot-starter:jar:3.3.4:compile [INFO] - org.springframework.boot:spring-boot-starter-json:jar:3.3.4:compile [INFO] | - com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.17.1:compile [INFO] | - com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.17.1:compile [INFO] | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.17.1:compile [INFO] - org.springframework.boot:spring-boot-starter-tomcat:jar:3.3.4:compile [INFO] | - org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.28:compile [INFO] | - org.apache.tomcat.embed:tomcat-embed-el:jar:10.1.28:compile [INFO] | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:10.1.28:compile [INFO] - org.springframework:spring-web:jar:6.1.12:compile [INFO] - org.springframework:spring-webmvc:jar:6.1.12:compile看到没有spring-boot-starter-web这个节点下面所有子依赖都被拉出来了。Tomcat 是嵌入式的Jackson 用于 JSON 序列化Jackson 的三个模块Jdk8、JSR310、ParameterNames也都是新版其中jsr310用于 Java 8 时间类型和 JSON 之间的互相转换。如果手写这些坐标版本号你要全部操心一遍但用 Starter 一个都不用写。如果想知道为什么版本号是 2.17.1 而不是 2.17.0可以直接到spring-boot-dependenciesBOM 里去看。本地 Maven 仓库里这个 BOM POM 文件在org/springframework/boot/spring-boot-dependencies/3.3.4/下打开就能查所有组件版本约束。4.4 一个 Spring Boot 3.x 项目引入数据库 Starter更完整的例子只跑一个 starter-web 可能感觉还不过瘾我再加一个数据库场景。我在这里加入 MyBatis 场景介绍因为这是国内 Java 项目里最常见的组合也是热搜词里经常提到的东西。引入 MyBatis 的起步依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency引入 MySQL 驱动这个不属于 Starter就是普通驱动dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencyapplication.yml 再写上数据源信息spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.starterdemo.entity一个 Mapper 接口写在 java 目录下对应 XML 放在 resources/mapper 下项目启动时 MyBatis 自动配置类会扫描到它们。这就是 mybatis starter 起的作用它监听到SqlSessionFactory类存在同时容器里有了DataSourceBean就自动创建SqlSessionFactory再通过MapperScan或注解扫描 Mapper 接口。你完全不用手工写SqlSessionFactoryBean。注意mybatis-spring-boot-starter 也是需要在dependencyManagement之外写版本号的。因为它不在 Spring Boot 官方 BOM 里。它的版本兼容矩阵是 MyBatis 社区维护的如果你用的是 Spring Boot 3.3.x就选 mybatis-spring-boot-starter 3.0.x如果你还在用 Spring Boot 2.7.x那就得选 2.3.x 或 2.2.x。对应关系千万别搞反老项目直接升 Boot 3 然后沿用 MyBatis starter 2.x成绩就是启动报错。4.5 观察自动配置是否生效一个非常实用的小命令如果有一次你不确定某个 Starter 的自动配置到底有没有起作用可以在application.yml或启动参数里加这个配置debug: true启动后控制台会打出一份Positive matches和Negative matches的自动配置报告。Positive 是明确生效的自动配置类Negative 是没生效的。像这样Positive matches: WebMvcAutoConfiguration - ConditionalOnClass 找到 Servlet、DispatcherServlet、WebMvcConfigurer 等类OnClassCondition Negative matches: RedisAutoConfiguration - 未找到 RedisOperations 类OnClassCondition这个功能在排查“为什么我引了依赖但没效果”时是神器。排查思路就是我下面要聊的重点。5. 常见问题与排查技巧实录5.1 “引了依赖但自动配置没生效”怎么办这个问题在开发中非常常见。大部分情况都出在条件注解上。第一次遇到这种情况时我花了两三个小时查代码最后才发现根本不是代码问题而是 jar 根本没拉进来。排查步骤建议这样走检查是否有AutoConfiguration.imports文件进入本机 Maven 仓库对应 jar 目录用压缩工具打开 jar查看META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports如果没有这个文件说明这个“Starter”不是一个规范的 Spring Boot 自动配置库。打开 debug 报告在application.yml里开debug: true看目标自动配置类出现在 Positive 还是 Negative再对照 Negative 的原因比如“没有找到某个类”“条件属性不匹配”。检查是否被自定义配置覆盖如果你的项目里自己也定义了一个同名 Bean比如定义了一个RestTemplatespring-boot-starter-web 就不会再自动创建默认的RestTemplate其实默认的也不算自动创建需要额外建 Bean。这是ConditionalOnMissingBean的逻辑。举个例子第一次用spring-boot-starter-data-redis我在项目里定义了一个自定义连接工厂RedisConnectionFactory结果 redis 功能起不来。后来把 debug 打开看到报告显示RedisAutoConfiguration因为ConditionalOnMissingBean(RedisConnectionFactory.class)被跳过这才反应过来其实没必要自己定义默认的 lettuce 连接工厂已经够用。5.2 版本冲突多个 BOM 混用时的优先级Spring Boot 3.x 项目经常需要跟其他中间件联合使用比如 Spring Cloud Alibaba、Dubbo或者是某个内部封装的基础库它们往往也提供自己的 BOM。于是会出现多个 BOM 同时约束同一个组件版本的情况最难受的问题就是Netty 版本冲突、Jackson 版本冲突。Maven 处理dependencyManagement的规则是先声明的优先并且子项目的声明优先于父 POM。所以如果你的 pom.xml 里dependencyManagement中先声明了自己的 BOM那么就以你的为准。遇到版本冲突时不用抓瞎直接在dependencyManagement最前面把你需要固定版本的坐标和版本号写进去就能强制锁定。还有另一个坑传递依赖导致的 jar 冲突比如某依赖强制引入了低版本 Jackson你可能就需要用exclusions排除掉。尽量别用什么 mvn 命令去改全局版本那不优雅还是优先在 BOM 层级解决。5.3 常见异常速查表异常现象根本原因解决方案启动报ClassNotFoundException: javax.servlet.*使用了 Spring Boot 2.x 或老 jar依赖的还是 javax 命名空间升级到 jakarta 命名空间的依赖包Spring Boot 3.x 只支持 jakarta引入多模块项目基础模块的 Starter 版本错乱子模块没有统一继承父 POM 或没有导入 BOM检查每个模块的父 POM尽量统一用 spring-boot-starter-parentMybatisAutoConfiguration报找不到SqlSessionFactory数据源配置缺失或 mybatis starter 版本与 Boot 3 不兼容检查 datasource 配置与 mybatis starter 版本对应关系自定义ObjectMapper不生效JacksonAutoConfiguration的ConditionalOnMissingBean被满足认为容器已有自定义的不会再帮你配确保自定义 ObjectMapper 上标注Bean且类路径在自动配置扫描之前如果不行排除自动配置类项目启动很慢加载了一堆不该加载的自动配置spring-boot-starter 传递依赖带入了不需要的组件只按需引入必要的 starter非必要的可以排除传递依赖或调整 debug 报告分析5.4 一个易被忽略的点Starter 不是越多越好很多教程里把“引一个 starter 就能用”说得神乎其神导致新手容易走向另一个极端恨不得把几十个 starter 全塞进一个模块里。这是大忌。每引入一个 Starter等于引入了一批可能被自动激活的配置组件。只被启动而未被用到的自动配置类虽然大部分条件注解会挡住它的加载但不是所有组件都会按需关闭有些组件还是会占用初始化时间、端口探测、连接池创建甚至时机不对还会报错。比如你把spring-boot-starter-data-jpa引了进来但完全不配数据源项目启动的默认数据源初始化流程还是会去构建一个 DataSource Bean导致启动失败。我的经验是一个技术场景对应一个 Starter能不引就不引如果某个功能一段时间不用宁可分隔成独立服务也不要全部堆在同一个 Web 工程里。这也是微服务拆分的核心动机之一依赖隔离能让启动速度和问题排查都更清爽。6. 踩坑过后总结的实操心得最后分享几个我自己较有价值的实操体会。第一Starter 版本不是越新越好而是和 Spring Boot 主版本匹配。Spring Boot 3.3.x 项目最好使用与之配套的第三方 starter 新版本。比如 MyBatis starter 3.0.x 就是为 Boot 3 准备的。如果你只看“网上教程写的旧版本号”会有很多坑。我现在处理版本问题的第一反应永远是去官方文档查 Spring Boot 版本兼容矩阵而不是试错试到晚上。第二理解SpringBootApplication里那三个注解的分工。它其实是由SpringBootConfiguration、EnableAutoConfiguration、ComponentScan组合出来的。自动配置的核心是EnableAutoConfiguration。如果你在某些模块里没有加SpringBootApplication那自动配置就完全不会发生你引入的 starter 就只是一个普通 jar什么都不会发生。这一点我之前在一个多模块项目里吃过亏子模块里写了 Configuration但没加 EnableAutoConfiguration 或 SpringBootApplication导致 RedisTemplate 一直为 null。第三如果用exclusions排除传递依赖记得在注释里说明排除原因。否则三个月后你回头看 pom完全想不通当时为什么要那么写。我自己吃过这个亏曾在 pom 里排除了某个库的旧 netty后来新同事不知道原因又把旧版本坐标加回来结果线上问题查了两天才定位。第四一定要利用好Spring Boot 3.x 的自动配置报告。我前几年特别喜欢在项目出问题时直接搜网上“为什么 Redis 自动配置不生效”但效率极低。现在我的做法是debug: true一开正负匹配报告一拉对照着条件注解看问题原因基本能在十分钟内定位。工具就在身边用好它比到处查答案靠谱得多。如果你还在学习 Spring Boot 的阶段不妨试着自己写一个极简的自定义 Starter 出来——一个 jar 里放一个自动配置类和一个 imports 文件在另一个工程里引入会发现整个过程通透很多。毕竟真正理解一个机制最有效的方式就是亲手还原一遍它。