目标 :能独立创建 Spring Boot 项目,写出基础 REST 接口
学习时长 :1~2 周
前置要求:Java 基础、Maven/Gradle 基础、HTTP 协议了解
目录
- [Spring Boot 是什么](#Spring Boot 是什么)
- [Spring、Spring MVC、Spring Boot 的关系](#Spring、Spring MVC、Spring Boot 的关系)
- [第一个 Spring Boot 项目](#第一个 Spring Boot 项目)
- 项目结构说明
- [application.yml 配置文件](#application.yml 配置文件)
- [Controller、Service、Repository 分层](#Controller、Service、Repository 分层)
- [REST API 开发基础](#REST API 开发基础)
- 常用注解入门
- [接口测试工具 Postman](#接口测试工具 Postman)
- [简单 CRUD 实战](#简单 CRUD 实战)
- 面试高频题
1. Spring Boot 是什么
Spring Boot 是由 Pivotal 团队开发的开箱即用的 Spring 应用快速构建框架,于 2014 年发布。
核心设计哲学
| 哲学 | 说明 |
|---|---|
| 约定优于配置 | 提供合理默认值,减少配置量 |
| 开箱即用 | Starter 依赖自动装配,无需手动配置 |
| 独立运行 | 内嵌 Tomcat/Jetty,打成 Jar 直接运行 |
| 生产就绪 | Actuator 提供健康检查、监控、指标等 |
Spring Boot 解决了什么问题
传统 Spring 项目的痛点:
- 繁重的 XML 配置(applicationContext.xml、web.xml)
- 依赖版本冲突
- 部署需要外部 Tomcat
- 环境搭建成本高
Spring Boot 的解决方案:
✅ 注解替代 XML,自动配置替代手动配置
✅ Starter BOM 统一管理依赖版本
✅ 内嵌服务器,jar 包一键启动
✅ Spring Initializr 30 秒生成项目骨架
2. Spring、Spring MVC、Spring Boot 的关系
┌─────────────────────────────────────────┐
│ Spring Boot │ ← 自动配置层(整合者)
│ ┌───────────────────────────────────┐ │
│ │ Spring MVC │ │ ← Web 层框架
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Spring Core │ │ │ ← IoC/DI/AOP 核心
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
| 框架 | 定位 | 核心能力 |
|---|---|---|
| Spring Core | 基础框架 | IoC 容器、DI 依赖注入、AOP 面向切面 |
| Spring MVC | Web 框架 | DispatcherServlet、Controller、视图解析 |
| Spring Boot | 整合框架 | 自动配置、Starter、内嵌服务器、Actuator |
📌 一句话记忆:Spring 是地基,Spring MVC 是楼,Spring Boot 是装修好的精装房。
3. 第一个 Spring Boot 项目
方式一:Spring Initializr(推荐)
Project:Maven
Language:Java
Spring Boot:3.2.x
Java:17
Dependencies:Spring Web、Lombok
方式二:IDEA 内置向导
File → New → Project → Spring Initializr
最简启动类
java
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Hello World Controller
java
@RestController // = @Controller + @ResponseBody
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "World") String name) {
return "Hello, " + name + "!";
}
}
启动验证
bash
mvn spring-boot:run
# 访问:http://localhost:8080/hello?name=SpringBoot
# 返回:Hello, SpringBoot!
4. 项目结构说明
my-project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/demo/
│ │ │ ├── DemoApplication.java ← 启动类(放在最外层包)
│ │ │ ├── controller/ ← 接收请求,参数校验
│ │ │ │ └── UserController.java
│ │ │ ├── service/ ← 业务逻辑
│ │ │ │ ├── UserService.java ← 接口
│ │ │ │ └── impl/
│ │ │ │ └── UserServiceImpl.java ← 实现类
│ │ │ ├── repository/ ← 数据访问
│ │ │ │ └── UserRepository.java
│ │ │ ├── entity/ ← 数据库实体
│ │ │ │ └── User.java
│ │ │ ├── dto/ ← 数据传输对象(入参)
│ │ │ ├── vo/ ← 视图对象(出参)
│ │ │ └── config/ ← 配置类
│ │ └── resources/
│ │ ├── application.yml ← 主配置文件
│ │ ├── application-dev.yml ← 开发环境配置
│ │ ├── application-prod.yml ← 生产环境配置
│ │ ├── static/ ← 静态资源(css/js/img)
│ │ └── templates/ ← 模板文件(Thymeleaf)
│ └── test/
│ └── java/ ← 单元测试
└── pom.xml ← Maven 依赖管理
⚠️ 注意 :启动类必须放在所有包的最外层 ,否则
@ComponentScan扫描不到子包
5. application.yml 配置文件
基础配置
yaml
server:
port: 8080 # 服务端口
servlet:
context-path: /api # 应用根路径
spring:
application:
name: my-service # 应用名(服务注册、链路追踪会用到)
datasource:
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # none/validate/update/create/create-drop
show-sql: true
logging:
level:
com.example: DEBUG # 包级别日志
org.hibernate.SQL: DEBUG # 打印 SQL
Properties vs YAML 对比
properties
# application.properties(传统写法)
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/db
yaml
# application.yml(层级结构,推荐)
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
@Value 读取配置
java
@Value("${spring.application.name}")
private String appName;
@Value("${server.port:8080}") // 有默认值
private int port;
@ConfigurationProperties 批量绑定(推荐)
java
@Data
@Component
@ConfigurationProperties(prefix = "app.oss")
public class OssProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucket;
}
yaml
app:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
access-key: LTAI5t...
secret-key: abc123...
bucket: my-bucket
6. Controller、Service、Repository 分层
三层架构职责
请求 → Controller → Service → Repository → Database
↑ ↑
参数校验 业务逻辑 数据访问
| 层 | 注解 | 职责 | 禁止事项 |
|---|---|---|---|
| Controller | @RestController |
接收请求、参数校验、调用 Service | 不写业务逻辑 |
| Service | @Service |
业务逻辑、事务控制 | 不写 SQL |
| Repository | @Repository |
数据库访问 | 不写业务逻辑 |
标准代码结构
java
// Controller 层:只做参数校验和调用 Service
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public Result<User> create(@Valid @RequestBody CreateUserDTO dto) {
return Result.success(userService.createUser(dto));
}
}
// Service 层:核心业务逻辑
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Transactional
public User createUser(CreateUserDTO dto) {
// 业务校验
if (userRepository.existsByEmail(dto.getEmail())) {
throw new BusinessException("邮箱已存在");
}
// 转换 + 保存
User user = BeanUtils.copyProperties(dto, User.class);
return userRepository.save(user);
}
}
// Repository 层:只做数据访问
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByEmail(String email);
}
7. REST API 开发基础
HTTP 方法与 CRUD 对应关系
| HTTP 方法 | 操作 | 示例 | 状态码 |
|---|---|---|---|
GET |
查询 | GET /users / GET /users/1 |
200 |
POST |
新增 | POST /users |
201 |
PUT |
全量更新 | PUT /users/1 |
200 |
PATCH |
部分更新 | PATCH /users/1 |
200 |
DELETE |
删除 | DELETE /users/1 |
204 |
RESTful URL 设计规范
✅ 正确
GET /api/v1/users 查询用户列表
GET /api/v1/users/1 查询单个用户
POST /api/v1/users 创建用户
PUT /api/v1/users/1 更新用户
DELETE /api/v1/users/1 删除用户
GET /api/v1/users/1/orders 查询用户的订单
❌ 错误(动词放在 URL 中)
GET /api/getUser?id=1
POST /api/createUser
POST /api/deleteUser?id=1
参数接收方式
java
// 1. 路径参数:/users/1
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) { ... }
// 2. 查询参数:/users?page=1&size=10
@GetMapping("/users")
public Page<User> listUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) { ... }
// 3. 请求体(JSON):POST /users Body: {...}
@PostMapping("/users")
public User createUser(@RequestBody @Valid CreateUserDTO dto) { ... }
// 4. 请求头
@GetMapping("/profile")
public User getProfile(@RequestHeader("Authorization") String token) { ... }
8. 常用注解入门
组件注解
| 注解 | 作用 |
|---|---|
@SpringBootApplication |
启动类,组合注解(扫描+配置+自动装配) |
@Component |
通用组件,注册为 Bean |
@Controller |
Web 控制器,返回视图 |
@RestController |
RESTful 控制器,返回 JSON |
@Service |
业务层组件 |
@Repository |
数据访问层组件 |
@Configuration |
配置类,等价于 XML 配置文件 |
注入注解
| 注解 | 作用 |
|---|---|
@Autowired |
按类型注入(Spring 原生) |
@Resource |
按名称注入(JDK 标准) |
@Qualifier |
配合 @Autowired,指定 Bean 名称 |
@Value |
注入配置文件的值 |
请求映射注解
| 注解 | 等效 |
|---|---|
@RequestMapping |
通用映射 |
@GetMapping |
@RequestMapping(method = GET) |
@PostMapping |
@RequestMapping(method = POST) |
@PutMapping |
@RequestMapping(method = PUT) |
@DeleteMapping |
@RequestMapping(method = DELETE) |
9. 接口测试工具 Postman
基本使用
- 下载安装:https://www.postman.com
- 新建请求:
New → HTTP Request - 选择方法(GET/POST/PUT/DELETE)
- 输入 URL
发送 JSON 请求体
Method: POST
URL: http://localhost:8080/api/v1/users
Headers:
Content-Type: application/json
Body (raw JSON):
{
"username": "zhangsan",
"email": "zhangsan@example.com",
"age": 25
}
使用环境变量
Environment: dev
{{base_url}} = http://localhost:8080
请求 URL: {{base_url}}/api/v1/users
常用内置工具
- Swagger UI (推荐):访问
http://localhost:8080/swagger-ui.html在线测试 - IntelliJ HTTP Client :
.http文件,直接在 IDEA 中测试 - curl:命令行测试
bash
# GET 请求
curl http://localhost:8080/api/v1/users
# POST JSON
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"test","email":"test@example.com"}'
10. 简单 CRUD 实战
本章 Demo 对应 chapter01-quickstart 模块,实现了完整的用户 CRUD。
核心接口清单
| 方法 | 路径 | 描述 |
|---|---|---|
| GET | /api/v1/users?page=0&size=10 |
分页查询用户 |
| GET | /api/v1/users/{id} |
查询单个用户 |
| POST | /api/v1/users |
创建用户 |
| PUT | /api/v1/users/{id} |
更新用户信息 |
| DELETE | /api/v1/users/{id} |
删除用户(软删除) |
| GET | /api/v1/users/search?keyword=xx |
搜索用户 |
启动 Demo
bash
cd springboot-demo/chapter01-quickstart
mvn spring-boot:run
# 访问 Swagger:http://localhost:8081/swagger-ui/index.html
# 访问 H2 控制台:http://localhost:8081/h2-console
# JDBC URL: jdbc:h2:mem:chapter01db
11. 面试高频题
Q1:Spring Boot 和 Spring 的区别是什么?
Spring Boot 是对 Spring 的封装,提供自动配置、Starter 依赖管理和内嵌服务器,让开发者专注业务代码,无需写大量配置。
Q2:@SpringBootApplication 包含了哪些注解?
三个:
@SpringBootConfiguration(等价于 @Configuration)、@EnableAutoConfiguration(开启自动装配)、@ComponentScan(包扫描)。
Q3:Spring Boot 支持哪些内嵌服务器?
默认 Tomcat,可以切换为 Jetty 或 Undertow,只需排除 Tomcat 依赖并引入对应 Starter。
Q4:@RestController 和 @Controller 的区别?
@RestController = @Controller + @ResponseBody,前者直接返回 JSON,后者用于返回视图(如 Thymeleaf 模板)。
Q5:Spring Boot 的 application.yml 和 application.properties 有什么区别?
功能相同,YAML 支持层级结构、列表更简洁,可读性更好,推荐使用 YAML。
Q6:如何读取 application.yml 中的配置?
有三种方式:
@Value单个注入、@ConfigurationProperties批量绑定、Environment.getProperty()编程式读取。
Q7:Spring Boot 的启动流程是什么?
SpringApplication.run()→ 加载配置 → 创建 ApplicationContext → 刷新容器(Bean 实例化、自动配置)→ 触发 ApplicationReadyEvent → 启动完成。
Q8:@ComponentScan 默认扫描哪个包?
扫描启动类所在包及其所有子包,这也是为什么启动类要放在最外层包的原因。
Q9:@Autowired 和 @Resource 的区别?
@Autowired是 Spring 注解,默认按类型注入;@Resource是 JDK 标准注解,默认按名称注入。构造器注入推荐用@RequiredArgsConstructor(Lombok)。
Q10:为什么推荐构造器注入而不是字段注入?
构造器注入:依赖不可变(final)、依赖不可为空、便于单元测试(不依赖 Spring 容器)、能发现循环依赖。字段注入隐藏了依赖关系,不利于测试。
下一篇:02_核心篇_SpringBoot常用开发能力.md
12. Spring Boot Actuator 详解(专家必知)
知识点 1:Actuator 是什么,能做什么
Spring Boot Actuator 提供了一套生产就绪的监控和管理端点,让运维和监控系统可以实时观察应用状态,无需改代码。
核心端点一览:
| 端点 | 地址 | 说明 |
|---|---|---|
health |
/actuator/health |
应用健康状态(含DB/Redis/MQ等组件) |
info |
/actuator/info |
应用信息(版本、构建时间等) |
metrics |
/actuator/metrics |
性能指标(JVM内存、GC、HTTP请求数) |
env |
/actuator/env |
当前所有环境变量和配置 |
loggers |
/actuator/loggers |
动态修改日志级别(运行时无需重启) |
threaddump |
/actuator/threaddump |
线程快照(排查死锁/阻塞) |
heapdump |
/actuator/heapdump |
下载堆内存转储文件 |
prometheus |
/actuator/prometheus |
Prometheus格式指标(需引入依赖) |
知识点 2:生产环境安全配置
⚠️ 默认只暴露
health和info,其余端点需显式配置。生产环境切勿全量暴露。
yaml
management:
endpoints:
web:
exposure:
# 生产推荐:只暴露必要端点
include: "health,info,metrics,prometheus,loggers"
# 开发调试时可以全部暴露
# include: "*"
endpoint:
health:
show-details: when-authorized # 认证后才显示详情
show-components: always
loggers:
enabled: true
# Actuator 端口独立(不对外暴露)
server:
port: 9090
安全配置原则:
heapdump和env端点包含敏感信息,生产禁止对外暴露- Actuator 端口(如9090)只对内网/监控系统开放
- 结合 Spring Security 对 Actuator 端点单独设置权限
知识点 3:自定义健康检查
java
/**
* 自定义健康检查:检查第三方依赖的可用性
* 访问 /actuator/health 时会聚合所有 HealthIndicator 的结果
*/
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
private final ExternalServiceClient client;
public ExternalServiceHealthIndicator(ExternalServiceClient client) {
this.client = client;
}
@Override
public Health health() {
try {
boolean available = client.ping();
if (available) {
return Health.up()
.withDetail("service", "external-payment")
.withDetail("status", "reachable")
.build();
}
return Health.down()
.withDetail("service", "external-payment")
.withDetail("reason", "ping timeout")
.build();
} catch (Exception e) {
return Health.down(e)
.withDetail("service", "external-payment")
.build();
}
}
}
健康状态聚合规则:
- 所有组件 UP → 整体 UP(HTTP 200)
- 任一组件 DOWN → 整体 DOWN(HTTP 503)
- Kubernetes 的 readinessProbe 就是调用此接口
知识点 4:自定义 Info 端点
yaml
# application.yml
info:
app:
name: "@project.artifactId@" # 从 Maven pom.xml 读取
version: "@project.version@"
description: "订单服务"
build:
time: "@maven.build.timestamp@"
java
// 动态 Info 贡献者(运行时添加信息)
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("startup-time",
ManagementFactory.getRuntimeMXBean().getStartTime());
builder.withDetail("active-profiles",
Arrays.toString(environment.getActiveProfiles()));
}
}
知识点 5:Prometheus 指标接入(生产标配)
xml
<!-- 引入 Prometheus 端点支持 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
yaml
management:
endpoints:
web:
exposure:
include: "health,prometheus"
metrics:
tags:
application: ${spring.application.name} # 所有指标附带应用名标签
environment: ${spring.profiles.active}
/actuator/prometheus 端点暴露后,Prometheus 可以定时采集,Grafana 订阅展示。
常用监控指标:
| 指标 | 含义 |
|---|---|
jvm_memory_used_bytes |
JVM 堆/非堆内存使用 |
jvm_gc_pause_seconds |
GC停顿时间分布 |
http_server_requests_seconds |
HTTP 接口响应时间分布(含P99) |
hikaricp_connections_active |
连接池活跃连接数 |
process_cpu_usage |
应用 CPU 使用率 |
13. Spring Boot 3.x 新特性与现代化实践
知识点 1:虚拟线程(JDK 21 + Spring Boot 3.2)
什么是虚拟线程 :
JDK 21 引入的轻量级线程,由 JVM 调度(不是 OS 线程)。创建成本极低,可以创建数百万个,用于解决传统线程模型在高并发 I/O 场景下的瓶颈。
传统线程 vs 虚拟线程对比:
| 维度 | 传统平台线程 | 虚拟线程 |
|---|---|---|
| 创建成本 | 重(~1MB 栈内存) | 轻(KB级别) |
| 数量 | 通常数百个 | 可达数百万个 |
| 阻塞行为 | 阻塞OS线程(占资源) | 仅挂起虚拟线程(不占OS线程) |
| 适用场景 | CPU密集型 | I/O密集型(HTTP、数据库、文件) |
Spring Boot 3.2 一行配置开启:
yaml
spring:
threads:
virtual:
enabled: true # 将 Tomcat/调度器切换到虚拟线程
开启后,每个 HTTP 请求都运行在独立的虚拟线程中,数据库调用阻塞时不占用平台线程,吞吐量显著提升。
注意事项:
- 虚拟线程不适合 CPU 密集型任务(如图像处理、加密运算),这类任务仍用平台线程池
ThreadLocal使用无问题,但建议用ScopedValue(JDK 21 引入)替代
知识点 2:GraalVM 原生镜像
Spring Boot 3.x 原生支持编译成 GraalVM Native Image(本地可执行文件):
bash
# 编译为本地可执行文件(无需 JVM)
mvn -Pnative native:compile
# 编译结果:
# target/myapp(Linux 可执行文件,~50MB)
# 启动时间:< 100ms(对比 JVM 的 2-5秒)
# 内存占用:减少 50-80%
适用场景:Serverless / FaaS(函数计算)、容器化部署追求极速启动。
限制:不支持反射(需额外配置)、动态代理受限,Spring Boot 已内置大量 AOT 提示。
知识点 3:@HttpExchange 声明式 HTTP 客户端
Spring Boot 3.x 内置声明式 HTTP 客户端(类似 Feign,无需额外依赖):
java
// 声明接口
@HttpExchange("https://api.github.com")
public interface GitHubClient {
@GetExchange("/users/{username}")
GitHubUser getUser(@PathVariable String username);
@PostExchange("/repos/{owner}/{repo}/issues")
Issue createIssue(
@PathVariable String owner,
@PathVariable String repo,
@RequestBody CreateIssueRequest request
);
}
// 注册为 Bean
@Configuration
public class HttpClientConfig {
@Bean
public GitHubClient gitHubClient() {
WebClient webClient = WebClient.builder()
.defaultHeader("Authorization", "Bearer " + token)
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient))
.build();
return factory.createClient(GitHubClient.class);
}
}
// 直接注入使用
@Service
@RequiredArgsConstructor
public class GitHubService {
private final GitHubClient gitHubClient;
public GitHubUser getUser(String username) {
return gitHubClient.getUser(username);
}
}
14. Spring Boot 启动流程深度解析
知识点 1:SpringApplication.run 做了什么
text
SpringApplication.run(MyApp.class, args)
│
├── 1. 创建 SpringApplication 对象
│ 推断应用类型(SERVLET / REACTIVE / NONE)
│ 加载 ApplicationContextInitializer(从 spring.factories)
│ 加载 ApplicationListener(从 spring.factories)
│ 推断 mainApplicationClass(打印启动日志用)
│
├── 2. run() 方法执行
│ ① 创建 SpringApplicationRunListeners(启动事件广播)
│ ② 发布 ApplicationStartingEvent
│ ③ 准备环境(ConfigurableEnvironment)
│ 加载 application.yml / application.properties
│ 解析命令行参数、环境变量
│ 发布 ApplicationEnvironmentPreparedEvent
│ ④ 创建 ApplicationContext(AnnotationConfigServletWebServerApplicationContext)
│ ⑤ prepareContext()
│ 执行 ApplicationContextInitializer
│ 注册 mainApplicationClass 为 BeanDefinition
│ ⑥ refreshContext() ← 核心!完整 Spring IoC 容器初始化
│ 扫描 @ComponentScan 下的所有类
│ 执行自动配置(AutoConfiguration)
│ 启动内嵌 Tomcat(onRefresh 阶段)
│ ⑦ afterRefresh()
│ 执行 ApplicationRunner / CommandLineRunner
│ ⑧ 发布 ApplicationStartedEvent
│ ⑨ 发布 ApplicationReadyEvent(应用就绪,可接收流量)
│
└── 返回 ConfigurableApplicationContext
知识点 2:如何在启动完成后执行初始化逻辑
有三种方式,执行顺序和使用场景不同:
java
// 方式1:@PostConstruct(Bean 级别,最早执行)
// 场景:单个 Bean 初始化自己的状态(如加载本地缓存)
@Component
@Slf4j
public class CacheInitializer {
private final ProductRepository productRepository;
private final Map<Long, Product> localCache = new ConcurrentHashMap<>();
@PostConstruct // 属性注入完成后立即执行
public void init() {
log.info("开始预热本地缓存...");
productRepository.findAll().forEach(p -> localCache.put(p.getId(), p));
log.info("本地缓存预热完成,共 {} 条", localCache.size());
}
}
// 方式2:ApplicationRunner(应用就绪后执行)
// 场景:需要整个 Spring 容器就绪后才能执行的初始化
@Component
@Order(1) // 多个 Runner 时控制顺序
@Slf4j
public class DatabaseHealthRunner implements ApplicationRunner {
private final DataSource dataSource;
@Override
public void run(ApplicationArguments args) throws Exception {
// 应用完全启动后再检查
try (Connection conn = dataSource.getConnection()) {
log.info("数据库连接正常:{}", conn.getMetaData().getDatabaseProductName());
}
// 可以访问命令行参数
if (args.containsOption("init-data")) {
log.info("检测到 --init-data 参数,执行数据初始化...");
}
}
}
// 方式3:CommandLineRunner(更简单的命令行参数访问)
@Component
@Order(2)
public class ReportRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
// args 是原始命令行参数数组
for (String arg : args) {
System.out.println("启动参数: " + arg);
}
}
}
执行顺序 :@PostConstruct → ApplicationRunner/CommandLineRunner(按 @Order 排序)
15. 配置文件体系详解
知识点 1:YAML 高级语法
yaml
# 基础类型
name: 张三
age: 25
salary: 10000.50
active: true
# 字符串注意事项
simple: hello world # 不需要引号
with-colon: "包含:冒号" # 包含特殊字符需要引号
multiline: | # | 保留换行符
第一行
第二行
folded: > # > 折叠换行为空格
这是一段很长的文本
会被折叠成一行
# List 写法
hobbies:
- 阅读
- 编程
- 游泳
# 也可以写成:hobbies: [阅读, 编程, 游泳]
# Map 写法
address:
province: 广东
city: 深圳
district: 南山区
# 嵌套
servers:
- host: 192.168.1.1
port: 8080
name: 主服务器
- host: 192.168.1.2
port: 8081
name: 备用服务器
# 多文档块(同一文件多个配置,用 --- 分隔)
---
spring:
profiles: dev
server:
port: 8080
---
spring:
profiles: prod
server:
port: 80
知识点 2:配置绑定的三种方式对比
java
// 方式1:@Value(简单值,支持 SpEL 表达式)
@RestController
public class DemoController {
@Value("${server.port}")
private int port;
@Value("${app.name:默认应用名}") // 有默认值
private String appName;
@Value("#{${app.servers}}") // SpEL 解析 Map
private Map<String, String> servers;
@Value("${app.tags:}") // 列表(逗号分隔)
private List<String> tags;
}
// 方式2:@ConfigurationProperties(推荐,强类型,支持校验)
@Data
@Component
@ConfigurationProperties(prefix = "app")
@Validated // 开启 JSR-303 校验
public class AppConfig {
@NotEmpty
private String name;
@Min(1) @Max(65535)
private int port = 8080;
@NotNull
private DatabaseConfig database = new DatabaseConfig();
private List<String> admins = new ArrayList<>();
@Data
public static class DatabaseConfig {
@NotBlank
private String url;
private int maxPoolSize = 10;
private Duration timeout = Duration.ofSeconds(30);
}
}
// 方式3:Environment(编程式获取,灵活但不推荐常用)
@Service
public class ConfigService {
@Autowired
private Environment env;
public void printConfig() {
String dbUrl = env.getProperty("spring.datasource.url");
int port = env.getProperty("server.port", Integer.class, 8080);
String[] profiles = env.getActiveProfiles();
}
}
application.yml 配置示例:
yaml
app:
name: 订单服务
port: 9001
admins:
- admin@example.com
- dev@example.com
database:
url: jdbc:mysql://localhost:3306/orders
max-pool-size: 20
timeout: 30s
知识点 3:配置优先级
text
优先级从高到低(高优先级覆盖低优先级):
1. 命令行参数 java -jar app.jar --server.port=9090
2. 操作系统环境变量 SERVER_PORT=9090
3. JVM 系统属性 -Dserver.port=9090
4. application-{profile}.yml(激活的环境配置)
5. application.yml
6. @PropertySource 注解引入的配置文件
7. @SpringBootApplication 类所在包的默认配置
16. RESTful API 设计规范与最佳实践
知识点 1:REST 设计原则
text
REST(Representational State Transfer)六大约束:
1. 资源标识(URI):
✅ GET /api/v1/users/123 ← 名词,复数
❌ GET /api/getUserById?id=123 ← 动词,不符合 REST
2. HTTP 方法语义:
GET → 查询(安全且幂等)
POST → 创建(非幂等)
PUT → 全量更新(幂等)
PATCH → 部分更新(幂等)
DELETE → 删除(幂等)
3. 状态码语义:
200 OK → 成功
201 Created → 创建成功(POST 后返回)
204 No Content → 成功但无响应体(DELETE 后返回)
400 Bad Request → 参数错误
401 Unauthorized → 未认证
403 Forbidden → 已认证但无权限
404 Not Found → 资源不存在
409 Conflict → 资源冲突(如用户名已存在)
422 Unprocessable → 语义错误(校验失败)
500 Internal Server Error → 服务端错误
4. API 版本管理:
推荐方案:URI 路径版本 /api/v1/users /api/v2/users
次选方案:请求头版本 Accept: application/vnd.myapp.v2+json
知识点 2:统一返回结构
java
// 统一响应体
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code; // 业务状态码(200=成功,非200=失败)
private String message; // 描述信息
private T data; // 业务数据
private long timestamp; // 时间戳(便于排查问题)
// 静态工厂方法
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data, System.currentTimeMillis());
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> fail(int code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
public static <T> Result<T> fail(ResultCode resultCode) {
return fail(resultCode.getCode(), resultCode.getMessage());
}
}
// 业务状态码枚举
public enum ResultCode {
SUCCESS(200, "操作成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "请先登录"),
FORBIDDEN(403, "权限不足"),
NOT_FOUND(404, "资源不存在"),
CONFLICT(409, "数据冲突"),
INTERNAL_ERROR(500, "服务器内部错误"),
// 业务码(从1000开始,避免与HTTP状态码冲突)
USER_NOT_FOUND(1001, "用户不存在"),
USER_DISABLED(1002, "账号已被禁用"),
PASSWORD_WRONG(1003, "密码错误"),
STOCK_NOT_ENOUGH(2001, "库存不足"),
ORDER_ALREADY_PAID(2002, "订单已支付");
private final int code;
private final String message;
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
}
// 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 参数校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Map<String, String>> handleValidationException(
MethodArgumentNotValidException e) {
Map<String, String> errors = new LinkedHashMap<>();
e.getBindingResult().getFieldErrors().forEach(err ->
errors.put(err.getField(), err.getDefaultMessage()));
log.warn("参数校验失败: {}", errors);
return Result.fail(400, "参数校验失败: " + errors);
}
// 业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
// 兜底异常
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
log.error("未知异常 [{}] {}", request.getMethod(), request.getRequestURI(), e);
return Result.fail(500, "服务器内部错误,请稍后重试");
}
}
// 业务异常基类
@Getter
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.code = resultCode.getCode();
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
}
// Controller 使用示例
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public Result<UserDTO> getUser(@PathVariable Long id) {
return Result.success(userService.findById(id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Result<UserDTO> createUser(@RequestBody @Valid CreateUserRequest request) {
return Result.success(userService.create(request));
}
@PutMapping("/{id}")
public Result<UserDTO> updateUser(@PathVariable Long id,
@RequestBody @Valid UpdateUserRequest request) {
return Result.success(userService.update(id, request));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
@GetMapping
public Result<Page<UserDTO>> listUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") @Max(100) int size,
@RequestParam(required = false) String keyword) {
return Result.success(userService.list(page, size, keyword));
}
}
17. 单元测试与集成测试
知识点 1:Spring Boot Test 分层测试
text
测试分层策略:
单元测试(速度最快):
Mockito 模拟依赖,只测 Service 层业务逻辑
不启动 Spring 容器
切片测试(中等速度):
@WebMvcTest → 只启动 MVC 层(Controller、Filter)
@DataJpaTest → 只启动 JPA 层(Repository,内存数据库)
@JsonTest → 只测试 JSON 序列化
集成测试(最慢):
@SpringBootTest → 启动完整 Spring 容器
配合 Testcontainers 使用真实数据库
知识点 2:Service 层单元测试(Mockito)
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks // 将 @Mock 注入到此对象
private UserService userService;
@Test
@DisplayName("创建用户:正常流程")
void createUser_success() {
// Given(准备数据)
CreateUserRequest request = new CreateUserRequest("test@email.com", "password123");
User savedUser = new User(1L, "test@email.com", "encodedPwd");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(passwordEncoder.encode(request.getPassword())).thenReturn("encodedPwd");
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When(执行)
UserDTO result = userService.create(request);
// Then(断言)
assertNotNull(result);
assertEquals("test@email.com", result.getEmail());
verify(userRepository).save(argThat(user ->
user.getEmail().equals("test@email.com") &&
user.getPassword().equals("encodedPwd")));
}
@Test
@DisplayName("创建用户:邮箱已存在抛出异常")
void createUser_emailExists_throwsException() {
CreateUserRequest request = new CreateUserRequest("exists@email.com", "password");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
assertThrows(BusinessException.class, () -> userService.create(request));
verify(userRepository, never()).save(any()); // 确保没有调用 save
}
}
知识点 3:Controller 层切片测试(MockMvc)
java
@WebMvcTest(UserController.class) // 只启动 MVC 层,不启动完整容器
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // 在 Spring 容器中注册 Mock Bean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("GET /api/v1/users/{id} - 用户存在返回200")
void getUser_exists_returns200() throws Exception {
UserDTO user = new UserDTO(1L, "test@email.com");
when(userService.findById(1L)).thenReturn(user);
mockMvc.perform(get("/api/v1/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.email").value("test@email.com"))
.andDo(print()); // 打印请求/响应(调试用)
}
@Test
@DisplayName("POST /api/v1/users - 参数校验失败返回400")
void createUser_invalidRequest_returns400() throws Exception {
CreateUserRequest request = new CreateUserRequest("invalid-email", ""); // 无效邮箱,空密码
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400));
}
}
知识点 4:集成测试(Testcontainers)
java
// 使用真实 MySQL 容器进行集成测试
@SpringBootTest
@Testcontainers
@Transactional // 每个测试后回滚
class UserRepositoryIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test_db")
.withUsername("test")
.withPassword("test");
// 动态注入容器的连接信息
@DynamicPropertySource
static void configureDataSource(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_exists_returnsUser() {
User user = new User("test@email.com", "password");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("test@email.com");
assertTrue(found.isPresent());
assertEquals("test@email.com", found.get().getEmail());
}
}