2026/10/19 11:23:38

Spring Boot核心解析:自动配置、内嵌Tomcat与快速开发实战

Spring Boot核心解析:自动配置、内嵌Tomcat与快速开发实战 1. 从Spring到Spring Boot当初我们为什么非换不可我在刚接触Java服务端开发的时候用的还是Spring MVC那套老写法。每次新开一个项目第一件事不是写业务代码而是花半天去凑配置web.xml要配、spring-context.xml要配、spring-mvc.xml要配还要把各种jar包按版本一个个对齐。运气好的时候一次跑通运气不好就陷入ClassNotFoundException和版本冲突的泥潭里一折腾就是一整天。后来Spring Boot出现我第一次用的时候心里是存疑的——一个main方法就能把Web服务跑起来连Tomcat都不用装直到自己亲手建了个工程、写了三行代码、启动、浏览器里看到输出才意识到这东西确实把Java后端开发的体验彻底改写了。Spring Boot本质上做的事情可以用一句话概括把Spring生态里那些需要你手动组装的东西变成了一套开箱即用的默认配置同时保留了按需覆盖的能力。它不是替代Spring的新框架而是基于Spring Framework的一种开发范式目的就是让你把精力从“配置框架”挪回“写业务”。这一篇我先从整体讲清楚Spring Boot解决了什么痛点、怎么快速起一个项目、核心注解都干了什么、配置到底怎么管理以及自动配置背后的逻辑。后续如果有机会再往深里拆启动过程源码、数据访问、安全控制这些东西。第一篇的定位是看完你能自己从零建一个能跑、能调、能配置的Spring Boot工程并且知道每一步为什么要这么干。如果你之前只写过Servlet、JSP或者用过SSM但没试过Spring Boot这篇的内容正好就是你的过渡桥梁。如果你已经写过一段时间Spring Boot也可以当查漏补缺看看有没有理解偏差的地方。2. 第一个Spring Boot项目从创建到跑起来全流程2.1 项目初始化推荐用脚手架而不是手动建创建一个Spring Boot工程我最推荐的方式是去 start.spring.io 这个网站生成基础工程然后导入IDE。你也可以用IDE自带的Spring Initializr入口本质是同一个东西。为什么不推荐自己手动建目录、自己加依赖因为Spring Boot的工程结构虽然不复杂但有一堆约定俗成的细节比如主类的位置、配置文件的命名、测试类的放法手搭很容易踩坑而脚手架生成的工程正好全部符合默认约定。选择构建工具时Maven和Gradle都可以我身边用Maven的居多这篇的示例都以Maven为例。Java版本按你自己环境来Spring Boot 3.x要求Java 17以上如果你还在用Java 8就只能选Spring Boot 2.7.x这条线。这一点第一次接触的人特别容易踩坑用Java 8环境去跑Spring Boot 3.x项目编译直接报错而且报错信息不太友好。依赖选择上一个最简单的Web项目只需要加一个Spring Web依赖它会把Spring MVC、内嵌Tomcat、Jackson序列化这些一并带进来。这块就体现了“起步依赖”的设计理念——以后你要加什么功能就加一个对应的starter比如操作数据库加spring-boot-starter-data-jpa做接口文档加spring-boot-starter-validation不用自己逐个找jar包。2.2 工程目录结构与主类的职责生成好的工程大致长这样src/main/java/com/example/demo DemoApplication.java src/main/resources application.properties src/test/java/com/example/demo DemoApplicationTests.java pom.xml那个带main方法的类就是入口一般放在根包位置。为什么要放在根包因为Spring Boot默认会扫描主类所在包及其子包下的所有组件Controller、Service、Repository这些。如果你把主类放在com.example.demo而Controller放在com.example.controller这种和主类不在同一层级甚至没有包含关系的包里扫描就会漏掉它接口全部404。这是一个非常常见的启动“成功”但请求“失败”的案例几乎每个群里都有人问过。主类的代码很简单核心就一个注解加一个启动方法package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }SpringApplication.run(...)这个调用干了很多事初始化Spring容器、触发自动配置、启动内嵌Web服务器、加载配置文件等等。初学者现阶段只需要记住这是整个应用的起点它一执行你的服务就起来了。2.3 跑一个最简单的接口验证环境工程建好后先在pom.xml里看一眼依赖是不是加载完整然后直接在IDE里运行DemoApplication的main方法。控制台会出现类似下面这样的一段日志Tomcat started on port(s): 8080 (http) with context path Started DemoApplication in 2.345 seconds看到Tomcat started on port 8080说明内嵌Tomcat已经被启动Spring Boot在背后已经把Web环境全部搭好而你并没有安装过任何独立的Tomcat也没有写一行配置。这就是内嵌容器的魅力。然后写一个最简单的HTTP接口验证一下package com.example.demo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; RestController public class HelloController { GetMapping(/hello) public String hello() { return Hello, Spring Boot!; } }重启应用浏览器访问http://localhost:8080/hello看到返回的字符串就算通关了。这里必须留意一个细节一定得重启因为Spring Boot默认没有开热部署。热部署要额外引入spring-boot-devtools依赖那又是后话。很多新手改了代码不重启浏览器里还是旧结果就以为代码写错了其实只是没生效而已。3. 核心注解逐个拆解用得熟更要知道为什么这样写3.1 SpringBootApplication三个注解缝在一起的复合注解SpringBootApplication是使用频率最高的注解它本身是一个复合注解相当于同时使用了Configuration、EnableAutoConfiguration、ComponentScan三个注解。Configuration表示这个类会作为配置来源里面可以定义Bean方法ComponentScan让Spring扫描并注册组件EnableAutoConfiguration是Spring Boot最核心的开关它开启自动配置机制后面专门用一节讲。你可以把这三个注解拆开单独写效果是一样的但没人这么干因为一个SpringBootApplication就能搞定。理解这个复合注解对排查问题特别有帮助。比如你发现某个Service没有被Spring管理调用时报空指针第一反应就应该是检查这个Service所在的包是否在主类所在包的子包下面。如果不在即使你在类上标了Service扫描器也看不到它。3.2 控制层注解RestController与请求映射RestController是Controller和ResponseBody的组合表示这个类的所有方法返回值直接写进HTTP响应体不走视图解析。写纯接口服务时用RestController写页面跳转时才用Controller配合模板引擎。请求映射注解有GetMapping、PostMapping、PutMapping、DeleteMapping、RequestMapping五兄弟。前四个是Mapping的语义化封装比如GetMapping等于RequestMapping(method RequestMethod.GET)。大多数情况下按语义直接用就行只有在你需要同时匹配多种请求方式时才用RequestBody和PathVariable这些参数绑定注解。举个例子RestController RequestMapping(/api/users) public class UserController { GetMapping(/{id}) public String getUser(PathVariable Long id) { return user id: id; } PostMapping public String createUser(RequestBody User user) { return create: user.getName(); } }PathVariable把URL路径中的{id}绑定到方法参数RequestBody把请求体里的JSON反序列化成User对象。这两个是最常用的绑定方式剩下的RequestParam用于?namexxx这种查询参数RequestHeader用于读取请求头各自分工明确。3.3 业务层与持久层注解让Spring托管对象Service层常用Service标注数据访问层用Repository两者都是Component的衍生注解作用都是让Spring扫描到并创建Bean。区别在于语义和附加行为Repository会把数据访问层的异常统一转换成Spring的DataAccessExceptionService则纯粹是语义标记方便开发者一眼看懂分层。这里想多说一句依赖注入的一个常见误区。新手经常在字段上直接加AutowiredAutowired private UserService userService;这样写能跑但在构造器注入面前它不算好的实践。推荐的做法是构造器注入private final UserService userService; public UserController(UserService userService) { this.userService userService; }如果类只有一个构造器连Autowired都可以省掉。Spring 4.3之后单个构造器会自动完成注入。这样做的好处是依赖关系清晰。构造时就能发现缺失的依赖也方便写单元测试的时候直接new对象传Mock。字段注入让你测试的时候想替换依赖还得动用反射麻烦得很。4. 配置管理的细节properties、yaml与多环境切换4.1 配置文件怎么写两种格式的取舍Spring Boot默认的配置文件是application.properties但你经常看到别人用application.yml两者可以并存同时存在时properties优先级更高。我的建议是只挑一个用别混着写不然排查顺序问题的时候容易绕晕。properties是keyvalue的扁平行式server.port8081 server.servlet.context-path/api spring.datasource.urljdbc:mysql://localhost:3306/demoyaml靠缩进表达层级更适合描述结构性配置server: port: 8081 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: 123456两种格式都有不少人用。我个人偏向yaml因为配置项多了以后层级关系一目了然不像properties那样前缀重复写一长串。但yaml有一个小坑它严格的缩进规则和键值对写法容易犯格式错误少一个空格整个配置就解析失败。没有特殊偏好就用properties反而省心。4.2 读取配置的两种姿势Value与ConfigurationProperties配置写进文件后代码里怎么读最直接的方式是ValueValue(${server.port}) private String serverPort;这种方式适合读单个配置项快捷轻便。但当你要读一组关联配置比如数据源的url、username、passwordValue就要写三个字段比较啰嗦。更好的做法是用ConfigurationProperties绑定到一个Java类Component ConfigurationProperties(prefix spring.datasource) public class DataSourceProperties { private String url; private String username; private String password; // 必须提供getter/setter }Spring会把application.yml里所有以spring.datasource开头的配置项自动填到这个类的同名属性中。好处是配置集中管理、类型安全编译期就能发现类型不匹配的问题。注意用ConfigurationProperties的类需要提供getter和setter因为Spring是通过setter完成绑定的。如果你用的是Java 17环境加Spring Boot 3.x还可以用record代替class体验更好。4.3 多环境配置dev、test、prod怎么切开发环境、测试环境、生产环境的配置几乎不可能完全一致数据库地址、日志级别、接口开关都可能不同。Spring Boot的Profile机制就是为此设计的。命名规则很简单application-{profile}.properties。比如application.properties // 公共配置 application-dev.properties // 开发环境 application-test.properties // 测试环境 application-prod.properties // 生产环境在application.properties里通过spring.profiles.active指定使用哪个环境spring.profiles.activedev启动的时候也可以临时覆盖java -jar demo.jar --spring.profiles.activeprod。命令行的优先级比配置文件高这在部署的时候特别有用。你可以把一个jar包同时部署到测试和生产服务器通过启动参数决定跑哪个环境不用再为了换环境去改配置文件重新打包。我实际项目里经常用组合基础配置放公共文件环境相关的差异放各自的profile文件敏感信息数据库密码、密钥不走配置文件而是通过环境变量注入。Spring Boot对环境变量有天然的映射规则比如环境变量SPRING_DATASOURCE_PASSWORD会被自动绑定到spring.datasource.password。这样就能把机密信息从代码仓库里摘出去降低泄露风险。5. 自动配置的秘密一个注解怎么拉起一整套运行环境5.1 启动日志里的AutoConfiguration线索每次Spring Boot启动时控制台会有一段DEBUG级别的日志记录了大量自动配置类的条件评估结果。初学者常常忽略这段日志但它恰恰是理解自动配置最好的入口。你可以在application.yml里临时开启自动配置报告debug: true启动后日志里会出现Positive matches和Negative matches两个区块。Positive列出了那些生效的自动配置Negative列出了哪些自动配置因为条件不满足没生效后面跟着原因。比如你没引入DataSource相关的依赖数据源的自动配置就会出现在Negative matches里原因是ConditionalOnClass判断DataSource类不在classpath中。这个报告是排查问题的利器。有时候你觉得某个配置“应该生效”但实际没有去Negative matches里看一眼条件就能找到原因。我第一次排查一个数据源配置不生效的问题时就是靠这份报告发现是缺了连接池的jar包而不是配置写错了。5.2 自动配置的底层机制条件注解自动配置的核心是一系列条件注解最常用的有四个ConditionalOnClassclasspath中存在指定类才生效ConditionalOnMissingBean容器里没有指定Bean才生效ConditionalOnProperty配置项满足条件才生效ConditionalOnWebApplication当应用是Web应用时才生效整个自动配置的过程可以简化成三步Spring Boot在classpath中寻找所有自动配置类用条件注解逐个判断这个配置该不该生效生效的配置向容器注册Bean。默认值全部由配置类里的属性绑定而来而你又可以通过配置文件覆盖这些默认值形成“约定优先按需覆盖”的机制。以内嵌Tomcat为例。Spring Boot检测到classpath里有spring-boot-starter-web提供的Tomcat相关类ConditionalOnClass成立于是自动配置类ServletWebServerFactoryAutoConfiguration生效创建出一个TomcatServletWebServerFactory Bean。这个工厂负责创建并启动内嵌Tomcat实例。你全程没有写一行配置但整个Web服务器就位了。5.3 排除不需要的自动配置自动配置也不是越多越好。有些场景下某些自动配置会干扰你比如你想自己实现一套数据源逻辑或者你引入了某些依赖但不想触发它对应的自动配置。这时有两个办法方法一在启动类上排除指定类型SpringBootApplication(exclude {DataSourceAutoConfiguration.class})方法二通过配置文件排除spring.autoconfigure.excludeorg.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration我实际遇到的场景是工程里引入了数据库驱动的jar包但暂时不想真正连数据库只做纯接口开发。这时DataSourceAutoConfiguration会尝试创建数据源因为没有配置url而启动报错。排除掉它之后工程就能在没有任何数据库配置的情况下正常启动。这个例子说明了自动配置的一个特点它默认假设你引入依赖就是想用它当这个假设和你的实际意图不一致时你要学会干预。自动配置省了你手动搭环境的时间但同时要求你对“什么时候该排除”有判断力。6. 内嵌Tomcat与打包部署开发、测试、生产三套思路6.1 内嵌容器的优势与切换Spring Boot应用默认把Tomcat打进jar包用一个java命令就能启动整个服务。这和传统的外部Tomcat部署模式比最大的优势是环境一致性——开发、测试、生产用的都是同一个启动方式不再有“本机能跑服务器跑不了”的环境差异问题。如果你想用别的容器比如Jetty或者Undertow只需要在pom.xml里排除spring-boot-starter-tomcat换成对应的starter即可。Undertow在高并发场景下内存占用表现更好Jetty的某些特性在长连接场景有优势。大多数业务系统用默认的Tomcat就够了没有特殊性能测试数据支撑前不建议为了赶时髦去换容器。6.2 打包成可执行jar并运行在项目根目录执行mvn clean package -DskipTests构建完成后target目录下会出现一个demo-0.0.1-SNAPSHOT.jar。这个jar和普通的依赖jar不一样它内部包含了内嵌容器和所有依赖因此被称为fat jar。运行极其简单java -jar target/demo-0.0.1-SNAPSHOT.jar默认端口8080如果想换端口可以用参数覆盖java -jar target/demo-0.0.1-SNAPSHOT.jar --server.port9090这种打包方式非常适合容器化部署因为镜像里只需要包含一个JRE和一个jar包启动命令就一行。6.3 war包部署的传统场景虽然Spring Boot官方推荐可执行jar的方式但有些公司的基础设施还沿用着外部Servlet容器统一管理多个应用的架构。这时需要将打包方式改成war在pom.xml里改一行packagingwar/packaging同时让启动类继承SpringBootServletInitializer并重写configure方法SpringBootApplication public class DemoApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(DemoApplication.class); } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }双保险是既能打成war放进外部Tomcat也能继续用main方法直接跑开发环境。是否需要打成war取决于团队现有的运维体系。没有历史包袱的话纯jar方案会省很多事。6.4 常见启动问题端口占用与OutOfMemory启动阶段最常遇到的两个问题我觉得值得单独列出来。端口占用是最常见的。Error starting ApplicationContext提示Port 8080 was already in use。排查方式确认是不是自己上一次启动的应用没关干净在IDE里看有没有残留的Java进程再确认有没有其他服务占了8080端口。临时解决是在启动参数加--server.port8081或者直接在配置文件里改端口。内存溢出相对隐蔽一些表现为启动卡住不动或者运行一段时间后抛OutOfMemoryError。内嵌Tomcat默认在容器内工作堆内存大小取决于Java进程的启动参数。生产部署时建议在启动脚本里显式设置JVM参数比如java -Xms512m -Xmx1024m -jar demo.jar-Xms是初始堆大小-Xmx是最大堆大小。这两个值要根据机器内存和业务量来调不是越大越好。设得太大反而会拖慢启动速度因为JVM需要分配并初始化那么大一片内存。7. 从零到能用一个完整的示例串联所有知识点前面各节都是零散的这一节把它们串成一个完整的“增删改查”小接口模拟一个非常常见的用户管理场景。你可以直接照着敲一遍跑通之后再回头看细节理解会深很多。先建一个用户实体package com.example.demo.entity; public class User { private Long id; private String name; private Integer age; // 构造器、getter/setter省略IDE可以自动生成 // 建议至少保留无参构造器JSON反序列化时需要 }再写一个Service类先用一个本地集合模拟数据存储后续换数据库时只改这一层package com.example.demo.service; import com.example.demo.entity.User; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; Service public class UserService { private final ListUser userStore new ArrayList(); private Long idCounter 1L; public User createUser(User user) { user.setId(idCounter); userStore.add(user); return user; } public ListUser findAll() { return new ArrayList(userStore); } public OptionalUser findById(Long id) { return userStore.stream() .filter(u - u.getId().equals(id)) .findFirst(); } }Controller层负责接收HTTP请求、调用Service、返回JSONpackage com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/users) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping public ListUser listUsers() { return userService.findAll(); } GetMapping(/{id}) public User getUser(PathVariable Long id) { return userService.findById(id) .orElseThrow(() - new RuntimeException(用户不存在)); } PostMapping public User createUser(RequestBody User user) { return userService.createUser(user); } }注意这里用的是构造器注入UserService字段用final修饰。这种写法省掉了Autowired注解依赖关系一目了然。可以配合curl命令来验证curl -X POST http://localhost:8080/api/users \ -H Content-Type: application/json \ -d {name:张三,age:25} curl http://localhost:8080/api/users第一个curl会返回创建成功的用户JSON第二个curl返回包含该用户的列表。整个过程从启动到请求再到返回完全没碰数据库也没改端口你体会到的就是Spring Boot“快速起一个Web服务”的能力。这个示例如果后面接入数据库只需要增加对应的starter和配置再把UserService里的集合换成Repository调用Controller层一行都不用改——分层的好处就在这里。打开http://localhost:8080/api/users如果能看到列表数据说明整个链路是通的。这个“Controller → Service → 数据存取”的三层结构其实就是绝大多数Java后端项目的基础骨架后面所有的复杂业务都是在这个骨架上长出来的。我第一次用这个示例跑通的时候最大的感触不是代码写得有多好而是“原来不用配web.xml、不用装Tomcat、不用管jar包版本一个main方法就能跑一个Web项目”。这种体验上的改变远大于任何技术细节上的优化。后来我帮别人搭项目也永远是从这样一个最小可运行的骨架开始先把链路跑通再逐步往上加功能。如果一上来就想着把所有框架整合齐了再启动反而会遇到一堆彼此纠缠的配置问题连错误根源都找不到。