Spring Boot 全家桶教程
从入门到精通,覆盖 Spring Boot 核心生态,不含微服务相关内容
第一章:Spring Boot 概述
1.1 什么是 Spring Boot
Spring Boot 是 Spring 框架的一个子项目,旨在简化 Spring 应用的初始搭建和开发过程。它通过自动配置和起步依赖两大核心机制,让开发者可以"开箱即用"地创建生产级别的 Spring 应用。
核心目标
- 为 Spring 开发提供更快的入门体验
- 开箱即用,但通过配置可快速调整
- 提供生产级特性(监控、健康检查、外部化配置)
- 无需代码生成和 XML 配置
与 Spring 框架的关系
Spring Framework(基础)
├── Spring Boot(加速器)
│ ├── Spring Boot Web(Web 开发)
│ ├── Spring Boot Data(数据访问)
│ ├── Spring Boot Security(安全)
│ ├── Spring Boot Actuator(监控)
│ └── ...
└── Spring Cloud(微服务,本文不涉及)
1.2 Spring Boot 的核心特性
| 特性 | 说明 |
|---|---|
| 起步依赖 | 将常用依赖分组,一键引入 |
| 自动配置 | 根据类路径内容自动配置 Bean |
| 嵌入式容器 | 内嵌 Tomcat、Jetty、Undertow |
| 外部化配置 | 支持 application.properties/yml |
| Actuator | 提供监控和管理端点 |
| 无代码生成 | 无需 XML,无需代码生成 |
第二章:环境搭建与快速入门
2.1 环境要求
| 工具 | 版本要求 |
|---|---|
| JDK | 17+ |
| Maven | 3.6+ |
| IDE | IntelliJ IDEA / VS Code / Eclipse |
2.2 创建项目
方式一:Spring Initializr(推荐)
访问 https://start.spring.io 或在 IDE 中直接创建:
- Project:Maven Project
- Language:Java
- Spring Boot:选择最新稳定版(如 3.x)
- Group:
com.example - Artifact:
demo - Dependencies:按需选择(如 Spring Web、Spring Data JPA 等)
方式二:Maven 手动创建
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 继承 Spring Boot 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3 主启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // 组合注解:@Configuration + @EnableAutoConfiguration + @ComponentScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication 详解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 标记为配置类
@EnableAutoConfiguration // 开启自动配置
@ComponentScan // 扫描当前包及其子包下的 Bean
public @interface SpringBootApplication {
// ...
}
2.4 第一个 REST 接口
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello Spring Boot!";
}
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return new User(id, "张三", "zhangsan@example.com");
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
private Long id;
private String name;
private String email;
}
访问 http://localhost:8080/api/hello 即可看到响应。
2.5 项目目录结构
src/main/java
├── com/example/demo/
│ ├── DemoApplication.java # 启动类
│ ├── config/ # 配置类
│ ├── controller/ # 控制器
│ ├── service/ # 服务接口
│ │ └── impl/ # 服务实现
│ ├── repository/ # 数据访问层(JPA)
│ ├── mapper/ # 数据访问层(MyBatis)
│ ├── entity/ # 实体类
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ └── common/ # 公共类(异常、工具等)
src/main/resources
├── application.yml # 主配置文件
├── application-dev.yml # 开发环境配置
├── application-prod.yml # 生产环境配置
└── static/ # 静态资源
src/test/java
└── com/example/demo/
└── DemoApplicationTests.java # 测试类
第三章:自动配置原理(核心)
3.1 @EnableAutoConfiguration 工作原理
自动配置是 Spring Boot 最核心的机制,理解其原理至关重要。
自动配置的加载流程
@SpringBootApplication
└── @EnableAutoConfiguration
└── @Import(AutoConfigurationImportSelector.class)
└── selectImports()
└── 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── 加载所有 AutoConfiguration 类
└── 根据 @Conditional 条件判断是否生效
关键文件
Spring Boot 3.x 中,自动配置类的声明位于:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容示例(部分):
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
...
每个 *AutoConfiguration 类上都有一组 @Conditional 注解,只有满足条件时才会生效。
3.2 条件注解家族
Spring Boot 提供了丰富的条件注解,用于控制自动配置是否生效:
| 注解 | 作用 |
|---|---|
@ConditionalOnClass |
类路径中存在指定类时生效 |
@ConditionalOnMissingClass |
类路径中不存在指定类时生效 |
@ConditionalOnBean |
容器中存在指定 Bean 时生效 |
@ConditionalOnMissingBean |
容器中不存在指定 Bean 时生效 |
@ConditionalOnProperty |
配置文件中存在指定属性时生效 |
@ConditionalOnResource |
类路径中存在指定资源时生效 |
@ConditionalOnWebApplication |
当前是 Web 应用时生效 |
@ConditionalOnNotWebApplication |
当前不是 Web 应用时生效 |
@ConditionalOnExpression |
SpEL 表达式为 true 时生效 |
@ConditionalOnJava |
JDK 版本满足条件时生效 |
@ConditionalOnJndi |
JNDI 资源存在时生效 |
@ConditionalOnSingleCandidate |
容器中指定 Bean 只有一个候选时生效 |
示例:自定义自动配置
// 1. 创建自动配置类
@Configuration
@ConditionalOnClass(MyService.class)
@ConditionalOnProperty(prefix = "my.service", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService() {
return new MyService();
}
}
// 2. 在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册
// 文件内容:
// com.example.demo.config.MyServiceAutoConfiguration
3.3 Spring Boot 配置加载顺序
Spring Boot 的配置加载遵循严格优先级(高到低):
- 命令行参数:
--server.port=8081 - JNDI 属性
- 系统属性:
System.getProperties() - 操作系统环境变量
application-{profile}.yml(带 profile 的配置文件)application.yml(主配置文件)@PropertySource注解的配置
后加载的配置会覆盖先加载的配置,但优先级高的配置会覆盖优先级低的配置。
第四章:配置文件详解
4.1 application.properties vs application.yml
properties 格式
# application.properties
server.port=8081
server.servlet.context-path=/api
spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
yml 格式(推荐)
# application.yml
server:
port: 8081
servlet:
context-path: /api
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
YAML 优点:层次清晰,减少重复前缀,支持多文档块。
4.2 多环境配置
方式一:多文件方式
application.yml # 公共配置
application-dev.yml # 开发环境
application-test.yml # 测试环境
application-prod.yml # 生产环境
方式二:单文件多文档块(YAML)
# 公共配置
server:
port: 8080
spring:
profiles:
active: dev
---
# 开发环境
spring:
config:
activate:
on-profile: dev
server:
port: 8081
---
# 生产环境
spring:
config:
activate:
on-profile: prod
server:
port: 8080
激活方式
# 命令行激活
java -jar demo.jar --spring.profiles.active=prod
# 环境变量激活
export SPRING_PROFILES_ACTIVE=prod
# application.yml 中指定
spring:
profiles:
active: dev
4.3 自定义配置绑定
方式一:@ConfigurationProperties
@Component
@ConfigurationProperties(prefix = "app.config")
@Data
public class AppConfig {
private String name;
private String version;
private List<String> authors;
private Map<String, String> contacts;
private Database database = new Database();
@Data
public static class Database {
private String url;
private String username;
private String password;
}
}
配置文件:
app:
config:
name: MyApp
version: 1.0.0
authors:
- 张三
- 李四
contacts:
email: admin@example.com
phone: "13800138000"
database:
url: jdbc:mysql://localhost:3306/demo
username: root
password: 123456
方式二:@Value
@Component
public class MyComponent {
@Value("${app.config.name}")
private String appName;
@Value("${app.config.version:1.0.0}") // 带默认值
private String version;
@Value("#{2 * 10}") // SpEL 表达式
private int computedValue;
}
@ConfigurationProperties vs @Value
| 特性 | @ConfigurationProperties |
@Value |
|---|---|---|
| 批量绑定 | ✅ 支持(前缀匹配) | ❌ 不支持(逐个绑定) |
| 类型安全 | ✅ 自动类型转换 | ⚠️ 需要手动处理 |
| JSR-303 校验 | ✅ 支持 @Validated |
❌ 不支持 |
| 复杂类型 | ✅ 支持 List、Map、嵌套 | ❌ 仅支持基本类型 |
| SpEL | ❌ 不支持 | ✅ 支持 |
| 推荐度 | 推荐 | 简单场景可用 |
4.4 随机值与占位符
app:
secret: ${random.value} # 随机字符串
number: ${random.int} # 随机整数
range: ${random.int(100,200)} # 指定范围随机数
uuid: ${random.uuid} # 随机 UUID
# 占位符引用
app:
name: MyApp
description: "This is ${app.name}" # 引用同文件中的属性
home: ${user.home} # 引用系统属性
第五章:Spring Boot Web 开发
5.1 常用注解
请求映射
| 注解 | 说明 |
|---|---|
@RestController |
@Controller + @ResponseBody |
@RequestMapping |
通用请求映射 |
@GetMapping |
GET 请求 |
@PostMapping |
POST 请求 |
@PutMapping |
PUT 请求 |
@DeleteMapping |
DELETE 请求 |
@PatchMapping |
PATCH 请求 |
参数绑定
| 注解 | 说明 |
|---|---|
@PathVariable |
获取路径参数 |
@RequestParam |
获取查询参数 |
@RequestBody |
获取请求体 |
@RequestHeader |
获取请求头 |
@CookieValue |
获取 Cookie |
@ModelAttribute |
获取模型属性 |
@SessionAttribute |
获取 Session 属性 |
示例:完整的 CRUD 控制器
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor // 使用 Lombok 生成构造器注入
public class UserController {
private final UserService userService;
@GetMapping
public Result<List<User>> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
return Result.success(userService.findAll(page, size));
}
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.success(userService.findById(id));
}
@PostMapping
public Result<User> create(@Valid @RequestBody UserCreateDTO dto) {
return Result.success(userService.create(dto));
}
@PutMapping("/{id}")
public Result<User> update(
@PathVariable Long id,
@Valid @RequestBody UserUpdateDTO dto) {
return Result.success(userService.update(id, dto));
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.success();
}
}
5.2 统一响应体
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
private long timestamp = System.currentTimeMillis();
public static <T> Result<T> success() {
return new Result<>(200, "success", null, System.currentTimeMillis());
}
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data, System.currentTimeMillis());
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null, System.currentTimeMillis());
}
}
5.3 统一异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 参数绑定异常(如类型转换失败)
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolation(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 校验异常
@ExceptionHandler(BindException.class)
public Result<Void> handleBind(BindException e) {
String message = e.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 自定义业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// 资源未找到
@ExceptionHandler(NoSuchElementException.class)
public Result<Void> handleNotFound(NoSuchElementException e) {
return Result.error(404, "资源不存在");
}
// 未授权
@ExceptionHandler(AccessDeniedException.class)
public Result<Void> handleAccessDenied(AccessDeniedException e) {
return Result.error(403, "权限不足");
}
// 全局兜底异常
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(500, "服务器内部错误,请联系管理员");
}
}
自定义业务异常
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {
private int code;
public BusinessException(String message) {
super(message);
this.code = 500;
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
}
5.4 参数校验
实体类校验
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度需在2-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 40, message = "密码长度需在6-40之间")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@NotNull(message = "性别不能为空")
private Integer gender;
@Future(message = "过期时间必须是未来时间")
private LocalDateTime expireTime;
}
分组校验
// 定义分组接口
public interface CreateGroup {}
public interface UpdateGroup {}
// 使用分组
@Data
public class UserDTO {
@Null(groups = CreateGroup.class, message = "新增时ID必须为空")
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
}
// 控制器中指定分组
@PostMapping
public Result<User> create(@Validated(CreateGroup.class) @RequestBody UserDTO dto) {
return Result.success(userService.create(dto));
}
@PutMapping("/{id}")
public Result<User> update(@Validated(UpdateGroup.class) @RequestBody UserDTO dto) {
return Result.success(userService.update(dto));
}
5.5 跨域配置(CORS)
全局配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
局部配置(@CrossOrigin)
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
// ...
}
5.6 拦截器
// 1. 定义拦截器
@Component
@Slf4j
public class RequestLogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
log.info("请求路径: {}, 方法: {}, IP: {}",
request.getRequestURI(),
request.getMethod(),
request.getRemoteAddr());
return true; // true: 继续执行,false: 中断请求
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
// 控制器执行后,视图渲染前
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 请求完成后(视图渲染后)
}
}
// 2. 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RequestLogInterceptor requestLogInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLogInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/login", "/register", "/static/**"); // 排除路径
}
}
5.7 静态资源处理
Spring Boot 默认静态资源映射:
classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/
访问 http://localhost:8080/index.html 会自动查找以上目录。
自定义静态资源配置
spring:
web:
resources:
static-locations: classpath:/static/, file:${user.dir}/uploads/
add-mappings: true
mvc:
static-path-pattern: /**
第六章:配置文件管理进阶
6.1 外部化配置
通过命令行参数
java -jar app.jar \
--server.port=8081 \
--spring.datasource.url=jdbc:mysql://localhost:3306/demo
通过环境变量
Spring Boot 会自动将环境变量转换为配置属性,规则是:
- 点号
.转换为下划线_ - 转换为大写
# Linux/Mac
export SERVER_PORT=8081
export SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/demo
java -jar app.jar
# Windows PowerShell
$env:SERVER_PORT=8081
$env:SPRING_DATASOURCE_URL="jdbc:mysql://localhost:3306/demo"
java -jar app.jar
6.2 @ConfigurationProperties 校验
@Component
@ConfigurationProperties(prefix = "app.database")
@Validated
@Data
public class DatabaseProperties {
@NotBlank(message = "数据库URL不能为空")
private String url;
@NotBlank(message = "数据库用户名不能为空")
private String username;
@NotBlank(message = "数据库密码不能为空")
private String password;
@Min(value = 1, message = "最大连接数不能小于1")
@Max(value = 100, message = "最大连接数不能大于100")
private int maxActive = 10;
@Min(value = 1000, message = "超时时间不能小于1000ms")
private long timeout = 30000;
}
6.3 配置元数据
在 META-INF/configuration-metadata.json 中添加元数据,IDE 即可获得自动提示:
{
"groups": [
{
"name": "app.database",
"type": "com.example.demo.config.DatabaseProperties",
"sourceType": "com.example.demo.config.DatabaseProperties"
}
],
"properties": [
{
"name": "app.database.url",
"type": "java.lang.String",
"description": "数据库连接URL",
"sourceType": "com.example.demo.config.DatabaseProperties"
},
{
"name": "app.database.max-active",
"type": "java.lang.Integer",
"description": "最大连接数",
"defaultValue": 10,
"sourceType": "com.example.demo.config.DatabaseProperties"
}
]
}
第七章:数据访问层
7.1 使用 JDBC
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari: # HikariCP 连接池配置(Spring Boot 默认)
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
使用 JdbcTemplate
@Repository
@RequiredArgsConstructor
public class UserDao {
private final JdbcTemplate jdbcTemplate;
public List<User> findAll() {
return jdbcTemplate.query(
"SELECT * FROM user",
new BeanPropertyRowMapper<>(User.class)
);
}
public User findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM user WHERE id = ?",
new BeanPropertyRowMapper<>(User.class),
id
);
}
public int save(User user) {
return jdbcTemplate.update(
"INSERT INTO user(username, password, email) VALUES(?, ?, ?)",
user.getUsername(), user.getPassword(), user.getEmail()
);
}
public int update(User user) {
return jdbcTemplate.update(
"UPDATE user SET username = ?, email = ? WHERE id = ?",
user.getUsername(), user.getEmail(), user.getId()
);
}
public int delete(Long id) {
return jdbcTemplate.update("DELETE FROM user WHERE id = ?", id);
}
}
7.2 使用 Spring Data JPA
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
配置
spring:
jpa:
hibernate:
ddl-auto: update # create | create-drop | update | validate | none
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQLDialect
实体类
@Entity
@Table(name = "user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, length = 100)
private String password;
@Column(length = 100)
private String email;
@Column(length = 20)
private String phone;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreationTimestamp // 自动填充创建时间
private LocalDateTime createdAt;
@UpdateTimestamp // 自动填充更新时间
private LocalDateTime updatedAt;
}
public enum UserStatus {
ACTIVE, INACTIVE, LOCKED
}
Repository 接口
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
// 方法命名查询
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
List<User> findByUsernameContaining(String keyword);
// 自定义 JPQL 查询
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
// 原生 SQL 查询
@Query(value = "SELECT * FROM user WHERE username = ?1", nativeQuery = true)
User findByUsernameNative(String username);
// 分页查询
Page<User> findByStatus(UserStatus status, Pageable pageable);
// 更新操作
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
// 删除操作
@Modifying
@Query("DELETE FROM User u WHERE u.status = :status")
int deleteByStatus(@Param("status") UserStatus status);
}
方法命名查询规则
| 关键词 | 示例 | JPQL 片段 |
|---|---|---|
And |
findByLastnameAndFirstname |
… WHERE x.lastname = ?1 AND x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
… WHERE x.lastname = ?1 OR x.firstname = ?2 |
Between |
findByAgeBetween |
… WHERE x.age BETWEEN ?1 AND ?2 |
LessThan |
findByAgeLessThan |
… WHERE x.age < ?1 |
GreaterThan |
findByAgeGreaterThan |
… WHERE x.age > ?1 |
Like |
findByNameLike |
… WHERE x.name LIKE ?1 |
Containing |
findByNameContaining |
… WHERE x.name LIKE %?1% |
In |
findByAgeIn |
… WHERE x.age IN ?1 |
OrderBy |
findByAgeOrderByNameDesc |
… ORDER BY x.name DESC |
Not |
findByNameNot |
… WHERE x.name <> ?1 |
IsNull |
findByAgeIsNull |
… WHERE x.age IS NULL |
True |
findByActiveTrue |
… WHERE x.active = true |
Top/First |
findTop10ByName |
限制结果条数 |
关联关系映射
// 一对一
@Entity
public class UserProfile {
@Id
private Long id;
@OneToOne
@MapsId
@JoinColumn(name = "user_id")
private User user;
private String avatar;
private String bio;
}
// 一对多(一方)
@Entity
public class User {
// ...
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Address> addresses;
}
// 多对一(多方)
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
private String province;
private String city;
private String detail;
}
// 多对多
@Entity
public class Student {
@Id
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses;
}
@Entity
public class Course {
@Id
private Long id;
@ManyToMany(mappedBy = "courses")
private Set<Student> students;
}
继承 JpaRepository 后可用的方法
// 基本 CRUD
userRepository.save(user); // 新增/更新
userRepository.saveAll(users); // 批量保存
userRepository.findById(id); // 按ID查询
userRepository.findAll(); // 查询全部
userRepository.findAllById(ids); // 批量查询
userRepository.count(); // 统计数量
userRepository.existsById(id); // 判断是否存在
userRepository.delete(user); // 删除
userRepository.deleteById(id); // 按ID删除
userRepository.deleteAll(); // 清空
// 分页排序
userRepository.findAll(PageRequest.of(0, 10)); // 分页
userRepository.findAll(Sort.by("createdAt").descending()); // 排序
userRepository.findAll(PageRequest.of(0, 10, Sort.by("id").asc())); // 分页+排序
// 动态查询(Specification)
userRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("status"), UserStatus.ACTIVE));
if (keyword != null) {
predicates.add(cb.like(root.get("username"), "%" + keyword + "%"));
}
return cb.and(predicates.toArray(new Predicate[0]));
});
7.3 使用 MyBatis(主流选择)
引入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true # 数据库下划线自动转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL 日志
cache-enabled: true # 开启二级缓存
lazy-loading-enabled: true
aggressive-lazy-loading: false
实体类
@Data
public class User {
private Long id;
private String username;
private String password;
private String email;
private String phone;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
Mapper 接口
@Mapper // 或在启动类上使用 @MapperScan
public interface UserMapper {
// ========== 基本 CRUD ==========
@Insert("INSERT INTO user(username, password, email, phone) VALUES(#{username}, #{password}, #{email}, #{phone})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(Long id);
@Update("UPDATE user SET username = #{username}, email = #{email} WHERE id = #{id}")
int update(User user);
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Long id);
@Select("SELECT * FROM user")
List<User> findAll();
// ========== 动态 SQL(配合 XML) ==========
List<User> findByCondition(UserQueryDTO query);
// ========== 分页查询 ==========
List<User> findByPage(PageParam page);
long countTotal();
// ========== 批量操作 ==========
int batchInsert(List<User> users);
int batchUpdate(List<User> users);
}
XML 映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="status" column="status"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<!-- 通用 SQL 片段 -->
<sql id="BaseColumns">
id, username, password, email, phone, status, created_at, updated_at
</sql>
<!-- 条件查询(动态 SQL) -->
<select id="findByCondition" resultMap="BaseResultMap">
SELECT <include refid="BaseColumns"/>
FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND created_at >= #{startTime}
</if>
<if test="endTime != null">
AND created_at <= #{endTime}
</if>
</where>
ORDER BY created_at DESC
</select>
<!-- 分页查询 -->
<select id="findByPage" resultMap="BaseResultMap">
SELECT <include refid="BaseColumns"/>
FROM user
ORDER BY id DESC
LIMIT #{offset}, #{size}
</select>
<!-- 批量插入 -->
<insert id="batchInsert">
INSERT INTO user(username, password, email, phone, status, created_at, updated_at)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.password}, #{user.email}, #{user.phone},
#{user.status}, NOW(), NOW())
</foreach>
</insert>
<!-- 批量更新 -->
<update id="batchUpdate" parameterType="list">
<foreach collection="list" item="user" separator=";">
UPDATE user
<set>
<if test="user.username != null">username = #{user.username},</if>
<if test="user.email != null">email = #{user.email},</if>
<if test="user.phone != null">phone = #{user.phone},</if>
</set>
WHERE id = #{user.id}
</foreach>
</update>
</mapper>
MyBatis-Plus 增强(推荐)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
// Mapper 继承 BaseMapper,即获得通用 CRUD
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 无需编写基本的 CRUD 方法
}
// 使用示例
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
// BaseMapper 提供的开箱即用方法
public void demo() {
// 新增
userMapper.insert(user);
// 删除
userMapper.deleteById(1L);
userMapper.delete(new LambdaQueryWrapper<User>()
.eq(User::getStatus, 0));
// 更新
userMapper.updateById(user);
userMapper.update(user, new LambdaUpdateWrapper<User>()
.eq(User::getId, 1L)
.set(User::getEmail, "new@email.com"));
// 查询
userMapper.selectById(1L);
userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, "admin"));
userMapper.selectList(new LambdaQueryWrapper<User>()
.like(User::getUsername, "张")
.orderByDesc(User::getCreatedAt));
// 分页
Page<User> page = userMapper.selectPage(
new Page<>(1, 10),
new LambdaQueryWrapper<User>()
.eq(User::getStatus, 1));
// 批量
userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L));
}
}
第八章:事务管理
8.1 Spring 事务管理
开启事务
@SpringBootApplication
@EnableTransactionManagement // 通常默认开启,可省略
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
使用 @Transactional
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final LogService logService;
@Transactional // 未指定则使用默认配置
public void createUser(User user) {
userMapper.insert(user);
// 插入日志
logService.saveLog("创建用户: " + user.getUsername());
// 如果这里抛出异常,上面的 insert 也会回滚
}
}
8.2 @Transactional 详解
常用参数
@Transactional(
propagation = Propagation.REQUIRED, // 事务传播行为
isolation = Isolation.READ_COMMITTED, // 隔离级别
timeout = 30, // 超时时间(秒)
readOnly = false, // 是否只读
rollbackFor = Exception.class, // 指定回滚异常
noRollbackFor = BusinessException.class // 指定不回滚异常
)
7 种事务传播行为
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) |
支持当前事务,不存在则新建 |
SUPPORTS |
支持当前事务,不存在则以非事务方式执行 |
MANDATORY |
必须在当前事务中运行,否则抛异常 |
REQUIRES_NEW |
挂起当前事务,新建事务执行 |
NOT_SUPPORTED |
以非事务方式执行,挂起当前事务 |
NEVER |
以非事务方式执行,存在事务则抛异常 |
NESTED |
在嵌套事务中执行(JDBC Savepoint 机制) |
事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
DEFAULT |
取决于数据库 | 取决于数据库 | 取决于数据库 |
READ_UNCOMMITTED |
✅ 可能 | ✅ 可能 | ✅ 可能 |
READ_COMMITTED(多数数据库默认) |
❌ | ✅ 可能 | ✅ 可能 |
REPEATABLE_READ(MySQL 默认) |
❌ | ❌ | ✅ 可能 |
SERIALIZABLE |
❌ | ❌ | ❌ |
8.3 事务失效的常见场景
// 场景 1:在同一个类中方法调用
@Service
public class UserService {
public void methodA() {
methodB(); // 直接调用,事务不会生效!
}
@Transactional
public void methodB() {
// ...
}
}
// 解决方案 1:注入自身代理
@Service
public class UserService {
@Autowired
private UserService self; // 注入代理对象
public void methodA() {
self.methodB(); // 通过代理调用,事务生效
}
@Transactional
public void methodB() {
// ...
}
}
// 场景 2:异常被捕获
@Transactional
public void method() {
try {
// ... 数据库操作
// 如果异常被捕获,事务不会回滚
} catch (Exception e) {
log.error("出错了", e);
// 没有抛出异常,事务提交
}
}
// 解决方案:捕获后需要抛出
@Transactional
public void method() {
try {
// ... 数据库操作
} catch (Exception e) {
log.error("出错了", e);
throw e; // 重新抛出,触发回滚
}
}
// 场景 3:rollbackFor 未指定,且抛出了非 RuntimeException
@Transactional // 默认只回滚 RuntimeException 和 Error
public void method() throws Exception {
throw new Exception("检查异常"); // 不会回滚!
}
// 解决方案:指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
throw new Exception("检查异常"); // 会回滚
}
8.4 编程式事务
@Service
@RequiredArgsConstructor
public class UserService {
private final TransactionTemplate transactionTemplate;
public void createUserWithTransaction(User user) {
transactionTemplate.execute(status -> {
try {
userMapper.insert(user);
logService.saveLog("创建用户: " + user.getUsername());
return null;
} catch (Exception e) {
status.setRollbackOnly(); // 手动回滚
throw e;
}
});
}
}
第九章:缓存
9.1 Spring Cache 抽象
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
开启缓存
@SpringBootApplication
@EnableCaching // 开启缓存抽象
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
缓存注解
@Service
public class UserService {
// @Cacheable:缓存存在则直接返回,不存在则执行方法并缓存
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
// 模拟耗时查询
slowQuery();
return userMapper.findById(id);
}
// @CachePut:每次都执行方法,并将结果更新到缓存
@CachePut(value = "users", key = "#user.id")
public User update(User user) {
userMapper.update(user);
return user;
}
// @CacheEvict:删除缓存
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) {
userMapper.deleteById(id);
}
// 组合:清空整个缓存
@CacheEvict(value = "users", allEntries = true)
public void clearCache() {
// 清空 users 缓存区域
}
// 多条件缓存
@Caching(
cacheable = @Cacheable("users"),
put = @CachePut(value = "users", key = "#result.email")
)
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
}
缓存条件
// condition:满足条件才缓存
@Cacheable(value = "users", condition = "#id > 0")
// unless:满足条件则不缓存
@Cacheable(value = "users", unless = "#result == null")
// 组合使用
@Cacheable(value = "users",
condition = "#id > 0",
unless = "#result.status == 0")
9.2 Redis 集成
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置
spring:
data:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
使用 RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON 序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(
LazyValidatorFactory.getDefaultPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
jsonSerializer.setObjectMapper(om);
// String 序列化
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// key 使用 String 序列化
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value 使用 JSON 序列化
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
@Service
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
private final StringRedisTemplate stringRedisTemplate;
// ========== String 操作 ==========
public void setString(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
public void setStringWithExpire(String key, String value, long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
// ========== Hash 操作 ==========
public void setHash(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public Object getHash(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
public Map<Object, Object> getHashAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
// ========== List 操作 ==========
public void pushToList(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
}
public Object popFromList(String key) {
return redisTemplate.opsForList().leftPop(key);
}
public List<Object> getList(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
// ========== Set 操作 ==========
public void addToSet(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public Set<Object> getSet(String key) {
return redisTemplate.opsForSet().members(key);
}
// ========== ZSet 操作 ==========
public void addToZSet(String key, Object value, double score) {
redisTemplate.opsForZSet().add(key, value, score);
}
public Set<Object> getZSetRange(String key, long start, long end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
// ========== 通用操作 ==========
public boolean expire(String key, long timeout, TimeUnit unit) {
return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit));
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void delete(String key) {
redisTemplate.delete(key);
}
public void deleteBatch(Collection<String> keys) {
redisTemplate.delete(keys);
}
// ========== 分布式锁(简化版)==========
public boolean tryLock(String key, String value, long timeout) {
return Boolean.TRUE.equals(
stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, timeout, TimeUnit.SECONDS));
}
public void unlock(String key, String value) {
String current = stringRedisTemplate.opsForValue().get(key);
if (value.equals(current)) {
stringRedisTemplate.delete(key);
}
}
}
9.3 使用 Redis 作为缓存实现
spring:
cache:
type: redis
redis:
time-to-live: 3600000 # 缓存过期时间(毫秒)
cache-null-values: true # 是否缓存空值
key-prefix: "cache:"
use-key-prefix: true
自定义 Redis 缓存管理器
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return builder -> {
// 默认配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
// users 缓存:30 分钟过期
configMap.put("users", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.prefixName("users:"));
// roles 缓存:1 小时过期
configMap.put("roles", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.prefixName("roles:"));
// verifyCodes 缓存:5 分钟过期
configMap.put("verifyCodes", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.prefixName("vc:")
.disableCachingNullValues());
builder.withInitialCacheConfigurations(configMap);
};
}
}
第十章:异步与定时任务
10.1 异步任务
开启异步
@SpringBootApplication
@EnableAsync // 开启异步支持
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
使用 @Async
@Service
public class AsyncService {
@Async // 在异步线程中执行
public CompletableFuture<String> sendEmail(String to, String content) {
try {
// 模拟发送邮件耗时
Thread.sleep(2000);
log.info("邮件发送成功: {}", to);
return CompletableFuture.completedFuture("发送成功");
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
@Async
public void generateReport(Long reportId) {
// 生成报表...
log.info("报表生成完成: {}", reportId);
}
}
// 调用异步方法
@Service
public class ReportController {
@Autowired
private AsyncService asyncService;
public void createReport() {
// 异步调用,立即返回
asyncService.generateReport(1L);
// 等待异步任务结果
CompletableFuture<String> future = asyncService.sendEmail("test@test.com", "内容");
future.thenAccept(result -> log.info("结果: {}", result));
}
}
自定义线程池
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setKeepAliveSeconds(60); // 线程存活时间
executor.setThreadNamePrefix("async-"); // 线程名前缀
executor.setWaitForTasksToCompleteOnShutdown(true); // 优雅关闭
executor.setAwaitTerminationSeconds(30); // 等待秒数
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("异步任务执行失败: {}.{}", method.getDeclaringClass(), method.getName(), ex);
}
}
10.2 定时任务
开启定时任务
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
使用 @Scheduled
@Component
@Slf4j
public class ScheduledTasks {
// 固定延迟:上次执行完毕后 5 秒执行
@Scheduled(fixedDelay = 5000)
public void taskWithFixedDelay() {
log.info("固定延迟任务执行: {}", LocalDateTime.now());
}
// 固定频率:每隔 5 秒执行一次(不考虑上次执行时间)
@Scheduled(fixedRate = 5000)
public void taskWithFixedRate() {
log.info("固定频率任务执行: {}", LocalDateTime.now());
}
// 初始延迟 10 秒后,再按固定频率执行
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void taskWithInitialDelay() {
log.info("含初始延迟的任务执行: {}", LocalDateTime.now());
}
// Cron 表达式
// 每天凌晨 2 点执行
@Scheduled(cron = "0 0 2 * * ?")
public void taskWithCron() {
log.info("定时备份任务执行: {}", LocalDateTime.now());
}
// 每 5 分钟执行一次
@Scheduled(cron = "0 */5 * * * ?")
public void taskEvery5Minutes() {
log.info("每5分钟任务执行: {}", LocalDateTime.now());
}
// 工作日早 9 点执行
@Scheduled(cron = "0 0 9 ? * MON-FRI")
public void taskOnWorkdays() {
log.info("工作日任务执行: {}", LocalDateTime.now());
}
}
Cron 表达式详解
格式:秒 分 时 日 月 周
示例:
"0 0 2 * * ?" 每天凌晨 2 点
"0 0/5 * * * ?" 每 5 分钟
"0 0 9-18 * * ?" 每天 9 点到 18 点每小时
"0 30 23 * * ?" 每天 23:30
"0 0 0 1 * ?" 每月 1 日零点
"0 0 6 ? * MON" 每周一早上 6 点
"0 0 3 ? * 6#1" 每月第一个周五 3 点
| 字段 | 取值范围 | 允许的特殊字符 |
|---|---|---|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 时 | 0-23 | , - * / |
| 日 | 1-31 | , - * ? / L W |
| 月 | 1-12 或 JAN-DEC | , - * / |
| 周 | 1-7 或 SUN-SAT | , - * ? / L # |
?表示不指定值(日和周互斥)L表示最后一天W表示最近的工作日#表示第几个星期几
自定义线程池
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 线程池大小
scheduler.setThreadNamePrefix("scheduled-"); // 线程名前缀
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
第十一章:日志管理
11.1 集成 Logback(Spring Boot 默认)
配置文件 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<!-- 从 application.yml 读取配置 -->
<springProperty scope="context" name="appName" source="spring.application.name"/>
<springProperty scope="context" name="logPath" source="logging.file.path" defaultValue="./logs"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出(INFO) -->
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}-info-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 文件输出(ERROR) -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}-error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出(重要:避免日志 I/O 影响性能) -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="FILE_ERROR"/>
</appender>
<!-- 包级别日志控制 -->
<logger name="org.springframework" level="WARN"/>
<logger name="org.mybatis" level="INFO"/>
<logger name="com.example.demo" level="DEBUG"/>
<!-- 根日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC"/>
</root>
</configuration>
application.yml 配置
logging:
level:
root: info
com.example.demo: debug
org.springframework.web: warn
file:
path: ./logs
name: ${logging.file.path}/app.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
logback:
rollingpolicy:
max-history: 30
max-file-size: 10MB
11.2 使用 Lombok 的 @Slf4j
@RestController
@Slf4j // 自动生成 log 字段
public class UserController {
@GetMapping("/users/{id}")
public Result<User> getById(@PathVariable Long id) {
log.debug("查询用户: id={}", id);
try {
User user = userService.findById(id);
log.info("查询成功: {}", user);
return Result.success(user);
} catch (Exception e) {
log.error("查询用户失败: id={}", id, e);
return Result.error("查询失败");
}
}
}
第十二章:Actuator 监控
12.1 引入与配置
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,logfile,loggers
exclude: shutdown
endpoint:
health:
show-details: always
show-components: always
metrics:
enabled: true
info:
enabled: true
info:
env:
enabled: true
12.2 内置端点
| 端点 | 路径 | 说明 |
|---|---|---|
health |
/actuator/health |
应用健康状态 |
info |
/actuator/info |
应用信息 |
metrics |
/actuator/metrics |
应用指标 |
env |
/actuator/env |
环境配置 |
logfile |
/actuator/logfile |
日志文件 |
loggers |
/actuator/loggers |
日志级别 |
mappings |
/actuator/mappings |
URL 映射 |
beans |
/actuator/beans |
Spring 容器中的 Bean |
threaddump |
/actuator/threaddump |
线程快照 |
heapdump |
/actuator/heapdump |
堆转储 |
scheduledtasks |
/actuator/scheduledtasks |
定时任务 |
caches |
/actuator/caches |
缓存管理 |
自定义 Info
info:
app:
name: '@project.name@' # 读取 Maven 属性
version: '@project.version@'
description: '@project.description@'
java:
version: '@java.version@'
build:
artifact: '@project.artifactId@'
// 或通过代码自定义
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("custom",
Map.of("author", "张三",
"startupTime", System.currentTimeMillis()));
}
}
12.3 自定义健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(3)) {
return Health.up()
.withDetail("database", "MySQL")
.withDetail("status", "connected")
.build();
} else {
return Health.down()
.withDetail("reason", "数据库连接无效")
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("reason", "数据库连接失败")
.build();
}
}
}
第十三章:Spring Security
13.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入后所有接口默认被保护,会生成默认密码(启动日志中可查看)。
13.2 基础配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级别安全注解
@RequiredArgsConstructor
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(前后端分离项目)
.csrf(AbstractHttpConfigurer::disable)
// 无状态(前后端分离)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 请求授权
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录注册放行
.requestMatchers("/api/public/**").permitAll() // 公共接口放行
.requestMatchers("/actuator/health").permitAll() // 健康检查放行
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // Swagger 放行
.anyRequest().authenticated() // 其他请求需认证
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(
new ObjectMapper().writeValueAsString(
Result.error(401, "未授权,请先登录")
));
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(
new ObjectMapper().writeValueAsString(
Result.error(403, "权限不足")
));
})
);
return http.build();
}
// 密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 推荐 BCrypt 加密
}
// JWT 认证过滤器(见下方)
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
13.3 JWT 认证实现
JWT 工具类
@Component
public class JwtUtils {
@Value("${jwt.secret:defaultSecretKey123456}")
private String secret;
@Value("${jwt.expiration:86400000}") // 默认 24 小时
private long expiration;
private final SecretKey key;
public JwtUtils() {
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
}
// 生成 Token
public String generateToken(String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key)
.compact();
}
// 从 Token 中提取用户名
public String getUsernameFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// 验证 Token
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
JWT 认证过滤器
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
登录接口
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtils jwtUtils;
@PostMapping("/login")
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword()));
// 生成 Token
String token = jwtUtils.generateToken(authentication.getName());
return Result.success(new LoginResponse(token, "Bearer", jwtUtils.getExpiration()));
}
@PostMapping("/register")
public Result<Void> register(@Valid @RequestBody RegisterRequest request) {
userService.register(request);
return Result.success();
}
}
13.4 方法级别安全
@RestController
@RequestMapping("/api/users")
public class UserController {
// 只有 ADMIN 角色可访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public Result<List<User>> list() {
return Result.success(userService.findAll());
}
// 登录用户可访问
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
return Result.success(userService.findById(id));
}
// 拥有指定权限可访问
@PreAuthorize("hasAuthority('user:create')")
@PostMapping
public Result<User> create(@RequestBody User user) {
return Result.success(userService.create(user));
}
// 当前用户只能操作自己的数据
@PreAuthorize("#id == authentication.principal.id")
@PutMapping("/{id}")
public Result<User> update(@PathVariable Long id, @RequestBody User user) {
return Result.success(userService.update(id, user));
}
// 组合条件
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.success();
}
// 方法执行后校验
@PostAuthorize("returnObject.data.username == authentication.principal.username")
@GetMapping("/profile")
public Result<User> profile() {
return Result.success(userService.getCurrentUser());
}
}
第十四章:测试
14.1 单元测试
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
基础测试
@SpringBootTest // 启动完整 Spring 容器
class DemoApplicationTests {
@Autowired
private UserService userService;
@Test
void contextLoads() {
assertThat(userService).isNotNull();
}
}
Web 层测试
@WebMvcTest(UserController.class) // 仅加载 Web 层
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // Mock 业务层
private UserService userService;
@Test
void testGetUser() throws Exception {
// 准备 Mock 数据
User user = new User(1L, "张三", "zhangsan@example.com");
when(userService.findById(1L)).thenReturn(user);
// 发送请求并验证
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.username").value("张三"))
.andExpect(jsonPath("$.data.email").value("zhangsan@example.com"));
// 验证服务层被调用
verify(userService, times(1)).findById(1L);
}
@Test
void testCreateUser() throws Exception {
UserCreateDTO dto = new UserCreateDTO("newuser", "123456", "new@test.com");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
verify(userService, times(1)).create(any(UserCreateDTO.class));
}
@Test
void testValidation() throws Exception {
// 测试参数校验
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(400));
}
}
数据层测试
@DataJpaTest // 仅加载 JPA 相关组件
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void testSaveAndFind() {
// 准备数据
User user = new User();
user.setUsername("test");
user.setPassword("123456");
user.setStatus(UserStatus.ACTIVE);
// 保存
User saved = entityManager.persistAndFlush(user);
// 查询
Optional<User> found = userRepository.findById(saved.getId());
assertThat(found).isPresent();
assertThat(found.get().getUsername()).isEqualTo("test");
}
@Test
void testFindByUsername() {
User user = new User();
user.setUsername("admin");
user.setPassword("123456");
user.setStatus(UserStatus.ACTIVE);
entityManager.persistAndFlush(user);
Optional<User> found = userRepository.findByUsername("admin");
assertThat(found).isPresent();
}
}
MyBatis Mapper 测试
@MybatisTest // 加载 MyBatis 相关组件
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
// 或使用内嵌数据库
// @AutoConfigureTestDatabase
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testInsertAndSelect() {
User user = new User();
user.setUsername("test");
user.setPassword("password");
user.setEmail("test@test.com");
int result = userMapper.insert(user);
assertThat(result).isEqualTo(1);
assertThat(user.getId()).isNotNull();
User found = userMapper.findById(user.getId());
assertThat(found.getUsername()).isEqualTo("test");
}
@Test
void testFindByCondition() {
// 测试动态 SQL
UserQueryDTO query = new UserQueryDTO();
query.setUsername("张");
List<User> users = userMapper.findByCondition(query);
assertThat(users).isNotEmpty();
}
}
14.2 集成测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testFullFlow() {
// 1. 创建用户
UserCreateDTO dto = new UserCreateDTO("integration", "123456", "int@test.com");
ResponseEntity<Result> createResponse = restTemplate.postForEntity(
"http://localhost:" + port + "/api/users",
dto, Result.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(createResponse.getBody().getCode()).isEqualTo(200);
// 2. 查询用户列表
ResponseEntity<Result> listResponse = restTemplate.exchange(
"http://localhost:" + port + "/api/users?page=1&size=10",
HttpMethod.GET, null, Result.class);
assertThat(listResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
第十五章:部署与运维
15.1 打包
# 打包(跳过测试)
mvn clean package -DskipTests
# 打包为可执行 JAR
mvn clean package -DskipTests -Dspring-boot.repackage=true
# 打包后文件位置
# target/demo-0.0.1-SNAPSHOT.jar
15.2 运行
# 基础启动
java -jar demo-0.0.1-SNAPSHOT.jar
# 指定端口
java -jar demo-0.0.1-SNAPSHOT.jar --server.port=8081
# 指定环境
java -jar demo-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
# JVM 参数调优
java -Xms512m -Xmx1024m \
-XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/logs/dump.hprof \
-jar demo-0.0.1-SNAPSHOT.jar
15.3 容器化部署(Docker)
Dockerfile
# 多阶段构建
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# 时区
ENV TZ=Asia/Shanghai
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml
version: '3.8'
services:
app:
build: .
container_name: demo-app
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/demo
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root123
- SPRING_DATA_REDIS_HOST=redis
depends_on:
- db
- redis
networks:
- app-network
db:
image: mysql:8.0
container_name: demo-db
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: demo
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
networks:
- app-network
redis:
image: redis:7-alpine
container_name: demo-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mysql-data:
redis-data:
15.4 JVM 参数说明
# 内存配置
-Xms512m # 初始堆大小
-Xmx1024m # 最大堆大小
-XX:MetaspaceSize=128m # 元空间初始大小
-XX:MaxMetaspaceSize=256m # 元空间最大大小
# GC 配置
-XX:+UseG1GC # 使用 G1 垃圾回收器
-XX:MaxGCPauseMillis=200 # GC 最大暂停时间(ms)
# 调试配置
-XX:+HeapDumpOnOutOfMemoryError # OOM 时生成堆转储
-XX:HeapDumpPath=/path/to/dump # 堆转储路径
-Xlog:gc*:file=/var/logs/gc.log # GC 日志
# 远程调试
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
第十六章:最佳实践
16.1 项目结构
src/main/java/com/example/demo/
├── common/ # 公共模块
│ ├── exception/ # 异常定义
│ ├── result/ # 统一响应
│ └── constant/ # 常量定义
├── config/ # 配置类
├── controller/ # 控制器
├── service/ # 服务接口
│ └── impl/ # 服务实现
├── repository/ # 持久层(JPA)
├── mapper/ # 持久层(MyBatis)
├── entity/ # 实体类
├── dto/ # 数据传输对象
├── vo/ # 视图对象
├── converter/ # 对象转换器
└── util/ # 工具类
16.2 分层架构规范
Controller 层 → 接收请求,参数校验,调用 Service
↓
Service 层 → 业务逻辑,事务管理
↓
Repository/Mapper 层 → 数据访问
↓
Database → 数据库
原则:
- Controller 不包含业务逻辑
- Service 层处理业务逻辑和事务
- 层与层之间通过接口调用
- 使用 DTO 进行层间数据传递
16.3 异常处理流程
Controller
↓ 抛出异常
GlobalExceptionHandler
↓ 统一处理
Result.error()
↓ 返回给前端
16.4 日志规范
// ✅ 正确:使用占位符
log.debug("查询用户: id={}, name={}", id, name);
// ❌ 错误:字符串拼接(即使不输出也会执行)
log.debug("查询用户: id=" + id + ", name=" + name);
// ✅ 正确:记录异常完整堆栈
log.error("操作失败", exception);
// ❌ 错误:只记录异常消息
log.error("操作失败: " + exception.getMessage());
16.5 数据库规范
-- 表名:小写加下划线
CREATE TABLE user_account (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
status TINYINT DEFAULT 1 COMMENT '状态: 0-禁用 1-启用',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
规范要点:
- 表名使用小写+下划线
- 字段名使用小写+下划线
- 必须包含
id主键、created_at、updated_at - 使用
utf8mb4字符集 - 合理添加索引
- 字段使用合适的类型和长度
16.6 API 设计规范
// ✅ 正确:RESTful 风格
GET /api/users # 列表
GET /api/users/{id} # 详情
POST /api/users # 新增
PUT /api/users/{id} # 全量更新
PATCH /api/users/{id} # 部分更新
DELETE /api/users/{id} # 删除
// ✅ 正确:统一响应格式
{
"code": 200,
"message": "success",
"data": { ... },
"timestamp": 1700000000000
}
// ❌ 错误:直接返回实体
// 应该始终使用统一响应体包装
// ✅ 正确:版本管理
/api/v1/users
/api/v2/users
16.7 常见性能优化
数据库优化
// ✅ 正确:使用索引
@Table(indexes = {
@Index(name = "idx_username", columnList = "username"),
@Index(name = "idx_status_created", columnList = "status,createdAt")
})
// ✅ 正确:只查询需要的字段
@Query("SELECT u.id, u.username FROM User u")
// ❌ 避免:N+1 查询
// 使用 @EntityGraph 或 JOIN FETCH
@EntityGraph(attributePaths = {"roles"})
@Query("SELECT u FROM User u WHERE u.id = :id")
Optional<User> findByIdWithRoles(@Param("id") Long id);
缓存优化
// ✅ 正确:对频繁查询的接口使用缓存
@Cacheable(value = "users", unless = "#result == null")
// ✅ 正确:更新数据时同步更新缓存
@CachePut(value = "users", key = "#user.id")
// ✅ 正确:删除数据时清空相关缓存
@CacheEvict(value = "users", allEntries = true)
连接池配置
spring:
datasource:
hikari:
minimum-idle: 10
maximum-pool-size: 50
idle-timeout: 600000
max-lifetime: 1800000
connection-timeout: 30000
pool-name: HikariPool
第十七章:常见问题与解决方案
17.1 启动失败
端口被占用
# 查看端口占用
netstat -ano | findstr :8080
# 修改端口
server.port=8081
# 或杀掉进程
taskkill /PID <PID> /F
数据库连接失败
# 检查配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
依赖冲突
<!-- 排除冲突依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
17.2 自动配置不生效
// 检查条件注解是否满足
// 可以在 application.yml 中开启调试日志
debug: true
// 或
logging:
level:
org.springframework.boot.autoconfigure: DEBUG
// 启动日志中会输出:
// Positive matches: (匹配成功的自动配置)
// Negative matches: (匹配失败的自动配置,含原因)
// Exclusions: (被排除的自动配置)
17.3 @Autowired 警告
// ❌ 字段注入(不推荐)
@Autowired
private UserService userService;
// ✅ 构造器注入(推荐,Lombok)
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
}
// ✅ 设值注入(可选依赖)
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
17.4 跨域问题
// 确认 CORS 配置已生效
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}
17.5 事务不回滚
// 检查点 1:rollbackFor 是否正确
@Transactional(rollbackFor = Exception.class) // 覆盖所有异常
// 检查点 2:是否在同级方法调用
// 事务只对代理对象外部调用生效
// 检查点 3:是否捕获了异常
// 注意:不要 try-catch 后不抛出
// 检查点 4:数据库引擎是否支持事务
// MySQL MyISAM 不支持事务,需使用 InnoDB
附录:常用配置速查
application.yml 全配置模板
# ========== 服务器配置 ==========
server:
port: 8080
servlet:
context-path: /
tomcat:
max-connections: 10000
max-threads: 200
min-spare-threads: 10
connection-timeout: 5000ms
# ========== Spring 核心配置 ==========
spring:
application:
name: demo-app
# 配置激活
profiles:
active: dev
# ========== 数据源 ==========
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 5
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# ========== JPA ==========
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
format_sql: true
# ========== Redis ==========
data:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
# ========== 缓存 ==========
cache:
type: redis
redis:
time-to-live: 3600000
cache-null-values: true
# ========== Jackson ==========
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
default-property-inclusion: non_null
# ========== 文件上传 ==========
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 50MB
# ========== 日志 ==========
logging:
level:
root: info
com.example.demo: debug
file:
path: ./logs
# ========== Actuator ==========
management:
endpoints:
web:
exposure:
include: health,info,metrics,loggers
endpoint:
health:
show-details: always
# ========== MyBatis ==========
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Maven POM 常用依赖
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 安全 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 开发工具 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
💡 本文档专注 Spring Boot 核心生态,涵盖自动配置原理、Web 开发、数据访问(JPA/MyBatis)、事务管理、缓存(Redis)、异步定时任务、安全(Spring Security + JWT)、监控(Actuator)、测试、部署运维及最佳实践。不涉及 Spring Cloud 微服务相关内容。
如需深入学习某个主题,建议结合官方文档和实际项目练习。
Comments | 0条评论