本备忘单旨在快速理解 Spring 框架的核心思想与 Spring Boot 的自动化配置、约定优于配置的理念。它提供了从基础 IoC/AOP 到 Web 开发、数据持久化、异步任务和测试等场景中最常用、最核心的代码片段和配置示例。
一个软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭。当需要为软件增加新功能时,应当通过增加新的代码来实现,而不是去修改那些已经被测试过且运行正常的旧代码。
传统的高耦合结构中,上层依赖下层,导致下层模块的改动会连锁影响上层,违反了开-闭原则。
依赖倒置原则倡导我们应该 面向抽象(接口)编程,而不是面向具体实现编程。其目标是让上层不再依赖下层,实现依赖关系的“倒置”。
Spring 框架正是为解决传统编程中对象创建和关系管理的难题而生,它通过将对象的创建权和关系管理权从业务代码中移交出去,引出了其核心思想——控制反转。
控制反转 (Inversion of Control, IoC) 是一种核心设计思想,其主要目的就是用来降低代码之间的耦合度。核心理念是:将对象的创建权和对象之间关系的管理权交出去,由一个独立的第三方容器来负责。
依赖注入 (Dependency Injection, DI) 是实现 IoC 思想最常见、最重要的方式。Spring 是一个完美实现了 IoC 思想,并以 DI 作为其核心机制的顶级容器框架。
Spring 是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,旨在简化企业级 Java 应用的开发。它通过促进松耦合和可测试性,让我们能将精力完全集中在核心业务逻辑的实现上。
| 模块 (Module) | 核心功能描述 |
|---|---|
| Spring Core | (框架基石) 实现了控制反转 (IoC),是所有功能的基础。 |
| Spring AOP | (切面编程) 提供完整的面向切面编程支持。 |
| Spring DAO | (数据访问) 对原生 JDBC 进行了抽象和封装。 |
| Spring ORM | (对象关系映射) 提供对 MyBatis、Hibernate 等主流 ORM 框架的集成支持。 |
| Spring Context | (应用上下文) 提供国际化、事件传播等企业级服务。 |
| Spring Web MVC | (Web 框架) Spring 自带的、成熟的 MVC 框架。 |
| Spring Webflux | (响应式 Web) 完全异步、非阻塞的响应式 Web 框架。 |
| Spring Web | (集成支持) 用于集成 Struts 等早期的第三方 Web 框架。 |
| 核心特性 | 说明 |
|---|---|
| 轻量 | 无论是 JAR 包大小还是运行时资源开销,Spring 都极其轻量。 |
| 非侵入式 | 业务代码不依赖于 Spring 的特定 API,易于复用和测试。 |
| 控制反转 (IoC) | Spring 的灵魂。由容器被动地将依赖注入对象,促进松耦合。 |
| 面向切面 (AOP) | 允许我们将业务逻辑与系统服务(如事务、日志)优雅地分离。 |
| 容器 | 负责管理对象的配置、创建、装配及其完整的生命周期。 |
| 框架 | 将简单的组件通过声明式(XML 或注解)的方式组合成复杂的应用。 |
Spring Framework 6.x 要求 JDK 的最低版本为 Java 17。
学习指引: 本章涉及的 XML 配置和手动创建容器在现代 Spring Boot 开发中已几乎完全被自动化配置和注解所取代。学习本章的目的是为了理解 Spring IoC 的底层工作原理。
<dependencies>
<!-- 基础 IoC 容器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.9</version>
</dependency>
<!-- 测试框架 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
<!-- (推荐) 日志框架 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.25.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.25.1</version>
</dependency>
</dependencies>
Bean 是 Spring IoC 容器管理的基本单元,本质上就是一个 POJO。
// src/main/java/com/example/spring6/bean/User.java
package com.example.spring6.bean;
public class User {
public User() {
System.out.println("User 的无参数构造方法执行。");
}
}
<!-- src/main/resources/beans.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userBean" class="com.example.spring6.bean.User"/>
</beans>
// src/test/java/com/example/spring6/test/Spring6Test.java
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Spring6Test {
@Test
public void testFirst() {
// 1. 手动创建 Spring 容器对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 2. 从容器中获取 Bean
// 方式一:返回 Object,需要强转
Object userBean = applicationContext.getBean("userBean");
// 方式二:传入 Class 类型,无需强转
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}
}
使用 <property> 标签的 ref 属性引用另一个 Bean 的 id。
<bean id="userDaoBean" class="com.example.spring6.bean.UserDao"/>
<bean id="userServiceBean" class="com.example.spring6.bean.UserService">
<!-- 将 id 为 userDaoBean 的对象注入到 userDao 属性中 -->
<property name="userDao" ref="userDaoBean"/>
</bean>
使用 <property> 标签的 value 属性注入简单类型值。
<bean id="userBean" class="com.example.spring6.bean.User">
<property name="name" value="Prorise"/>
<property name="age" value="25"/>
</bean>
<bean id="personBean" class="com.example.spring6.bean.Person">
<!-- List -->
<property name="interests">
<list>
<value>编程</value>
<value>游戏</value>
</list>
</property>
<!-- Map -->
<property name="family">
<map>
<entry key="father" value="老王"/>
<entry key="mother" value="老李"/>
</map>
</property>
</bean>
null: 使用 <null/> 标签。
<property name="name"><null/></property>
CDATA 块。
<property name="expression">
<value><![CDATA[ a < b && b > c ]]></value>
</property>
使用 <constructor-arg> 标签,在实例化 Bean 的同时完成依赖注入。
<bean id="userDaoBean" class="com.example.spring6.bean.UserDao"/>
<bean id="userServiceBean" class="com.example.spring6.bean.UserService">
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>
在现代 Spring/Boot 中已不再使用,了解即可。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c" ...>
<!-- p 命名空间简化 Setter 注入 -->
<bean id="user2" class="com.example.spring6.bean.User" p:name="Prorise" p:age="25"/>
<!-- c 命名空间简化构造器注入 -->
<bean id="service2" class="com.example.spring6.bean.UserService" c:userDao-ref="userDaoBean"/>
</beans>
将易变配置移至 .properties 文件,通过 <context:property-placeholder> 加载。
<!-- beans.xml -->
<beans xmlns:context="http://www.springframework.org/schema/context" ...>
<!-- 加载 jdbc.properties 文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
</bean>
</beans>
# jdbc.properties
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/testdb
通过 <bean> 标签的 scope 属性来定义作用域。
| 作用域 | 描述 | 创建时机 |
|---|---|---|
singleton (默认) | 在整个 IoC 容器中,只有一个共享的实例。 | 容器启动时。 |
prototype | 每一次请求(getBean())都会创建一个新的实例。 | 第一次被请求时。 |
<!-- 默认是 singleton -->
<bean id="singletonBean" class="com.example.spring6.bean.SpringBean" />
<!-- prototype 作用域 -->
<bean id="prototypeBean" class="com.example.spring6.bean.SpringBean" scope="prototype"/>
<bean id="apiClient" class="com.example.spring6.bean.LegacyApiClient" factory-method="getInstance"/>
<bean id="poolManager" class="com.example.spring6.bean.ConnectionPoolManager"/>
<bean id="connectionPool" factory-bean="poolManager" factory-method="createPool"/>
FactoryBean 接口,将复杂的实例化逻辑封装在 getObject() 方法中。
<bean id="product" class="com.example.spring6.bean.ProductFactoryBean"/>
获取 product 得到的是 getObject() 的返回值;获取 &product 得到的是 FactoryBean 本身。BeanPostProcessor 的 postProcessBeforeInitialization 执行。@PostConstruct 注解的方法或 init-method 指定的方法。BeanPostProcessor 的 postProcessAfterInitialization 执行。@PreDestroy 注解的方法或 destroy-method 指定的方法。| 回调方式 | 优点 | 示例 |
|---|---|---|
XML (init-method) | 代码无侵入 | <bean ... init-method="init" destroy-method="destroy"/> |
注解 (@PostConstruct) | JSR-250标准,推荐 | @PostConstruct public void init() {} |
BeanPostProcessor | 强大,可对所有 Bean 生效 | public class MyBPP implements BeanPostProcessor { ... } |
使用 IDEA 或 start.spring.io 官方脚手架创建项目。
Spring WebJarpom.xml 核心依赖:
<!-- 继承自 spring-boot-starter-parent,统一管理版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
</parent>
<dependencies>
<!-- Web 场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- 可执行 JAR 打包插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
@RestController// SpringBootDemoApplication.java
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
// HelloController.java
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, Spring Boot!";
}
}
Spring Boot 打包生成的 JAR (Fat JAR) 内置了所有依赖和 Tomcat 服务器,可以通过 java -jar 命令直接运行。
# 打包
mvn package
# 运行
java -jar target/spring-boot-demo-0.0.1-SNAPSHOT.jar
@SpringBootApplication 是一个复合注解,包含了:
@EnableAutoConfiguration: 启用自动配置,根据类路径上的依赖自动配置 Bean。@ComponentScan: 启用组件扫描,默认扫描启动类所在的包及其子包。@SpringBootConfiguration: 声明启动类本身是一个配置类。自动配置类通过 @ConditionalOn... 系列注解实现按需加载。
| 特性 | .properties 文件 | .yml 文件 |
|---|---|---|
| 格式 | 扁平的键值对 | 层级的树状结构,更清晰 |
| 可读性 | 配置多时可读性差 | 非常适合描述复杂的配置 |
| 优先级 | 高于 .yml | 低于 .properties |
| 建议 | 仅在需要覆盖 .yml 时少量使用 | 推荐使用 |
YAML 语法:使用空格缩进,严禁 Tab;冒号后必须有空格。
@Value (读取单个值)@Component
public class AppInfo {
// 读取 app.name
@Value("${app.name}")
private String name;
// 如果 app.port 不存在,使用默认值 8080
@Value("${app.port:8080}")
private Integer port;
}
@ConfigurationProperties (类型安全绑定) 将一个前缀下的所有属性,整体地绑定到一个 POJO 上。
# application.yml
datasource:
mysql:
url: jdbc:mysql://localhost:3306/prod
username: prod_user
@Data
@Component
@ConfigurationProperties(prefix = "datasource.mysql")
public class MySQLProperties {
private String url;
private String username;
}
通过 application-{profile}.yml 文件为不同环境(dev, test, prod)创建独立配置。
# application.yml (主配置文件)
spring:
profiles:
active: dev # 指定默认激活的环境
# application-dev.yml
server:
port: 8080
# application-prod.yml
server:
port: 80
激活 Profile 的方式 (高优先级覆盖低优先级):
application.yml 中配置 spring.profiles.active。java -jar my-app.jar --spring.profiles.active=prod。@RequestMapping 及其衍生注解| 注解 | HTTP 方法 |
|---|---|
@GetMapping | GET |
@PostMapping | POST |
@PutMapping | PUT |
@DeleteMapping | DELETE |
@PatchMapping | PATCH |
@RestController
@RequestMapping("/users") // 类级别,定义父路径
public class UserController {
@GetMapping("/all")
public String getAllUsers() { /* ... */ }
// {id} 是一个路径占位符
@GetMapping("/{id}")
public String getUserById(@PathVariable("id") Long id) {
return "正在查询 ID 为: " + id + " 的用户";
}
}
/users/{id}@PathVariable: 从 URL 路径中获取动态值。?: 匹配单个字符。*: 匹配0或多个字符(不含 /)。**: 匹配0或多个路径段(含 /,通常用于末尾)。// 只有当请求中包含 version=2 的参数时才匹配
@GetMapping(value = "/precise", params = "version=2")
public String testParams() { /* ... */ }
// 只有当请求头中包含 X-API-VERSION 时才匹配
@GetMapping(value = "/precise", headers = "X-API-VERSION")
public String testHeaders() { /* ... */ }
| 注解 | 作用 | 位置 |
|---|---|---|
@PathVariable | 获取路径变量 | URL 路径 (/users/{id}) |
@RequestParam | 获取查询参数 | URL 查询参数 (?name=value) |
@RequestHeader | 获取请求头 | HTTP Headers |
@CookieValue | 获取 Cookie | Cookie |
@RequestBody | 获取请求体 | Request Body (通常是 JSON) |
@GetMapping("/search")
public String searchUsers(
@RequestParam String keyword,
@RequestParam(required = false, defaultValue = "1") Integer page) {
// ...
}
@PostMapping("/create")
public String createUser(@RequestBody User user) {
// ...
}
// 自动封装:当参数是 POJO 且没有 @RequestBody 时,
// Spring MVC 会自动将同名请求参数绑定到 POJO 属性上。
@GetMapping("/filter")
public String filterUsers(UserQuery query) {
// ...
}
| 类型 | 全称 | 约定包名 | 核心职责 |
|---|---|---|---|
| PO | Persistent Object | entity | 持久化对象,与数据库表一一对应。 |
| DTO | Data Transfer Object | dto | 数据传输对象,用于接收前端请求数据。 |
| VO | View Object | vo | 视图对象,用于返回给前端的展示数据。 |
vo.setName(po.getUsername());BeanUtil:
BeanUtil.copyProperties(source, target): 拷贝同名同类型属性。@Alias("username"): 在目标类属性上使用,解决名称不匹配问题。为了让所有 API 返回结构统一,通常会创建一个 Result<T> 类。
@Getter
public final class Result<T> {
private final Integer code;
private final String message;
private final T data;
// 静态工厂方法
public static <T> Result<T> success(T data) { /* ... */ }
public static <T> Result<T> error(ResultCode code) { /* ... */ }
}
在 Controller 中,推荐使用 ResponseEntity<Result<T>> 来精确控制 HTTP 状态码和响应体。
@GetMapping("/{id}")
public ResponseEntity<Result<UserVO>> getUserById(@PathVariable Long id) {
UserVO userVO = userService.findUserById(id);
if (userVO != null) {
return ResponseEntity.ok(Result.success(userVO));
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Result.error(ResultCode.UserNotFound));
}
}
当需要将请求参数(如 String)自动转换为自定义类型(如 Enum)时,可以实现 Converter 接口。
// 1. 实现 Converter 接口
@Component // 注册为 Bean 后,Spring Boot 会自动应用
public class StringToUserStatusEnumConverter implements Converter<String, UserStatusEnum> {
@Override
public UserStatusEnum convert(String source) {
int code = Integer.parseInt(source);
return UserStatusEnum.fromCode(code);
}
}
// 2. Controller 中直接使用
@GetMapping("/users")
public List<UserVO> getUsers(@RequestParam(required=false) UserStatusEnum status) {
// ...
}
通过 Jackson 注解,可以精确控制 Java 对象与 JSON 之间的转换。
| 注解 | 作用 |
|---|---|
@JsonProperty | 建立 Java 属性和 JSON 字段的双向映射,解决命名不一致。 |
@JsonFormat | 序列化时,将日期时间类型格式化为指定字符串样式。 |
@JsonIgnore | 完全忽略某个属性,防止敏感信息泄露。 |
@JsonInclude(Include.NON_NULL) | 序列化时,忽略值为 null 的字段。 |
@Data
public class UserVO {
@JsonProperty("user_name") // username -> user_name
private String name;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
通过 Jakarta Bean Validation API 和 @Validated 实现声明式参数校验。
spring-boot-starter-validation@Data
public class UserSaveDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Email(message = "邮箱格式不正确")
private String email;
}
@RestController
@Validated // 可以在类上添加
public class UserController {
@PostMapping("/users")
public void saveUser(@Validated @RequestBody UserSaveDTO dto) {
// ...
}
}
校验失败会抛出 MethodArgumentNotValidException,可由全局异常处理器捕获。通过 @RestControllerAdvice 和 @ExceptionHandler,可以集中处理所有 Controller 抛出的异常,返回统一的 Result 格式。
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Result.error(ex.getResultCode()));
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Void>> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Result.error(message));
}
// 兜底处理所有未知异常
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleUnknownException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.error("系统异常,请联系管理员"));
}
}
通过实现 WebMvcConfigurer 接口进行全局 CORS 配置是最佳实践。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 对所有路径生效
.allowedOrigins("http://localhost:5173") // 允许的前端源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600);
}
}
# application.yml
spring:
servlet:
multipart:
enabled: true
max-file-size: 2MB
app:
upload:
dir: /path/to/your/uploads/ # 文件存储物理路径
让浏览器能直接通过 URL 访问上传的文件。
// WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${app.upload.dir}")
private String uploadDir;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 对外暴露 /uploads/** 路径,映射到物理磁盘的 uploadDir
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadDir);
}
}
@RestController
@RequestMapping("/files")
public class FileController {
@Value("${app.upload.dir}")
private String uploadDir;
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
String newFileName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(file.getOriginalFilename());
file.transferTo(new File(uploadDir, newFileName));
return Result.success("/uploads/" + newFileName); // 返回可访问的 URL
}
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) {
File file = new File(uploadDir, filename);
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.body(resource);
}
}
| 核心概念 | 作用/比喻 | 简明解释 |
|---|---|---|
| 连接点 (Join Point) | 所有可能的时机 | 程序执行过程中可以插入切面的点,如方法执行。 |
| 切点 (Pointcut) | 选定的具体时机 | 一个表达式,精确定义了通知将在哪里执行。 |
| 通知 (Advice) | 要做的具体事情 | 在切点匹配的连接点上执行的代码。 |
| 切面 (Aspect) | 事情和时机的组合 | 切点和通知的结合体,封装了横切关注点。 |
@Before: 目标方法执行前。@AfterReturning: 目标方法成功返回后。@AfterThrowing: 目标方法抛出异常后。@After: 无论成功或异常,在目标方法之后都会执行。@Around: 环绕通知,最强大,可完全控制目标方法执行。@annotation: 匹配被特定注解标记的方法。
@Pointcut("@annotation(com.example.democommon.annotation.SimpleCache)")
execution: 匹配方法签名。
// 匹配 com.example.demosystem.service 包及其子包下所有 public 方法
@Pointcut("execution(public * com.example.demosystem.service..*.*(..))")
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
@Pointcut("execution(public * com.example.demosystem.service..*.*(..))")
public void serviceMethods() {}
@Around("serviceMethods()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed(); // 执行原始方法
} finally {
long end = System.currentTimeMillis();
log.info("{}.{} 执行耗时: {} ms", pjp.getTarget().getClass().getSimpleName(), pjp.getSignature().getName(), (end - start));
}
}
}
@Transactional 注解是 Spring AOP 的经典应用,用于声明方法内的数据库操作受事务管理。
@Service
public class AccountService {
@Transactional(rollbackFor = Exception.class)
public void transfer(Integer from, Integer to, BigDecimal amount) {
accountMapper.decrease(from, amount);
// 如果此处发生异常,整个事务会回滚
if (true) { throw new RuntimeException("模拟异常"); }
accountMapper.increase(to, amount);
}
}
定义事务在方法调用链中的传递规则。
REQUIRED (默认): 加入当前事务,如没有则新建。REQUIRES_NEW: 总是新建一个独立的事务。@Transactional 失效场景 public。this.xxx()),绕过了代理。catch 且未重新抛出。RuntimeException 回滚,可通过 rollbackFor 属性修改。通过注解为方法增加缓存能力,无需修改业务逻辑。
spring-boot-starter-data-redis (已包含 spring-boot-starter-cache)@EnableCaching。# application.yml
spring:
data:
redis:
host: localhost
port: 6379
CacheConfig Bean 来定制序列化方式。| 注解 | 核心作用 |
|---|---|
@Cacheable | 读/写缓存:先查缓存,没有则执行方法并存入缓存。 |
@CachePut | 更新缓存:总是执行方法,并将返回值更新到缓存。 |
@CacheEvict | 删除缓存:执行方法后,从缓存中移除条目。 |
@Service
public class UserServiceImpl implements UserService {
// SpEL 表达式: #id 表示取方法的 id 参数作为 key
@Cacheable(cacheNames = "users", key = "#id")
public UserVO findUserById(Long id) { /* ... */ }
@CachePut(cacheNames = "users", key = "#dto.id")
public UserVO updateUser(UserEditDTO dto) { /* ... */ }
@CacheEvict(cacheNames = "users", key = "#id")
public void deleteUserById(Long id) { /* ... */ }
}
Spring Boot 3.2+ 推荐的同步 HTTP 客户端,替代 RestTemplate。
// 1. 在配置类中创建 Bean
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.build();
}
// 2. 在 Service 中注入并使用
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final RestClient restClient;
public PostDTO getUserFirstPost(Integer userId) {
return restClient.get()
.uri("/posts?userId={userId}", userId)
.retrieve()
// 优雅地处理 5xx 错误
.onStatus(HttpStatusCode::is5xxServerError, (req, resp) -> {
throw new RetryableException("外部服务暂时不可用");
})
.body(new ParameterizedTypeReference<List<PostDTO>>() {})
.get(0);
}
}
为可能失败的操作(如外部 API 调用)增加自动重试能力。
spring-retry@EnableRetry。@Service
public class ExternalApiService {
@Retryable(
include = RetryableException.class, // 只对特定异常重试
maxAttempts = 3, // 最多重试3次
backoff = @Backoff(delay = 2000) // 每次重试间隔2秒
)
public PostDTO getUserFirstPost(Integer userId) { /* ... */ }
// 当所有重试都失败后,此方法将被调用作为降级方案
@Recover
public PostDTO recover(RetryableException e, Integer userId) {
log.error("调用外部 API 达到最大重试次数后仍然失败", e);
return null; // 返回默认值或抛出最终异常
}
}
将耗时操作(如发邮件)异步化,提升 API 响应速度。
@EnableAsync。@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("MyAsync-");
executor.initialize();
return executor;
}
}
@Service
public class NotificationService {
@Async
public void sendWelcomeEmailAsync(String username) {
// ... 模拟耗时操作 ...
}
}
主流程调用 sendWelcomeEmailAsync 后会立即返回,不会等待。无需人工干预,按预定时间自动触发任务。
@EnableScheduling。@Component
@Slf4j
public class SystemTasks {
// 每隔 5 秒执行一次 (从上一次任务开始计时)
@Scheduled(fixedRate = 5000)
public void runAtFixedRate() { /* ... */ }
// 上一次任务结束后,延迟 3 秒再执行
@Scheduled(fixedDelay = 3000)
public void runAtFixedDelay() { /* ... */ }
// 每天凌晨 3 点执行
@Scheduled(cron = "0 0 3 * * ?")
public void archiveOldLogs() { /* ... */ }
}
前置条件: JDK 21+ & Spring Boot 3.2+
极大地提升 I/O 密集型应用的吞吐量,线程不再是瓶颈。
一键开启:
# application.yml
spring:
threads:
virtual:
enabled: true
开启后,Tomcat 会使用虚拟线程处理每个 HTTP 请求。这与 @Async 使用的平台线程池是互补的,而非替代关系。
| 注解 | 作用 |
|---|---|
@ExtendWith(MockitoExtension.class) | 启用 Mockito 扩展。 |
@InjectMocks | 创建被测试类的真实实例,并自动注入 @Mock 对象。 |
@Mock | 创建依赖项的模拟 (假) 对象。 |
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserMapper userMapper;
@Test
void testFindUserById_whenUserExists() {
// GIVEN: "打桩",设定模拟对象的行为
User mockUser = new User();
mockUser.setId(1L);
when(userMapper.selectById(1L)).thenReturn(mockUser);
// WHEN: 执行被测试方法
UserVO resultVO = userService.findUserById(1L);
// THEN: 验证结果
assertThat(resultVO.getId()).isEqualTo(1L);
}
}
行为验证 (verify): 用于测试 void 方法或验证交互。
// 验证 userMapper.deleteById(1L) 方法被调用了恰好 1 次
verify(userMapper, times(1)).deleteById(1L);
@WebMvcTest 创建一个只包含 Web 层组件的轻量级 Spring 测试上下文。
| 注解 | 作用 |
|---|---|
@WebMvcTest(controllers = ...) | 指定要测试的 Controller,启动一个 Web 测试环境。 |
@Autowired private MockMvc mockMvc; | 注入用于模拟 HTTP 请求的 MockMvc 对象。 |
@MockBean | 在 Spring 测试容器中,用一个模拟 (假) Bean 替换掉真实的 Bean。 |
@WebMvcTest(controllers = UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // 用 MockBean 替换真实的 UserService
private UserService userService;
@Test
void testGetUserById_whenUserExists() throws Exception {
// GIVEN: "打桩" Service 层的行为
UserVO mockUserVO = new UserVO();
mockUserVO.setId(1L);
when(userService.findUserById(1L)).thenReturn(mockUserVO);
// WHEN & THEN: 模拟 HTTP 请求并验证响应
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.id").value(1L));
}
}