Spring Boot 基础教程

Spring Boot 基础教程

带你系统学习Spring Boot 3.5.6,结合JDK 25的新特性,从基础到实践,全面掌握Spring Boot开发技能。

章节规划

  1. Spring Boot简介与环境搭建
  2. 第一个Spring Boot应用
  3. Spring Boot配置体系
  4. Spring Boot Web开发
  5. 数据库操作 - MyBatis篇
  6. 数据库操作 - MyBatis-Plus篇
  7. 数据库操作 - JPA篇
  8. 服务集成与高级特性
  9. 测试与部署

第1章:Spring Boot简介与环境搭建

1.1 什么是Spring Boot

Spring Boot是由Pivotal团队开发的Spring框架的子项目,它简化了Spring应用的初始搭建和开发过程。

通过自动配置、起步依赖等特性,Spring Boot让开发者能够快速构建独立运行的、生产级别的Spring应用。

Spring Boot 3.5.6基于Spring Framework 6.2.x,要求JDK 17及以上版本,我们将使用最新的JDK 25进行开发。

1.2 开发环境准备

  • JDK 25:确保正确配置JAVA_HOME环境变量
  • Maven 3.9.x 或 Gradle 8.7+
  • IDE:IntelliJ IDEA 2025.2 或 Eclipse 2025-06
  • 数据库:MySQL 8.4.6 或者 MySQL 9.4.0

1.3 环境验证

确认JDK安装成功:

java -version
# 应显示类似:openjdk version "25" 2025-09-16

确认Maven安装成功:

mvn -version
# 应显示Maven版本信息及正确的Java版本

第2章:第一个Spring Boot应用

2.1 项目创建

我们将使用Spring Initializr创建项目:

  1. 访问 https://start.spring.io/

  2. 选择:

    • Project: Maven
    • Language: Java
    • Spring Boot: 3.5.6
    • Group: com.lihaozhe
    • Artifact: springboot-tutorial
    • Name: springboot-tutorial
    • Description: First Spring Boot Application
    • Package name: com.lihaozhe
    • Packaging: Jar
    • Java: 25
  3. 添加依赖:Spring Web

  4. IntelliJ IDEA 创建工程

    IDEA 新建 springboot 工程

    IDEA 新建 springboot 工程

2.2 项目结构解析

生成的项目结构如下:

springboot-tutorial/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── lihaozhe/
│   │   │           └── SpringbootTutorialApplication.java
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── static/
│   │       └── templates/
│   └── test/
│       └── java/
│           └── com/
│               └── lihaozhe/
│                   └── SpringbootTutorialApplicationTests.java
├── pom.xml
└── README.md

2.3 完整的pom.xml

<?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">
  <!-- 模型版本,固定为4.0.0 -->
  <modelVersion>4.0.0</modelVersion>

  <!-- 继承Spring Boot的父工程,统一管理依赖版本 -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.6</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <!-- 项目基本信息 -->
  <groupId>com.lihaozhe</groupId>
  <artifactId>springboot-tutorial</artifactId>
  <version>1.0.0</version>
  <name>springboot-tutorial</name>
  <description>springboot-tutorial</description>

  <properties>
    <!-- JDK版本配置 -->
    <java.version>25</java.version>
  </properties>
  <dependencies>
    <!-- Spring Web Starter:包含Spring MVC和嵌入式Tomcat -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

    <!-- Spring Boot Test Starter:包含测试相关依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <!-- 构建配置 -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <annotationProcessorPaths>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </path>
          </annotationProcessorPaths>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>


2.4 主程序类

package com.lihaozhe;

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

/**
 * Spring Boot应用的主入口类
 * -@SpringBootApplication 是一个组合注解,包含:
 *  - @SpringBootConfiguration:标记此类为配置类
 *  - @EnableAutoConfiguration:启用Spring Boot的自动配置机制
 *  - @ComponentScan:启用组件扫描,自动发现并注册Bean
 */
@SpringBootApplication
public class SpringbootTutorialApplication {

  /**
   * 程序入口方法
   *
   * @param args 命令行参数
   */
  public static void main(String[] args) {
    // 启动Spring Boot应用
    // SpringApplication.run()方法会创建Spring容器,启动嵌入式服务器
    SpringApplication.run(SpringbootTutorialApplication.class, args);
  }

}

2.5 创建第一个控制器

package com.lihaozhe.controller;

import org.springframework.web.bind.annotation.*;

/**
 * 第一个控制器类
 * -@RestController 是一个组合注解,包含:
 * - @Controller:标记此类为控制器
 * - @ResponseBody:将方法返回值直接作为HTTP响应体
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/hello")
public class HelloController {
  /**
   * 处理根路径的GET请求
   *
   * @return 响应消息
   */
  @GetMapping("/home")
  public String home() {
    // 返回简单字符串,将直接作为HTTP响应体
    return "Hello, Spring Boot 3.5.6!";
  }

  /**
   * 处理带路径变量的GET请求
   *
   * @param name 路径中的变量
   * @return 包含名称的问候消息
   */
  @GetMapping("/helloWithPath/{name}")
  public String helloWithPath(@PathVariable String name) {
    // 使用JDK 21引入的文本块特性,使字符串更易读
    return """
           Hello, %s!
           Welcome to Spring Boot 3.5.6 tutorial.
           """.formatted(name);
  }

  /**
   * 处理带查询参数的GET请求
   *
   * @param name 查询参数,可以为null
   * @return 包含名称的问候消息
   */
  @GetMapping("/greet")
  public String greet(@RequestParam(required = false) String name) {
    // 如果没有提供name参数,使用默认值
    String actualName = (name == null || name.isEmpty()) ? "Guest" : name;
    return "Greetings, " + actualName + "!";
  }
}

2.6 配置文件

application.properties(除springcloud工程外 单体 springboot工程使用的人已经不多了 不推荐 )

# 服务器端口配置,默认为8080
server.port=8080

# 应用名称
spring.application.name=springboot-tutorial

# 日志级别配置
logging.level.root=INFO
logging.level.com.lihaozhe=DEBUG

application.yaml 或者 application.yml(推荐)

server:
  # 服务器端口配置,默认为8080
  port: 8080

spring:
  application:
    # 应用名称
    name: springboot-tutorial

# 日志级别配置
logging:
  level:
    root: INFO
    com:
      lihaozhe: DEBUG

2.7 前端页面

我们创建一个简单的HTML页面,放在src/main/resources/static目录下:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Spring Boot tutorial</title>
  <!-- 使用Tailwind CSS进行样式设计 -->
  <!--<script src="https://cdn.tailwindcss.com"></script>-->
  <script src="./js/tailwindcss.js"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
  <h1 class="text-3xl font-bold text-center text-blue-600 mb-6">
    Spring Boot 3.5.6 tutorial
  </h1>

  <div class="mb-6">
    <p class="text-gray-700 mb-4">
      This is a simple Spring Boot application demonstrating basic features.
    </p>

    <div class="space-y-3">
      <a href="/hello/home" class="block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-center">
        Home
      </a>
      <a href="/hello/helloWithPath/World" class="block px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors text-center">
        Hello World
      </a>
      <a href="/hello/greet?name=Spring" class="block px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600 transition-colors text-center">
        Greet Spring
      </a>
    </div>
  </div>

  <div class="text-center text-gray-500 text-sm">
    <p>Running on Spring Boot 3.5.6 with JDK 25</p>
  </div>
</div>
</body>
</html>

2.8 测试类

package com.lihaozhe;

import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

/**
 * Spring Boot应用的测试类
 * - @SpringBootTest 表示这是一个Spring Boot测试类,会加载完整的应用上下文
 * - @AutoConfigureMockMvc 自动配置MockMvc,用于模拟HTTP请求
 */
@SpringBootTest
@AutoConfigureMockMvc
class SpringbootTutorialApplicationTests {

  /**
   * 注入MockMvc实例,用于模拟HTTP请求和验证响应
   */
  @Autowired
  private MockMvc mvc;

  /**
   * 测试根路径的GET请求
   */
  @Test
  void testHome() throws Exception {
    // 模拟GET请求访问根路径,并验证响应状态和内容
    mvc.perform(MockMvcRequestBuilders.get("/hello/home").accept(org.springframework.http.MediaType.TEXT_PLAIN))
        .andExpect(status().isOk())
        .andExpect(content().string(equalTo("Hello, Spring Boot 3.5.6!")));
  }

  /**
   * 测试带路径变量的请求
   */
  @Test
  void testHelloWithPath() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/hello/helloWithPath/Test").accept(org.springframework.http.MediaType.TEXT_PLAIN))
        .andExpect(status().isOk())
        .andExpect(content().string(equalTo("Hello, Test!\nWelcome to Spring Boot 3.5.6 tutorial.\n")));
  }

  /**
   * 测试带查询参数的请求
   */
  @Test
  void testGreet() throws Exception {
    // 测试带参数的情况
    mvc.perform(MockMvcRequestBuilders.get("/hello/greet").param("name", "JUnit")
            .accept(org.springframework.http.MediaType.TEXT_PLAIN))
        .andExpect(status().isOk())
        .andExpect(content().string(equalTo("Greetings, JUnit!")));

    // 测试不带参数的情况
    mvc.perform(MockMvcRequestBuilders.get("/hello/greet")
            .accept(org.springframework.http.MediaType.TEXT_PLAIN))
        .andExpect(status().isOk())
        .andExpect(content().string(equalTo("Greetings, Guest!")));
  }
}

2.9 运行与测试

  1. 运行应用
    • 方式1:在IDE中运行SpringbootTutorialApplication类的main方法
    • 方式2:使用Maven命令:mvn spring-boot:run
  2. 访问应用
    • 打开浏览器,访问 http://localhost:8080/hello/home 查看根路径响应
    • 访问 http://localhost:8080/hello/helloWithPath/YourName 测试路径变量
    • 访问 http://localhost:8080/hello/greet?name=YourName 测试查询参数
    • 访问 http://localhost:8080/index.html 查看前端页面
  3. 运行测试
    • 在IDE中运行测试类
    • 或使用Maven命令:mvn test

2.10 开发思路总结

  1. 项目初始化:使用Spring Initializr快速创建项目结构,选择合适的依赖
  2. 主程序类:通过@SpringBootApplication注解标识,作为应用入口
  3. 控制器开发:使用@RestController创建REST接口,通过@RequestMapping系列注解映射请求
  4. 配置管理:使用application.properties配置应用参数
  5. 前端页面:将静态资源放在static目录下,Spring Boot会自动映射
  6. 测试策略:使用MockMvc模拟HTTP请求,验证接口功能

通过这个简单的示例,我们已经掌握了Spring Boot的基本开发流程。下一章我们将深入学习Spring Boot的配置体系。


第3章:Spring Boot配置体系

3.1 配置文件类型

Spring Boot支持多种配置文件格式,最常用的有两种:

  • properties格式:传统的键值对格式
  • YAML格式:层次结构清晰,更易读

3.1.1 YAML配置文件示例

application.yaml

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /demo

# 应用信息配置
spring:
  application:
    name: spring-boot-tutorial
  
  # 日志配置
  logging:
    level:
      root: INFO
      com.lihaozhe: DEBUG

# 自定义配置
app:
  title: Spring Boot 3.5.6 Tutorial
  version: 1.0.0
  features:
    - Web
    - Database
    - Security
  contact:
    email: support@lihaozhe.com
    phone: 123-456-7890

3.1.2 两种配置文件的对比

特性propertiesYAML
语法键值对,使用.分隔层次结构,使用缩进
可读性中等高,尤其对于复杂配置
列表支持繁琐简洁
多环境支持支持支持
流行度传统,应用广泛现代,越来越流行

3.2 配置注入方式

Spring Boot提供了多种方式将配置注入到Bean中:

3.2.1 使用@Value注解

package com.lihaozhe.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 使用@Value注解注入配置的示例类
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Component
public class ValueConfig {

  // 注入简单值
  @Value("${app.title}")
  private String appTitle;

  // 注入简单值,提供默认值
  @Value("${app.copyright:Copyright 2025 lihaozhe.com}")
  private String appCopyright;

  // 注入系统属性
  @Value("${java.version}")
  private String javaVersion;

  // 注入环境变量
  @Value("${USERNAME:default-user}")
  private String username;

  // Getter方法
  public String getAppTitle() {
    return appTitle;
  }

  public String getAppCopyright() {
    return appCopyright;
  }

  public String getJavaVersion() {
    return javaVersion;
  }

  public String getUsername() {
    return username;
  }
}

3.2.2 使用@ConfigurationProperties

package com.lihaozhe.config;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 使用@ConfigurationProperties注入配置的示例类
 * prefix = "app" 表示绑定配置中以app为前缀的属性
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Component
@ConfigurationProperties(prefix = "app")
public class AppConfig {

  // 应用标题
  private String title;

  // 应用版本
  private String version;

  // 应用特性列表
  private List<String> features;

  // 联系信息内部类
  private Contact contact;

  // Getter和Setter方法
  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getVersion() {
    return version;
  }

  public void setVersion(String version) {
    this.version = version;
  }

  public List<String> getFeatures() {
    return features;
  }

  public void setFeatures(List<String> features) {
    this.features = features;
  }

  public Contact getContact() {
    return contact;
  }

  public void setContact(Contact contact) {
    this.contact = contact;
  }

  /**
   * 联系信息内部类
   * 对应配置中的app.contact层级
   */
  public static class Contact {
    private String email;
    private String phone;

    // Getter和Setter方法
    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }

    public String getPhone() {
      return phone;
    }

    public void setPhone(String phone) {
      this.phone = phone;
    }
  }
}

3.2.3 配置注入控制器示例

package com.lihaozhe.controller;

import com.lihaozhe.config.AppConfig;
import com.lihaozhe.config.ValueConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 展示配置注入效果的控制器
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/config")
//@RequiredArgsConstructor
public class ConfigController {
  // 注入使用@ConfigurationProperties的配置类
  private final AppConfig appConfig;

  // 注入使用@Value的配置类
  private final ValueConfig valueConfig;

  /**
   * 构造函数注入
   *
   * @param appConfig   使用@ConfigurationProperties的配置类
   * @param valueConfig 使用@Value的配置类
   */
  @Autowired
  public ConfigController(AppConfig appConfig, ValueConfig valueConfig) {
    this.appConfig = appConfig;
    this.valueConfig = valueConfig;
  }

  /**
   * 展示@ConfigurationProperties注入的配置
   *
   * @return 配置信息
   */
  @GetMapping("/app")
  public AppConfig getAppConfig() {
    return appConfig;
  }

  /**
   * 展示@Value注入的配置
   *
   * @return 配置信息
   */
  @GetMapping("/value")
  public String getValueConfig() {
    // 使用文本块构建响应信息
    return """
        App Title: %s
        App Copyright: %s
        Java Version: %s
        Username: %s
        """.formatted(
        valueConfig.getAppTitle(),
        valueConfig.getAppCopyright(),
        valueConfig.getJavaVersion(),
        valueConfig.getUsername()
    );
  }
}

3.3 多环境配置

Spring Boot支持为不同环境(开发、测试、生产等)提供不同的配置。

3.3.1 多环境配置文件命名

  • 开发环境:application-dev.yml
  • 测试环境:application-test.yml
  • 生产环境:application-prod.yml

3.3.2 开发环境配置

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /

# 应用信息配置
spring:
  application:
    name: spring-boot-tutorial
  # 开发环境数据库配置
  datasource:
    url: jdbc:mysql://36.41.67.11:3306/dev_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: lihaozhe
    driver-class-name: com.mysql.cj.jdbc.Driver
  # 日志配置
  logging:
    level:
      root: INFO
      com.lihaozhe: DEBUG

# 自定义配置
app:
  title: Spring Boot 3.5.6 Tutorial
  version: 1.0.0
  features:
    - Web
    - Database
    - Security
  contact:
    email: support@lihaozhe.com
    phone: 123-456-7890

3.3.3 生产环境配置

# 服务器配置
server:
  port: 80
  servlet:
    context-path: /

# 应用信息配置
spring:
  application:
    name: spring-boot-tutorial
  # 开发环境数据库配置
  datasource:
    url: jdbc:mysql://ALIYUN:3306/prod_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: lihaozhe
    driver-class-name: com.mysql.cj.jdbc.Driver
  # 日志配置
  logging:
    level:
      root: INFO
      com.lihaozhe: DEBUG

# 自定义配置
app:
  title: Spring Boot 3.5.6 Tutorial
  version: 1.0.0
  features:
    - Web
    - Database
    - Security
  contact:
    email: support@lihaozhe.com
    phone: 123-456-7890

3.3.4 激活特定环境

在主配置文件中指定激活的环境:

# 在application.yml中添加
spring:
  profiles:
    active: dev  # 激活开发环境

或者通过命令行参数指定:

# 运行时指定生产环境
java -jar springboot-tutorial-0.0.1.jar --server.port=8888 --spring.profiles.active=prod

3.4 外部化配置

Spring Boot支持多种外部化配置方式,优先级从高到低:

  1. 命令行参数
  2. 操作系统环境变量
  3. application-{profile}.properties/yaml
  4. application.properties/yaml

3.4.1 命令行参数示例

# 运行时指定端口和环境
java -jar springboot-tutorial-0.0.1.jar --server.port=8888 --spring.profiles.active=dev

3.4.2 环境变量示例

在Linux/Mac中:

# 设置环境变量
export SPRING_PROFILES_ACTIVE=prod
export SERVER_PORT=8081

# 运行应用
java -jar springboot-tutorial-0.0.1.jar 

在Windows中:

# 设置环境变量
set SPRING_PROFILES_ACTIVE=prod
set SERVER_PORT=8081

# 运行应用
java -jar springboot-tutorial-0.0.1.jar 

3.5 配置优先级演示控制器

package com.lihaozhe.controller;

import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 展示当前环境配置的控制器
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
public class ProfileController {
  // 注入环境对象,用于获取当前激活的环境
  private final Environment environment;

  /**
   * 构造函数注入
   *
   * @param environment 环境对象
   */
  public ProfileController(Environment environment) {
    this.environment = environment;
  }

  /**
   * 获取当前激活的环境配置
   *
   * @return 环境信息
   */
  @GetMapping("/profile")
  public Map<String, Object> getCurrentProfile() {
    Map<String, Object> profileInfo = new HashMap<>();

    // 获取当前激活的环境
    profileInfo.put("activeProfiles", environment.getActiveProfiles());

    // 获取服务器端口
    profileInfo.put("serverPort", environment.getProperty("server.port"));

    // 获取数据库URL
    profileInfo.put("databaseUrl", environment.getProperty("spring.datasource.url"));

    return profileInfo;
  }
}

3.6 本章pom.xml文件

<?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>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.6</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.lihaozhe</groupId>
  <artifactId>springboot-tutorial</artifactId>
  <version>0.0.1</version>
  <name>springboot-tutorial</name>
  <description>springboot-tutorial</description>
  
  <properties>
    <java.version>25</java.version>
  </properties>
  <dependencies>
    <!-- Spring Web Starter -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- 配置处理器,用于@ConfigurationProperties的自动提示 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>

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

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

  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <annotationProcessorPaths>
            <path>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-configuration-processor</artifactId>
            </path>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </path>
          </annotationProcessorPaths>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

3.7 前端页面展示配置信息

我们创建一个简单的HTML页面,放在src/main/resources/static目录下:

config.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Spring Boot Configuration</title>
  <!--<script src="https://cdn.tailwindcss.com"></script>-->
  <!--<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">-->
  <script src="./js/tailwindcss.js"></script>
  <link href="./css/font-awesome.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
<div class="container mx-auto px-4 py-8 max-w-5xl">
  <header class="mb-8">
    <h1 class="text-4xl font-bold text-center text-blue-600 mb-2">
      <i class="fa fa-cogs mr-2"></i>Spring Boot Configuration
    </h1>
    <p class="text-center text-gray-600">Demonstrating configuration features in Spring Boot 3.5.6</p>
  </header>

  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
    <!-- 应用配置卡片 -->
    <div class="bg-white rounded-lg shadow-md p-6">
      <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
        <i class="fa fa-info-circle text-blue-500 mr-2"></i>Application Config
      </h2>
      <div id="appConfig" class="space-y-2 text-gray-700">
        <p class="text-gray-500"><i class="fa fa-spinner fa-spin mr-2"></i>Loading configuration...</p>
      </div>
    </div>

    <!-- 环境配置卡片 -->
    <div class="bg-white rounded-lg shadow-md p-6">
      <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
        <i class="fa fa-server text-green-500 mr-2"></i>Environment Info
      </h2>
      <div id="profileInfo" class="space-y-2 text-gray-700">
        <p class="text-gray-500"><i class="fa fa-spinner fa-spin mr-2"></i>Loading environment info...</p>
      </div>
    </div>

    <!-- Value配置卡片 -->
    <div class="bg-white rounded-lg shadow-md p-6 md:col-span-2">
      <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
        <i class="fa fa-tag text-purple-500 mr-2"></i>@Value Configurations
      </h2>
      <pre id="valueConfig" class="bg-gray-100 p-4 rounded overflow-x-auto text-gray-700">
<i class="fa fa-spinner fa-spin mr-2"></i>Loading @Value configurations...</pre>
    </div>
  </div>
</div>

<script>
    // 获取并显示应用配置
    fetch('/config/app')
        .then(response => response.json())
        .then(data => {
            const appConfigDiv = document.getElementById('appConfig');
            appConfigDiv.innerHTML = `
          <p><strong>Title:</strong> ${data.title}</p>
          <p><strong>Version:</strong> ${data.version}</p>
          <p><strong>Features:</strong></p>
          <ul class="list-disc pl-5">
            ${data.features.map(feature => `<li>${feature}</li>`).join('')}
          </ul>
          <p><strong>Contact Email:</strong> ${data.contact.email}</p>
          <p><strong>Contact Phone:</strong> ${data.contact.phone}</p>
        `;
        })
        .catch(error => {
            document.getElementById('appConfig').innerHTML =
                `<p class="text-red-500"><i class="fa fa-exclamation-circle mr-1"></i>Error loading config: ${error.message}</p>`;
        });

    // 获取并显示环境信息
    fetch('/profile')
        .then(response => response.json())
        .then(data => {
            const profileDiv = document.getElementById('profileInfo');
            profileDiv.innerHTML = `
          <p><strong>Active Profiles:</strong> ${data.activeProfiles.join(', ') || 'default'}</p>
          <p><strong>Server Port:</strong> ${data.serverPort}</p>
          <p><strong>Database URL:</strong> ${data.databaseUrl}</p>
        `;
        })
        .catch(error => {
            document.getElementById('profileInfo').innerHTML =
                `<p class="text-red-500"><i class="fa fa-exclamation-circle mr-1"></i>Error loading profile: ${error.message}</p>`;
        });

    // 获取并显示@Value配置
    fetch('/config/value')
        .then(response => response.text())
        .then(text => {
            document.getElementById('valueConfig').textContent = text;
        })
        .catch(error => {
            document.getElementById('valueConfig').innerHTML =
                `<span class="text-red-500"><i class="fa fa-exclamation-circle mr-1"></i>Error loading @Value config: ${error.message}</span>`;
        });
</script>
</body>
</html>

3.8 测试配置注入

package com.lihaozhe.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;

/**
 * 测试配置注入功能的测试类
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@SpringBootTest
class ConfigTest {

  // 注入环境对象
  @Autowired
  private Environment environment;

  // 注入使用@ConfigurationProperties的配置类
  @Autowired
  private AppConfig appConfig;

  // 注入使用@Value的配置类
  @Autowired
  private ValueConfig valueConfig;

  /**
   * 测试环境配置
   */
  @Test
  void testEnvironment() {
    // 验证服务器端口配置
    String port = environment.getProperty("server.port");
    assertEquals("8080", port);

    // 验证激活的环境
    String[] activeProfiles = environment.getActiveProfiles();
    assertTrue(activeProfiles.length > 0);
    assertEquals("dev", activeProfiles[0]);
  }

  /**
   * 测试@ConfigurationProperties配置注入
   */
  @Test
  void testAppConfig() {
    // 验证简单属性
    assertEquals("Spring Boot 3.5.6 Tutorial", appConfig.getTitle());
    assertEquals("1.0.0", appConfig.getVersion());

    // 验证列表属性
    assertNotNull(appConfig.getFeatures());
    assertEquals(3, appConfig.getFeatures().size());
    assertTrue(appConfig.getFeatures().contains("Web"));

    // 验证嵌套属性
    assertNotNull(appConfig.getContact());
    assertEquals("support@lihaozhe.com", appConfig.getContact().getEmail());
    assertEquals("123-456-7890", appConfig.getContact().getPhone());
  }

  /**
   * 测试@Value配置注入
   */
  @Test
  void testValueConfig() {
    // 验证注入的属性
    assertEquals("Spring Boot 3.5.6 Tutorial", valueConfig.getAppTitle());
    assertEquals("Copyright 2025 lihaozhe.com", valueConfig.getAppCopyright());

    // 验证系统属性注入
    assertNotNull(valueConfig.getJavaVersion());
    assertTrue(valueConfig.getJavaVersion().startsWith("25"));
  }
}

3.9 开发思路总结

  1. 配置文件选择:根据项目复杂度选择properties或YAML格式,复杂配置优先选择YAML
  2. 配置注入策略
    • 简单配置使用@Value注解
    • 复杂配置或相关配置组使用@ConfigurationProperties
  3. 多环境管理
    • 为不同环境创建独立配置文件
    • 通过spring.profiles.active指定激活环境
    • 敏感信息(如密码)应使用环境变量或配置中心
  4. 配置优先级:了解不同配置方式的优先级,避免配置冲突
  5. 配置验证:编写测试验证配置是否正确注入

通过本章学习,你应该掌握了Spring Boot的配置体系,能够根据实际需求选择合适的配置方式,并能灵活管理不同环境的配置。


第4章:Spring Boot Web开发

4.1 Spring Boot Web核心组件

Spring Boot Web基于Spring MVC,提供了以下核心组件:

  • 控制器(Controller):处理HTTP请求
  • 处理器映射(Handler Mapping):将请求映射到控制器方法
  • 视图解析器(View Resolver):解析视图
  • 拦截器(Interceptor):处理请求前后的逻辑
  • 过滤器(Filter):对请求进行过滤处理

4.2 RESTful API开发

RESTful API是一种软件架构风格,用于创建可扩展的Web服务。

4.2.1 创建实体类

package com.lihaozhe.entity;

import java.time.LocalDateTime;

/**
 * 用户实体类
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public class User {
  // 用户ID
  private Long id;

  // 用户名
  private String username;

  // 电子邮件
  private String email;

  // 创建时间
  private LocalDateTime createdAt;

  // 无参构造函数
  public User() {
  }

  // 带参构造函数
  public User(String username, String email, LocalDateTime createdAt) {
    this.username = username;
    this.email = email;
    this.createdAt = createdAt;
  }

  // 全参构造函数
  public User(Long id, String username, String email) {
    this.id = id;
    this.username = username;
    this.email = email;
    this.createdAt = LocalDateTime.now();
  }

  // Getter和Setter方法
  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public LocalDateTime getCreatedAt() {
    return createdAt;
  }

  public void setCreatedAt(LocalDateTime createdAt) {
    this.createdAt = createdAt;
  }
}

4.2.2 创建服务层

用户服务接口

package com.lihaozhe.service;

import com.lihaozhe.entity.User;

import java.util.List;
import java.util.Optional;

/**
 * 用户服务接口
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public interface UserService {
  /**
   * 获取所有用户
   *
   * @return 用户列表
   */
  List<User> getAllUsers();

  /**
   * 根据ID获取用户
   *
   * @param id 用户ID
   * @return 可选的用户对象
   */
  Optional<User> getUserById(Long id);

  /**
   * 创建新用户
   *
   * @param user 用户对象
   * @return 创建的用户
   */
  User createUser(User user);

  /**
   * 更新用户
   *
   * @param id 用户ID
   * @param user 用户对象
   * @return 更新后的用户
   */
  Optional<User> updateUser(Long id, User user);

  /**
   * 删除用户
   *
   * @param id 用户ID
   * @return 是否删除成功
   */
  boolean deleteUser(Long id);
}

用户服务接口实现类

package com.lihaozhe.service.impl;

import com.lihaozhe.entity.User;
import com.lihaozhe.service.UserService;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户服务实现类
 * 使用内存Map模拟数据库存储
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Service
public class UserServiceImpl implements UserService {
  // 内存存储用户数据
  private final Map<Long, User> users = new ConcurrentHashMap<>();
  // 自增ID生成器
  private final AtomicLong idGenerator = new AtomicLong(1);

  // 初始化一些测试数据
  public UserServiceImpl() {
    users.put(1L, new User(1L, "admin", "admin@lihaozhe.com"));
    users.put(2L, new User(2L, "user1", "user1@lihaozhe.com"));
    users.put(3L, new User(3L, "user2", "user2@lihaozhe.com"));
    idGenerator.set(4);
  }

  /**
   * 获取所有用户
   */
  @Override
  public List<User> getAllUsers() {
    return new ArrayList<>(users.values());
  }

  /**
   * 根据ID获取用户
   */
  @Override
  public Optional<User> getUserById(Long id) {
    return Optional.ofNullable(users.get(id));
  }

  /**
   * 创建新用户
   */
  @Override
  public User createUser(User user) {
    // 生成新ID
    Long id = idGenerator.getAndIncrement();
    user.setId(id);
    user.setCreatedAt(LocalDateTime.now());
    users.put(id, user);
    return user;
  }

  /**
   * 更新用户
   */
  @Override
  public Optional<User> updateUser(Long id, User user) {
    // 检查用户是否存在
    if (!users.containsKey(id)) {
      return Optional.empty();
    }

    // 更新用户信息
    User existingUser = users.get(id);
    if (user.getUsername() != null) {
      existingUser.setUsername(user.getUsername());
    }
    if (user.getEmail() != null) {
      existingUser.setEmail(user.getEmail());
    }

    users.put(id, existingUser);
    return Optional.of(existingUser);
  }

  /**
   * 删除用户
   */
  @Override
  public boolean deleteUser(Long id) {
    // 移除用户并返回是否成功
    return users.remove(id) != null;
  }
}

4.2.3 创建REST控制器

package com.lihaozhe.controller;

import com.lihaozhe.entity.User;
import com.lihaozhe.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

/**
 * 用户REST控制器
 * 处理所有与用户相关的HTTP请求
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/users")
public class UserController {
  // 用户服务
  private final UserService userService;

  /**
   * 构造函数注入
   *
   * @param userService 用户服务
   */
  public UserController(UserService userService) {
    this.userService = userService;
  }

  /**
   * 获取所有用户
   *
   * @return 用户列表
   */
  @GetMapping
  public List<User> getAllUsers() {
    return userService.getAllUsers();
  }

  /**
   * 根据ID获取用户
   *
   * @param id 用户ID
   * @return 响应实体,包含用户或404状态
   */
  @GetMapping("/{id}")
  public ResponseEntity<User> getUserById(@PathVariable("id") Long id) {
    return userService.getUserById(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
  }

  /**
   * 创建新用户
   *
   * @param user 用户对象
   * @return 响应实体,包含创建的用户和201状态
   */
  @PostMapping()
  public ResponseEntity<User> createUser(@RequestBody User user) {
    User createdUser = userService.createUser(user);
    // 返回201 Created和创建的用户,Location头包含新资源的URL
    return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
  }

  /**
   * 更新用户
   *
   * @param id   用户ID
   * @param user 更新的用户信息
   * @return 响应实体,包含更新后的用户或404状态
   */
  @PutMapping("/{id}")
  public ResponseEntity<User> updateUser(
      @PathVariable Long id,
      @RequestBody User user) {
    // 更新用户
    return userService.updateUser(id, user)
        // 如果更新成功,返回200 OK和更新后的用户
        .map(ResponseEntity::ok)
        // 如果用户不存在,返回404 Not Found
        .orElseGet(() -> ResponseEntity.notFound().build());
  }

  /**
   * 删除用户
   *
   * @param id 用户ID
   * @return 响应实体,204状态表示成功,404表示用户不存在
   */
  @DeleteMapping("/{id}")
  public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    // 检查用户是否存在
    if (userService.deleteUser(id)) {
      // 删除成功,返回204 No Content
      return ResponseEntity.noContent().build();
    } else {
      // 用户不存在,返回404 Not Found
      return ResponseEntity.notFound().build();
    }
  }
}

4.3 请求参数处理

Spring Boot提供了多种处理请求参数的方式:

package com.lihaozhe.controller;

import java.util.Map;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 展示不同请求参数处理方式的控制器
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/params")
public class RequestParamController {

  /**
   * 处理查询参数
   *
   * @param name 名称参数,可为空
   * @param age  年龄参数,有默认值
   * @return 参数信息
   */
  @GetMapping("/query")
  public String handleQueryParams(
      @RequestParam(required = false) String name,
      @RequestParam(defaultValue = "18") int age) {
    return "Name: " + name + ", Age: " + age;
  }

  /**
   * 处理路径变量
   *
   * @param id     用户ID
   * @param action 操作名称
   * @return 路径变量信息
   */
  @GetMapping("/path/{id}/{action}")
  public String handlePathVariables(
      @PathVariable Long id,
      @PathVariable String action) {
    return "ID: " + id + ", Action: " + action;
  }

  /**
   * 处理请求头
   *
   * @param userAgent 用户代理
   * @param accept    接受的内容类型
   * @return 请求头信息
   */
  @GetMapping("/headers")
  public String handleHeaders(
      @RequestHeader("User-Agent") String userAgent,
      @RequestHeader("Accept") String accept) {
    return "User-Agent: " + userAgent + "\nAccept: " + accept;
  }

  /**
   * 获取所有请求参数
   *
   * @param params 所有请求参数的Map
   * @return 参数信息
   */
  @GetMapping("/all")
  public String handleAllParams(@RequestParam Map<String, String> params) {
    return "Parameters: " + params.toString();
  }
}

4.4 全局异常处理

4.4.1 资源未找到异常类

package com.lihaozhe.exception;

import java.io.Serial;

/**
 * 自定义资源未找到异常
 * 当请求的资源不存在时抛出
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public class ResourceNotFoundException extends RuntimeException {

  @Serial
  private static final long serialVersionUID = -7020305706345070391L;

  /**
   * 构造函数
   *
   * @param message 异常消息
   */
  public ResourceNotFoundException(String message) {
    super(message);
  }

  /**
   * 构造函数,根据资源名称和ID生成消息
   *
   * @param resourceName 资源名称
   * @param fieldName    字段名称
   * @param fieldValue   字段值
   */
  public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
    super("%s not found with %s : '%s'".formatted(resourceName, fieldName, fieldValue));
  }
}


4.4.2 全局异常处理器

package com.lihaozhe.exception;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

/**
 * 全局异常处理器
 * 统一处理应用中的异常
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

  /**
   * 处理资源未找到异常
   *
   * @param ex      异常对象
   * @param request Web请求
   * @return 包含错误信息的响应实体
   */
  @ExceptionHandler(ResourceNotFoundException.class)
  public ResponseEntity<Object> handleResourceNotFoundException(
      ResourceNotFoundException ex, WebRequest request) {

    // 构建错误响应体
    Map<String, Object> body = new HashMap<>();
    body.put("timestamp", LocalDateTime.now());
    body.put("status", HttpStatus.NOT_FOUND.value());
    body.put("error", "Not Found");
    body.put("message", ex.getMessage());
    body.put("path", request.getDescription(false).replace("uri=", ""));

    return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
  }

  /**
   * 处理请求参数验证异常
   *
   * @param ex      异常对象
   * @param request Web请求
   * @return 包含错误信息的响应实体
   */
  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<Object> handleMethodArgumentNotValid(
      MethodArgumentNotValidException ex, WebRequest request) {

    // 收集所有验证错误
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors().forEach(error ->
        errors.put(error.getField(), error.getDefaultMessage()));

    // 构建错误响应体
    Map<String, Object> body = new HashMap<>();
    body.put("timestamp", LocalDateTime.now());
    body.put("status", HttpStatus.BAD_REQUEST.value());
    body.put("error", "Bad Request");
    body.put("message", "Validation failed");
    body.put("errors", errors);
    body.put("path", request.getDescription(false).replace("uri=", ""));

    return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
  }

  /**
   * 处理所有其他未捕获的异常
   *
   * @param ex      异常对象
   * @param request Web请求
   * @return 包含错误信息的响应实体
   */
  @ExceptionHandler(Exception.class)
  public ResponseEntity<Object> handleGlobalException(
      Exception ex, WebRequest request) {

    // 构建错误响应体
    Map<String, Object> body = new HashMap<>();
    body.put("timestamp", LocalDateTime.now());
    body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
    body.put("error", "Internal Server Error");
    body.put("message", ex.getMessage());
    body.put("path", request.getDescription(false).replace("uri=", ""));

    return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
  }
}

4.5 数据验证

使用JSR-303/JSR-380进行数据验证:

4.5.1 带数据验证的用户实体类

package com.lihaozhe.entity;

import jakarta.validation.constraints.*;

import java.time.LocalDate;

/**
 * 带有数据验证注解的用户实体类
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public class ValidatedUser {
  private Long id;

  // 用户名不能为空,长度在3-50之间
  @NotBlank(message = "Username is required")
  @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
  private String username;

  // 电子邮件必须符合格式
  @NotBlank(message = "Email is required")
  @Email(message = "Email should be valid")
  private String email;

  // 出生日期必须是过去的日期
  @NotNull(message = "Date of birth is required")
  @Past(message = "Date of birth must be in the past")
  private LocalDate dateOfBirth;

  // Getter和Setter方法
  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public LocalDate getDateOfBirth() {
    return dateOfBirth;
  }

  public void setDateOfBirth(LocalDate dateOfBirth) {
    this.dateOfBirth = dateOfBirth;
  }
}

4.5.2 数据验证控制器

package com.lihaozhe.controller;

import com.lihaozhe.entity.ValidatedUser;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


import jakarta.validation.Valid;

/**
 * 处理数据验证的控制器
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/validation")
public class ValidationController {

  /**
   * 接收并验证用户数据
   *
   * @param user 带有验证注解的用户对象
   * @return 验证通过的用户对象
   */
  @PostMapping("/users")
  public ResponseEntity<ValidatedUser> createValidatedUser(@Valid @RequestBody ValidatedUser user) {
    // 如果验证通过,会执行到这里
    // 实际应用中可能会保存用户到数据库
    return ResponseEntity.ok(user);
  }
}

4.6 本章pom.xml文件

<?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">
  <!-- 模型版本,固定为4.0.0 -->
  <modelVersion>4.0.0</modelVersion>

  <!-- 继承Spring Boot的父工程,统一管理依赖版本 -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.6</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>

  <!-- 项目基本信息 -->
  <groupId>com.lihaozhe</groupId>
  <artifactId>springboot-tutorial</artifactId>
  <version>1.0.0</version>
  <name>springboot-tutorial</name>
  <description>springboot-tutorial</description>

  <properties>
    <!-- JDK版本配置 -->
    <java.version>25</java.version>
  </properties>
  <dependencies>
    <!-- Spring Web Starter:包含Spring MVC和嵌入式Tomcat -->
    <!-- Spring Web Starter -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

    <!-- 配置处理器,用于@ConfigurationProperties的自动提示 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>

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

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

    <!-- JSON处理器-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>
  </dependencies>
  <!-- 构建配置 -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <annotationProcessorPaths>
            <path>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-configuration-processor</artifactId>
            </path>
            <path>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </path>
          </annotationProcessorPaths>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

4.7 前端页面测试REST API

api-test.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>REST API Test</title>
  <!--<script src="https://cdn.tailwindcss.com"></script>-->
  <!--<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">-->
  <script src="./js/tailwindcss.js"></script>
  <link href="./css/font-awesome.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
<div class="container mx-auto px-4 py-8 max-w-6xl">
  <header class="mb-8">
    <h1 class="text-4xl font-bold text-center text-blue-600 mb-2">
      <i class="fa fa-exchange mr-2"></i>REST API Test Tool
    </h1>
    <p class="text-center text-gray-600">Testing Spring Boot RESTful API endpoints</p>
  </header>

  <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
    <!-- API导航 -->
    <div class="lg:col-span-1">
      <div class="bg-white rounded-lg shadow-md p-4 sticky top-4">
        <h2 class="text-xl font-semibold text-gray-800 mb-4">API Endpoints</h2>

        <div class="space-y-2">
          <button class="api-btn w-full text-left px-4 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700"
                  data-endpoint="/api/users" data-method="GET">
            <span class="inline-block w-10 text-center bg-green-500 text-white rounded mr-2">GET</span>
            /api/users
          </button>

          <button class="api-btn w-full text-left px-4 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700"
                  data-endpoint="/api/users/1" data-method="GET">
            <span class="inline-block w-10 text-center bg-green-500 text-white rounded mr-2">GET</span>
            /api/users/{id}
          </button>

          <button class="api-btn w-full text-left px-4 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700"
                  data-endpoint="/api/users" data-method="POST">
            <span class="inline-block w-10 text-center bg-purple-500 text-white rounded mr-2">POST</span>
            /api/users
          </button>

          <button class="api-btn w-full text-left px-4 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700"
                  data-endpoint="/api/users/1" data-method="PUT">
            <span class="inline-block w-10 text-center bg-yellow-500 text-white rounded mr-2">PUT</span>
            /api/users/{id}
          </button>

          <button class="api-btn w-full text-left px-4 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700"
                  data-endpoint="/api/users/1" data-method="DELETE">
            <span class="inline-block w-10 text-center bg-red-500 text-white rounded mr-2">DELETE</span>
            /api/users/{id}
          </button>

          <button class="api-btn w-full text-left px-4 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700"
                  data-endpoint="/validation/users" data-method="POST">
            <span class="inline-block w-10 text-center bg-purple-500 text-white rounded mr-2">POST</span>
            /validation/users
          </button>
        </div>
      </div>
    </div>

    <!-- API测试区域 -->
    <div class="lg:col-span-2 space-y-6">
      <!-- 请求区域 -->
      <div class="bg-white rounded-lg shadow-md p-6">
        <div class="flex items-center mb-4">
          <select id="method" class="bg-gray-100 px-4 py-2 rounded mr-4 font-bold">
            <option value="GET">GET</option>
            <option value="POST">POST</option>
            <option value="PUT">PUT</option>
            <option value="DELETE">DELETE</option>
          </select>
          <input type="text" id="endpoint" class="flex-grow px-4 py-2 border border-gray-300 rounded"
                 value="/api/users">
          <button id="sendBtn" class="ml-4 bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded">
            <i class="fa fa-paper-plane mr-1"></i>Send
          </button>
        </div>

        <div id="requestBodyContainer" class="mt-4">
          <h3 class="text-lg font-semibold mb-2">Request Body</h3>
          <textarea id="requestBody" class="w-full p-4 border border-gray-300 rounded h-40 font-mono text-sm">
{
  "username": "newuser",
  "email": "newuser@lihaozhe.com"
}
            </textarea>
        </div>
      </div>

      <!-- 响应区域 -->
      <div class="bg-white rounded-lg shadow-md p-6">
        <div class="flex justify-between items-center mb-4">
          <h3 class="text-lg font-semibold">Response</h3>
          <span id="statusCode" class="px-3 py-1 rounded text-white bg-gray-500">
              Status: --
            </span>
        </div>
        <div id="responseTime" class="text-sm text-gray-500 mb-2">
          Response time: -- ms
        </div>
        <pre id="responseBody" class="w-full p-4 bg-gray-100 rounded h-64 overflow-auto font-mono text-sm">
Waiting for request...
          </pre>
      </div>
    </div>
  </div>
</div>

<script>
    // DOM元素
    const methodSelect = document.getElementById('method');
    const endpointInput = document.getElementById('endpoint');
    const requestBodyTextarea = document.getElementById('requestBody');
    const requestBodyContainer = document.getElementById('requestBodyContainer');
    const sendBtn = document.getElementById('sendBtn');
    const statusCodeSpan = document.getElementById('statusCode');
    const responseTimeSpan = document.getElementById('responseTime');
    const responseBodyPre = document.getElementById('responseBody');
    const apiButtons = document.querySelectorAll('.api-btn');

    // 根据HTTP方法显示/隐藏请求体
    function toggleRequestBody() {
        const method = methodSelect.value;
        if (method === 'GET' || method === 'DELETE') {
            requestBodyContainer.classList.add('hidden');
        } else {
            requestBodyContainer.classList.remove('hidden');
        }
    }

    // 发送API请求
    async function sendRequest() {
        const method = methodSelect.value;
        const endpoint = endpointInput.value;

        // 重置响应区域
        statusCodeSpan.textContent = 'Loading...';
        statusCodeSpan.className = 'px-3 py-1 rounded text-white bg-yellow-500';
        responseTimeSpan.textContent = 'Response time: calculating...';
        responseBodyPre.textContent = 'Sending request...';

        try {
            // 记录开始时间
            const startTime = performance.now();

            // 配置请求选项
            const options = {
                method: method,
                headers: {
                    'Content-Type': 'application/json',
                }
            };

            // 对于需要请求体的方法,添加请求体
            if (method === 'POST' || method === 'PUT') {
                try {
                    // 验证JSON格式
                    const body = JSON.parse(requestBodyTextarea.value.trim() || '{}');
                    options.body = JSON.stringify(body);
                } catch (e) {
                    throw new Error('Invalid JSON in request body: ' + e.message);
                }
            }

            // 发送请求
            const response = await fetch(endpoint, options);

            // 计算响应时间
            const endTime = performance.now();
            const responseTime = Math.round(endTime - startTime);

            // 更新状态码
            statusCodeSpan.textContent = `Status: ${response.status} ${response.statusText}`;

            // 根据状态码设置颜色
            if (response.ok) {
                statusCodeSpan.className = 'px-3 py-1 rounded text-white bg-green-500';
            } else if (response.status >= 400 && response.status < 500) {
                statusCodeSpan.className = 'px-3 py-1 rounded text-white bg-orange-500';
            } else {
                statusCodeSpan.className = 'px-3 py-1 rounded text-white bg-red-500';
            }

            // 更新响应时间
            responseTimeSpan.textContent = `Response time: ${responseTime} ms`;

            // 解析响应体
            const contentType = response.headers.get('content-type');
            let responseBody;

            if (contentType && contentType.includes('application/json')) {
                responseBody = await response.json();
                // 格式化JSON
                responseBodyPre.textContent = JSON.stringify(responseBody, null, 2);
            } else {
                responseBody = await response.text();
                responseBodyPre.textContent = responseBody;
            }

        } catch (error) {
            // 处理错误
            statusCodeSpan.textContent = 'Error';
            statusCodeSpan.className = 'px-3 py-1 rounded text-white bg-red-500';
            responseTimeSpan.textContent = 'Response time: N/A';
            responseBodyPre.textContent = 'Error: ' + error.message;
        }
    }

    // 绑定事件监听器
    methodSelect.addEventListener('change', toggleRequestBody);
    sendBtn.addEventListener('click', sendRequest);

    // API按钮点击事件
    apiButtons.forEach(button => {
        button.addEventListener('click', () => {
            const endpoint = button.getAttribute('data-endpoint');
            const method = button.getAttribute('data-method');

            // 更新表单
            endpointInput.value = endpoint;
            methodSelect.value = method;

            // 切换请求体显示
            toggleRequestBody();

            // 设置默认请求体
            if (method === 'POST' && endpoint === '/api/users') {
                requestBodyTextarea.value = `{
  "username": "newuser",
  "email": "newuser@lihaozhe.com"
}`;
            } else if (method === 'PUT' && endpoint.startsWith('/api/users/')) {
                requestBodyTextarea.value = `{
  "username": "updateduser",
  "email": "updated@lihaozhe.com"
}`;
            } else if (method === 'POST' && endpoint === '/validation/users') {
                requestBodyTextarea.value = `{
  "username": "valid",
  "email": "valid@lihaozhe.com",
  "dateOfBirth": "2000-01-01"
}`;
            }
        });
    });

    // 初始化页面
    toggleRequestBody();

    // 按Enter键发送请求
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && e.ctrlKey) {
            sendRequest();
        }
    });
</script>
</body>
</html>

4.8 测试Web控制器

4.9 开发思路总结

  1. RESTful API设计

    • 使用合适的HTTP方法(GET、POST、PUT、DELETE)表示操作类型
    • 使用URL路径表示资源
    • 使用HTTP状态码表示请求结果
    • 返回一致的JSON响应格式
  2. 分层架构

    • 控制器(Controller):处理HTTP请求,返回响应
    • 服务层(Service):包含业务逻辑
    • 实体类(Entity):表示数据模型
  3. 异常处理

    • 使用@ControllerAdvice创建全局异常处理器
    • 自定义异常类表示特定业务异常
    • 返回包含详细信息的错误响应
  4. 数据验证

    • 使用JSR-303/JSR-380注解进行数据验证
    • 在控制器方法参数上使用@Valid触发验证
    • 统一处理验证失败的异常
  5. 测试策略

    • 使用@WebMvcTest测试控制器层
    • 使用MockMvc模拟HTTP请求
    • 使用Mockito模拟服务层依赖

通过本章学习,你应该掌握了Spring Boot Web开发的核心技能,能够设计和实现RESTful API,处理请求参数,进行数据验证,并编写相应的测试。


第5章:数据库操作 - MyBatis篇

5.1 MyBatis简介

MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。

MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

Spring Boot通过spring-boot-starter-mybatis提供了对MyBatis的自动配置支持。

5.2 准备工作

5.2.1 创建数据库和表

首先,我们需要创建一个MySQL数据库和用户表:

schema.sql

-- 创建数据库
CREATE DATABASE IF NOT EXISTS springboot_demo;

-- 使用数据库
USE springboot_demo;

-- 创建用户表
CREATE TABLE IF NOT EXISTS user (
                                    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
                                    username VARCHAR(50) NOT NULL COMMENT '用户名',
                                    email VARCHAR(100) NOT NULL COMMENT '电子邮件',
                                    password VARCHAR(100)  DEFAULT '123456' COMMENT '密码',
                                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
                                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
                                    status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
                                    UNIQUE KEY uk_username (username),
                                    UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 插入测试数据
INSERT INTO user (username, email) VALUES
                                                 ('admin', 'admin@lihaozhe.com'),
                                                 ('user1', 'user1@lihaozhe.com'),
                                                 ('user2', 'user2@lihaozhe.com');


select id, username, email, password, created_at, updated_at, status from user;

5.2.2 配置文件

application.yaml

# 服务器配置
server:
  port: 8080

# 应用配置
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/springboot_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: lihaozhe
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池配置
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000

# MyBatis配置
mybatis:
  #  mapper.xml文件位置
  mapper-locations: classpath:mybatis/mappers/*.xml
  # 实体类包路径
  type-aliases-package: com.lihaozhe.entity
  # 配置MyBatis的全局属性
  configuration:
    # 开启驼峰命名转换
    map-underscore-to-camel-case: true
    # 日志级别
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 日志配置
logging:
  level:
    com.lihaozhe: DEBUG

5.2.3 pom.xml文件

<?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>
  
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.6</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  
  <groupId>com.lihaozhe</groupId>
  <artifactId>springboot-mybatis</artifactId>
  <version>0.0.1</version>
  <name>springboot-mybatis</name>
  <description>Spring Boot MyBatis Tutorial</description>
  
  <properties>
    <java.version>25</java.version>
  </properties>
  
  <dependencies>
    <!-- Spring Web Starter -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MyBatis Starter -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>3.0.5</version>
    </dependency>
    
    <!-- MySQL驱动 -->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>
    
    <!-- 验证API -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!-- 配置处理器 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    
    <!-- lombok -->
    <dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
    
    <!-- 测试依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    
    <!-- MyBatis测试支持 -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter-test</artifactId>
      <version>3.0.5</version>
      <scope>test</scope>
    </dependency>
    
    <!-- 数据库迁移工具 -->
    <dependency>
      <groupId>org.flywaydb</groupId>
      <artifactId>flyway-core</artifactId>
    </dependency>
  </dependencies>

  <build>
		<plugins>
      <!-- Spring Boot Maven插件 -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.springframework.boot</groupId>
							<artifactId>spring-boot-configuration-processor</artifactId>
						</path>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

5.3 实现数据库操作

5.3.1 实体类

package com.lihaozhe.entity;

import java.time.LocalDateTime;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/**
 * 用户实体类,对应数据库user表
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public class User {
  
  // 用户ID
  private Long id;
  
  // 用户名
  @NotBlank(message = "用户名不能为空")
  @Size(min = 3, max = 50, message = "用户名长度必须在3-50之间")
  private String username;
  
  // 电子邮件
  @NotBlank(message = "邮箱不能为空")
  @Email(message = "邮箱格式不正确")
  private String email;
  
  // 密码
  @NotBlank(message = "密码不能为空")
  @Size(min = 6, message = "密码长度不能少于6位")
  private String password;
  
  // 创建时间
  private LocalDateTime createdAt;
  
  // 更新时间
  private LocalDateTime updatedAt;
  
  // 状态:1-正常,0-禁用
  private Integer status;
  
  // Getter和Setter方法
  public Long getId() {
    return id;
  }
  
  public void setId(Long id) {
    this.id = id;
  }
  
  public String getUsername() {
    return username;
  }
  
  public void setUsername(String username) {
    this.username = username;
  }
  
  public String getEmail() {
    return email;
  }
  
  public void setEmail(String email) {
    this.email = email;
  }
  
  public String getPassword() {
    return password;
  }
  
  public void setPassword(String password) {
    this.password = password;
  }
  
  public LocalDateTime getCreatedAt() {
    return createdAt;
  }
  
  public void setCreatedAt(LocalDateTime createdAt) {
    this.createdAt = createdAt;
  }
  
  public LocalDateTime getUpdatedAt() {
    return updatedAt;
  }
  
  public void setUpdatedAt(LocalDateTime updatedAt) {
    this.updatedAt = updatedAt;
  }
  
  public Integer getStatus() {
    return status;
  }
  
  public void setStatus(Integer status) {
    this.status = status;
  }
}

5.3.2 Mapper接口

package com.lihaozhe.mapper;

import java.util.List;
import java.util.Optional;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.lihaozhe.entity.User;

/**
 * 用户Mapper接口
 * 定义与用户相关的数据库操作
 */
@Mapper
public interface UserMapper {
  
  /**
   * 查询所有用户
   * 
   * @return 用户列表
   */
  List<User> findAll();
  
  /**
   * 根据ID查询用户
   * 
   * @param id 用户ID
   * @return 可选的用户对象
   */
  Optional<User> findById(@Param("id") Long id);
  
  /**
   * 根据用户名查询用户
   * 
   * @param username 用户名
   * @return 可选的用户对象
   */
  Optional<User> findByUsername(@Param("username") String username);
  
  /**
   * 插入新用户
   * 
   * @param user 用户对象
   * @return 影响的行数
   */
  int insert(User user);
  
  /**
   * 更新用户信息
   * 
   * @param user 用户对象
   * @return 影响的行数
   */
  int update(User user);
  
  /**
   * 根据ID删除用户
   * 
   * @param id 用户ID
   * @return 影响的行数
   */
  int deleteById(@Param("id") Long id);
  
  /**
   * 根据状态查询用户
   * 
   * @param status 状态:1-正常,0-禁用
   * @return 用户列表
   */
  List<User> findByStatus(@Param("status") Integer status);
}

5.3.3 Mapper XML文件

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

<!-- 命名空间对应Mapper接口 -->
<mapper namespace="com.lihaozhe.mapper.UserMapper">

  <!-- 定义结果集映射 -->
  <resultMap id="BaseResultMap" type="user">
    <id column="id" property="id" jdbcType="BIGINT"/>
    <result column="username" property="username" jdbcType="VARCHAR"/>
    <result column="email" property="email" jdbcType="VARCHAR"/>
    <result column="password" property="password" jdbcType="VARCHAR"/>
    <result column="created_at" property="createdAt" jdbcType="TIMESTAMP"/>
    <result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/>
    <result column="status" property="status" jdbcType="TINYINT"/>
  </resultMap>

  <!-- 定义基础查询字段 -->
  <sql id="Base_Column_List">
    id, username, email, password, created_at, updated_at, status
  </sql>

  <!-- 查询所有用户 -->
  <select id="findAll" resultType="user">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    ORDER BY id ASC
  </select>

  <!-- 根据ID查询用户 -->
  <select id="findById" parameterType="java.lang.Long" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE id = #{id}
  </select>

  <!-- 根据用户名查询用户 -->
  <select id="findByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE username = #{username}
  </select>

  <!-- 插入新用户 -->
  <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user
    (username, email, password, created_at, updated_at, status)
    VALUES
    (#{username}, #{email}, #{password}, NOW(), NOW(),
    <choose>
      <when test="status != null">#{status}</when>
      <otherwise>1</otherwise>
    </choose>)
  </insert>

  <!-- 更新用户信息 -->
  <update id="update" parameterType="User">
    UPDATE user
    <set>
      <if test="username != null">username = #{username},</if>
      <if test="email != null">email = #{email},</if>
      <if test="password != null">password = #{password},</if>
      <if test="status != null">status = #{status},</if>
      updated_at = NOW()
    </set>
    WHERE id = #{id}
  </update>

  <!-- 根据ID删除用户 -->
  <delete id="deleteById" parameterType="java.lang.Long">
    DELETE FROM user
    WHERE id = #{id}
  </delete>

  <!-- 根据状态查询用户 -->
  <select id="findByStatus" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE status = #{status}
    ORDER BY id ASC
  </select>
</mapper>

5.3.4 服务层

package com.lihaozhe.service;

import com.lihaozhe.entity.User;
import com.lihaozhe.exception.ResourceNotFoundException;

import java.util.List;

/**
 * 用户服务接口
 * 定义用户相关的业务逻辑
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public interface UserService {

  /**
   * 获取所有用户
   *
   * @return 用户列表
   */
  List<User> getAllUsers();

  /**
   * 根据ID获取用户
   *
   * @param id 用户ID
   * @return 用户对象
   * @throws ResourceNotFoundException 如果用户不存在
   */
  User getUserById(Long id);

  /**
   * 根据用户名获取用户
   *
   * @param username 用户名
   * @return 用户对象
   * @throws ResourceNotFoundException 如果用户不存在
   */
  User getUserByUsername(String username);

  /**
   * 创建新用户
   *
   * @param user 用户对象
   * @return 创建的用户
   */
  User createUser(User user);

  /**
   * 更新用户
   *
   * @param id 用户ID
   * @param user 用户对象
   * @return 更新后的用户
   * @throws ResourceNotFoundException 如果用户不存在
   */
  User updateUser(Long id, User user);

  /**
   * 删除用户
   *
   * @param id 用户ID
   * @throws ResourceNotFoundException 如果用户不存在
   */
  void deleteUser(Long id);

  /**
   * 根据状态获取用户
   *
   * @param status 状态:1-正常,0-禁用
   * @return 用户列表
   */
  List<User> getUsersByStatus(Integer status);
}

package com.lihaozhe.service.impl;

import com.lihaozhe.entity.User;
import com.lihaozhe.exception.ResourceNotFoundException;
import com.lihaozhe.mapper.UserMapper;
import com.lihaozhe.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 用户服务实现类
 * 实现用户相关的业务逻辑,使用MyBatis操作数据库
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Service
//@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
  // 用户Mapper
  private final UserMapper userMapper;

  /**
   * 构造函数注入
   *
   * @param userMapper 用户Mapper
   */
  public UserServiceImpl(UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  /**
   * 获取所有用户
   */
  @Override
  public List<User> getAllUsers() {
    return userMapper.findAll();
  }

  /**
   * 根据ID获取用户
   */
  @Override
  public User getUserById(Long id) {
    // 查询用户,如果不存在则抛出异常
    return userMapper.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
  }

  /**
   * 根据用户名获取用户
   */
  @Override
  public User getUserByUsername(String username) {
    // 查询用户,如果不存在则抛出异常
    return userMapper.findByUsername(username)
        .orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
  }

  /**
   * 创建新用户
   */
  @Override
  @Transactional
  public User createUser(User user) {
    // 设置默认状态为1(正常)
    if (user.getStatus() == null) {
      user.setStatus(1);
    }
    // 插入用户
    userMapper.insert(user);
    // 返回插入的用户(包含自动生成的ID)
    return user;
  }

  /**
   * 更新用户
   */
  @Override
  @Transactional
  public User updateUser(Long id, User user) {
    // 检查用户是否存在
    User existingUser = getUserById(id);

    // 更新用户信息
    if (user.getUsername() != null) {
      existingUser.setUsername(user.getUsername());
    }
    if (user.getEmail() != null) {
      existingUser.setEmail(user.getEmail());
    }
    if (user.getPassword() != null) {
      existingUser.setPassword(user.getPassword());
    }
    if (user.getStatus() != null) {
      existingUser.setStatus(user.getStatus());
    }

    // 执行更新
    userMapper.update(existingUser);
    return existingUser;
  }

  /**
   * 删除用户
   */
  @Override
  @Transactional
  public void deleteUser(Long id) {
    // 检查用户是否存在
    getUserById(id);

    // 执行删除
    userMapper.deleteById(id);
  }

  /**
   * 根据状态获取用户
   */
  @Override
  public List<User> getUsersByStatus(Integer status) {
    return userMapper.findByStatus(status);
  }
}

5.3.5 控制器

package com.lihaozhe.controller;

import java.time.LocalDate;
import java.util.List;

import com.github.pagehelper.PageInfo;
import com.lihaozhe.vo.LayPage;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.lihaozhe.entity.User;
import com.lihaozhe.service.UserService;

import jakarta.validation.Valid;

/**
 * 用户控制器
 * 处理用户相关的HTTP请求
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/users")
public class UserController {
  // 用户服务
  private final UserService userService;

  /**
   * 构造函数注入
   *
   * @param userService 用户服务
   */
  public UserController(UserService userService) {
    this.userService = userService;
  }

  /**
   * 获取所有用户
   *
   * @return 用户列表
   */
  @GetMapping
  public List<User> getAllUsers() {
    return userService.getAllUsers();
  }

  /**
   * 根据ID获取用户
   *
   * @param id 用户ID
   * @return 响应实体,包含用户
   */
  @GetMapping("/{id}")
  public ResponseEntity<User> getUserById(@PathVariable Long id) {
    User user = userService.getUserById(id);
    return ResponseEntity.ok(user);
  }

  /**
   * 根据用户名获取用户
   *
   * @param username 用户名
   * @return 响应实体,包含用户
   */
  @GetMapping("/username/{username}")
  public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
    User user = userService.getUserByUsername(username);
    return ResponseEntity.ok(user);
  }

  /**
   * 根据状态获取用户
   *
   * @param status 状态:1-正常,0-禁用
   * @return 用户列表
   */
  @GetMapping("/status")
  public List<User> getUsersByStatus(@RequestParam Integer status) {
    return userService.getUsersByStatus(status);
  }

  /**
   * 创建新用户
   *
   * @param user 用户对象,经过验证
   * @return 响应实体,包含创建的用户和201状态
   */
  @PostMapping
  public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
    User createdUser = userService.createUser(user);
    return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
  }

  /**
   * 更新用户
   *
   * @param id   用户ID
   * @param user 用户对象
   * @return 响应实体,包含更新后的用户
   */
  @PutMapping("/{id}")
  public ResponseEntity<User> updateUser(@PathVariable Long id,
                                         @RequestBody User user) {
    User updatedUser = userService.updateUser(id, user);
    return ResponseEntity.ok(updatedUser);
  }

  /**
   * 删除用户
   *
   * @param id 用户ID
   * @return 响应实体,204状态表示成功
   */
  @DeleteMapping("/{id}")
  public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    userService.deleteUser(id);
    return ResponseEntity.noContent().build();
  }

}

5.4 前端页面

mybatis-users.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <!-- 字符编码设置 -->
  <meta charset="UTF-8">
  <!-- 响应式视图设置 -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- 页面标题 -->
  <title>MyBatis User Management</title>
  <!-- 引入Tailwind CSS -->
  <!-- <script src="https://cdn.tailwindcss.com"></script> -->
  <script src="./js/tailwindcss.js"></script>
  <!-- 引入Font Awesome图标库 -->
  <!-- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"> -->
  <link href="./css/font-awesome.min.css" rel="stylesheet">
</head>
<body class="bg-gray-50">
  <!-- 主容器 -->
  <div class="container mx-auto px-4 py-8 max-w-6xl">
    <!-- 页面头部 -->
    <header class="mb-8">
      <h1 class="text-4xl font-bold text-center text-blue-600 mb-2">
        <i class="fa fa-database mr-2"></i>User Management with MyBatis
      </h1>
      <p class="text-center text-gray-600">Spring Boot 3.5.6 + MyBatis lihaozhe</p>
    </header>
    
    <!-- 操作按钮区 -->
    <div class="mb-6 flex flex-wrap justify-between items-center gap-4">
      <!-- 刷新用户列表按钮 -->
      <button id="refreshBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded flex items-center">
        <i class="fa fa-refresh mr-2"></i>Refresh Users
      </button>
      
      <!-- 添加新用户按钮 -->
      <button id="addUserBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded flex items-center">
        <i class="fa fa-plus mr-2"></i>Add New User
      </button>
    </div>
    
    <!-- 用户列表区 -->
    <div class="bg-white rounded-lg shadow-md overflow-hidden">
      <table class="min-w-full divide-y divide-gray-200">
        <!-- 表头 -->
        <thead class="bg-gray-50">
          <tr>
            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              ID
            </th>
            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Username
            </th>
            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Email
            </th>
            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Status
            </th>
            <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Created At
            </th>
            <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
              Actions
            </th>
          </tr>
        </thead>
        <!-- 表体 - 用户数据将通过JavaScript动态加载 -->
        <tbody id="userTableBody" class="bg-white divide-y divide-gray-200">
          <tr>
            <td colspan="6" class="px-6 py-4 text-center text-gray-500">
              <i class="fa fa-spinner fa-spin mr-2"></i>Loading users...
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
  
  <!-- 添加/编辑用户模态框 -->
  <div id="userModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
    <div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6 transform transition-all">
      <div class="flex justify-between items-center mb-4">
        <h2 id="modalTitle" class="text-2xl font-bold text-gray-800">Add New User</h2>
        <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700">
          <i class="fa fa-times text-xl"></i>
        </button>
      </div>
      
      <!-- 用户表单 -->
      <form id="userForm">
        <!-- 隐藏字段 - 用于编辑时存储用户ID -->
        <input type="hidden" id="userId">
        
        <!-- 用户名输入 -->
        <div class="mb-4">
          <label for="username" class="block text-gray-700 mb-2">Username</label>
          <input type="text" id="username" name="username" 
                 class="w-full px-3 py-2 border border-gray-300 rounded"
                 required>
          <p id="usernameError" class="text-red-500 text-sm mt-1 hidden"></p>
        </div>
        
        <!-- 邮箱输入 -->
        <div class="mb-4">
          <label for="email" class="block text-gray-700 mb-2">Email</label>
          <input type="email" id="email" name="email" 
                 class="w-full px-3 py-2 border border-gray-300 rounded"
                 required>
          <p id="emailError" class="text-red-500 text-sm mt-1 hidden"></p>
        </div>
        
        <!-- 密码输入 -->
        <div class="mb-4">
          <label for="password" class="block text-gray-700 mb-2">Password</label>
          <input type="password" id="password" name="password" 
                 class="w-full px-3 py-2 border border-gray-300 rounded">
          <p class="text-gray-500 text-sm mt-1">Leave blank to keep current password</p>
          <p id="passwordError" class="text-red-500 text-sm mt-1 hidden"></p>
        </div>
        
        <!-- 状态选择 -->
        <div class="mb-6">
          <label for="status" class="block text-gray-700 mb-2">Status</label>
          <select id="status" name="status" 
                  class="w-full px-3 py-2 border border-gray-300 rounded">
            <option value="1">Active</option>
            <option value="0">Inactive</option>
          </select>
        </div>
        
        <!-- 表单按钮 -->
        <div class="flex justify-end space-x-3">
          <button type="button" id="cancelBtn" 
                  class="px-4 py-2 border border-gray-300 rounded hover:bg-gray-100">
            Cancel
          </button>
          <button type="submit" 
                  class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
            Save User
          </button>
        </div>
      </form>
    </div>
  </div>
  
  <!-- 确认删除模态框 -->
  <div id="deleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
    <div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
      <h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Delete</h3>
      <p class="text-gray-600 mb-6">
        Are you sure you want to delete this user? This action cannot be undone.
      </p>
      <!-- 隐藏字段 - 存储要删除的用户ID -->
      <input type="hidden" id="deleteUserId">
      <!-- 删除确认按钮 -->
      <div class="flex justify-end space-x-3">
        <button id="cancelDeleteBtn" 
                class="px-4 py-2 border border-gray-300 rounded hover:bg-gray-100">
          Cancel
        </button>
        <button id="confirmDeleteBtn" 
                class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
          Delete
        </button>
      </div>
    </div>
  </div>

  <script>
    // DOM元素引用
    const userTableBody = document.getElementById('userTableBody');
    const refreshBtn = document.getElementById('refreshBtn');
    const addUserBtn = document.getElementById('addUserBtn');
    const userModal = document.getElementById('userModal');
    const deleteModal = document.getElementById('deleteModal');
    const modalTitle = document.getElementById('modalTitle');
    const userForm = document.getElementById('userForm');
    const userIdInput = document.getElementById('userId');
    const usernameInput = document.getElementById('username');
    const emailInput = document.getElementById('email');
    const passwordInput = document.getElementById('password');
    const statusInput = document.getElementById('status');
    const closeModalBtn = document.getElementById('closeModalBtn');
    const cancelBtn = document.getElementById('cancelBtn');
    const deleteUserIdInput = document.getElementById('deleteUserId');
    const cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
    const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
    
    // 错误提示元素
    const usernameError = document.getElementById('usernameError');
    const emailError = document.getElementById('emailError');
    const passwordError = document.getElementById('passwordError');
    
    // 页面加载时获取用户列表
    document.addEventListener('DOMContentLoaded', () => {
      fetchUsers();
      
      // 绑定事件监听器
      refreshBtn.addEventListener('click', fetchUsers);
      addUserBtn.addEventListener('click', openAddUserModal);
      closeModalBtn.addEventListener('click', closeModal);
      cancelBtn.addEventListener('click', closeModal);
      userForm.addEventListener('submit', handleFormSubmit);
      cancelDeleteBtn.addEventListener('click', closeDeleteModal);
      confirmDeleteBtn.addEventListener('click', handleDeleteConfirm);
    });
    
    // 获取所有用户
    async function fetchUsers() {
      try {
        // 显示加载状态
        userTableBody.innerHTML = `
          <tr>
            <td colspan="6" class="px-6 py-4 text-center text-gray-500">
              <i class="fa fa-spinner fa-spin mr-2"></i>Loading users...
            </td>
          </tr>
        `;
        
        // 发送GET请求获取用户列表
        const response = await fetch('/api/users');
        
        // 检查响应状态
        if (!response.ok) {
          throw new Error('Failed to fetch users');
        }
        
        // 解析响应数据
        const users = await response.json();
        
        // 渲染用户列表
        renderUserTable(users);
        
      } catch (error) {
        // 显示错误信息
        userTableBody.innerHTML = `
          <tr>
            <td colspan="6" class="px-6 py-4 text-center text-red-500">
              <i class="fa fa-exclamation-circle mr-2"></i>Error: ${error.message}
            </td>
          </tr>
        `;
        console.error('Error fetching users:', error);
      }
    }
    
    // 渲染用户表格
    function renderUserTable(users) {
      // 检查用户列表是否为空
      if (users.length === 0) {
        userTableBody.innerHTML = `
          <tr>
            <td colspan="6" class="px-6 py-4 text-center text-gray-500">
              No users found. Click "Add New User" to create one.
            </td>
          </tr>
        `;
        return;
      }
      
      // 生成表格行
      const tableRows = users.map(user => `
        <tr>
          <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
            ${user.id}
          </td>
          <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
            ${escapeHtml(user.username)}
          </td>
          <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
            ${escapeHtml(user.email)}
          </td>
          <td class="px-6 py-4 whitespace-nowrap">
            <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full 
              ${user.status === 1 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}">
              ${user.status === 1 ? 'Active' : 'Inactive'}
            </span>
          </td>
          <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
            ${formatDate(user.createdAt)}
          </td>
          <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
            <button onclick="editUser(${user.id})" 
                    class="text-indigo-600 hover:text-indigo-900 mr-3">
              Edit
            </button>
            <button onclick="openDeleteModal(${user.id})" 
                    class="text-red-600 hover:text-red-900">
              Delete
            </button>
          </td>
        </tr>
      `).join('');
      
      // 设置表格内容
      userTableBody.innerHTML = tableRows;
      
      // 为动态生成的按钮添加全局函数
      window.editUser = editUser;
      window.openDeleteModal = openDeleteModal;
    }
    
    // 打开添加用户模态框
    function openAddUserModal() {
      // 重置表单
      userForm.reset();
      userIdInput.value = '';
      modalTitle.textContent = 'Add New User';
      
      // 隐藏错误提示
      hideAllErrors();
      
      // 显示模态框
      userModal.classList.remove('hidden');
    }
    
    // 编辑用户
    async function editUser(id) {
      try {
        // 隐藏错误提示
        hideAllErrors();
        
        // 获取用户详情
        const response = await fetch(`/api/users/${id}`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch user details');
        }
        
        const user = await response.json();
        
        // 填充表单
        userIdInput.value = user.id;
        usernameInput.value = user.username;
        emailInput.value = user.email;
        statusInput.value = user.status;
        
        // 更改模态框标题
        modalTitle.textContent = 'Edit User';
        
        // 显示模态框
        userModal.classList.remove('hidden');
        
      } catch (error) {
        alert(`Error editing user: ${error.message}`);
        console.error('Error editing user:', error);
      }
    }
    
    // 打开删除确认模态框
    function openDeleteModal(id) {
      deleteUserIdInput.value = id;
      deleteModal.classList.remove('hidden');
    }
    
    // 关闭模态框
    function closeModal() {
      userModal.classList.add('hidden');
    }
    
    // 关闭删除确认模态框
    function closeDeleteModal() {
      deleteModal.classList.add('hidden');
    }
    
    // 处理表单提交
    async function handleFormSubmit(event) {
      // 阻止表单默认提交行为
      event.preventDefault();
      
      // 隐藏所有错误提示
      hideAllErrors();
      
      // 获取表单数据
      const id = userIdInput.value;
      const userData = {
        username: usernameInput.value.trim(),
        email: emailInput.value.trim(),
        status: parseInt(statusInput.value)
      };
      
      // 只有在密码不为空时才添加到请求数据中
      if (passwordInput.value.trim() !== '') {
        userData.password = passwordInput.value.trim();
      }
      
      try {
        let response;
        
        // 判断是添加还是编辑操作
        if (id) {
          // 编辑用户 - PUT请求
          response = await fetch(`/api/users/${id}`, {
            method: 'PUT',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
          });
        } else {
          // 添加用户 - POST请求
          response = await fetch('/api/users', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
          });
        }
        
        // 解析响应数据
        const result = await response.json();
        
        // 检查响应状态
        if (!response.ok) {
          // 处理验证错误
          if (response.status === 400 && result.errors) {
            displayValidationErrors(result.errors);
            return;
          }
          
          throw new Error(result.message || 'Failed to save user');
        }
        
        // 操作成功,关闭模态框并刷新用户列表
        closeModal();
        fetchUsers();
        
      } catch (error) {
        alert(`Error saving user: ${error.message}`);
        console.error('Error saving user:', error);
      }
    }
    
    // 处理删除确认
    async function handleDeleteConfirm() {
      const id = deleteUserIdInput.value;
      
      try {
        // 发送删除请求
        const response = await fetch(`/api/users/${id}`, {
          method: 'DELETE'
        });
        
        if (!response.ok) {
          throw new Error('Failed to delete user');
        }
        
        // 删除成功,关闭模态框并刷新用户列表
        closeDeleteModal();
        fetchUsers();
        
      } catch (error) {
        alert(`Error deleting user: ${error.message}`);
        console.error('Error deleting user:', error);
      }
    }
    
    // 显示验证错误
    function displayValidationErrors(errors) {
      // 显示用户名错误
      if (errors.username) {
        usernameError.textContent = errors.username;
        usernameError.classList.remove('hidden');
      }
      
      // 显示邮箱错误
      if (errors.email) {
        emailError.textContent = errors.email;
        emailError.classList.remove('hidden');
      }
      
      // 显示密码错误
      if (errors.password) {
        passwordError.textContent = errors.password;
        passwordError.classList.remove('hidden');
      }
    }
    
    // 隐藏所有错误提示
    function hideAllErrors() {
      usernameError.classList.add('hidden');
      emailError.classList.add('hidden');
      passwordError.classList.add('hidden');
    }
    
    // 格式化日期
    function formatDate(dateString) {
      if (!dateString) return '';
      const date = new Date(dateString);
      return date.toLocaleString();
    }
    
    // HTML转义,防止XSS攻击
    function escapeHtml(unsafe) {
      if (!unsafe) return '';
      return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    }
  </script>
</body>
</html>
    

5.5 MyBatis测试类

5.5.1 持久层测试类

package com.lihaozhe.mapper;

import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.mybatis.AutoConfigureMybatis;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import com.lihaozhe.entity.User;

// 标记为Spring Boot测试类
@SpringBootTest
// 自动配置MyBatis
@AutoConfigureMybatis
class UserMapperTest {

  // 注入UserMapper依赖
  @Autowired
  private UserMapper userMapper;
  
  // 测试查询所有用户
  @Test
  void testFindAll() {
    // 调用mapper方法
    List<User> users = userMapper.findAll();
    
    // 验证结果不为空且至少有一条记录
    assertNotNull(users);
    assertTrue(users.size() > 0);
  }
  
  // 测试根据ID查询用户
  @Test
  void testFindById() {
    // 测试查询存在的用户ID
    Optional<User> user = userMapper.findById(1L);
    
    // 验证用户存在
    assertTrue(user.isPresent());
    assertEquals("admin", user.get().getUsername());
    
    // 测试查询不存在的用户ID
    Optional<User> nonExistentUser = userMapper.findById(999L);
    assertFalse(nonExistentUser.isPresent());
  }
  
  // 测试根据用户名查询用户
  @Test
  void testFindByUsername() {
    // 测试查询存在的用户名
    Optional<User> user = userMapper.findByUsername("admin");
    
    // 验证用户存在
    assertTrue(user.isPresent());
    assertEquals(1L, user.get().getId());
    
    // 测试查询不存在的用户名
    Optional<User> nonExistentUser = userMapper.findByUsername("nonexistent");
    assertFalse(nonExistentUser.isPresent());
  }
  
  // 测试插入用户,使用事务确保测试数据不会污染数据库
  @Test
  @Transactional
  void testInsert() {
    // 创建新用户对象
    User user = new User();
    user.setUsername("testuser");
    user.setEmail("test@lihaozhe.com");
    user.setPassword("password123");
    user.setStatus(1);
    
    // 执行插入操作
    int rowsAffected = userMapper.insert(user);
    
    // 验证插入成功
    assertEquals(1, rowsAffected);
    // 验证ID已自动生成
    assertNotNull(user.getId());
    
    // 验证数据已正确插入
    Optional<User> insertedUser = userMapper.findById(user.getId());
    assertTrue(insertedUser.isPresent());
    assertEquals("testuser", insertedUser.get().getUsername());
  }
  
  // 测试更新用户
  @Test
  @Transactional
  void testUpdate() {
    // 先查询一个存在的用户
    Optional<User> userOptional = userMapper.findById(1L);
    assertTrue(userOptional.isPresent());
    
    User user = userOptional.get();
    // 修改用户信息
    user.setEmail("updated@lihaozhe.com");
    
    // 执行更新操作
    int rowsAffected = userMapper.update(user);
    assertEquals(1, rowsAffected);
    
    // 验证更新结果
    Optional<User> updatedUser = userMapper.findById(1L);
    assertTrue(updatedUser.isPresent());
    assertEquals("updated@lihaozhe.com", updatedUser.get().getEmail());
  }
  
  // 测试删除用户
  @Test
  @Transactional
  void testDeleteById() {
    // 先插入一个测试用户
    User user = new User();
    user.setUsername("todelete");
    user.setEmail("delete@lihaozhe.com");
    user.setPassword("password");
    userMapper.insert(user);
    
    Long userId = user.getId();
    assertNotNull(userId);
    
    // 执行删除操作
    int rowsAffected = userMapper.deleteById(userId);
    assertEquals(1, rowsAffected);
    
    // 验证用户已被删除
    Optional<User> deletedUser = userMapper.findById(userId);
    assertFalse(deletedUser.isPresent());
  }
  
  // 测试根据状态查询用户
  @Test
  void testFindByStatus() {
    // 查询正常状态(1)的用户
    List<User> activeUsers = userMapper.findByStatus(1);
    assertNotNull(activeUsers);
    // 确保查询结果中的用户状态都是1
    activeUsers.forEach(user -> assertEquals(1, user.getStatus()));
    
    // 查询禁用状态(0)的用户,可能为空
    List<User> inactiveUsers = userMapper.findByStatus(0);
    assertNotNull(inactiveUsers);
    // 确保查询结果中的用户状态都是0
    inactiveUsers.forEach(user -> assertEquals(0, user.getStatus()));
  }
}

5.5.2 业务处测试类

package com.lihaozhe.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.lihaozhe.entity.User;
import com.lihaozhe.exception.ResourceNotFoundException;
import com.lihaozhe.mapper.UserMapper;
import com.lihaozhe.service.impl.UserServiceImpl;

// 使用Mockito扩展
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

  // 模拟UserMapper依赖
  @Mock
  private UserMapper userMapper;
  
  // 将模拟的依赖注入到测试对象中
  @InjectMocks
  private UserServiceImpl userService;
  
  // 测试获取所有用户
  @Test
  void getAllUsers() {
    // 准备测试数据
    User user1 = new User();
    user1.setId(1L);
    user1.setUsername("user1");
    
    User user2 = new User();
    user2.setId(2L);
    user2.setUsername("user2");
    
    // 模拟mapper返回数据
    when(userMapper.findAll()).thenReturn(Arrays.asList(user1, user2));
    
    // 调用服务方法
    List<User> users = userService.getAllUsers();
    
    // 验证结果
    assertNotNull(users);
    assertEquals(2, users.size());
    assertEquals("user1", users.get(0).getUsername());
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findAll();
  }
  
  // 测试根据ID获取存在的用户
  @Test
  void getUserById_ExistingId() {
    // 准备测试数据
    User user = new User();
    user.setId(1L);
    user.setUsername("admin");
    
    // 模拟mapper返回数据
    when(userMapper.findById(1L)).thenReturn(Optional.of(user));
    
    // 调用服务方法
    User foundUser = userService.getUserById(1L);
    
    // 验证结果
    assertNotNull(foundUser);
    assertEquals(1L, foundUser.getId());
    assertEquals("admin", foundUser.getUsername());
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findById(1L);
  }
  
  // 测试根据ID获取不存在的用户
  @Test
  void getUserById_NonExistingId() {
    // 模拟mapper返回空
    when(userMapper.findById(99L)).thenReturn(Optional.empty());
    
    // 验证会抛出预期的异常
    assertThrows(ResourceNotFoundException.class, () -> {
      userService.getUserById(99L);
    });
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findById(99L);
  }
  
  // 测试创建用户
  @Test
  void createUser() {
    // 准备测试数据
    User userToCreate = new User();
    userToCreate.setUsername("newuser");
    userToCreate.setEmail("new@lihaozhe.com");
    userToCreate.setPassword("password");
    
    // 模拟mapper的插入方法
    when(userMapper.insert(any(User.class))).thenAnswer(invocation -> {
      User user = invocation.getArgument(0);
      user.setId(10L); // 设置一个ID模拟自动生成
      return 1;
    });
    
    // 调用服务方法
    User createdUser = userService.createUser(userToCreate);
    
    // 验证结果
    assertNotNull(createdUser);
    assertNotNull(createdUser.getId());
    assertEquals(10L, createdUser.getId());
    assertEquals("newuser", createdUser.getUsername());
    // 验证状态被设置为默认值1
    assertEquals(1, createdUser.getStatus());
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).insert(any(User.class));
  }
  
  // 测试更新用户
  @Test
  void updateUser_ExistingId() {
    // 准备测试数据
    User existingUser = new User();
    existingUser.setId(1L);
    existingUser.setUsername("admin");
    existingUser.setEmail("admin@lihaozhe.com");
    existingUser.setPassword("oldpass");
    
    User updateData = new User();
    updateData.setEmail("updated@lihaozhe.com");
    
    // 模拟mapper方法
    when(userMapper.findById(1L)).thenReturn(Optional.of(existingUser));
    when(userMapper.update(any(User.class))).thenReturn(1);
    
    // 调用服务方法
    User updatedUser = userService.updateUser(1L, updateData);
    
    // 验证结果
    assertNotNull(updatedUser);
    assertEquals(1L, updatedUser.getId());
    // 验证用户名保持不变
    assertEquals("admin", updatedUser.getUsername());
    // 验证邮箱已更新
    assertEquals("updated@lihaozhe.com", updatedUser.getEmail());
    // 验证密码保持不变
    assertEquals("oldpass", updatedUser.getPassword());
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findById(1L);
    verify(userMapper, times(1)).update(existingUser);
  }
  
  // 测试更新不存在的用户
  @Test
  void updateUser_NonExistingId() {
    // 准备测试数据
    User updateData = new User();
    updateData.setEmail("updated@lihaozhe.com");
    
    // 模拟mapper返回空
    when(userMapper.findById(99L)).thenReturn(Optional.empty());
    
    // 验证会抛出预期的异常
    assertThrows(ResourceNotFoundException.class, () -> {
      userService.updateUser(99L, updateData);
    });
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findById(99L);
    // 验证update方法未被调用
    verify(userMapper, never()).update(any(User.class));
  }
  
  // 测试删除用户
  @Test
  void deleteUser_ExistingId() {
    // 准备测试数据
    User existingUser = new User();
    existingUser.setId(1L);
    existingUser.setUsername("admin");
    
    // 模拟mapper方法
    when(userMapper.findById(1L)).thenReturn(Optional.of(existingUser));
    when(userMapper.deleteById(1L)).thenReturn(1);
    
    // 调用服务方法
    userService.deleteUser(1L);
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findById(1L);
    verify(userMapper, times(1)).deleteById(1L);
  }
  
  // 测试删除不存在的用户
  @Test
  void deleteUser_NonExistingId() {
    // 模拟mapper返回空
    when(userMapper.findById(99L)).thenReturn(Optional.empty());
    
    // 验证会抛出预期的异常
    assertThrows(ResourceNotFoundException.class, () -> {
      userService.deleteUser(99L);
    });
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findById(99L);
    // 验证delete方法未被调用
    verify(userMapper, never()).deleteById(anyLong());
  }
  
  // 测试根据状态获取用户
  @Test
  void getUsersByStatus() {
    // 准备测试数据
    User user1 = new User();
    user1.setId(1L);
    user1.setUsername("user1");
    user1.setStatus(1);
    
    User user2 = new User();
    user2.setId(2L);
    user2.setUsername("user2");
    user2.setStatus(1);
    
    // 模拟mapper返回数据
    when(userMapper.findByStatus(1)).thenReturn(Arrays.asList(user1, user2));
    
    // 调用服务方法
    List<User> activeUsers = userService.getUsersByStatus(1);
    
    // 验证结果
    assertNotNull(activeUsers);
    assertEquals(2, activeUsers.size());
    activeUsers.forEach(user -> assertEquals(1, user.getStatus()));
    
    // 验证mapper方法被调用
    verify(userMapper, times(1)).findByStatus(1);
  }
}

5.6 MyBatis分页插件PageHelper

5.6.1 完整pom.xml

<?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>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.6</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>com.lihaozhe</groupId>
	<artifactId>springboot-mybatis</artifactId>
	<version>0.0.1</version>
	<name>springboot-mybatis</name>
	<description>Spring Boot MyBatis Tutorial</description>

	<properties>
		<java.version>25</java.version>
	</properties>

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

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

		<!-- MySQL驱动 -->
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>

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

		<!-- 配置处理器 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>

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

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

		<!-- MyBatis测试支持 -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter-test</artifactId>
			<version>3.0.5</version>
			<scope>test</scope>
		</dependency>


		<!-- MyBatis pagehelper 分页插件 -->
		<dependency>
			<groupId>com.github.pagehelper</groupId>
			<artifactId>pagehelper-spring-boot-starter</artifactId>
			<version>2.1.1</version>
		</dependency>

		<!-- 数据库迁移工具 -->
		<!-- Caused by: org.flywaydb.core.api.FlywayException: Unsupported Database: MySQL 8.4 -->
		<!--
		<dependency>
			<groupId>org.flywaydb</groupId>
			<artifactId>flyway-core</artifactId>
			<version>11.14.1</version>
		</dependency>
		-->
	</dependencies>

	<build>
		<plugins>
			<!-- Spring Boot Maven插件 -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.springframework.boot</groupId>
							<artifactId>spring-boot-configuration-processor</artifactId>
						</path>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

5.6.2 application.yml

# 服务器配置
server:
  port: 8000

# 应用配置
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/springboot_demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: lihaozhe
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池配置
    hikari:
      maximum-pool-size: 1000
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000

# MyBatis配置
mybatis:
  #  mapper.xml文件位置
  mapper-locations: classpath:mybatis/mappers/*.xml
  # 实体类包路径
  type-aliases-package: com.lihaozhe.entity
  # 配置MyBatis的全局属性
  configuration:
    # 开启驼峰命名转换
    map-underscore-to-camel-case: true
    # 日志级别
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# PageHelper 分页配置
pagehelper:
  helper-dialect: mysql      # 数据库方言
  reasonable: true           # 页码≤0 时自动查第 1 页
  support-methods-arguments: true
  params: count=countSql     # 自动生成 count 语句
  page-size-zero: false      # 单页条数为 0 时是否返回全部

# 日志配置
logging:
  level:
    com.lihaozhe: DEBUG

5.6.3 业务层

/**
 * 使用 PageHelper 插件进行分页查询
 *
 * @param status   状态:1-正常,0-禁用
 * @param pageNum  查询页码号
 * @param pageSize 每页记录数
 * @return PageInfo 分页信息
 */
PageInfo<User> page(Integer status, Integer pageNum, Integer pageSize);
@Override
public PageInfo<User> page(Integer status, Integer pageNum, Integer pageSize) {
  // 在mybatis获取连接之后查询之前开启 PageHelper 分页
  PageHelper.startPage(pageNum, pageSize);

  // 调用持久层
  List<User> userList = userMapper.findByStatus(status);
  // 清空密码
  userList = userList.stream().peek(user -> user.setPassword(null)).toList();

  // 封装分页信息
  return new PageInfo<>(userList);
}

5.6.4 控制器

/**
 * 使用 PageHelper 插件进行分页查询
 *
 * @param status   状态:1-正常,0-禁用
 * @param pageNum  查询页码号
 * @param pageSize 每页记录数
 * @return 响应实体,PageInfo分页信息
 */
@GetMapping("/page/{status}/{pageNum}/{pageSize}")
public ResponseEntity<PageInfo<User>> page(@PathVariable("status") Integer status,
                                           @PathVariable("pageNum") Integer pageNum,
                                           @PathVariable("pageSize") Integer pageSize) {
  PageInfo<User> info = userService.page(status, pageNum, pageSize);
  return ResponseEntity.ok(info);
}.

5.7 MyBatis动态SQL示例

5.7.1 支持动态查询的UserMapper接口

package com.lihaozhe.mapper;

import com.lihaozhe.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 用户Mapper接口
 * 定义与用户相关的数据库操作
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Mapper
public interface UserMapper {

  /**
   * 查询所有用户
   *
   * @return 用户列表
   */
  List<User> findAll();

  /**
   * 根据ID查询用户
   *
   * @param id 用户ID
   * @return 可选的用户对象
   */
  Optional<User> findById(@Param("id") Long id);

  /**
   * 根据用户名查询用户
   *
   * @param username 用户名
   * @return 可选的用户对象
   */
  Optional<User> findByUsername(@Param("username") String username);

  /**
   * 插入新用户
   *
   * @param user 用户对象
   * @return 影响的行数
   */
  int insert(User user);

  /**
   * 更新用户信息
   *
   * @param user 用户对象
   * @return 影响的行数
   */
  int update(User user);

  /**
   * 根据ID删除用户
   *
   * @param id 用户ID
   * @return 影响的行数
   */
  int deleteById(@Param("id") Long id);

  /**
   * 根据状态查询用户
   *
   * @param status 状态:1-正常,0-禁用
   * @return 用户列表
   */
  List<User> findByStatus(@Param("status") Integer status);

  /**
   * 动态条件查询用户
   *
   * @param params 包含查询条件的Map
   * @return 用户列表
   */
  List<User> findByDynamicConditions(@Param("params") Map<String, Object> params);
}

5.7.2 包含动态查询的UserMapper.xml文件

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

<!-- 命名空间对应Mapper接口 -->
<mapper namespace="com.lihaozhe.mapper.UserMapper">

  <!-- 定义结果集映射 -->
  <resultMap id="BaseResultMap" type="user">
    <id column="id" property="id" jdbcType="BIGINT"/>
    <result column="username" property="username" jdbcType="VARCHAR"/>
    <result column="email" property="email" jdbcType="VARCHAR"/>
    <result column="password" property="password" jdbcType="VARCHAR"/>
    <result column="created_at" property="createdAt" jdbcType="TIMESTAMP"/>
    <result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/>
    <result column="status" property="status" jdbcType="TINYINT"/>
  </resultMap>

  <!-- 定义基础查询字段 -->
  <sql id="Base_Column_List">
    id, username, email, password, created_at, updated_at, status
  </sql>

  <!-- 查询所有用户 -->
  <select id="findAll" resultType="user">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    ORDER BY id ASC
  </select>

  <!-- 根据ID查询用户 -->
  <select id="findById" parameterType="java.lang.Long" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE id = #{id}
  </select>

  <!-- 根据用户名查询用户 -->
  <select id="findByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE username = #{username}
  </select>

  <!-- 插入新用户 -->
  <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user
    (username, email, password, created_at, updated_at, status)
    VALUES
    (#{username}, #{email}, #{password}, NOW(), NOW(),
    <choose>
      <when test="status != null">#{status}</when>
      <otherwise>1</otherwise>
    </choose>)
  </insert>

  <!-- 更新用户信息 -->
  <update id="update" parameterType="User">
    UPDATE user
    <set>
      <if test="username != null">username = #{username},</if>
      <if test="email != null">email = #{email},</if>
      <if test="password != null">password = #{password},</if>
      <if test="status != null">status = #{status},</if>
      updated_at = NOW()
    </set>
    WHERE id = #{id}
  </update>

  <!-- 根据ID删除用户 -->
  <delete id="deleteById" parameterType="java.lang.Long">
    DELETE FROM user
    WHERE id = #{id}
  </delete>

  <!-- 根据状态查询用户 -->
  <select id="findByStatus" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE status = #{status}
    ORDER BY id ASC
  </select>

  <!-- 动态条件查询用户 -->
  <select id="findByDynamicConditions" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    <where>
      <!-- 如果提供了status参数,则添加状态条件 -->
      <if test="params.status != null">
        AND status = #{params.status}
      </if>

      <!-- 如果提供了username参数,则添加用户名模糊查询条件 -->
      <if test="params.username != null and params.username != ''">
        AND username LIKE CONCAT('%', #{params.username}, '%')
      </if>

      <!-- 如果提供了email参数,则添加邮箱模糊查询条件 -->
      <if test="params.email != null and params.email != ''">
        AND email LIKE CONCAT('%', #{params.email}, '%')
      </if>

      <!-- 如果提供了startDate参数,则添加创建时间起始条件 -->
      <if test="params.startDate != null">
        AND created_at &gt;= #{params.startDate}
      </if>

      <!-- 如果提供了endDate参数,则添加创建时间结束条件 -->
      <if test="params.endDate != null">
        AND created_at &lt;= #{params.endDate}
      </if>
    </where>
    ORDER BY id ASC

    <!-- 如果提供了limit参数,则添加 LIMIT 条件 -->
    <if test="params.limit != null">
      LIMIT #{params.limit}
    </if>
  </select>
</mapper>

5.7.3 使用动态查询的业务层

package com.lihaozhe.service;

import com.github.pagehelper.PageInfo;
import com.lihaozhe.entity.User;
import com.lihaozhe.exception.ResourceNotFoundException;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 用户服务接口
 * 定义用户相关的业务逻辑
 *
 * @author 李昊哲
 * @version 1.0.0
 */
public interface UserService {

  /**
   * 获取所有用户
   *
   * @return 用户列表
   */
  List<User> getAllUsers();

  /**
   * 根据ID获取用户
   *
   * @param id 用户ID
   * @return 用户对象
   * @throws ResourceNotFoundException 如果用户不存在
   */
  User getUserById(Long id);

  /**
   * 根据用户名获取用户
   *
   * @param username 用户名
   * @return 用户对象
   * @throws ResourceNotFoundException 如果用户不存在
   */
  User getUserByUsername(String username);

  /**
   * 创建新用户
   *
   * @param user 用户对象
   * @return 创建的用户
   */
  User createUser(User user);

  /**
   * 更新用户
   *
   * @param id   用户ID
   * @param user 用户对象
   * @return 更新后的用户
   * @throws ResourceNotFoundException 如果用户不存在
   */
  User updateUser(Long id, User user);

  /**
   * 删除用户
   *
   * @param id 用户ID
   * @throws ResourceNotFoundException 如果用户不存在
   */
  void deleteUser(Long id);

  /**
   * 根据状态获取用户
   *
   * @param status 状态:1-正常,0-禁用
   * @return 用户列表
   */
  List<User> getUsersByStatus(Integer status);

  /**
   * 使用 PageHelper 插件进行分页查询
   *
   * @param status   状态:1-正常,0-禁用
   * @param pageNum  查询页码号
   * @param pageSize 每页记录数
   * @return PageInfo 分页信息
   */
  PageInfo<User> page(Integer status, Integer pageNum, Integer pageSize);

  /**
   * 动态条件查询用户
   *
   * @param status    状态:1-正常,0-禁用
   * @param username  用户名(模糊查询)
   * @param email     邮箱(模糊查询)
   * @param startDate 开始日期(创建时间)
   * @param endDate   结束日期(创建时间)
   * @param limit     限制查询数量
   * @return 用户列表
   */
  public List<User> findUsersByConditions(Integer status,
                                          String username,
                                          String email,
                                          LocalDate startDate,
                                          LocalDate endDate,
                                          Integer limit);
}

package com.lihaozhe.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.lihaozhe.entity.User;
import com.lihaozhe.exception.ResourceNotFoundException;
import com.lihaozhe.mapper.UserMapper;
import com.lihaozhe.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 用户服务实现类
 * 实现用户相关的业务逻辑,使用MyBatis操作数据库
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@Service
//@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
  // 用户Mapper
  private final UserMapper userMapper;

  /**
   * 构造函数注入
   *
   * @param userMapper 用户Mapper
   */
  public UserServiceImpl(UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  /**
   * 获取所有用户
   */
  @Override
  public List<User> getAllUsers() {
    return userMapper.findAll();
  }

  /**
   * 根据ID获取用户
   */
  @Override
  public User getUserById(Long id) {
    // 查询用户,如果不存在则抛出异常
    return userMapper.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
  }

  /**
   * 根据用户名获取用户
   */
  @Override
  public User getUserByUsername(String username) {
    // 查询用户,如果不存在则抛出异常
    return userMapper.findByUsername(username)
        .orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
  }

  /**
   * 创建新用户
   */
  @Override
  @Transactional
  public User createUser(User user) {
    // 设置默认状态为1(正常)
    if (user.getStatus() == null) {
      user.setStatus(1);
    }
    // 插入用户
    userMapper.insert(user);
    // 返回插入的用户(包含自动生成的ID)
    return user;
  }

  /**
   * 更新用户
   */
  @Override
  @Transactional
  public User updateUser(Long id, User user) {
    // 检查用户是否存在
    User existingUser = getUserById(id);

    // 更新用户信息
    if (user.getUsername() != null) {
      existingUser.setUsername(user.getUsername());
    }
    if (user.getEmail() != null) {
      existingUser.setEmail(user.getEmail());
    }
    if (user.getPassword() != null) {
      existingUser.setPassword(user.getPassword());
    }
    if (user.getStatus() != null) {
      existingUser.setStatus(user.getStatus());
    }

    // 执行更新
    userMapper.update(existingUser);
    return existingUser;
  }

  /**
   * 删除用户
   */
  @Override
  @Transactional
  public void deleteUser(Long id) {
    // 检查用户是否存在
    getUserById(id);

    // 执行删除
    userMapper.deleteById(id);
  }

  /**
   * 根据状态获取用户
   */
  @Override
  public List<User> getUsersByStatus(Integer status) {
    return userMapper.findByStatus(status);
  }

  @Override
  public PageInfo<User> page(Integer status, Integer pageNum, Integer pageSize) {
    // 在mybatis获取连接之后查询之前开启 PageHelper 分页
    PageHelper.startPage(pageNum, pageSize);

    // 调用持久层
    List<User> userList = userMapper.findByStatus(status);
    userList = userList.stream().peek(user -> user.setPassword(null)).toList();

    // 封装分页信息
    return new PageInfo<>(userList);
  }

  /**
   * 动态条件查询用户
   *
   * @param status    状态:1-正常,0-禁用
   * @param username  用户名(模糊查询)
   * @param email     邮箱(模糊查询)
   * @param startDate 开始日期(创建时间)
   * @param endDate   结束日期(创建时间)
   * @param limit     限制查询数量
   * @return 用户列表
   */
  @Override
  public List<User> findUsersByConditions(Integer status,
                                          String username,
                                          String email,
                                          LocalDate startDate,
                                          LocalDate endDate,
                                          Integer limit) {

    // 创建参数Map
    Map<String, Object> params = new HashMap<>();

    // 设置查询参数(只添加非空参数)
    if (status != null) {
      params.put("status", status);
    }
    if (username != null && !username.trim().isEmpty()) {
      params.put("username", username.trim());
    }
    if (email != null && !email.trim().isEmpty()) {
      params.put("email", email.trim());
    }

    // 处理日期参数,转换为LocalDateTime
    if (startDate != null) {
      // 开始日期的起始时间(00:00:00)
      params.put("startDate", LocalDateTime.of(startDate, LocalTime.MIN));
    }
    if (endDate != null) {
      // 结束日期的结束时间(23:59:59)
      params.put("endDate", LocalDateTime.of(endDate, LocalTime.MAX));
    }

    if (limit != null && limit > 0) {
      params.put("limit", limit);
    }

    // 调用mapper的动态查询方法
    return userMapper.findByDynamicConditions(params);
  }
}

5.7.4 支持动态查询的控制器

package com.lihaozhe.controller;

import java.time.LocalDate;
import java.util.List;

import com.github.pagehelper.PageInfo;
import com.lihaozhe.vo.LayPage;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.lihaozhe.entity.User;
import com.lihaozhe.service.UserService;

import jakarta.validation.Valid;

/**
 * 用户控制器
 * 处理用户相关的HTTP请求
 *
 * @author 李昊哲
 * @version 1.0.0
 */
@RestController
@RequestMapping("/api/users")
public class UserController {
  // 用户服务
  private final UserService userService;

  /**
   * 构造函数注入
   *
   * @param userService 用户服务
   */
  public UserController(UserService userService) {
    this.userService = userService;
  }

  /**
   * 获取所有用户
   *
   * @return 用户列表
   */
  @GetMapping
  public List<User> getAllUsers() {
    return userService.getAllUsers();
  }

  /**
   * 根据ID获取用户
   *
   * @param id 用户ID
   * @return 响应实体,包含用户
   */
  @GetMapping("/{id}")
  public ResponseEntity<User> getUserById(@PathVariable Long id) {
    User user = userService.getUserById(id);
    return ResponseEntity.ok(user);
  }

  /**
   * 根据用户名获取用户
   *
   * @param username 用户名
   * @return 响应实体,包含用户
   */
  @GetMapping("/username/{username}")
  public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
    User user = userService.getUserByUsername(username);
    return ResponseEntity.ok(user);
  }

  /**
   * 根据状态获取用户
   *
   * @param status 状态:1-正常,0-禁用
   * @return 用户列表
   */
  @GetMapping("/status")
  public List<User> getUsersByStatus(@RequestParam Integer status) {
    return userService.getUsersByStatus(status);
  }

  /**
   * 创建新用户
   *
   * @param user 用户对象,经过验证
   * @return 响应实体,包含创建的用户和201状态
   */
  @PostMapping
  public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
    User createdUser = userService.createUser(user);
    return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
  }

  /**
   * 更新用户
   *
   * @param id   用户ID
   * @param user 用户对象
   * @return 响应实体,包含更新后的用户
   */
  @PutMapping("/{id}")
  public ResponseEntity<User> updateUser(@PathVariable Long id,
                                         @RequestBody User user) {
    User updatedUser = userService.updateUser(id, user);
    return ResponseEntity.ok(updatedUser);
  }

  /**
   * 删除用户
   *
   * @param id 用户ID
   * @return 响应实体,204状态表示成功
   */
  @DeleteMapping("/{id}")
  public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    userService.deleteUser(id);
    return ResponseEntity.noContent().build();
  }

  /**
   * 使用 PageHelper 插件进行分页查询
   *
   * @param status   状态:1-正常,0-禁用
   * @param pageNum  查询页码号
   * @param pageSize 每页记录数
   * @return 响应实体,PageInfo分页信息
   */
  @GetMapping("/page/{status}/{pageNum}/{pageSize}")
  public ResponseEntity<PageInfo<User>> page(@PathVariable("status") Integer status,
                                             @PathVariable("pageNum") Integer pageNum,
                                             @PathVariable("pageSize") Integer pageSize) {
    PageInfo<User> info = userService.page(status, pageNum, pageSize);
    return ResponseEntity.ok(info);
  }

  /**
   * 使用 PageHelper 插件进行分页查询
   *
   * @param status   状态:1-正常,0-禁用
   * @param pageNum  查询页码号
   * @param pageSize 每页记录数
   * @return 响应实体,PageInfo分页信息
   */
  @GetMapping("/layPage")
  public ResponseEntity<LayPage<User>> layPage(Integer status, Integer pageNum, Integer pageSize) {
    PageInfo<User> info = userService.page(status, pageNum, pageSize);
    LayPage<User> layPage = new LayPage<>(info.getTotal(), info.getList());
    return ResponseEntity.ok(layPage);
  }

  /**
   * 动态条件查询用户
   *
   * @param status    状态:1-正常,0-禁用
   * @param username  用户名(模糊查询)
   * @param email     邮箱(模糊查询)
   * @param startDate 开始日期(创建时间)
   * @param endDate   结束日期(创建时间)
   * @param limit     限制查询数量
   * @return 用户列表
   */
  @GetMapping("/search")
  public List<User> searchUsers(@RequestParam(required = false) Integer status,
                                @RequestParam(required = false) String username,
                                @RequestParam(required = false) String email,
                                @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
                                @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
                                @RequestParam(required = false) Integer limit) {

    return userService.findUsersByConditions(status, username, email, startDate, endDate, limit);
  }

}

5.8 开发思路总结

  1. MyBatis核心组件

    • Mapper接口:定义数据库操作方法
    • Mapper XML文件:编写SQL语句,实现接口方法
    • SqlSession:MyBatis核心,用于执行SQL命令
  2. 数据库操作最佳实践

    • 使用接口和XML分离的方式组织代码
    • 对数据库操作添加事务管理(@Transactional)
    • 对查询结果使用POJO对象封装
    • 使用resultMap定义结果映射关系
    • 对表字段和Java对象属性进行驼峰命名转换
  3. 动态SQL使用场景

    • 多条件组合查询
    • 选择性更新字段
    • 动态生成排序和分页条件
    • 批量操作
  4. 分页查询实现

    • 基于LIMIT关键字实现基础分页
    • 计算总记录数实现完整分页功能
    • 封装分页结果(当前页、每页条数、总记录数、总页数)
  5. 测试策略

    • 对Mapper接口进行集成测试,验证SQL正确性
    • 对Service层进行单元测试,使用Mock隔离数据库
    • 测试异常情况和边界条件

通过本章学习,你应该掌握了Spring Boot整合MyBatis进行数据库操作的方法,包括基本CRUD操作、分页查询、动态SQL等核心功能,能够根据实际需求设计和实现数据库访问层。

第6章. MyBatis高级特性

6.1 MyBatis缓存机制

MyBatis提供了两级缓存机制,用于提高查询性能:

  • 一级缓存:SqlSession级别的缓存,默认开启
  • 二级缓存:Mapper级别的缓存,需要手动配置开启

6.1.1 MyBatis缓存配置

applicaltion.yml

# MyBatis配置
mybatis:
  #  mapper.xml文件位置
  mapper-locations: classpath:mybatis/mappers/*.xml
  # 实体类包路径
  type-aliases-package: com.lihaozhe.entity
  # 配置MyBatis的全局属性
  configuration:
    # 开启驼峰命名转换
    map-underscore-to-camel-case: true
    # 日志级别
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 开启二级缓存总开关
    cache-enabled: true

6.1.2 映射配置文件启用二级缓存

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

<!-- 命名空间对应Mapper接口 -->
<mapper namespace="com.lihaozhe.mapper.UserMapper">

  <!-- 开启当前Mapper的二级缓存 -->
  <!-- eviction: 缓存回收策略,LRU表示最近最少使用 -->
  <!-- flushInterval: 刷新间隔,单位毫秒,60000表示1分钟 -->
  <!-- size: 缓存最多存储的对象数量 -->
  <!-- readOnly: 是否只读,true表示只读 -->
  <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>

  <!-- 定义结果集映射 -->
  <resultMap id="BaseResultMap" type="user">
    <id column="id" property="id" jdbcType="BIGINT"/>
    <result column="username" property="username" jdbcType="VARCHAR"/>
    <result column="email" property="email" jdbcType="VARCHAR"/>
    <result column="password" property="password" jdbcType="VARCHAR"/>
    <result column="created_at" property="createdAt" jdbcType="TIMESTAMP"/>
    <result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/>
    <result column="status" property="status" jdbcType="TINYINT"/>
  </resultMap>

  <!-- 定义基础查询字段 -->
  <sql id="Base_Column_List">
    id, username, email, password, created_at, updated_at, status
  </sql>

  <!-- 查询所有用户 - useCache="true"表示使用缓存 -->
  <select id="findAll" resultType="user" useCache="true">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    ORDER BY id ASC
  </select>

  <!-- 根据ID查询用户 - useCache="true"表示使用缓存 -->
  <select id="findById" parameterType="java.lang.Long" resultMap="BaseResultMap" useCache="true">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE id = #{id}
  </select>

  <!-- 根据用户名查询用户 -->
  <select id="findByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
    SELECT
    <include refid="Base_Column_List"/>
    FROM user
    WHERE username = #{username}
  </select>

  <!-- 更新用户信息 - flushCache="true"表示更新后清除缓存 -->
  <update id="update" parameterType="User" flushCache="true">
    UPDATE user
    <set>
      <if test="username != null">username = #{username},</if>
      <if test="email != null">email = #{email},</if>
      <if test="password != null">password = #{password},</if>
      <if test="status != null">status = #{status},</if>
      updated_at = NOW()
    </set>
    WHERE id = #{id}
  </update>

  <!-- 其他SQL语句与之前保持一致 -->
</mapper>

6.2 MyBatis类型处理器

类型处理器用于Java类型与数据库JDBC类型之间的转换,MyBatis提供了默认的类型处理器,也支持自定义。

6.3 MyBatis调用存储过程

MyBatis支持调用数据库存储过程,适用于复杂的业务逻辑实现。

6.3.1 数据库存储过程脚本

create_user_procedures.sql

-- 创建存储过程:根据状态统计用户数量
DELIMITER //
CREATE PROCEDURE CountUsersByStatus(
  IN statusParam TINYINT,  -- 输入参数:用户状态
  OUT totalCount INT      -- 输出参数:用户总数
)
BEGIN
  -- 统计指定状态的用户数量
  SELECT COUNT(*) INTO totalCount 
  FROM users 
  WHERE status = statusParam;
END //
DELIMITER ;

-- 创建存储过程:批量更新用户状态
DELIMITER //
CREATE PROCEDURE BatchUpdateUserStatus(
  IN oldStatus TINYINT,   -- 输入参数:原状态
  IN newStatus TINYINT,   -- 输入参数:新状态
  OUT affectedRows INT    -- 输出参数:受影响的行数
)
BEGIN
  -- 记录更新前的行数
  SELECT COUNT(*) INTO affectedRows 
  FROM users 
  WHERE status = oldStatus;
  
  -- 执行批量更新
  UPDATE users 
  SET status = newStatus, updated_at = NOW()
  WHERE status = oldStatus;
END //
DELIMITER ;
    

6.3.2 调用存储过程的Mapper接口

package com.lihaozhe.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 用户存储过程调用Mapper接口
 * 定义调用数据库存储过程的方法
 */
@Mapper
public interface UserProcedureMapper {
  
  /**
   * 调用存储过程统计指定状态的用户数量
   * 
   * @param status 用户状态
   * @return 统计数量
   */
  int countUsersByStatus(@Param("status") Integer status);
  
  /**
   * 调用存储过程批量更新用户状态
   * 
   * @param oldStatus 原状态
   * @param newStatus 新状态
   * @return 受影响的行数
   */
  int batchUpdateUserStatus(
    @Param("oldStatus") Integer oldStatus, 
    @Param("newStatus") Integer newStatus);
}
    

6.3.3 调用存储过程的映射配置文件

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

<!-- 命名空间对应Mapper接口 -->
<mapper namespace="com.lihaozhe.mapper.UserProcedureMapper">
  
  <!-- 调用存储过程:统计指定状态的用户数量 -->
  <select id="countUsersByStatus" statementType="CALLABLE">
    <!-- 指定调用的存储过程 -->
    {call CountUsersByStatus(
      #{status, mode=IN, jdbcType=TINYINT},  -- 输入参数
      #{totalCount, mode=OUT, jdbcType=INTEGER}  -- 输出参数
    )}
  </select>
  
  <!-- 调用存储过程:批量更新用户状态 -->
  <update id="batchUpdateUserStatus" statementType="CALLABLE">
    <!-- 指定调用的存储过程 -->
    {call BatchUpdateUserStatus(
      #{oldStatus, mode=IN, jdbcType=TINYINT},  -- 输入参数:原状态
      #{newStatus, mode=IN, jdbcType=TINYINT},  -- 输入参数:新状态
      #{affectedRows, mode=OUT, jdbcType=INTEGER}  -- 输出参数:受影响的行数
    )}
  </update>
</mapper>
    

6.3.4 调用存储过程的Service

package com.lihaozhe.service.impl;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.mapper.UserProcedureMapper;

/**
 * 用户存储过程服务类
 * 提供调用数据库存储过程的服务方法
 */
@Service
public class UserProcedureService {
  
  // 存储过程Mapper
  private final UserProcedureMapper userProcedureMapper;
  
  /**
   * 构造函数注入
   * 
   * @param userProcedureMapper 存储过程Mapper
   */
  public UserProcedureService(UserProcedureMapper userProcedureMapper) {
    this.userProcedureMapper = userProcedureMapper;
  }
  
  /**
   * 统计指定状态的用户数量
   * 
   * @param status 用户状态
   * @return 统计数量
   */
  public int countUsersByStatus(Integer status) {
    // 调用存储过程
    return userProcedureMapper.countUsersByStatus(status);
  }
  
  /**
   * 批量更新用户状态
   * 
   * @param oldStatus 原状态
   * @param newStatus 新状态
   * @return 受影响的行数
   */
  @Transactional
  public int batchUpdateUserStatus(Integer oldStatus, Integer newStatus) {
    // 调用存储过程
    return userProcedureMapper.batchUpdateUserStatus(oldStatus, newStatus);
  }
}
    

第7章. 项目整合与部署

7.1 整合Spring Security

为用户管理系统添加认证和授权功能。

7.2 项目打包与部署

第8章. 总结与扩展

8.1 项目总结

本项目实现了一个基于Spring Boot和MyBatis的用户管理系统,包含以下核心功能:

  1. 基础CRUD操作:实现了用户的增删改查功能
  2. 分页查询:支持按页获取用户数据,提高大数据量下的查询效率
  3. 动态SQL:根据不同条件动态生成SQL语句,灵活应对复杂查询需求
  4. 缓存机制:利用MyBatis的一级和二级缓存,减少数据库访问次数
  5. 类型转换:通过自定义类型处理器,实现Java类型与数据库类型的灵活转换
  6. 存储过程调用:演示了如何调用数据库存储过程处理复杂业务逻辑
  7. 安全认证:整合Spring Security实现用户认证和权限控制
  8. 项目部署:提供了生产环境配置和部署脚本

8.2 扩展方向

  1. 添加Redis缓存:整合Redis实现分布式缓存,提高系统性能
  2. 实现接口限流:使用令牌桶算法或漏桶算法,防止接口被恶意调用
  3. 添加API文档:整合Swagger/OpenAPI,自动生成API文档
  4. 实现数据导出:支持将用户数据导出为Excel或CSV格式
  5. 添加全文搜索:整合Elasticsearch实现用户信息的全文检索
  6. 实现异步任务:使用Spring的异步功能处理耗时操作
  7. 添加消息队列:整合RabbitMQ或Kafka,实现系统解耦
  8. 实现分布式事务:使用Seata等框架处理分布式环境下的事务一致性

通过本项目的学习,你应该掌握了Spring Boot整合MyBatis开发数据库应用的核心技能,能够根据实际需求进行扩展和优化,开发出高性能、可维护的企业级应用。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李昊哲小课

桃李不言下自成蹊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值