Spring Boot 到底解决了什么问题
在 Spring Boot 出现之前,用 Spring 搭一个 Web 项目要先配 web.xml、写一堆 XML、管理各种依赖版本,一个「能跑起来的空壳」就能消耗半天。Spring Boot 的核心贡献不是新框架,而是约定优于配置:它把常见场景的默认值全部内置,你只需要声明要开哪些能力。
具体来说,它靠三件事达成这个目标:一是 starter 依赖,用一个坐标拉进一整套经过验证的库版本;二是自动装配,启动时根据类路径里存在什么,自动帮你把对应组件装配好;三是内嵌容器,不用单独装 Tomcat,直接 java -jar 就能跑。
理解 Spring Boot 的关键,是把它看成「Spring 的脚手架 + 默认配置集合」。你随时可以用自定义配置覆盖默认值,但绝大多数时候,默认值已经够用。
- starter一个依赖坐标聚合一组库,版本由父 POM 统一管理。
- 自动装配启动时按类路径条件判断,自动注册所需的 Bean。
- 依赖注入对象不再自己 new,而是由容器创建并通过构造方法注入。
- REST 接口用注解把 Java 方法映射到 HTTP 路径,请求响应自动序列化。
最小工程:依赖与启动类
创建一个 Spring Boot 工程最省事的办法是用官方脚手架,但手工理解依赖结构更有利于排错。最小可运行的 Web 工程只需要父 POM 和两个 starter:spring-boot-starter-web 提供 Web 与内嵌容器,spring-boot-starter-test 提供测试支持。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
然后是启动类。注意两个细节:@SpringBootApplication 是一个组合注解,它同时包含了配置类、自动装配与组件扫描三件事;而组件扫描默认从启动类所在的包开始向下递归,所以启动类必须放在最外层的包,否则控制器不会被扫到。
package com.jjy.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);
}
}
常用的 starter 还有几个方向:spring-boot-starter-data-jpa 处理数据库、spring-boot-starter-validation 做参数校验、spring-boot-starter-actuator 暴露健康检查与指标。需要什么加什么,未使用的依赖不要留在工程里。
第一个 REST 接口
REST 接口的写法在 Spring Boot 里非常直观:一个加了 @RestController 的类,方法上加路径映射注解,返回值会被自动序列化成 JSON。不需要手动拼字符串,也不需要在配置里注册映射。
package com.jjy.demo.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/greetings")
public class GreetingController {
@GetMapping("/{name}")
public Map<String, Object> hello(@PathVariable String name,
@RequestParam(defaultValue = "cn") String lang) {
String text = "en".equals(lang) ? "Hello, " + name : "你好," + name;
return Map.of("lang", lang, "message", text);
}
}
启动应用后访问 /api/greetings/阿布?lang=cn,就能拿到一段 JSON。几个容易混淆的点值得记牢:@RestController 等于 @Controller 加 @ResponseBody,所以方法返回值直接作为响应体;@RequestMapping 写在类上作为前缀,写在方法上表示具体路径;HTTP 方法注解有 Get / Post / Put / Delete 等,语义上要与其对应。
- GET 用于查询,应当幂等且不产生副作用。
- POST 用于创建,请求体携带数据。
- PUT 用于整体更新,PATCH 用于局部更新。
- DELETE 用于删除,同样应当幂等。
依赖注入:把对象交给容器
依赖注入是 Spring 的灵魂。它的核心思想是控制反转:对象不再自己 new 出协作者,而是由容器负责创建并「注入」进来。带来的直接好处是解耦——控制器只依赖接口,测试时可以换成替身实现,而不用改动业务代码。
| 注入方式 | 写法 | 建议 |
|---|---|---|
| 构造方法注入 | 把依赖写成构造参数 | 首选:依赖明确、字段可 final、便于测试 |
| Setter 注入 | 在 setter 上标注解 | 仅用于可选依赖 |
| 字段注入 | 直接在字段上标注解 | 尽量避免:隐藏依赖、难以单测 |
只要类被 @Service、@Component、@Repository 等注解标记,Spring 就会把它注册成 Bean;单构造方法时连 @Autowired 都可以省略。下面是一个标准的服务层写法:
package com.jjy.demo.service;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Locale;
@Service
public class GreetingService {
private final TimeProvider timeProvider;
public GreetingService(TimeProvider timeProvider) { // 构造注入,无需 @Autowired
this.timeProvider = timeProvider;
}
public String greet(String name, String lang) {
String who = name == null ? "访客" : name.trim();
String prefix = "en".equals(lang.toLowerCase(Locale.ROOT)) ? "Hello" : "你好";
return prefix + "," + who + ",今天是 " + timeProvider.today();
}
}
Bean 默认是单例的,也就是整个容器里只创建一个实例,因此它必须是无状态的——不要在 Bean 里存与单次请求相关的数据,那属于方法内的局部变量。需要不同作用域时,可以显式声明为 prototype 或 request 级别,但要清楚这会带来额外的创建开销。
请求参数、请求体与统一返回
真实接口需要接收结构化数据,也需要稳定统一的响应格式。参数绑定方面,Spring 用注解区分数据来源:@RequestParam 取查询串或表单字段,@PathVariable 取路径占位符,@RequestBody 把 JSON 反序列化成对象,@RequestHeader 取请求头。
package com.jjy.demo.web;
import com.jjy.demo.service.GreetingService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/greetings")
public class GreetingController {
private final GreetingService service;
public GreetingController(GreetingService service) {
this.service = service;
}
public record GreetRequest(String name, String lang) { }
public record ApiResponse(int code, String message) { }
@PostMapping
public ApiResponse create(@RequestBody GreetRequest req) {
String text = service.greet(req.name(), req.lang());
return new ApiResponse(0, text);
}
}
把响应包成一个统一结构是工程里最常见的做法,前端只需要判断 code 就能区分成功与失败。record 是 Java 16 引入的语法糖,用来声明不可变的数据载体,非常适合做请求与响应对象。如果字段很多,也可以换成普通的 DTO 类,加上 Lombok 之类的工具减少样板代码。
需要注意 JSON 字段与 Java 属性名的映射:默认按名称匹配,若不一致可以用注解指定。日期时间类型建议统一用 ISO 格式,避免不同客户端解析差异。
配置文件与多环境切换
Spring Boot 支持 properties 与 yml 两种配置格式,yml 层级清晰更受欢迎。配置文件放在 src/main/resources 下,名为 application.yml;通过 spring.profiles.active 可以切换到不同环境的配置文件,避免把生产参数硬编码进代码。
server:
port: 8080
spring:
application:
name: jjy-demo
profiles:
active: dev
logging:
level:
root: info
com.jjy.demo: debug
需要区分环境时,再补一个 application-dev.yml,把数据库地址、调试日志等开发专用配置写在里面。读取配置有两种常用方式:
@Value("${server.port}"):适合少量、零散的单值注入。@ConfigurationProperties:把一组相关配置绑定到一个类上,类型安全、便于校验,推荐用于成组的业务配置。
一个容易被忽略的细节是配置优先级:命令行参数高于环境变量,环境变量高于配置文件,外部配置文件高于打包进 jar 的配置文件。排查「为什么线上读到的不是这个值」时,按这个顺序逐层确认,通常几步就能定位。
分层结构、校验与全局异常
工程一旦超过几个接口,就需要清晰的分层。最常见的是三层结构:Controller 只负责接收请求与组织响应,Service 承载业务规则,Repository 负责数据访问。层与层之间通过接口或明确的类型通信,避免控制器里直接写 SQL 这类越界操作。
- Controller:参数绑定、调用 Service、包装响应,不写业务判断。
- Service:业务规则、事务边界、跨模块编排。
- Repository:数据持久化,屏蔽具体存储细节。
参数校验不要散落在方法体里,用注解声明式完成:给字段加 @NotBlank、@Min 等约束,再在控制器参数上加 @Valid,不符合约束时会自动抛出校验异常。配合全局异常处理器,把所有异常统一转换成响应对象:
package com.jjy.demo.web;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse handleInvalid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldError() == null
? "参数不合法"
: e.getBindingResult().getFieldError().getDefaultMessage();
return new ApiResponse(400, msg);
}
@ExceptionHandler(IllegalArgumentException.class)
public ApiResponse handleBadArg(IllegalArgumentException e) {
return new ApiResponse(400, e.getMessage());
}
}
这样一来,业务代码只需要专注判断,错误信息自然从异常流向统一的出口。日志同理:用 SLF4J 的接口打日志,而不是 System.out.println,既能分级输出,也能在出问题时按 traceId 串起一整条调用链。
打包部署与继续深入的方向
Spring Boot 默认打出可执行 jar,内嵌容器会随 jar 一起发布,因此部署非常轻。常用命令两条即可:
# 打包(跳过测试可加 -DskipTests)
mvn clean package
# 运行,可通过参数覆盖配置
java -jar target/demo-0.0.1-SNAPSHOT.jar --server.port=9090
生产环境部署还要考虑几件事:用 systemd 或容器编排守护进程,把日志写成滚动文件,通过环境变量注入敏感配置而不是写进仓库,并用 actuator 暴露健康检查供探针使用。这些都属于「上线前必须补的一课」。
接下来往哪个方向深入,取决于你想解决的问题:接口越来越多就去学接口文档与统一鉴权;数据量增长就去学 JPA 与连接池调优;接口变慢就先学 JVM 与内存模型,理解对象在堆里怎么分配、垃圾回收如何触发。有了 Spring Boot 这条主线,后续能力都可以挂在它上面逐步补齐。
框架的价值在于让你把注意力放在业务上,但框架出问题时的排查能力,依然来自对底层原理的理解。会用是起点,知道为什么能跑起来才是分水岭。