WSの小屋

Spring Boot 全家桶教程

从入门到精通,覆盖 Spring Boot 核心生态,不含微服务相关内容


第一章:Spring Boot 概述

1.1 什么是 Spring Boot

Spring Boot 是 Spring 框架的一个子项目,旨在简化 Spring 应用的初始搭建和开发过程。它通过自动配置起步依赖两大核心机制,让开发者可以"开箱即用"地创建生产级别的 Spring 应用。

核心目标

  • 为 Spring 开发提供更快的入门体验
  • 开箱即用,但通过配置可快速调整
  • 提供生产级特性(监控、健康检查、外部化配置)
  • 无需代码生成和 XML 配置

与 Spring 框架的关系

Spring Framework(基础)
    ├── Spring Boot(加速器)
    │   ├── Spring Boot Web(Web 开发)
    │   ├── Spring Boot Data(数据访问)
    │   ├── Spring Boot Security(安全)
    │   ├── Spring Boot Actuator(监控)
    │   └── ...
    └── Spring Cloud(微服务,本文不涉及)

1.2 Spring Boot 的核心特性

特性 说明
起步依赖 将常用依赖分组,一键引入
自动配置 根据类路径内容自动配置 Bean
嵌入式容器 内嵌 Tomcat、Jetty、Undertow
外部化配置 支持 application.properties/yml
Actuator 提供监控和管理端点
无代码生成 无需 XML,无需代码生成

第二章:环境搭建与快速入门

2.1 环境要求

工具 版本要求
JDK 17+
Maven 3.6+
IDE IntelliJ IDEA / VS Code / Eclipse

2.2 创建项目

方式一:Spring Initializr(推荐)

访问 https://start.spring.io 或在 IDE 中直接创建:

  • Project:Maven Project
  • Language:Java
  • Spring Boot:选择最新稳定版(如 3.x)
  • Groupcom.example
  • Artifactdemo
  • Dependencies:按需选择(如 Spring Web、Spring Data JPA 等)

方式二:Maven 手动创建

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 继承 Spring Boot 父工程 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Web 起步依赖 -->
        <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>

    <build>
        <plugins>
            <!-- Maven 打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.3 主启动类

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication  // 组合注解:@Configuration + @EnableAutoConfiguration + @ComponentScan
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication 详解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration      // 标记为配置类
@EnableAutoConfiguration      // 开启自动配置
@ComponentScan               // 扫描当前包及其子包下的 Bean
public @interface SpringBootApplication {
    // ...
}

2.4 第一个 REST 接口

@RestController
@RequestMapping("/api")
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello Spring Boot!";
    }

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        return new User(id, "张三", "zhangsan@example.com");
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
    private Long id;
    private String name;
    private String email;
}

访问 http://localhost:8080/api/hello 即可看到响应。

2.5 项目目录结构

src/main/java
├── com/example/demo/
│   ├── DemoApplication.java          # 启动类
│   ├── config/                        # 配置类
│   ├── controller/                    # 控制器
│   ├── service/                       # 服务接口
│   │   └── impl/                      # 服务实现
│   ├── repository/                    # 数据访问层(JPA)
│   ├── mapper/                        # 数据访问层(MyBatis)
│   ├── entity/                        # 实体类
│   ├── dto/                           # 数据传输对象
│   ├── vo/                            # 视图对象
│   └── common/                        # 公共类(异常、工具等)
src/main/resources
├── application.yml                    # 主配置文件
├── application-dev.yml                # 开发环境配置
├── application-prod.yml               # 生产环境配置
└── static/                            # 静态资源
src/test/java
└── com/example/demo/
    └── DemoApplicationTests.java      # 测试类

第三章:自动配置原理(核心)

3.1 @EnableAutoConfiguration 工作原理

自动配置是 Spring Boot 最核心的机制,理解其原理至关重要。

自动配置的加载流程

@SpringBootApplication
    └── @EnableAutoConfiguration
            └── @Import(AutoConfigurationImportSelector.class)
                    └── selectImports()
                            └── 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
                                    └── 加载所有 AutoConfiguration 类
                                            └── 根据 @Conditional 条件判断是否生效

关键文件

Spring Boot 3.x 中,自动配置类的声明位于:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容示例(部分):

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
...

每个 *AutoConfiguration 类上都有一组 @Conditional 注解,只有满足条件时才会生效。

3.2 条件注解家族

Spring Boot 提供了丰富的条件注解,用于控制自动配置是否生效:

注解 作用
@ConditionalOnClass 类路径中存在指定类时生效
@ConditionalOnMissingClass 类路径中不存在指定类时生效
@ConditionalOnBean 容器中存在指定 Bean 时生效
@ConditionalOnMissingBean 容器中不存在指定 Bean 时生效
@ConditionalOnProperty 配置文件中存在指定属性时生效
@ConditionalOnResource 类路径中存在指定资源时生效
@ConditionalOnWebApplication 当前是 Web 应用时生效
@ConditionalOnNotWebApplication 当前不是 Web 应用时生效
@ConditionalOnExpression SpEL 表达式为 true 时生效
@ConditionalOnJava JDK 版本满足条件时生效
@ConditionalOnJndi JNDI 资源存在时生效
@ConditionalOnSingleCandidate 容器中指定 Bean 只有一个候选时生效

示例:自定义自动配置

// 1. 创建自动配置类
@Configuration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(prefix = "my.service", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService() {
        return new MyService();
    }
}

// 2. 在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册
// 文件内容:
// com.example.demo.config.MyServiceAutoConfiguration

3.3 Spring Boot 配置加载顺序

Spring Boot 的配置加载遵循严格优先级(高到低):

  1. 命令行参数--server.port=8081
  2. JNDI 属性
  3. 系统属性:System.getProperties()
  4. 操作系统环境变量
  5. application-{profile}.yml(带 profile 的配置文件)
  6. application.yml(主配置文件)
  7. @PropertySource 注解的配置

后加载的配置会覆盖先加载的配置,但优先级高的配置会覆盖优先级低的配置。


第四章:配置文件详解

4.1 application.properties vs application.yml

properties 格式

# application.properties
server.port=8081
server.servlet.context-path=/api

spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

yml 格式(推荐)

# application.yml
server:
  port: 8081
  servlet:
    context-path: /api

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

YAML 优点:层次清晰,减少重复前缀,支持多文档块。

4.2 多环境配置

方式一:多文件方式

application.yml           # 公共配置
application-dev.yml       # 开发环境
application-test.yml      # 测试环境
application-prod.yml      # 生产环境

方式二:单文件多文档块(YAML)

# 公共配置
server:
  port: 8080

spring:
  profiles:
    active: dev

---
# 开发环境
spring:
  config:
    activate:
      on-profile: dev

server:
  port: 8081

---
# 生产环境
spring:
  config:
    activate:
      on-profile: prod

server:
  port: 8080

激活方式

# 命令行激活
java -jar demo.jar --spring.profiles.active=prod

# 环境变量激活
export SPRING_PROFILES_ACTIVE=prod

# application.yml 中指定
spring:
  profiles:
    active: dev

4.3 自定义配置绑定

方式一:@ConfigurationProperties

@Component
@ConfigurationProperties(prefix = "app.config")
@Data
public class AppConfig {
    private String name;
    private String version;
    private List<String> authors;
    private Map<String, String> contacts;
    private Database database = new Database();

    @Data
    public static class Database {
        private String url;
        private String username;
        private String password;
    }
}

配置文件:

app:
  config:
    name: MyApp
    version: 1.0.0
    authors:
      - 张三
      - 李四
    contacts:
      email: admin@example.com
      phone: "13800138000"
    database:
      url: jdbc:mysql://localhost:3306/demo
      username: root
      password: 123456

方式二:@Value

@Component
public class MyComponent {

    @Value("${app.config.name}")
    private String appName;

    @Value("${app.config.version:1.0.0}")  // 带默认值
    private String version;

    @Value("#{2 * 10}")                    // SpEL 表达式
    private int computedValue;
}

@ConfigurationProperties vs @Value

特性 @ConfigurationProperties @Value
批量绑定 ✅ 支持(前缀匹配) ❌ 不支持(逐个绑定)
类型安全 ✅ 自动类型转换 ⚠️ 需要手动处理
JSR-303 校验 ✅ 支持 @Validated ❌ 不支持
复杂类型 ✅ 支持 List、Map、嵌套 ❌ 仅支持基本类型
SpEL ❌ 不支持 ✅ 支持
推荐度 推荐 简单场景可用

4.4 随机值与占位符

app:
  secret: ${random.value}          # 随机字符串
  number: ${random.int}            # 随机整数
  range: ${random.int(100,200)}    # 指定范围随机数
  uuid: ${random.uuid}             # 随机 UUID
# 占位符引用
app:
  name: MyApp
  description: "This is ${app.name}"  # 引用同文件中的属性
  home: ${user.home}                  # 引用系统属性

第五章:Spring Boot Web 开发

5.1 常用注解

请求映射

注解 说明
@RestController @Controller + @ResponseBody
@RequestMapping 通用请求映射
@GetMapping GET 请求
@PostMapping POST 请求
@PutMapping PUT 请求
@DeleteMapping DELETE 请求
@PatchMapping PATCH 请求

参数绑定

注解 说明
@PathVariable 获取路径参数
@RequestParam 获取查询参数
@RequestBody 获取请求体
@RequestHeader 获取请求头
@CookieValue 获取 Cookie
@ModelAttribute 获取模型属性
@SessionAttribute 获取 Session 属性

示例:完整的 CRUD 控制器

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor  // 使用 Lombok 生成构造器注入
public class UserController {

    private final UserService userService;

    @GetMapping
    public Result<List<User>> list(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return Result.success(userService.findAll(page, size));
    }

    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        return Result.success(userService.findById(id));
    }

    @PostMapping
    public Result<User> create(@Valid @RequestBody UserCreateDTO dto) {
        return Result.success(userService.create(dto));
    }

    @PutMapping("/{id}")
    public Result<User> update(
            @PathVariable Long id,
            @Valid @RequestBody UserUpdateDTO dto) {
        return Result.success(userService.update(id, dto));
    }

    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.success();
    }
}

5.2 统一响应体

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp = System.currentTimeMillis();

    public static <T> Result<T> success() {
        return new Result<>(200, "success", null, System.currentTimeMillis());
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data, System.currentTimeMillis());
    }

    public static <T> Result<T> error(int code, String message) {
        return new Result<>(code, message, null, System.currentTimeMillis());
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null, System.currentTimeMillis());
    }
}

5.3 统一异常处理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidation(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }

    // 参数绑定异常(如类型转换失败)
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<Void> handleConstraintViolation(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream()
                .map(v -> v.getPropertyPath() + ": " + v.getMessage())
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }

    // 校验异常
    @ExceptionHandler(BindException.class)
    public Result<Void> handleBind(BindException e) {
        String message = e.getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }

    // 自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusiness(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // 资源未找到
    @ExceptionHandler(NoSuchElementException.class)
    public Result<Void> handleNotFound(NoSuchElementException e) {
        return Result.error(404, "资源不存在");
    }

    // 未授权
    @ExceptionHandler(AccessDeniedException.class)
    public Result<Void> handleAccessDenied(AccessDeniedException e) {
        return Result.error(403, "权限不足");
    }

    // 全局兜底异常
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error(500, "服务器内部错误,请联系管理员");
    }
}

自定义业务异常

@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
    private int code;

    public BusinessException(String message) {
        super(message);
        this.code = 500;
    }

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
}

5.4 参数校验

实体类校验

@Data
public class UserCreateDTO {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度需在2-20之间")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 40, message = "密码长度需在6-40之间")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;

    @NotNull(message = "性别不能为空")
    private Integer gender;

    @Future(message = "过期时间必须是未来时间")
    private LocalDateTime expireTime;
}

分组校验

// 定义分组接口
public interface CreateGroup {}
public interface UpdateGroup {}

// 使用分组
@Data
public class UserDTO {
    @Null(groups = CreateGroup.class, message = "新增时ID必须为空")
    @NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
    private Long id;

    @NotBlank(message = "用户名不能为空")
    private String username;
}

// 控制器中指定分组
@PostMapping
public Result<User> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) {
    return Result.success(userService.create(dto));
}

@PutMapping("/{id}")
public Result<User> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) {
    return Result.success(userService.update(dto));
}

5.5 跨域配置(CORS)

全局配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

局部配置(@CrossOrigin

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
    // ...
}

5.6 拦截器

// 1. 定义拦截器
@Component
@Slf4j
public class RequestLogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        log.info("请求路径: {}, 方法: {}, IP: {}",
                request.getRequestURI(),
                request.getMethod(),
                request.getRemoteAddr());
        return true;  // true: 继续执行,false: 中断请求
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) {
        // 控制器执行后,视图渲染前
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) {
        // 请求完成后(视图渲染后)
    }
}

// 2. 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RequestLogInterceptor requestLogInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestLogInterceptor)
                .addPathPatterns("/**")           // 拦截所有请求
                .excludePathPatterns("/login", "/register", "/static/**");  // 排除路径
    }
}

5.7 静态资源处理

Spring Boot 默认静态资源映射:

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

访问 http://localhost:8080/index.html 会自动查找以上目录。

自定义静态资源配置

spring:
  web:
    resources:
      static-locations: classpath:/static/, file:${user.dir}/uploads/
      add-mappings: true
  mvc:
    static-path-pattern: /**

第六章:配置文件管理进阶

6.1 外部化配置

通过命令行参数

java -jar app.jar \
    --server.port=8081 \
    --spring.datasource.url=jdbc:mysql://localhost:3306/demo

通过环境变量

Spring Boot 会自动将环境变量转换为配置属性,规则是:

  • 点号 . 转换为下划线 _
  • 转换为大写
# Linux/Mac
export SERVER_PORT=8081
export SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/demo
java -jar app.jar

# Windows PowerShell
$env:SERVER_PORT=8081
$env:SPRING_DATASOURCE_URL="jdbc:mysql://localhost:3306/demo"
java -jar app.jar

6.2 @ConfigurationProperties 校验

@Component
@ConfigurationProperties(prefix = "app.database")
@Validated
@Data
public class DatabaseProperties {

    @NotBlank(message = "数据库URL不能为空")
    private String url;

    @NotBlank(message = "数据库用户名不能为空")
    private String username;

    @NotBlank(message = "数据库密码不能为空")
    private String password;

    @Min(value = 1, message = "最大连接数不能小于1")
    @Max(value = 100, message = "最大连接数不能大于100")
    private int maxActive = 10;

    @Min(value = 1000, message = "超时时间不能小于1000ms")
    private long timeout = 30000;
}

6.3 配置元数据

META-INF/configuration-metadata.json 中添加元数据,IDE 即可获得自动提示:

{
  "groups": [
    {
      "name": "app.database",
      "type": "com.example.demo.config.DatabaseProperties",
      "sourceType": "com.example.demo.config.DatabaseProperties"
    }
  ],
  "properties": [
    {
      "name": "app.database.url",
      "type": "java.lang.String",
      "description": "数据库连接URL",
      "sourceType": "com.example.demo.config.DatabaseProperties"
    },
    {
      "name": "app.database.max-active",
      "type": "java.lang.Integer",
      "description": "最大连接数",
      "defaultValue": 10,
      "sourceType": "com.example.demo.config.DatabaseProperties"
    }
  ]
}

第七章:数据访问层

7.1 使用 JDBC

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:  # HikariCP 连接池配置(Spring Boot 默认)
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

使用 JdbcTemplate

@Repository
@RequiredArgsConstructor
public class UserDao {

    private final JdbcTemplate jdbcTemplate;

    public List<User> findAll() {
        return jdbcTemplate.query(
                "SELECT * FROM user",
                new BeanPropertyRowMapper<>(User.class)
        );
    }

    public User findById(Long id) {
        return jdbcTemplate.queryForObject(
                "SELECT * FROM user WHERE id = ?",
                new BeanPropertyRowMapper<>(User.class),
                id
        );
    }

    public int save(User user) {
        return jdbcTemplate.update(
                "INSERT INTO user(username, password, email) VALUES(?, ?, ?)",
                user.getUsername(), user.getPassword(), user.getEmail()
        );
    }

    public int update(User user) {
        return jdbcTemplate.update(
                "UPDATE user SET username = ?, email = ? WHERE id = ?",
                user.getUsername(), user.getEmail(), user.getId()
        );
    }

    public int delete(Long id) {
        return jdbcTemplate.update("DELETE FROM user WHERE id = ?", id);
    }
}

7.2 使用 Spring Data JPA

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

配置

spring:
  jpa:
    hibernate:
      ddl-auto: update  # create | create-drop | update | validate | none
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQLDialect

实体类

@Entity
@Table(name = "user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 50)
    private String username;

    @Column(nullable = false, length = 100)
    private String password;

    @Column(length = 100)
    private String email;

    @Column(length = 20)
    private String phone;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private UserStatus status;

    @CreationTimestamp  // 自动填充创建时间
    private LocalDateTime createdAt;

    @UpdateTimestamp   // 自动填充更新时间
    private LocalDateTime updatedAt;
}

public enum UserStatus {
    ACTIVE, INACTIVE, LOCKED
}

Repository 接口

@Repository
public interface UserRepository extends JpaRepository<User, Long>,
                                         JpaSpecificationExecutor<User> {

    // 方法命名查询
    Optional<User> findByUsername(String username);

    Optional<User> findByEmail(String email);

    List<User> findByStatus(UserStatus status);

    List<User> findByUsernameContaining(String keyword);

    // 自定义 JPQL 查询
    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
    List<User> findByEmailDomain(@Param("domain") String domain);

    // 原生 SQL 查询
    @Query(value = "SELECT * FROM user WHERE username = ?1", nativeQuery = true)
    User findByUsernameNative(String username);

    // 分页查询
    Page<User> findByStatus(UserStatus status, Pageable pageable);

    // 更新操作
    @Modifying
    @Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);

    // 删除操作
    @Modifying
    @Query("DELETE FROM User u WHERE u.status = :status")
    int deleteByStatus(@Param("status") UserStatus status);
}

方法命名查询规则

关键词 示例 JPQL 片段
And findByLastnameAndFirstname … WHERE x.lastname = ?1 AND x.firstname = ?2
Or findByLastnameOrFirstname … WHERE x.lastname = ?1 OR x.firstname = ?2
Between findByAgeBetween … WHERE x.age BETWEEN ?1 AND ?2
LessThan findByAgeLessThan … WHERE x.age < ?1
GreaterThan findByAgeGreaterThan … WHERE x.age > ?1
Like findByNameLike … WHERE x.name LIKE ?1
Containing findByNameContaining … WHERE x.name LIKE %?1%
In findByAgeIn … WHERE x.age IN ?1
OrderBy findByAgeOrderByNameDesc … ORDER BY x.name DESC
Not findByNameNot … WHERE x.name <> ?1
IsNull findByAgeIsNull … WHERE x.age IS NULL
True findByActiveTrue … WHERE x.active = true
Top/First findTop10ByName 限制结果条数

关联关系映射

// 一对一
@Entity
public class UserProfile {
    @Id
    private Long id;

    @OneToOne
    @MapsId
    @JoinColumn(name = "user_id")
    private User user;

    private String avatar;
    private String bio;
}

// 一对多(一方)
@Entity
public class User {
    // ...

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Address> addresses;
}

// 多对一(多方)
@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    private String province;
    private String city;
    private String detail;
}

// 多对多
@Entity
public class Student {
    @Id
    private Long id;

    @ManyToMany
    @JoinTable(
        name = "student_course",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses;
}

@Entity
public class Course {
    @Id
    private Long id;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students;
}

继承 JpaRepository 后可用的方法

// 基本 CRUD
userRepository.save(user);                    // 新增/更新
userRepository.saveAll(users);                // 批量保存
userRepository.findById(id);                  // 按ID查询
userRepository.findAll();                     // 查询全部
userRepository.findAllById(ids);              // 批量查询
userRepository.count();                       // 统计数量
userRepository.existsById(id);                // 判断是否存在
userRepository.delete(user);                  // 删除
userRepository.deleteById(id);                // 按ID删除
userRepository.deleteAll();                   // 清空

// 分页排序
userRepository.findAll(PageRequest.of(0, 10));                         // 分页
userRepository.findAll(Sort.by("createdAt").descending());             // 排序
userRepository.findAll(PageRequest.of(0, 10, Sort.by("id").asc()));   // 分页+排序

// 动态查询(Specification)
userRepository.findAll((root, query, cb) -> {
    List<Predicate> predicates = new ArrayList<>();
    predicates.add(cb.equal(root.get("status"), UserStatus.ACTIVE));
    if (keyword != null) {
        predicates.add(cb.like(root.get("username"), "%" + keyword + "%"));
    }
    return cb.and(predicates.toArray(new Predicate[0]));
});

7.3 使用 MyBatis(主流选择)

引入依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.4</version>
</dependency>

配置

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.demo.entity
  configuration:
    map-underscore-to-camel-case: true  # 数据库下划线自动转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # SQL 日志
    cache-enabled: true  # 开启二级缓存
    lazy-loading-enabled: true
    aggressive-lazy-loading: false

实体类

@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String phone;
    private Integer status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

Mapper 接口

@Mapper  // 或在启动类上使用 @MapperScan
public interface UserMapper {

    // ========== 基本 CRUD ==========

    @Insert("INSERT INTO user(username, password, email, phone) VALUES(#{username}, #{password}, #{email}, #{phone})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteById(Long id);

    @Update("UPDATE user SET username = #{username}, email = #{email} WHERE id = #{id}")
    int update(User user);

    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(Long id);

    @Select("SELECT * FROM user")
    List<User> findAll();

    // ========== 动态 SQL(配合 XML) ==========

    List<User> findByCondition(UserQueryDTO query);

    // ========== 分页查询 ==========

    List<User> findByPage(PageParam page);

    long countTotal();

    // ========== 批量操作 ==========

    int batchInsert(List<User> users);

    int batchUpdate(List<User> users);
}

XML 映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.demo.mapper.UserMapper">

    <!-- 结果映射 -->
    <resultMap id="BaseResultMap" type="User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="email" column="email"/>
        <result property="phone" column="phone"/>
        <result property="status" column="status"/>
        <result property="createdAt" column="created_at"/>
        <result property="updatedAt" column="updated_at"/>
    </resultMap>

    <!-- 通用 SQL 片段 -->
    <sql id="BaseColumns">
        id, username, password, email, phone, status, created_at, updated_at
    </sql>

    <!-- 条件查询(动态 SQL) -->
    <select id="findByCondition" resultMap="BaseResultMap">
        SELECT <include refid="BaseColumns"/>
        FROM user
        <where>
            <if test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            </if>
            <if test="email != null and email != ''">
                AND email = #{email}
            </if>
            <if test="status != null">
                AND status = #{status}
            </if>
            <if test="startTime != null">
                AND created_at &gt;= #{startTime}
            </if>
            <if test="endTime != null">
                AND created_at &lt;= #{endTime}
            </if>
        </where>
        ORDER BY created_at DESC
    </select>

    <!-- 分页查询 -->
    <select id="findByPage" resultMap="BaseResultMap">
        SELECT <include refid="BaseColumns"/>
        FROM user
        ORDER BY id DESC
        LIMIT #{offset}, #{size}
    </select>

    <!-- 批量插入 -->
    <insert id="batchInsert">
        INSERT INTO user(username, password, email, phone, status, created_at, updated_at)
        VALUES
        <foreach collection="list" item="user" separator=",">
            (#{user.username}, #{user.password}, #{user.email}, #{user.phone},
             #{user.status}, NOW(), NOW())
        </foreach>
    </insert>

    <!-- 批量更新 -->
    <update id="batchUpdate" parameterType="list">
        <foreach collection="list" item="user" separator=";">
            UPDATE user
            <set>
                <if test="user.username != null">username = #{user.username},</if>
                <if test="user.email != null">email = #{user.email},</if>
                <if test="user.phone != null">phone = #{user.phone},</if>
            </set>
            WHERE id = #{user.id}
        </foreach>
    </update>

</mapper>

MyBatis-Plus 增强(推荐)

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.9</version>
</dependency>
// Mapper 继承 BaseMapper,即获得通用 CRUD
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 无需编写基本的 CRUD 方法
}

// 使用示例
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;

    // BaseMapper 提供的开箱即用方法
    public void demo() {
        // 新增
        userMapper.insert(user);

        // 删除
        userMapper.deleteById(1L);
        userMapper.delete(new LambdaQueryWrapper<User>()
                .eq(User::getStatus, 0));

        // 更新
        userMapper.updateById(user);
        userMapper.update(user, new LambdaUpdateWrapper<User>()
                .eq(User::getId, 1L)
                .set(User::getEmail, "new@email.com"));

        // 查询
        userMapper.selectById(1L);
        userMapper.selectOne(new LambdaQueryWrapper<User>()
                .eq(User::getUsername, "admin"));
        userMapper.selectList(new LambdaQueryWrapper<User>()
                .like(User::getUsername, "张")
                .orderByDesc(User::getCreatedAt));

        // 分页
        Page<User> page = userMapper.selectPage(
                new Page<>(1, 10),
                new LambdaQueryWrapper<User>()
                        .eq(User::getStatus, 1));

        // 批量
        userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L));
    }
}

第八章:事务管理

8.1 Spring 事务管理

开启事务

@SpringBootApplication
@EnableTransactionManagement  // 通常默认开启,可省略
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

使用 @Transactional

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;
    private final LogService logService;

    @Transactional  // 未指定则使用默认配置
    public void createUser(User user) {
        userMapper.insert(user);

        // 插入日志
        logService.saveLog("创建用户: " + user.getUsername());
        // 如果这里抛出异常,上面的 insert 也会回滚
    }
}

8.2 @Transactional 详解

常用参数

@Transactional(
    propagation = Propagation.REQUIRED,     // 事务传播行为
    isolation = Isolation.READ_COMMITTED,    // 隔离级别
    timeout = 30,                            // 超时时间(秒)
    readOnly = false,                        // 是否只读
    rollbackFor = Exception.class,           // 指定回滚异常
    noRollbackFor = BusinessException.class  // 指定不回滚异常
)

7 种事务传播行为

传播行为 说明
REQUIRED(默认) 支持当前事务,不存在则新建
SUPPORTS 支持当前事务,不存在则以非事务方式执行
MANDATORY 必须在当前事务中运行,否则抛异常
REQUIRES_NEW 挂起当前事务,新建事务执行
NOT_SUPPORTED 以非事务方式执行,挂起当前事务
NEVER 以非事务方式执行,存在事务则抛异常
NESTED 在嵌套事务中执行(JDBC Savepoint 机制)

事务隔离级别

隔离级别 脏读 不可重复读 幻读
DEFAULT 取决于数据库 取决于数据库 取决于数据库
READ_UNCOMMITTED ✅ 可能 ✅ 可能 ✅ 可能
READ_COMMITTED(多数数据库默认) ✅ 可能 ✅ 可能
REPEATABLE_READ(MySQL 默认) ✅ 可能
SERIALIZABLE

8.3 事务失效的常见场景

// 场景 1:在同一个类中方法调用
@Service
public class UserService {

    public void methodA() {
        methodB();  // 直接调用,事务不会生效!
    }

    @Transactional
    public void methodB() {
        // ...
    }
}

// 解决方案 1:注入自身代理
@Service
public class UserService {
    @Autowired
    private UserService self;  // 注入代理对象

    public void methodA() {
        self.methodB();  // 通过代理调用,事务生效
    }

    @Transactional
    public void methodB() {
        // ...
    }
}

// 场景 2:异常被捕获
@Transactional
public void method() {
    try {
        // ... 数据库操作
        // 如果异常被捕获,事务不会回滚
    } catch (Exception e) {
        log.error("出错了", e);
        // 没有抛出异常,事务提交
    }
}

// 解决方案:捕获后需要抛出
@Transactional
public void method() {
    try {
        // ... 数据库操作
    } catch (Exception e) {
        log.error("出错了", e);
        throw e;  // 重新抛出,触发回滚
    }
}

// 场景 3:rollbackFor 未指定,且抛出了非 RuntimeException
@Transactional  // 默认只回滚 RuntimeException 和 Error
public void method() throws Exception {
    throw new Exception("检查异常");  // 不会回滚!
}

// 解决方案:指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
    throw new Exception("检查异常");  // 会回滚
}

8.4 编程式事务

@Service
@RequiredArgsConstructor
public class UserService {

    private final TransactionTemplate transactionTemplate;

    public void createUserWithTransaction(User user) {
        transactionTemplate.execute(status -> {
            try {
                userMapper.insert(user);
                logService.saveLog("创建用户: " + user.getUsername());
                return null;
            } catch (Exception e) {
                status.setRollbackOnly();  // 手动回滚
                throw e;
            }
        });
    }
}

第九章:缓存

9.1 Spring Cache 抽象

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

开启缓存

@SpringBootApplication
@EnableCaching  // 开启缓存抽象
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

缓存注解

@Service
public class UserService {

    // @Cacheable:缓存存在则直接返回,不存在则执行方法并缓存
    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        // 模拟耗时查询
        slowQuery();
        return userMapper.findById(id);
    }

    // @CachePut:每次都执行方法,并将结果更新到缓存
    @CachePut(value = "users", key = "#user.id")
    public User update(User user) {
        userMapper.update(user);
        return user;
    }

    // @CacheEvict:删除缓存
    @CacheEvict(value = "users", key = "#id")
    public void delete(Long id) {
        userMapper.deleteById(id);
    }

    // 组合:清空整个缓存
    @CacheEvict(value = "users", allEntries = true)
    public void clearCache() {
        // 清空 users 缓存区域
    }

    // 多条件缓存
    @Caching(
        cacheable = @Cacheable("users"),
        put = @CachePut(value = "users", key = "#result.email")
    )
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }
}

缓存条件

// condition:满足条件才缓存
@Cacheable(value = "users", condition = "#id > 0")

// unless:满足条件则不缓存
@Cacheable(value = "users", unless = "#result == null")

// 组合使用
@Cacheable(value = "users",
           condition = "#id > 0",
           unless = "#result.status == 0")

9.2 Redis 集成

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

配置

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password:
      database: 0
      timeout: 10000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: -1ms

使用 RedisTemplate

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // JSON 序列化
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
                LazyValidatorFactory.getDefaultPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL);
        jsonSerializer.setObjectMapper(om);

        // String 序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // key 使用 String 序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // value 使用 JSON 序列化
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}
@Service
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;

    // ========== String 操作 ==========

    public void setString(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    public void setStringWithExpire(String key, String value, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    public String getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    // ========== Hash 操作 ==========

    public void setHash(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public Object getHash(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    public Map<Object, Object> getHashAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // ========== List 操作 ==========

    public void pushToList(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    public Object popFromList(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    public List<Object> getList(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    // ========== Set 操作 ==========

    public void addToSet(String key, Object... values) {
        redisTemplate.opsForSet().add(key, values);
    }

    public Set<Object> getSet(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    // ========== ZSet 操作 ==========

    public void addToZSet(String key, Object value, double score) {
        redisTemplate.opsForZSet().add(key, value, score);
    }

    public Set<Object> getZSetRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    // ========== 通用操作 ==========

    public boolean expire(String key, long timeout, TimeUnit unit) {
        return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit));
    }

    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    public void delete(String key) {
        redisTemplate.delete(key);
    }

    public void deleteBatch(Collection<String> keys) {
        redisTemplate.delete(keys);
    }

    // ========== 分布式锁(简化版)==========

    public boolean tryLock(String key, String value, long timeout) {
        return Boolean.TRUE.equals(
                stringRedisTemplate.opsForValue()
                        .setIfAbsent(key, value, timeout, TimeUnit.SECONDS));
    }

    public void unlock(String key, String value) {
        String current = stringRedisTemplate.opsForValue().get(key);
        if (value.equals(current)) {
            stringRedisTemplate.delete(key);
        }
    }
}

9.3 使用 Redis 作为缓存实现

spring:
  cache:
    type: redis
    redis:
      time-to-live: 3600000  # 缓存过期时间(毫秒)
      cache-null-values: true  # 是否缓存空值
      key-prefix: "cache:"
      use-key-prefix: true

自定义 Redis 缓存管理器

@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return builder -> {
            // 默认配置
            Map<String, RedisCacheConfiguration> configMap = new HashMap<>();

            // users 缓存:30 分钟过期
            configMap.put("users", RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofMinutes(30))
                    .prefixName("users:"));

            // roles 缓存:1 小时过期
            configMap.put("roles", RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofHours(1))
                    .prefixName("roles:"));

            // verifyCodes 缓存:5 分钟过期
            configMap.put("verifyCodes", RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofMinutes(5))
                    .prefixName("vc:")
                    .disableCachingNullValues());

            builder.withInitialCacheConfigurations(configMap);
        };
    }
}

第十章:异步与定时任务

10.1 异步任务

开启异步

@SpringBootApplication
@EnableAsync  // 开启异步支持
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

使用 @Async

@Service
public class AsyncService {

    @Async  // 在异步线程中执行
    public CompletableFuture<String> sendEmail(String to, String content) {
        try {
            // 模拟发送邮件耗时
            Thread.sleep(2000);
            log.info("邮件发送成功: {}", to);
            return CompletableFuture.completedFuture("发送成功");
        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }

    @Async
    public void generateReport(Long reportId) {
        // 生成报表...
        log.info("报表生成完成: {}", reportId);
    }
}

// 调用异步方法
@Service
public class ReportController {

    @Autowired
    private AsyncService asyncService;

    public void createReport() {
        // 异步调用,立即返回
        asyncService.generateReport(1L);

        // 等待异步任务结果
        CompletableFuture<String> future = asyncService.sendEmail("test@test.com", "内容");
        future.thenAccept(result -> log.info("结果: {}", result));
    }
}

自定义线程池

@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);                // 核心线程数
        executor.setMaxPoolSize(10);                // 最大线程数
        executor.setQueueCapacity(100);             // 队列容量
        executor.setKeepAliveSeconds(60);           // 线程存活时间
        executor.setThreadNamePrefix("async-");     // 线程名前缀
        executor.setWaitForTasksToCompleteOnShutdown(true);  // 优雅关闭
        executor.setAwaitTerminationSeconds(30);             // 等待秒数
        executor.setRejectedExecutionHandler(
                new ThreadPoolExecutor.CallerRunsPolicy());  // 拒绝策略
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
                log.error("异步任务执行失败: {}.{}", method.getDeclaringClass(), method.getName(), ex);
    }
}

10.2 定时任务

开启定时任务

@SpringBootApplication
@EnableScheduling  // 开启定时任务
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

使用 @Scheduled

@Component
@Slf4j
public class ScheduledTasks {

    // 固定延迟:上次执行完毕后 5 秒执行
    @Scheduled(fixedDelay = 5000)
    public void taskWithFixedDelay() {
        log.info("固定延迟任务执行: {}", LocalDateTime.now());
    }

    // 固定频率:每隔 5 秒执行一次(不考虑上次执行时间)
    @Scheduled(fixedRate = 5000)
    public void taskWithFixedRate() {
        log.info("固定频率任务执行: {}", LocalDateTime.now());
    }

    // 初始延迟 10 秒后,再按固定频率执行
    @Scheduled(fixedRate = 5000, initialDelay = 10000)
    public void taskWithInitialDelay() {
        log.info("含初始延迟的任务执行: {}", LocalDateTime.now());
    }

    // Cron 表达式
    // 每天凌晨 2 点执行
    @Scheduled(cron = "0 0 2 * * ?")
    public void taskWithCron() {
        log.info("定时备份任务执行: {}", LocalDateTime.now());
    }

    // 每 5 分钟执行一次
    @Scheduled(cron = "0 */5 * * * ?")
    public void taskEvery5Minutes() {
        log.info("每5分钟任务执行: {}", LocalDateTime.now());
    }

    // 工作日早 9 点执行
    @Scheduled(cron = "0 0 9 ? * MON-FRI")
    public void taskOnWorkdays() {
        log.info("工作日任务执行: {}", LocalDateTime.now());
    }
}

Cron 表达式详解

格式:秒 分 时 日 月 周

示例:
"0 0 2 * * ?"        每天凌晨 2 点
"0 0/5 * * * ?"      每 5 分钟
"0 0 9-18 * * ?"     每天 9 点到 18 点每小时
"0 30 23 * * ?"      每天 23:30
"0 0 0 1 * ?"        每月 1 日零点
"0 0 6 ? * MON"      每周一早上 6 点
"0 0 3 ? * 6#1"      每月第一个周五 3 点
字段 取值范围 允许的特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L #

? 表示不指定值(日和周互斥) L 表示最后一天 W 表示最近的工作日 # 表示第几个星期几

自定义线程池

@Configuration
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);                    // 线程池大小
        scheduler.setThreadNamePrefix("scheduled-");  // 线程名前缀
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(30);
        scheduler.initialize();
        taskRegistrar.setTaskScheduler(scheduler);
    }
}

第十一章:日志管理

11.1 集成 Logback(Spring Boot 默认)

配置文件 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">

    <!-- 从 application.yml 读取配置 -->
    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <springProperty scope="context" name="logPath" source="logging.file.path" defaultValue="./logs"/>

    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 文件输出(INFO) -->
    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/${appName}-info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${appName}-info-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 文件输出(ERROR) -->
    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/${appName}-error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${appName}-error-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>90</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 异步输出(重要:避免日志 I/O 影响性能) -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>512</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <appender-ref ref="FILE_INFO"/>
        <appender-ref ref="FILE_ERROR"/>
    </appender>

    <!-- 包级别日志控制 -->
    <logger name="org.springframework" level="WARN"/>
    <logger name="org.mybatis" level="INFO"/>
    <logger name="com.example.demo" level="DEBUG"/>

    <!-- 根日志级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC"/>
    </root>
</configuration>

application.yml 配置

logging:
  level:
    root: info
    com.example.demo: debug
    org.springframework.web: warn
  file:
    path: ./logs
    name: ${logging.file.path}/app.log
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
  logback:
    rollingpolicy:
      max-history: 30
      max-file-size: 10MB

11.2 使用 Lombok 的 @Slf4j

@RestController
@Slf4j  // 自动生成 log 字段
public class UserController {

    @GetMapping("/users/{id}")
    public Result<User> getById(@PathVariable Long id) {
        log.debug("查询用户: id={}", id);
        try {
            User user = userService.findById(id);
            log.info("查询成功: {}", user);
            return Result.success(user);
        } catch (Exception e) {
            log.error("查询用户失败: id={}", id, e);
            return Result.error("查询失败");
        }
    }
}

第十二章:Actuator 监控

12.1 引入与配置

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,env,logfile,loggers
        exclude: shutdown
  endpoint:
    health:
      show-details: always
      show-components: always
    metrics:
      enabled: true
    info:
      enabled: true
  info:
    env:
      enabled: true

12.2 内置端点

端点 路径 说明
health /actuator/health 应用健康状态
info /actuator/info 应用信息
metrics /actuator/metrics 应用指标
env /actuator/env 环境配置
logfile /actuator/logfile 日志文件
loggers /actuator/loggers 日志级别
mappings /actuator/mappings URL 映射
beans /actuator/beans Spring 容器中的 Bean
threaddump /actuator/threaddump 线程快照
heapdump /actuator/heapdump 堆转储
scheduledtasks /actuator/scheduledtasks 定时任务
caches /actuator/caches 缓存管理

自定义 Info

info:
  app:
    name: '@project.name@'        # 读取 Maven 属性
    version: '@project.version@'
    description: '@project.description@'
  java:
    version: '@java.version@'
  build:
    artifact: '@project.artifactId@'
// 或通过代码自定义
@Component
public class AppInfoContributor implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("custom",
                Map.of("author", "张三",
                       "startupTime", System.currentTimeMillis()));
    }
}

12.3 自定义健康检查

@Component
public class DatabaseHealthIndicator implements HealthIndicator {

    @Autowired
    private DataSource dataSource;

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            if (conn.isValid(3)) {
                return Health.up()
                        .withDetail("database", "MySQL")
                        .withDetail("status", "connected")
                        .build();
            } else {
                return Health.down()
                        .withDetail("reason", "数据库连接无效")
                        .build();
            }
        } catch (Exception e) {
            return Health.down(e)
                    .withDetail("reason", "数据库连接失败")
                    .build();
        }
    }
}

第十三章:Spring Security

13.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

引入后所有接口默认被保护,会生成默认密码(启动日志中可查看)。

13.2 基础配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // 开启方法级别安全注解
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF(前后端分离项目)
            .csrf(AbstractHttpConfigurer::disable)

            // 无状态(前后端分离)
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            // 请求授权
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()            // 登录注册放行
                .requestMatchers("/api/public/**").permitAll()          // 公共接口放行
                .requestMatchers("/actuator/health").permitAll()        // 健康检查放行
                .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()  // Swagger 放行
                .anyRequest().authenticated()                           // 其他请求需认证
            )

            // 添加 JWT 过滤器
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)

            // 异常处理
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.getWriter().write(
                        new ObjectMapper().writeValueAsString(
                            Result.error(401, "未授权,请先登录")
                        ));
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.getWriter().write(
                        new ObjectMapper().writeValueAsString(
                            Result.error(403, "权限不足")
                        ));
                })
            );

        return http.build();
    }

    // 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();  // 推荐 BCrypt 加密
    }

    // JWT 认证过滤器(见下方)
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }
}

13.3 JWT 认证实现

JWT 工具类

@Component
public class JwtUtils {

    @Value("${jwt.secret:defaultSecretKey123456}")
    private String secret;

    @Value("${jwt.expiration:86400000}")  // 默认 24 小时
    private long expiration;

    private final SecretKey key;

    public JwtUtils() {
        this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    }

    // 生成 Token
    public String generateToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(key)
                .compact();
    }

    // 从 Token 中提取用户名
    public String getUsernameFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    // 验证 Token
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

JWT 认证过滤器

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtils jwtUtils;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain)
            throws ServletException, IOException {

        String token = extractToken(request);

        if (token != null && jwtUtils.validateToken(token)) {
            String username = jwtUtils.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

登录接口

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtils jwtUtils;

    @PostMapping("/login")
    public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
        // 认证
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getUsername(), request.getPassword()));

        // 生成 Token
        String token = jwtUtils.generateToken(authentication.getName());

        return Result.success(new LoginResponse(token, "Bearer", jwtUtils.getExpiration()));
    }

    @PostMapping("/register")
    public Result<Void> register(@Valid @RequestBody RegisterRequest request) {
        userService.register(request);
        return Result.success();
    }
}

13.4 方法级别安全

@RestController
@RequestMapping("/api/users")
public class UserController {

    // 只有 ADMIN 角色可访问
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping
    public Result<List<User>> list() {
        return Result.success(userService.findAll());
    }

    // 登录用户可访问
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        return Result.success(userService.findById(id));
    }

    // 拥有指定权限可访问
    @PreAuthorize("hasAuthority('user:create')")
    @PostMapping
    public Result<User> create(@RequestBody User user) {
        return Result.success(userService.create(user));
    }

    // 当前用户只能操作自己的数据
    @PreAuthorize("#id == authentication.principal.id")
    @PutMapping("/{id}")
    public Result<User> update(@PathVariable Long id, @RequestBody User user) {
        return Result.success(userService.update(id, user));
    }

    // 组合条件
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.success();
    }

    // 方法执行后校验
    @PostAuthorize("returnObject.data.username == authentication.principal.username")
    @GetMapping("/profile")
    public Result<User> profile() {
        return Result.success(userService.getCurrentUser());
    }
}

第十四章:测试

14.1 单元测试

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

基础测试

@SpringBootTest  // 启动完整 Spring 容器
class DemoApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    void contextLoads() {
        assertThat(userService).isNotNull();
    }
}

Web 层测试

@WebMvcTest(UserController.class)  // 仅加载 Web 层
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean  // Mock 业务层
    private UserService userService;

    @Test
    void testGetUser() throws Exception {
        // 准备 Mock 数据
        User user = new User(1L, "张三", "zhangsan@example.com");
        when(userService.findById(1L)).thenReturn(user);

        // 发送请求并验证
        mockMvc.perform(get("/api/users/1")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.username").value("张三"))
                .andExpect(jsonPath("$.data.email").value("zhangsan@example.com"));

        // 验证服务层被调用
        verify(userService, times(1)).findById(1L);
    }

    @Test
    void testCreateUser() throws Exception {
        UserCreateDTO dto = new UserCreateDTO("newuser", "123456", "new@test.com");

        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200));

        verify(userService, times(1)).create(any(UserCreateDTO.class));
    }

    @Test
    void testValidation() throws Exception {
        // 测试参数校验
        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(400));
    }
}

数据层测试

@DataJpaTest  // 仅加载 JPA 相关组件
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void testSaveAndFind() {
        // 准备数据
        User user = new User();
        user.setUsername("test");
        user.setPassword("123456");
        user.setStatus(UserStatus.ACTIVE);

        // 保存
        User saved = entityManager.persistAndFlush(user);

        // 查询
        Optional<User> found = userRepository.findById(saved.getId());

        assertThat(found).isPresent();
        assertThat(found.get().getUsername()).isEqualTo("test");
    }

    @Test
    void testFindByUsername() {
        User user = new User();
        user.setUsername("admin");
        user.setPassword("123456");
        user.setStatus(UserStatus.ACTIVE);
        entityManager.persistAndFlush(user);

        Optional<User> found = userRepository.findByUsername("admin");
        assertThat(found).isPresent();
    }
}

MyBatis Mapper 测试

@MybatisTest  // 加载 MyBatis 相关组件
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
// 或使用内嵌数据库
// @AutoConfigureTestDatabase
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsertAndSelect() {
        User user = new User();
        user.setUsername("test");
        user.setPassword("password");
        user.setEmail("test@test.com");

        int result = userMapper.insert(user);
        assertThat(result).isEqualTo(1);
        assertThat(user.getId()).isNotNull();

        User found = userMapper.findById(user.getId());
        assertThat(found.getUsername()).isEqualTo("test");
    }

    @Test
    void testFindByCondition() {
        // 测试动态 SQL
        UserQueryDTO query = new UserQueryDTO();
        query.setUsername("张");

        List<User> users = userMapper.findByCondition(query);
        assertThat(users).isNotEmpty();
    }
}

14.2 集成测试

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void testFullFlow() {
        // 1. 创建用户
        UserCreateDTO dto = new UserCreateDTO("integration", "123456", "int@test.com");
        ResponseEntity<Result> createResponse = restTemplate.postForEntity(
                "http://localhost:" + port + "/api/users",
                dto, Result.class);

        assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(createResponse.getBody().getCode()).isEqualTo(200);

        // 2. 查询用户列表
        ResponseEntity<Result> listResponse = restTemplate.exchange(
                "http://localhost:" + port + "/api/users?page=1&size=10",
                HttpMethod.GET, null, Result.class);

        assertThat(listResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

第十五章:部署与运维

15.1 打包

# 打包(跳过测试)
mvn clean package -DskipTests

# 打包为可执行 JAR
mvn clean package -DskipTests -Dspring-boot.repackage=true

# 打包后文件位置
# target/demo-0.0.1-SNAPSHOT.jar

15.2 运行

# 基础启动
java -jar demo-0.0.1-SNAPSHOT.jar

# 指定端口
java -jar demo-0.0.1-SNAPSHOT.jar --server.port=8081

# 指定环境
java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

# JVM 参数调优
java -Xms512m -Xmx1024m \
     -XX:+UseG1GC \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/logs/dump.hprof \
     -jar demo-0.0.1-SNAPSHOT.jar

15.3 容器化部署(Docker)

Dockerfile

# 多阶段构建
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

# 时区
ENV TZ=Asia/Shanghai

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

docker-compose.yml

version: '3.8'

services:
  app:
    build: .
    container_name: demo-app
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/demo
      - SPRING_DATASOURCE_USERNAME=root
      - SPRING_DATASOURCE_PASSWORD=root123
      - SPRING_DATA_REDIS_HOST=redis
    depends_on:
      - db
      - redis
    networks:
      - app-network

  db:
    image: mysql:8.0
    container_name: demo-db
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: demo
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: demo-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql-data:
  redis-data:

15.4 JVM 参数说明

# 内存配置
-Xms512m              # 初始堆大小
-Xmx1024m             # 最大堆大小
-XX:MetaspaceSize=128m # 元空间初始大小
-XX:MaxMetaspaceSize=256m # 元空间最大大小

# GC 配置
-XX:+UseG1GC          # 使用 G1 垃圾回收器
-XX:MaxGCPauseMillis=200 # GC 最大暂停时间(ms)

# 调试配置
-XX:+HeapDumpOnOutOfMemoryError  # OOM 时生成堆转储
-XX:HeapDumpPath=/path/to/dump   # 堆转储路径
-Xlog:gc*:file=/var/logs/gc.log  # GC 日志

# 远程调试
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

第十六章:最佳实践

16.1 项目结构

src/main/java/com/example/demo/
├── common/                  # 公共模块
│   ├── exception/           # 异常定义
│   ├── result/              # 统一响应
│   └── constant/            # 常量定义
├── config/                  # 配置类
├── controller/              # 控制器
├── service/                 # 服务接口
│   └── impl/                # 服务实现
├── repository/              # 持久层(JPA)
├── mapper/                  # 持久层(MyBatis)
├── entity/                  # 实体类
├── dto/                     # 数据传输对象
├── vo/                      # 视图对象
├── converter/               # 对象转换器
└── util/                    # 工具类

16.2 分层架构规范

Controller 层    → 接收请求,参数校验,调用 Service
    ↓
Service 层      → 业务逻辑,事务管理
    ↓
Repository/Mapper 层 → 数据访问
    ↓
Database        → 数据库

原则

  • Controller 不包含业务逻辑
  • Service 层处理业务逻辑和事务
  • 层与层之间通过接口调用
  • 使用 DTO 进行层间数据传递

16.3 异常处理流程

Controller
    ↓ 抛出异常
GlobalExceptionHandler
    ↓ 统一处理
Result.error()
    ↓ 返回给前端

16.4 日志规范

// ✅ 正确:使用占位符
log.debug("查询用户: id={}, name={}", id, name);

// ❌ 错误:字符串拼接(即使不输出也会执行)
log.debug("查询用户: id=" + id + ", name=" + name);

// ✅ 正确:记录异常完整堆栈
log.error("操作失败", exception);

// ❌ 错误:只记录异常消息
log.error("操作失败: " + exception.getMessage());

16.5 数据库规范

-- 表名:小写加下划线
CREATE TABLE user_account (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20),
    status TINYINT DEFAULT 1 COMMENT '状态: 0-禁用 1-启用',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_username (username),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

规范要点

  • 表名使用小写+下划线
  • 字段名使用小写+下划线
  • 必须包含 id 主键、created_atupdated_at
  • 使用 utf8mb4 字符集
  • 合理添加索引
  • 字段使用合适的类型和长度

16.6 API 设计规范

// ✅ 正确:RESTful 风格
GET    /api/users          # 列表
GET    /api/users/{id}     # 详情
POST   /api/users          # 新增
PUT    /api/users/{id}     # 全量更新
PATCH  /api/users/{id}     # 部分更新
DELETE /api/users/{id}     # 删除

// ✅ 正确:统一响应格式
{
    "code": 200,
    "message": "success",
    "data": { ... },
    "timestamp": 1700000000000
}

// ❌ 错误:直接返回实体
// 应该始终使用统一响应体包装

// ✅ 正确:版本管理
/api/v1/users
/api/v2/users

16.7 常见性能优化

数据库优化

// ✅ 正确:使用索引
@Table(indexes = {
    @Index(name = "idx_username", columnList = "username"),
    @Index(name = "idx_status_created", columnList = "status,createdAt")
})

// ✅ 正确:只查询需要的字段
@Query("SELECT u.id, u.username FROM User u")

// ❌ 避免:N+1 查询
// 使用 @EntityGraph 或 JOIN FETCH
@EntityGraph(attributePaths = {"roles"})
@Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findByIdWithRoles(@Param("id") Long id);

缓存优化

// ✅ 正确:对频繁查询的接口使用缓存
@Cacheable(value = "users", unless = "#result == null")

// ✅ 正确:更新数据时同步更新缓存
@CachePut(value = "users", key = "#user.id")

// ✅ 正确:删除数据时清空相关缓存
@CacheEvict(value = "users", allEntries = true)

连接池配置

spring:
  datasource:
    hikari:
      minimum-idle: 10
      maximum-pool-size: 50
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-timeout: 30000
      pool-name: HikariPool

第十七章:常见问题与解决方案

17.1 启动失败

端口被占用

# 查看端口占用
netstat -ano | findstr :8080

# 修改端口
server.port=8081

# 或杀掉进程
taskkill /PID <PID> /F

数据库连接失败

# 检查配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver

依赖冲突

<!-- 排除冲突依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

17.2 自动配置不生效

// 检查条件注解是否满足
// 可以在 application.yml 中开启调试日志
debug: true

// 或
logging:
  level:
    org.springframework.boot.autoconfigure: DEBUG

// 启动日志中会输出:
// Positive matches:   (匹配成功的自动配置)
// Negative matches:   (匹配失败的自动配置,含原因)
// Exclusions:         (被排除的自动配置)

17.3 @Autowired 警告

// ❌ 字段注入(不推荐)
@Autowired
private UserService userService;

// ✅ 构造器注入(推荐,Lombok)
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;
}

// ✅ 设值注入(可选依赖)
@Autowired
public void setUserService(UserService userService) {
    this.userService = userService;
}

17.4 跨域问题

// 确认 CORS 配置已生效
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("*")
                .allowedHeaders("*")
                .allowCredentials(true);
    }
}

17.5 事务不回滚

// 检查点 1:rollbackFor 是否正确
@Transactional(rollbackFor = Exception.class)  // 覆盖所有异常

// 检查点 2:是否在同级方法调用
// 事务只对代理对象外部调用生效

// 检查点 3:是否捕获了异常
// 注意:不要 try-catch 后不抛出

// 检查点 4:数据库引擎是否支持事务
// MySQL MyISAM 不支持事务,需使用 InnoDB

附录:常用配置速查

application.yml 全配置模板

# ========== 服务器配置 ==========
server:
  port: 8080
  servlet:
    context-path: /
  tomcat:
    max-connections: 10000
    max-threads: 200
    min-spare-threads: 10
    connection-timeout: 5000ms

# ========== Spring 核心配置 ==========
spring:
  application:
    name: demo-app

  # 配置激活
  profiles:
    active: dev

  # ========== 数据源 ==========
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

  # ========== JPA ==========
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false
    properties:
      hibernate:
        format_sql: true

  # ========== Redis ==========
  data:
    redis:
      host: localhost
      port: 6379
      password:
      database: 0
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

  # ========== 缓存 ==========
  cache:
    type: redis
    redis:
      time-to-live: 3600000
      cache-null-values: true

  # ========== Jackson ==========
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai
    default-property-inclusion: non_null

  # ========== 文件上传 ==========
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      max-request-size: 50MB

# ========== 日志 ==========
logging:
  level:
    root: info
    com.example.demo: debug
  file:
    path: ./logs

# ========== Actuator ==========
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,loggers
  endpoint:
    health:
      show-details: always

# ========== MyBatis ==========
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.demo.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Maven POM 常用依赖

<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 校验 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.4</version>
    </dependency>

    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>3.5.9</version>
    </dependency>

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>

    <!-- 安全 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- JWT -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>

    <!-- 缓存 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <!-- Actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- 数据库驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- 开发工具 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
</dependencies>

💡 本文档专注 Spring Boot 核心生态,涵盖自动配置原理、Web 开发、数据访问(JPA/MyBatis)、事务管理、缓存(Redis)、异步定时任务、安全(Spring Security + JWT)、监控(Actuator)、测试、部署运维及最佳实践。不涉及 Spring Cloud 微服务相关内容。

如需深入学习某个主题,建议结合官方文档和实际项目练习。

Comments | 0条评论