Spring Boot 备忘清单

本备忘单旨在快速理解 Spring 框架的核心思想与 Spring Boot 的自动化配置、约定优于配置的理念。它提供了从基础 IoC/AOP 到 Web 开发、数据持久化、异步任务和测试等场景中最常用、最核心的代码片段和配置示例。

Spring 核心思想

OCP 开闭原则

一个软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭。当需要为软件增加新功能时,应当通过增加新的代码来实现,而不是去修改那些已经被测试过且运行正常的旧代码。

传统的高耦合结构中,上层依赖下层,导致下层模块的改动会连锁影响上层,违反了开-闭原则。

DIP 依赖倒置原则

依赖倒置原则倡导我们应该 面向抽象(接口)编程,而不是面向具体实现编程。其目标是让上层不再依赖下层,实现依赖关系的“倒置”。

Spring 框架正是为解决传统编程中对象创建和关系管理的难题而生,它通过将对象的创建权和关系管理权从业务代码中移交出去,引出了其核心思想——控制反转。

IoC 控制反转与 DI 依赖注入

控制反转 (Inversion of Control, IoC) 是一种核心设计思想,其主要目的就是用来降低代码之间的耦合度。核心理念是:将对象的创建权和对象之间关系的管理权交出去,由一个独立的第三方容器来负责。

依赖注入 (Dependency Injection, DI) 是实现 IoC 思想最常见、最重要的方式。Spring 是一个完美实现了 IoC 思想,并以 DI 作为其核心机制的顶级容器框架。

Spring 框架概述

Spring 简介

Spring 是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,旨在简化企业级 Java 应用的开发。它通过促进松耦合和可测试性,让我们能将精力完全集中在核心业务逻辑的实现上。

Spring 6.x 核心模块

模块 (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。

  • IDE: IntelliJ IDEA 2024.1.4
  • 构建工具: Apache Maven 3.9.11
  • 项目 JDK: Java 17
  • Spring Framework: 6.2.9
  • 测试框架: JUnit 5.13.4

第一个 Spring 程序 (XML)

学习指引: 本章涉及的 XML 配置和手动创建容器在现代 Spring Boot 开发中已几乎完全被自动化配置和注解所取代。学习本章的目的是为了理解 Spring IoC 的底层工作原理。

1. 引入依赖 (pom.xml)

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

2. 定义 Bean

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 的无参数构造方法执行。");
    }
}

3. 创建 Spring 配置文件

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

4. 启动容器并获取 Bean

// 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);
    }
}

依赖注入 (XML)

Setter 注入

注入其他 Bean (ref)

使用 <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>

注入字面量 (value)

使用 <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>

简化配置 (p/c 命名空间)

在现代 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 的高级管理

Bean 的作用域 (Scope)

通过 <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 的实例化方式

  • 构造方法实例化 (默认): 调用无参构造器。
  • 静态工厂方法:
    <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 接口: 实现 FactoryBean 接口,将复杂的实例化逻辑封装在 getObject() 方法中。
    <bean id="product" class="com.example.spring6.bean.ProductFactoryBean"/>
    
    获取 product 得到的是 getObject() 的返回值;获取 &product 得到的是 FactoryBean 本身。

Bean 的生命周期

  1. 实例化: Spring 调用构造方法创建 Bean 实例。
  2. 属性填充: Spring 通过 DI 为 Bean 注入依赖。
  3. 初始化回调 (前): BeanPostProcessorpostProcessBeforeInitialization 执行。
  4. 初始化: 执行 @PostConstruct 注解的方法或 init-method 指定的方法。
  5. 初始化回调 (后): BeanPostProcessorpostProcessAfterInitialization 执行。
  6. Bean 可用: Bean 处于可用状态。
  7. 销毁: 容器关闭时,执行 @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 { ... }

Spring Boot 快速入门

创建项目 (Spring Initializr)

使用 IDEA 或 start.spring.io 官方脚手架创建项目。

  • 核心依赖: Spring Web
  • 打包方式: Jar

pom.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!";
    }
}

可执行 Fat JAR

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 vs .yml

特性.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;
}

多环境配置 (Profiles)

通过 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 的方式 (高优先级覆盖低优先级):

  1. application.yml 中配置 spring.profiles.active
  2. 通过命令行参数: java -jar my-app.jar --spring.profiles.active=prod

Spring MVC 核心

请求映射

@RequestMapping 及其衍生注解

注解HTTP 方法
@GetMappingGET
@PostMappingPOST
@PutMappingPUT
@DeleteMappingDELETE
@PatchMappingPATCH

组合与动态路径

@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 路径中获取动态值。

Ant 风格路径

  • ?: 匹配单个字符。
  • *: 匹配0或多个字符(不含 /)。
  • **: 匹配0或多个路径段(含 /,通常用于末尾)。

精准匹配 (params & headers)

// 只有当请求中包含 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获取 CookieCookie
@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) {
    // ...
}

分层架构实战

领域对象模型

类型全称约定包名核心职责
POPersistent Objectentity持久化对象,与数据库表一一对应。
DTOData Transfer Objectdto数据传输对象,用于接收前端请求数据。
VOView Objectvo视图对象,用于返回给前端的展示数据。

三层架构

  • Controller 层: Web 入口,参数校验,调用 Service。
  • Service 层: 业务核心,事务管理,组合 Mapper。
  • Mapper 层: 数据持久层,与数据库直接交互。

POJO 转换

  • 手动转换: vo.setName(po.getUsername());
  • Hutool 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 定制

通过 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;
}

Validation API

通过 Jakarta Bean Validation API 和 @Validated 实现声明式参数校验。

  1. 添加依赖: spring-boot-starter-validation
  2. 在 DTO 中添加注解:
    @Data
    public class UserSaveDTO {
        @NotBlank(message = "用户名不能为空")
        private String username;
        
        @Email(message = "邮箱格式不正确")
        private String email;
    }
    
  3. 在 Controller 中激活校验:
    @RestController
    @Validated // 可以在类上添加
    public class UserController {
        @PostMapping("/users")
        public void saveUser(@Validated @RequestBody UserSaveDTO dto) {
            // ...
        }
    }
    
    校验失败会抛出 MethodArgumentNotValidException,可由全局异常处理器捕获。

全局处理与特殊场景

全局异常处理 (@RestControllerAdvice)

通过 @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("系统异常,请联系管理员"));
    }
}

跨域配置 (CORS)

通过实现 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);
    }
}

文件处理

1. 配置

# application.yml
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 2MB
app:
  upload:
    dir: /path/to/your/uploads/ # 文件存储物理路径

2. 静态资源映射

让浏览器能直接通过 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);
    }
}

3. Controller 实现

@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);
    }
}

AOP 面向切面编程

核心术语

核心概念作用/比喻简明解释
连接点 (Join Point)所有可能的时机程序执行过程中可以插入切面的点,如方法执行。
切点 (Pointcut)选定的具体时机一个表达式,精确定义了通知将在哪里执行。
通知 (Advice)要做的具体事情在切点匹配的连接点上执行的代码
切面 (Aspect)事情和时机的组合切点通知的结合体,封装了横切关注点。

通知 (Advice) 类型

  • @Before: 目标方法执行
  • @AfterReturning: 目标方法成功返回后
  • @AfterThrowing: 目标方法抛出异常后
  • @After: 无论成功或异常,在目标方法之后都会执行。
  • @Around: 环绕通知,最强大,可完全控制目标方法执行。

切点 (Pointcut) 表达式

  • @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)

@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);
    }
}

传播行为 (propagation)

定义事务在方法调用链中的传递规则。

  • REQUIRED (默认): 加入当前事务,如没有则新建。
  • REQUIRES_NEW: 总是新建一个独立的事务。

@Transactional 失效场景

  • 方法非 public
  • 方法内部调用 (this.xxx()),绕过了代理。
  • 异常在方法内部被 catch 且未重新抛出。
  • 默认只对 RuntimeException 回滚,可通过 rollbackFor 属性修改。

Spring Cache 与 Redis

通过注解为方法增加缓存能力,无需修改业务逻辑。

  1. 添加依赖: spring-boot-starter-data-redis (已包含 spring-boot-starter-cache)
  2. 启用缓存: 在启动类上添加 @EnableCaching
  3. 配置 Redis:
    # application.yml
    spring:
      data:
        redis:
          host: localhost
          port: 6379
    
  4. 配置 JSON 序列化 (推荐): 创建 CacheConfig Bean 来定制序列化方式。
  5. 在 Service 方法上使用注解:
注解核心作用
@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) { /* ... */ }
}

现代 HTTP 调用 (RestClient)

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);
    }
}

弹性设计 (Spring Retry)

为可能失败的操作(如外部 API 调用)增加自动重试能力。

  1. 添加依赖: spring-retry
  2. 启用重试: 在启动类上添加 @EnableRetry
  3. 在方法上使用注解:
@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; // 返回默认值或抛出最终异常
    }
}

并发与调度

异步任务 (@Async)

将耗时操作(如发邮件)异步化,提升 API 响应速度。

  1. 启用异步: 在配置类上添加 @EnableAsync
  2. 自定义线程池 (生产环境必须):
    @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;
        }
    }
    
  3. 创建异步方法:
    @Service
    public class NotificationService {
        @Async
        public void sendWelcomeEmailAsync(String username) {
            // ... 模拟耗时操作 ...
        }
    }
    
    主流程调用 sendWelcomeEmailAsync 后会立即返回,不会等待。

定时任务 (@Scheduled)

无需人工干预,按预定时间自动触发任务。

  1. 启用调度: 在启动类上添加 @EnableScheduling
  2. 创建定时任务:
    @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+)

前置条件: JDK 21+ & Spring Boot 3.2+

极大地提升 I/O 密集型应用的吞吐量,线程不再是瓶颈。

一键开启:

# application.yml
spring:
  threads:
    virtual:
      enabled: true

开启后,Tomcat 会使用虚拟线程处理每个 HTTP 请求。这与 @Async 使用的平台线程池是互补的,而非替代关系。

质量保障 (Spring Test)

核心理念

  • 单元测试: 隔离地测试单个类(如 Service),依赖项被模拟 (Mock)。速度快,不启动 Spring 容器。
  • 集成测试: 测试多个组件间的协同工作(如 Controller -> Service),通常会启动一个测试用的 Spring 容器。

纯单元测试 (Mockito)

注解作用
@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);

集成测试 (MockMvc)

@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));
    }
}