第一篇:入门篇 — 认识 Spring Boot 与基础开发

目标:能独立创建 Spring Boot 项目,写出基础 REST 接口
学习时长:1~2 周
前置要求:Java 基础、Maven/Gradle 基础、HTTP 协议了解


目录

  1. Spring Boot 是什么
  2. Spring、Spring MVC、Spring Boot 的关系
  3. 第一个 Spring Boot 项目
  4. 项目结构说明
  5. application.yml 配置文件
  6. Controller、Service、Repository 分层
  7. REST API 开发基础
  8. 常用注解入门
  9. 接口测试工具 Postman
  10. 简单 CRUD 实战
  11. 面试高频题

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 MVCWeb 框架DispatcherServlet、Controller、视图解析
Spring Boot整合框架自动配置、Starter、内嵌服务器、Actuator

📌 一句话记忆:Spring 是地基,Spring MVC 是楼,Spring Boot 是装修好的精装房。


3. 第一个 Spring Boot 项目

方式一:Spring Initializr(推荐)

访问 https://start.spring.io

Project:Maven
Language:Java
Spring Boot:3.2.x
Java:17
Dependencies:Spring Web、Lombok

方式二:IDEA 内置向导

File → New → Project → Spring Initializr

最简启动类

@SpringBootApplication  // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Hello World Controller

@RestController  // = @Controller + @ResponseBody
public class HelloController {

    @GetMapping("/hello")
    public String hello(@RequestParam(defaultValue = "World") String name) {
        return "Hello, " + name + "!";
    }
}

启动验证

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 配置文件

基础配置

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 对比

# application.properties(传统写法)
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/db
# application.yml(层级结构,推荐)
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db

@Value 读取配置

@Value("${spring.application.name}")
private String appName;

@Value("${server.port:8080}")  // 有默认值
private int port;

@ConfigurationProperties 批量绑定(推荐)

@Data
@Component
@ConfigurationProperties(prefix = "app.oss")
public class OssProperties {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucket;
}
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数据库访问不写业务逻辑

标准代码结构

// 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/1200
POST新增POST /users201
PUT全量更新PUT /users/1200
PATCH部分更新PATCH /users/1200
DELETE删除DELETE /users/1204

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

参数接收方式

// 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
@ControllerWeb 控制器,返回视图
@RestControllerRESTful 控制器,返回 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

基本使用

  1. 下载安装:https://www.postman.com
  2. 新建请求:New → HTTP Request
  3. 选择方法(GET/POST/PUT/DELETE)
  4. 输入 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:命令行测试
# 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

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/prometheusPrometheus格式指标(需引入依赖)

知识点 2:生产环境安全配置

⚠️ 默认只暴露 healthinfo,其余端点需显式配置。生产环境切勿全量暴露。

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

安全配置原则

  • heapdumpenv 端点包含敏感信息,生产禁止对外暴露
  • Actuator 端口(如9090)只对内网/监控系统开放
  • 结合 Spring Security 对 Actuator 端点单独设置权限

知识点 3:自定义健康检查

/**
 * 自定义健康检查:检查第三方依赖的可用性
 * 访问 /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 端点

# application.yml
info:
  app:
    name: "@project.artifactId@"    # 从 Maven pom.xml 读取
    version: "@project.version@"
    description: "订单服务"
  build:
    time: "@maven.build.timestamp@"
// 动态 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 指标接入(生产标配)

<!-- 引入 Prometheus 端点支持 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: "health,prometheus"
  metrics:
    tags:
      application: ${spring.application.name}  # 所有指标附带应用名标签
      environment: ${spring.profiles.active}

/actuator/prometheus 端点暴露后,Prometheus 可以定时采集,Grafana 订阅展示。

常用监控指标

指标含义
jvm_memory_used_bytesJVM 堆/非堆内存使用
jvm_gc_pause_secondsGC停顿时间分布
http_server_requests_secondsHTTP 接口响应时间分布(含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 一行配置开启

spring:
  threads:
    virtual:
      enabled: true  # 将 Tomcat/调度器切换到虚拟线程

开启后,每个 HTTP 请求都运行在独立的虚拟线程中,数据库调用阻塞时不占用平台线程,吞吐量显著提升。

注意事项

  • 虚拟线程不适合 CPU 密集型任务(如图像处理、加密运算),这类任务仍用平台线程池
  • ThreadLocal 使用无问题,但建议用 ScopedValue(JDK 21 引入)替代

知识点 2:GraalVM 原生镜像

Spring Boot 3.x 原生支持编译成 GraalVM Native Image(本地可执行文件):

# 编译为本地可执行文件(无需 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,无需额外依赖):

// 声明接口
@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 做了什么

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:如何在启动完成后执行初始化逻辑

有三种方式,执行顺序和使用场景不同:

// 方式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);
        }
    }
}

执行顺序@PostConstructApplicationRunner/CommandLineRunner(按 @Order 排序)


15. 配置文件体系详解

知识点 1: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:配置绑定的三种方式对比

// 方式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 配置示例

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:配置优先级

优先级从高到低(高优先级覆盖低优先级):

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 设计原则

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:统一返回结构

// 统一响应体
@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 分层测试

测试分层策略:

单元测试(速度最快):
  Mockito 模拟依赖,只测 Service 层业务逻辑
  不启动 Spring 容器

切片测试(中等速度):
  @WebMvcTest    → 只启动 MVC 层(Controller、Filter)
  @DataJpaTest   → 只启动 JPA 层(Repository,内存数据库)
  @JsonTest      → 只测试 JSON 序列化

集成测试(最慢):
  @SpringBootTest → 启动完整 Spring 容器
  配合 Testcontainers 使用真实数据库

知识点 2:Service 层单元测试(Mockito)

@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)

@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)

// 使用真实 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());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值