Spring Boot 基础教程
带你系统学习Spring Boot 3.5.6,结合JDK 25的新特性,从基础到实践,全面掌握Spring Boot开发技能。
章节规划
- Spring Boot简介与环境搭建
- 第一个Spring Boot应用
- Spring Boot配置体系
- Spring Boot Web开发
- 数据库操作 - MyBatis篇
- 数据库操作 - MyBatis-Plus篇
- 数据库操作 - JPA篇
- 服务集成与高级特性
- 测试与部署
第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创建项目:
-
访问 https://start.spring.io/
-
选择:
- 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
-
添加依赖:Spring Web
-
IntelliJ IDEA 创建工程


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:在IDE中运行SpringbootTutorialApplication类的main方法
- 方式2:使用Maven命令:
mvn spring-boot:run
- 访问应用:
- 打开浏览器,访问 http://localhost:8080/hello/home 查看根路径响应
- 访问 http://localhost:8080/hello/helloWithPath/YourName 测试路径变量
- 访问 http://localhost:8080/hello/greet?name=YourName 测试查询参数
- 访问 http://localhost:8080/index.html 查看前端页面
- 运行测试:
- 在IDE中运行测试类
- 或使用Maven命令:
mvn test
2.10 开发思路总结
- 项目初始化:使用Spring Initializr快速创建项目结构,选择合适的依赖
- 主程序类:通过@SpringBootApplication注解标识,作为应用入口
- 控制器开发:使用@RestController创建REST接口,通过@RequestMapping系列注解映射请求
- 配置管理:使用application.properties配置应用参数
- 前端页面:将静态资源放在static目录下,Spring Boot会自动映射
- 测试策略:使用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 两种配置文件的对比
| 特性 | properties | YAML |
|---|---|---|
| 语法 | 键值对,使用.分隔 | 层次结构,使用缩进 |
| 可读性 | 中等 | 高,尤其对于复杂配置 |
| 列表支持 | 繁琐 | 简洁 |
| 多环境支持 | 支持 | 支持 |
| 流行度 | 传统,应用广泛 | 现代,越来越流行 |
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支持多种外部化配置方式,优先级从高到低:
- 命令行参数
- 操作系统环境变量
- application-{profile}.properties/yaml
- 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 开发思路总结
- 配置文件选择:根据项目复杂度选择properties或YAML格式,复杂配置优先选择YAML
- 配置注入策略:
- 简单配置使用@Value注解
- 复杂配置或相关配置组使用@ConfigurationProperties
- 多环境管理:
- 为不同环境创建独立配置文件
- 通过spring.profiles.active指定激活环境
- 敏感信息(如密码)应使用环境变量或配置中心
- 配置优先级:了解不同配置方式的优先级,避免配置冲突
- 配置验证:编写测试验证配置是否正确注入
通过本章学习,你应该掌握了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 开发思路总结
-
RESTful API设计:
- 使用合适的HTTP方法(GET、POST、PUT、DELETE)表示操作类型
- 使用URL路径表示资源
- 使用HTTP状态码表示请求结果
- 返回一致的JSON响应格式
-
分层架构:
- 控制器(Controller):处理HTTP请求,返回响应
- 服务层(Service):包含业务逻辑
- 实体类(Entity):表示数据模型
-
异常处理:
- 使用@ControllerAdvice创建全局异常处理器
- 自定义异常类表示特定业务异常
- 返回包含详细信息的错误响应
-
数据验证:
- 使用JSR-303/JSR-380注解进行数据验证
- 在控制器方法参数上使用@Valid触发验证
- 统一处理验证失败的异常
-
测试策略:
- 使用@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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
</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 >= #{params.startDate}
</if>
<!-- 如果提供了endDate参数,则添加创建时间结束条件 -->
<if test="params.endDate != null">
AND created_at <= #{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 开发思路总结
-
MyBatis核心组件
- Mapper接口:定义数据库操作方法
- Mapper XML文件:编写SQL语句,实现接口方法
- SqlSession:MyBatis核心,用于执行SQL命令
-
数据库操作最佳实践
- 使用接口和XML分离的方式组织代码
- 对数据库操作添加事务管理(@Transactional)
- 对查询结果使用POJO对象封装
- 使用resultMap定义结果映射关系
- 对表字段和Java对象属性进行驼峰命名转换
-
动态SQL使用场景
- 多条件组合查询
- 选择性更新字段
- 动态生成排序和分页条件
- 批量操作
-
分页查询实现
- 基于LIMIT关键字实现基础分页
- 计算总记录数实现完整分页功能
- 封装分页结果(当前页、每页条数、总记录数、总页数)
-
测试策略
- 对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的用户管理系统,包含以下核心功能:
- 基础CRUD操作:实现了用户的增删改查功能
- 分页查询:支持按页获取用户数据,提高大数据量下的查询效率
- 动态SQL:根据不同条件动态生成SQL语句,灵活应对复杂查询需求
- 缓存机制:利用MyBatis的一级和二级缓存,减少数据库访问次数
- 类型转换:通过自定义类型处理器,实现Java类型与数据库类型的灵活转换
- 存储过程调用:演示了如何调用数据库存储过程处理复杂业务逻辑
- 安全认证:整合Spring Security实现用户认证和权限控制
- 项目部署:提供了生产环境配置和部署脚本
8.2 扩展方向
- 添加Redis缓存:整合Redis实现分布式缓存,提高系统性能
- 实现接口限流:使用令牌桶算法或漏桶算法,防止接口被恶意调用
- 添加API文档:整合Swagger/OpenAPI,自动生成API文档
- 实现数据导出:支持将用户数据导出为Excel或CSV格式
- 添加全文搜索:整合Elasticsearch实现用户信息的全文检索
- 实现异步任务:使用Spring的异步功能处理耗时操作
- 添加消息队列:整合RabbitMQ或Kafka,实现系统解耦
- 实现分布式事务:使用Seata等框架处理分布式环境下的事务一致性
通过本项目的学习,你应该掌握了Spring Boot整合MyBatis开发数据库应用的核心技能,能够根据实际需求进行扩展和优化,开发出高性能、可维护的企业级应用。

7万+





