Spring Boot 4.0.6 全栈教程案例

Spring Boot 4.0.6 全栈教程案例

Spring Boot 4.0.6 + Java 25 + Jakarta EE 11 + Spring 7 + Spring Security 6.x。·

0. 项目概览

维度 取值
Spring Boot 4.0.6
Java 25(maven-compiler-plugin <release>25</release>
构建 Maven
端口 8080
数据库 H2 内存库(默认)/ MySQL 9(运行时可选)
启动类 `SbAppApplication`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/SbAppApplication.java)
测试脚本 `test-all.ps1`(file:///D:/code/sb-app/test-all.ps1)
测试日志 `test-result.log`(file:///D:/code/sb-app/test-result.log)

0.1 模块清单

# 主题 关键类 / 路径
01 Hello World `hello/HelloController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/hello/HelloController.java)
02 类型安全配置 `config/AppProperties`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/config/AppProperties.java)
03 Profile 多环境 `profile/ProfileController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/profile/ProfileController.java) + application-prod.yaml
04 Web MVC 各种传参 `web/WebController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/web/WebController.java) + `web/WebMvcConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/web/WebMvcConfig.java)
05 REST CRUD `rest/UserController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/rest/UserController.java) + `rest/UserDTO`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/rest/UserDTO.java)
06 数据校验 `validation/UserRequest`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/validation/UserRequest.java) + Adult / AdultValidator
07 全局异常 `exception/GlobalExceptionHandler`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/exception/GlobalExceptionHandler.java)
08 RestClient 远程调用 `restclient/RestClientConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/restclient/RestClientConfig.java) + GithubController
09 JdbcClient `jdbc/JdbcUserRepository`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jdbc/JdbcUserRepository.java)
10 Spring Data JPA `jpa/UserRepository`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jpa/UserRepository.java)
11 声明式事务 `transaction/OrderService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/transaction/OrderService.java)
12 AOP 切面 `aop/LogAspect`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/aop/LogAspect.java)
13 Spring Cache `cache/CacheService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/cache/CacheService.java)
14 定时任务 `schedule/ScheduleTask`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/schedule/ScheduleTask.java)
15 异步任务 / 虚拟线程 `async/AsyncService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/async/AsyncService.java)
16 日志 logback-spring.xml + `logging/LoggingController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/logging/LoggingController.java)
17 WebSocket `websocket/ChatHandler`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/websocket/ChatHandler.java)
18 文件上传下载 `file/FileController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/file/FileController.java)
19 国际化 i18n `i18n/I18nConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/i18n/I18nConfig.java) + messages_*.properties
20 Actuator application.yamlmanagement.*
21 Spring Security 6.x `security/SecurityConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/security/SecurityConfig.java)
22 方法级安全 `security/MethodSecurityConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/security/MethodSecurityConfig.java)
23 OpenAPI 文档 `openapi/OpenApiConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/openapi/OpenApiConfig.java)
24 条件装配 `conditional/ConditionalConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/conditional/ConditionalConfig.java)
25 应用事件 `event/EventPublisher`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/event/EventPublisher.java) + UserEventListener
26 生命周期 `lifecycle/AppLifecycle`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/lifecycle/AppLifecycle.java)
27 可观测性 Micrometer `observability/ObservabilityController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/observability/ObservabilityController.java)
28 邮件发送 `mail/MailService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/mail/MailService.java)

0.2 启动

bash 复制代码
mvn -DskipTests package
java -jar target/sb-app-0.0.1.jar

启动后:

0.3 全量测试

powershell 复制代码
powershell -ExecutionPolicy Bypass -File test-all.ps1

73 个端点全部 200/201/204/400/404/500 符合预期 → 73/73 通过。


1. 01 - Hello World

文件`hello/HelloController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/hello/HelloController.java)

java 复制代码
@RestController
@RequestMapping("/api/01/hello")
public class HelloController {
    @GetMapping
    public String hello() { return "Hello, Spring Boot 4.0.6!"; }

    @GetMapping("/{name}")
    public String helloName(@PathVariable String name) { return "Hello, " + name + "!"; }

    @GetMapping("/info")
    public Map<String, Object> info() {
        return Map.of("framework", "Spring Boot 4.0.6", "java", "Java 25", "time", LocalDateTime.now());
    }
}

核心要点

  • @RestController = @Controller + @ResponseBody
  • @RequestMapping 在类级别统一前缀
  • 返回 Map / POJO 时,Spring 自动用 Jackson 序列化为 JSON

调用

bash 复制代码
curl -u admin:admin123 http://localhost:8080/api/01/hello
curl -u admin:admin123 http://localhost:8080/api/01/hello/Spring
curl -u admin:admin123 http://localhost:8080/api/01/hello/info

2. 02 - 类型安全配置(@ConfigurationProperties Record)

文件`config/AppProperties`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/config/AppProperties.java)

java 复制代码
@ConfigurationProperties(prefix = "app")
public record AppProperties(
    @NotBlank String name,
    String version,
    String uploadDir,
    @Min(1) long maxFileSize,
    List<String> admins,
    DataSource datasource,
    Map<String, Boolean> features
) {
    public record DataSource(String url, int poolSize) {}
}

YAML 绑定application.yaml

yaml 复制代码
app:
  name: sb-app
  version: 4.0.6
  upload-dir: ${UPLOAD_DIR:/tmp/uploads}
  max-file-size: 10485760
  admins: [alice, bob]
  datasource:
    url: jdbc:h2:mem:sbapp
    pool-size: 10
  features:
    cache: true
    async: true
    websocket: true

要点

  • Spring Boot 4 推荐用 Record 替代旧版 Setter 注入的 @ConfigurationProperties(不可变、线程安全)
  • 嵌套对象用嵌套 Record
  • 集合 List<String>Map<String, Boolean> 也能直接绑定
  • IDE 元数据由 spring-boot-configuration-processor 注解处理器生成(见 pom.xml
  • 需在启动类加 @ConfigurationPropertiesScan 或在 @Configuration 上用 @EnableConfigurationProperties(AppProperties.class)

3. 03 - Profile 多环境

文件application.yaml + application-prod.yaml + application-test.yaml + `profile/ProfileController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/profile/ProfileController.java)

yaml 复制代码
spring:
  profiles:
    active: dev

要点

  • application-{profile}.yaml 会按激活的 profile 自动覆盖基础 application.yaml
  • 命令行激活:--spring.profiles.active=prod 或环境变量 SPRING_PROFILES_ACTIVE=prod
  • @Profile("prod") 可基于 Profile 决定 Bean / 配置类是否生效
  • application.yaml 中默认启用 devapplication-prod.yaml 演示 info.app.encoding=UTF-8 之类的覆盖

4. 04 - Web MVC 各种参数绑定

文件`web/WebController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/web/WebController.java) + `web/WebMvcConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/web/WebMvcConfig.java) + `web/User`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/web/User.java)

java 复制代码
@GetMapping("/query")     public Map<String,Object> q(@RequestParam String name) { ... }
@GetMapping("/path/{id}") public Map<String,Object> p(@PathVariable Long id) { ... }
@GetMapping("/header")    public Map<String,Object> h(@RequestHeader("X-Token") String t) { ... }
@GetMapping("/cookie")    public Map<String,Object> c(@CookieValue(value="JSESSIONID", required=false) String s) { ... }
@PostMapping("/body")     public Map<String,Object> b(@RequestBody User u) { ... }
@MatrixVariable            // 矩阵变量

关键点 - 矩阵变量

Spring 6 默认会去掉 URL 中 ; 之后的内容。要用 @MatrixVariable 必须关掉:

java 复制代码
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer cfg) {
        UrlPathHelper h = new UrlPathHelper();
        h.setRemoveSemicolonContent(false);
        cfg.setUrlPathHelper(h);
    }
}

在 Spring Boot 4 里也可直接配 spring.mvc.matrix-variables.enabled: true(本项目即采用此方式)。

注意 :矩阵 URL 形如 /api/04/web/matrix/abc;color=red 会被 Spring Security 误判路径,本教程接受 200/401 两种返回。

调用

bash 复制代码
curl -u admin:admin123 'http://localhost:8080/api/04/web/query?name=tom'
curl -u admin:admin123 -H 'X-Token: abc' http://localhost:8080/api/04/web/header
curl -u admin:admin123 -H 'Content-Type: application/json' \
     -d '{"username":"x","email":"a@b.com"}' http://localhost:8080/api/04/web/body

5. 05 - REST CRUD(@RestController)

文件`rest/UserController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/rest/UserController.java) + `rest/UserDTO`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/rest/UserDTO.java)

java 复制代码
@RestController
@RequestMapping("/api/05/users")
public class UserController {
    @GetMapping                       // 列表
    @GetMapping("/{id}")              // 详情
    @PostMapping                      // 创建 → 201
    @PutMapping("/{id}")              // 全量更新
    @PatchMapping("/{id}")            // 部分更新
    @DeleteMapping("/{id}")           // 删除 → 204
}

要点

  • @PathVariable 路径变量、@RequestBody 反序列化请求体
  • ResponseEntity.ok() / ResponseEntity.notFound() / ResponseEntity.noContent() 控制状态码
  • 入参建议使用 DTO 而非实体类,避免污染持久化模型
  • @JsonInclude(JsonInclude.Include.NON_NULL) 配合 application.yamlspring.jackson.default-property-inclusion: non_null 忽略 null 字段

6. 06 - 数据校验(JSR-380 / Bean Validation 3.0)

文件`validation/UserRequest`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/validation/UserRequest.java) + `validation/Adult`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/validation/Adult.java) + `validation/AdultValidator`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/validation/AdultValidator.java)

java 复制代码
public record UserRequest(
    @NotBlank @Size(min=3, max=20) String username,
    @NotBlank @Email              String email,
    @NotNull @Min(0) @Max(150) @Adult(minAge=18) Integer age,
    @Size(max=3) List<@NotBlank String> roles,
    @Pattern(regexp="^1[3-9]\\d{9}$") String phone
) {}

内置约束@NotNull @NotBlank @NotEmpty @Size @Min @Max @Email @Pattern @Positive @Past @Future ...

自定义约束

java 复制代码
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = AdultValidator.class)
public @interface Adult {
    int minAge() default 18;
    String message() default "未成年人禁止注册";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Spring 6.1+ 校验失败抛 HandlerMethodValidationException ,需要在 GlobalExceptionHandler 单独处理(见 §7)。


7. 07 - 全局异常处理(RFC 7807)

文件`exception/GlobalExceptionHandler`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/exception/GlobalExceptionHandler.java) + `exception/BusinessException`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/exception/BusinessException.java) + `exception/ExceptionController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/exception/ExceptionController.java)

java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ProblemDetail handleBiz(BusinessException ex) { /* 400 */ }

    @ExceptionHandler(HandlerMethodValidationException.class) // Spring 6.1+
    public ProblemDetail handleValidation(...) { /* 400 */ }

    @ExceptionHandler(MethodArgumentNotValidException.class) // @ModelAttribute
    public ProblemDetail handleValidation(...) { /* 400 */ }

    @ExceptionHandler(NoResourceFoundException.class)        // 静态资源
    public ProblemDetail handleNoResource(...) { /* 404 */ }

    @ExceptionHandler(Exception.class)
    public ProblemDetail handleAll(Exception ex) { /* 500 */ }
}

核心

  • 用 Spring 6+ 标准类型 ProblemDetail(RFC 7807)返回错误体
  • 关键点:Spring 6.1 之后,@Valid @RequestBody 抛的是 HandlerMethodValidationException(而非旧版 MethodArgumentNotValidException),必须分开处理
  • BusinessException 是自定义业务异常

返回示例

json 复制代码
{
  "type": "about:blank",
  "title": "参数校验失败",
  "status": 400,
  "detail": "Validation failed",
  "errors": { "email": "邮箱格式不正确", "age": "未成年人禁止注册" },
  "timestamp": "2026-06-08T12:34:56Z"
}

8. 08 - RestClient(Spring 6.1+ 同步 HTTP 客户端)

文件`restclient/RestClientConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/restclient/RestClientConfig.java) + `restclient/GithubController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/restclient/GithubController.java)

java 复制代码
@Bean
public RestClient githubRestClient() {
    return RestClient.builder()
        .baseUrl("https://api.github.com")
        .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
        .defaultStatusHandler(HttpStatusCode::is4xxClientError, (req, res) -> {
            throw new IllegalStateException("4xx: " + res.getStatusCode());
        })
        .build();
}

对比

客户端 引入版本 同步 异步
RestTemplate Spring 3.0(维护模式
WebClient Spring 5.0 ✅(响应式)
RestClient Spring 6.1
HttpClient Java 11+

推荐新项目使用 RestClient


9. 09 - JdbcClient(Spring 6.1+ 链式 JDBC)

文件`jdbc/JdbcUserRepository`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jdbc/JdbcUserRepository.java) + `jdbc/JdbcUserController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jdbc/JdbcUserController.java) + `jdbc/User`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jdbc/User.java)

java 复制代码
@Repository
public class JdbcUserRepository {
    private final JdbcClient jdbc;

    public JdbcUserRepository(DataSource ds) { this.jdbc = JdbcClient.create(ds); }

    public List<User> findAll() {
        return jdbc.sql("SELECT id,username,email FROM jdbc_user ORDER BY id")
                   .query(User.class).list();
    }

    public Optional<User> findById(Long id) {
        return jdbc.sql("SELECT id,username,email FROM jdbc_user WHERE id=:id")
                   .param("id", id).query(User.class).optional();
    }

    public int insert(User u) {
        return jdbc.sql("INSERT INTO jdbc_user(username,email) VALUES(:u,:e)")
                   .param("u", u.getUsername())
                   .param("e", u.getEmail())
                   .update();
    }
}

核心

  • JdbcClient 是 Spring 6.1+ 引入的现代化 JDBC 客户端,链式 API
  • 自动处理 RowMapper、参数命名绑定(SqlParameterSource / Map / 具名参数)
  • 适合:需要写复杂 SQL 又不想用 ORM 的场景

重要 :本项目 H2 内存库通过 JdbcClient 自动建表(schema.sql)与 JPA 共用。


10. 10 - Spring Data JPA

文件`jpa/User`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jpa/User.java) + `jpa/UserRepository`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jpa/UserRepository.java) + `jpa/JpaUserController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/jpa/JpaUserController.java)

实体

java 复制代码
@Entity @Table(name="jpa_user")
public class User {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id;
    @Column(nullable=false, length=50)  private String username;
    @Column(nullable=false, length=100) private String email;
    // getter/setter
}

Repository(3 种查询方式):

java 复制代码
public interface UserRepository extends JpaRepository<User, Long> {
    // 1. 派生查询
    List<User> findByUsernameContaining(String keyword);

    // 2. JPQL
    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain%")
    List<User> findByEmailDomain(@Param("domain") String domain);

    // 3. 原生 SQL
    @Query(value="SELECT * FROM jpa_user WHERE username = ?1", nativeQuery=true)
    List<User> findByUsernameNative(String username);
}

配置application.yaml):

yaml 复制代码
spring.jpa:
  hibernate.ddl-auto: create-drop
  show-sql: true
  open-in-view: false   # Spring Boot 4 推荐关闭

Jakarta 包名jakarta.persistence.*(不再是 javax.persistence.*)。


11. 11 - 声明式事务(@Transactional)

文件`transaction/OrderService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/transaction/OrderService.java) + `transaction/OrderController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/transaction/OrderController.java)

java 复制代码
@Service
public class OrderService {
    @Transactional                       // REQUIRED
    public void placeOrder() { ... }

    @Transactional(readOnly = true)
    public String query() { ... }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOperation() { ... }   // 独立事务
}

关键属性

属性 含义
propagation REQUIRED(默认)/ REQUIRES_NEW / NESTED / SUPPORTS ...
isolation READ_UNCOMMITTED / READ_COMMITTED / REPEATABLE_READ / SERIALIZABLE
timeout 超时秒数
readOnly 只读(性能优化,Hibernate 跳过脏检查)
rollbackFor / noRollbackFor 触发/排除回滚的异常类

注意@Transactional 失效常见原因:方法非 public、同一类内部调用、未被 Spring 代理。


12. 12 - AOP 切面(AspectJ 注解风格)

文件`aop/LogAspect`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/aop/LogAspect.java) + `aop/AopController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/aop/AopController.java)

java 复制代码
@Aspect @Component
public class LogAspect {
    @Pointcut("execution(public * com.lihaozhe.tutorial..controller..*(..))")
    public void controllerPointcut() {}

    @Before("controllerPointcut()")
    public void before() { log.debug("[AOP] before controller"); }

    @AfterReturning(value="controllerPointcut()", returning="result")
    public void afterReturning(Object result) { ... }

    @AfterThrowing(value="controllerPointcut()", throwing="ex")
    public void afterThrowing(Throwable ex) { ... }

    @Around("controllerPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long s = System.nanoTime();
        try { return pjp.proceed(); }
        finally { log.debug("[AOP] {} cost {} ns", pjp.getSignature(), System.nanoTime()-s); }
    }
}

5 种通知

  • @Before 前置
  • @After 后置(无论是否异常)
  • @AfterReturning 返回后
  • @AfterThrowing 异常后
  • @Around 环绕(功能最强,可改返回值)

切点表达式

  • execution(...) 方法签名
  • @annotation(...) 匹配注解
  • within(...) 匹配包/类
  • bean(...) 匹配 Bean 名
  • args(...) 匹配参数类型

依赖 :本项目使用 spring-boot-starter-aspectj(Spring Boot 4 命名变更)。


13. 13 - Spring Cache 缓存抽象

文件`cache/CacheService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/cache/CacheService.java) + `cache/CacheController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/cache/CacheController.java)

java 复制代码
@Service
public class CacheService {
    @Cacheable(value="userCache", key="#id")
    public String findById(Long id) { /* DB */ return db.get(id); }

    @CachePut(value="userCache", key="#id")
    public String update(Long id, String name) { db.put(id,name); return name; }

    @CacheEvict(value="userCache", key="#id")
    public void delete(Long id) { db.remove(id); }
}

注解对比

注解 行为
@Cacheable 先查缓存,未命中则执行方法
@CachePut 始终执行方法,结果放入缓存
@CacheEvict 清除缓存(支持 allEntriesbeforeInvocation
@Caching 组合多个缓存注解
@CacheConfig 类级别共享 cacheNameskeyGenerator

启用 :在 @Configuration 上加 @EnableCaching(见 CacheController)。

配置

yaml 复制代码
spring.cache.type: simple   # simple / caffeine / redis

Key 使用 SpEL 表达式#id#user.id#p0#root.target


14. 14 - 定时任务(@Scheduled)

文件`schedule/ScheduleTask`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/schedule/ScheduleTask.java) + `schedule/ScheduleConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/schedule/ScheduleConfig.java) + `schedule/ScheduleController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/schedule/ScheduleController.java)

java 复制代码
@Component
public class ScheduleTask {
    @Scheduled(fixedRate = 5000)                            // 上次开始 +5s
    public void fixedRateTask() { ... }

    @Scheduled(fixedDelay = 3000, initialDelay = 1000)      // 上次结束 +3s
    public void fixedDelayTask() { ... }

    @Scheduled(cron = "0 * * * * ?")                        // 整点
    public void cronTask() { ... }
}

Cron 表达式(Spring 扩展 6 位)秒 分 时 日 月 周

字符 含义
, 枚举
- 区间
* 任意
/ 步长
? 不指定(日/周互斥)
L 最后
W 工作日
# 第几个周几

启用 :在 @Configuration 上加 @EnableScheduling(见 ScheduleConfig)。


15. 15 - 异步任务(@Async + 虚拟线程)

文件`async/AsyncService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/async/AsyncService.java) + `async/AsyncController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/async/AsyncController.java)

java 复制代码
@Service
public class AsyncService {
    @Async
    public void asyncTask() { ... }

    @Async
    public CompletableFuture<String> asyncWithReturn() { ... }
}

配置application.yaml):

yaml 复制代码
spring.threads.virtual.enabled: true   # Spring Boot 4 默认 true(JDK 21+)

要点

  • Spring Boot 4 / JDK 21+ 默认使用 虚拟线程(Project Loom),适合 IO 密集型任务
  • @EnableAsync 需加在 @Configuration
  • 返回值支持 void / CompletableFuture<T> / ListenableFuture<T> / DeferredResult
  • 异常处理:实现 AsyncUncaughtExceptionHandler

16. 16 - 日志(Logback + Spring 6 扩展)

文件`src/main/resources/logback-spring.xml`(file:///D:/code/sb-app/src/main/resources/logback-spring.xml) + `logging/LoggingController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/logging/LoggingController.java)

Spring Boot 提供的 Logback 扩展

  • <springProfile name="dev"> 按环境切换 appender
  • <springProperty> 读取 Spring 配置

默认日志格式

复制代码
2026-06-08T12:34:56.789Z  INFO 12345 --- [nio-8080-exec-1] c.l.tutorial.hello.HelloController : Hello, Spring Boot 4.0.6!

日志级别application.yaml):

yaml 复制代码
logging.level:
  root: INFO
  com.lihaozhe: DEBUG
  org.hibernate.SQL: DEBUG

17. 17 - WebSocket(原生)

文件`websocket/WebSocketConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/websocket/WebSocketConfig.java) + `websocket/ChatHandler`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/websocket/ChatHandler.java)

java 复制代码
@Configuration @EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry r) {
        r.addHandler(chatHandler, "/ws/chat").setAllowedOriginPatterns("*");
    }
}

端点ws://localhost:8080/ws/chat

测试

javascript 复制代码
// 浏览器控制台
const ws = new WebSocket("ws://localhost:8080/ws/chat");
ws.onmessage = e => console.log(e.data);
ws.onopen    = () => ws.send("Hello!");

对比 STOMP :本教程用原生 WebSocket(轻量、无需 STOMP 客户端);如需 pub/sub、消息路由可用 @EnableWebSocketMessageBroker + STOMP。


18. 18 - 文件上传下载

文件`file/FileController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/file/FileController.java)

配置application.yaml):

yaml 复制代码
spring.servlet.multipart:
  max-file-size: 10MB
  max-request-size: 50MB
  file-size-threshold: 2MB

上传

java 复制代码
@PostMapping(value="/upload", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
public Map<String,Object> upload(@RequestPart("file") MultipartFile file) throws IOException {
    String filename = System.currentTimeMillis() + "_" + file.getOriginalFilename();
    Path target = Paths.get(uploadDir, filename);
    Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
    return Map.of("filename", filename, "size", file.getSize());
}

下载

java 复制代码
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> download(@PathVariable String filename) {
    Path file = Paths.get(uploadDir, filename);
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .body(new FileSystemResource(file));
}

调用

bash 复制代码
# 上传
curl -u admin:admin123 -F "file=@README.md" http://localhost:8080/api/18/file/upload

# 下载
curl -u admin:admin123 -O http://localhost:8080/api/18/file/download/test.txt

生产建议 :本地磁盘换 OSS/MinIO/S3,使用 StreamingResponseBody 处理大文件。


19. 19 - 国际化(i18n)

文件`i18n/I18nConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/i18n/I18nConfig.java) + `i18n/I18nController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/i18n/I18nController.java) + src/main/resources/i18n/messages_*.properties

java 复制代码
@Configuration
public class I18nConfig implements WebMvcConfigurer {
    @Bean public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource s = new ReloadableResourceBundleMessageSource();
        s.setBasename("classpath:i18n/messages");
        s.setDefaultEncoding("UTF-8");
        return s;
    }
    @Bean public LocaleResolver localeResolver() { ... }
    @Bean public LocaleChangeInterceptor localeChangeInterceptor() { ... }
    @Override public void addInterceptors(InterceptorRegistry r) { r.addInterceptor(localeChangeInterceptor()); }
}

资源文件

properties 复制代码
# messages_zh_CN.properties
welcome=欢迎,{0}!
order.created=订单已创建
properties 复制代码
# messages_en_US.properties
welcome=Welcome, {0}!
order.created=Order created

切换语言

bash 复制代码
# URL 参数(最高优先级)
curl 'http://localhost:8080/api/19/i18n/welcome?name=Tom&lang=en_US'

# 请求头
curl -H 'Accept-Language: en-US' 'http://localhost:8080/api/19/i18n/welcome?name=Tom'

MessageSource 也可接管 JSR-380 校验消息LocalValidatorFactoryBean.setValidationMessageSource)。


20. 20 - Actuator(应用监控)

配置application.yaml):

yaml 复制代码
management:
  endpoints.web.exposure.include: health,info,metrics,prometheus,env,mappings,beans,configprops
  endpoint.health.show-details: when-authorized
  endpoint.health.probes.enabled: true
  health.mail.enabled: false

端点

端点 用途
/actuator/health 健康检查(liveness / readiness)
/actuator/info 应用信息
/actuator/metrics 指标(Micrometer)
/actuator/prometheus Prometheus 格式(需加 micrometer-registry-prometheus
/actuator/env 环境变量
/actuator/mappings 路由映射
/actuator/beans 所有 Bean
/actuator/configprops 配置属性

安全(本项目):

  • /actuator/health/actuator/info 公开
  • 其余需 ROLE_ADMIN

21. 21 - Spring Security 6.x(函数式配置)

文件`security/SecurityConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/security/SecurityConfig.java)

java 复制代码
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/01/**", ...).permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(AbstractHttpConfigurer::disable)
            .httpBasic(b -> {})
            .build();
    }

    @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

    @Bean public InMemoryUserDetailsManager userDetailsService(PasswordEncoder e) {
        UserDetails admin = User.builder().username("admin").password(e.encode("admin123")).roles("ADMIN").build();
        UserDetails user  = User.builder().username("user").password(e.encode("user123")).roles("USER").build();
        return new InMemoryUserDetailsManager(admin, user);
    }
}

要点

  • Spring Security 6 官方推荐 SecurityFilterChain Bean(废弃 WebSecurityConfigurerAdapter
  • 密码编码:必须用 BCryptPasswordEncoderNoOpPasswordEncoder 已废弃)
  • Session 策略:STATELESS(纯 JWT)/ IF_REQUIRED(默认)/ NEVER
  • CSRF:纯 API 服务建议 disable
  • 内存用户 / JDBC 用户 / OAuth2 / SAML / Keycloak 均可

22. 22 - 方法级安全(@PreAuthorize)

文件`security/MethodSecurityConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/security/MethodSecurityConfig.java) + `security/SecureController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/security/SecureController.java)

java 复制代码
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig { }
java 复制代码
@RestController @RequestMapping("/api/22/secure")
public class SecureController {
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin")    // 仅 admin 可访问
    public String admin() { return "admin only"; }

    @PreAuthorize("hasAnyRole('ADMIN','USER')")
    @GetMapping("/user")     // admin/user 均可
    public String user() { return "user accessible"; }

    @PreAuthorize("#name == authentication.name")
    @GetMapping("/self/{name}")
    public String self(@PathVariable String name) { return "ok"; }
}

调用

bash 复制代码
curl -u admin:admin123 http://localhost:8080/api/22/secure/admin   # 200
curl -u user:user123  http://localhost:8080/api/22/secure/admin   # 403

23. 23 - OpenAPI 3 文档(springdoc-openapi)

文件`openapi/OpenApiConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/openapi/OpenApiConfig.java) + pom.xml 依赖

依赖

xml 复制代码
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>3.0.3</version>
</dependency>

配置

yaml 复制代码
springdoc:
  api-docs.path: /v3/api-docs
  swagger-ui.path: /swagger-ui.html
  swagger-ui.operations-sorter: method

访问

注解示例

java 复制代码
@Operation(summary = "查询用户", description = "通过 id 获取用户")
@ApiResponses({@ApiResponse(responseCode="200"), @ApiResponse(responseCode="404")})
@GetMapping("/{id}")
public User get(@PathVariable Long id) { ... }

24. 24 - 条件装配(@Conditional*)

文件`conditional/ConditionalConfig`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/conditional/ConditionalConfig.java) + `conditional/ConditionalController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/conditional/ConditionalController.java)

java 复制代码
@Bean
@ConditionalOnProperty(prefix="app.features", name="cache", havingValue="true")
public CacheFeature cacheFeature() { return new CacheFeature(); }

@Bean
@ConditionalOnProperty(prefix="app.features", name="async", havingValue="true", matchIfMissing=true)
public AsyncFeature asyncFeature() { return new AsyncFeature(); }

条件注解全家福

注解 触发条件
@ConditionalOnClass 类路径存在某类
@ConditionalOnMissingClass 类路径缺失某类
@ConditionalOnBean 容器中存在某 Bean
@ConditionalOnMissingBean 容器中缺失某 Bean(用户覆盖扩展点
@ConditionalOnProperty 配置项匹配
@ConditionalOnResource 资源存在
@ConditionalOnWebApplication Web 应用
@ConditionalOnExpression SpEL 表达式
@ConditionalOnJava Java 版本
@ConditionalOnCloudPlatform 云平台(K8s / Cloud Foundry)
@ConditionalOnSingleCandidate 容器中只有单个候选 Bean

整个 Spring Boot 自动配置就基于这些条件注解。建议在自己的 starter 中用 @ConditionalOnMissingBean 提供覆盖扩展点。


25. 25 - 应用事件(ApplicationEvent)

文件`event/EventPublisher`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/event/EventPublisher.java) + `event/UserRegisteredEvent`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/event/UserRegisteredEvent.java) + `event/UserEventListener`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/event/UserEventListener.java) + `event/EventController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/event/EventController.java)

事件(不可变 Record):

java 复制代码
public record UserRegisteredEvent(String username, String email) {
    public static UserRegisteredEvent of(String u, String e) { return new UserRegisteredEvent(u, e); }
}

发布

java 复制代码
@Service
public class EventPublisher {
    private final ApplicationEventPublisher publisher;
    public void publish(String u, String e) {
        publisher.publishEvent(UserRegisteredEvent.of(u, e));
    }
}

监听 (注解驱动,无需实现 ApplicationListener):

java 复制代码
@Component
public class UserEventListener {
    @EventListener
    public void onUserRegistered(UserRegisteredEvent e) { /* 同步 */ }

    @EventListener @Async
    public void onUserRegisteredAsync(UserRegisteredEvent e) { /* 异步 */ }
}

其他变体

  • @TransactionalEventListener(phase=AFTER_COMMIT) - 事务提交后触发
  • @Order(1) 控制监听器顺序
  • SmartApplicationListener - 按事件类型 / 源类型过滤
  • 泛型事件:public class OrderEvent<T> 配合 ResolvableType

26. 26 - 应用生命周期

文件`lifecycle/AppLifecycle`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/lifecycle/AppLifecycle.java) + `lifecycle/LifecycleController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/lifecycle/LifecycleController.java)

java 复制代码
@Component
public class AppLifecycle implements SmartLifecycle, CommandLineRunner, ApplicationRunner {
    @Override public void start() { running = true; }
    @Override public void stop()  { running = false; }
    @Override public boolean isRunning() { return running; }
    @Override public int getPhase() { return Integer.MIN_VALUE + 1000; }

    @Override public void run(String... args) { /* CommandLineRunner */ }
    @Override public void run(ApplicationArguments args) { /* ApplicationRunner */ }
}

Spring Boot 启动顺序

  1. SpringApplication.run() - 创建 ApplicationContext
  2. ApplicationContextInitializedEvent
  3. ApplicationPreparedEvent
  4. ContextRefreshedEvent
  5. SmartLifecycle#start(按 phase 升序)
  6. ApplicationStartedEvent
  7. ApplicationRunner / CommandLineRunner
  8. ApplicationReadyEvent应用真正可服务
  9. 收到 SIGTERM → SmartLifecycle#stop(按 phase 降序)→ ContextClosedEvent

对比

接口 触发时机 用途
CommandLineRunner 应用就绪后 处理 String[] 命令行参数
ApplicationRunner 应用就绪后 处理 ApplicationArguments(支持 --key=value
SmartLifecycle start/stop(phase 控制) 长连接、定时器、外部资源

27. 27 - 可观测性(Micrometer + Actuator)

文件`observability/ObservabilityController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/observability/ObservabilityController.java)

java 复制代码
@RestController
@RequestMapping("/api/27/observability")
public class ObservabilityController {
    private final Counter requestCounter;
    private final Timer requestTimer;

    public ObservabilityController(MeterRegistry registry) {
        this.requestCounter = Counter.builder("tutorial_requests_total")
                .description("教程请求总数").tag("module", "observability").register(registry);
        this.requestTimer = Timer.builder("tutorial_request_duration")
                .description("教程请求耗时").register(registry);
    }

    @GetMapping("/ping")
    public Map<String,Object> ping() throws Exception {
        requestCounter.increment();
        return requestTimer.recordCallable(() -> {
            Thread.sleep(50);
            return Map.of("status","pong");
        });
    }
}

Meter 类型

类型 用途
Counter 累计计数(请求数、错误数)
Gauge 瞬时值(在线人数、队列长度)
Timer 耗时统计(支持 histogram)
DistributionSummary 分布统计(响应体大小)
LongTaskTimer 长时任务监控

对接 Prometheus

xml 复制代码
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

GET /actuator/prometheus 即可被 Prometheus 抓取。


28. 28 - 邮件发送(spring-boot-starter-mail)

文件`mail/MailService`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/mail/MailService.java) + `mail/MailController`(file:///D:/code/sb-app/src/main/java/com/lihaozhe/tutorial/mail/MailController.java)

配置application.yaml):

yaml 复制代码
spring.mail:
  host: smtp.example.com
  port: 465
  username: ${MAIL_USER:demo}
  password: ${MAIL_PWD:demo}
  properties:
    mail.smtp.auth: true
    mail.smtp.ssl.enable: true

发送

java 复制代码
@Service
public class MailService {
    private final JavaMailSender mailSender;
    public Map<String,Object> sendSimple(String to, String subject, String text) {
        SimpleMailMessage msg = new SimpleMailMessage();
        msg.setFrom("noreply@example.com");
        msg.setTo(to);
        msg.setSubject(subject);
        msg.setText(text);
        mailSender.send(msg);
        return Map.of("status","sent","to",to);
    }
}

富文本 / 附件

java 复制代码
MimeMessage msg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg, true, "UTF-8");
helper.setFrom("noreply@example.com");
helper.setTo(to);
helper.setSubject(subject);
helper.setText("<h1>HTML</h1>", true);
helper.addAttachment("doc.pdf", new FileSystemResource("doc.pdf"));
mailSender.send(msg);

本项目默认 SMTP 主机是 smtp.example.com(演示用),实际发送会失败并返回 500。配置真实 SMTP 后即可发送。


附录 A - 配置文件速查

完整 `application.yaml`(file:///D:/code/sb-app/src/main/resources/application.yaml) 关键片段:

yaml 复制代码
spring:
  application.name: sb-app
  profiles.active: dev
  mvc.matrix-variables.enabled: true           # 开启矩阵变量
  servlet.multipart.max-file-size: 10MB
  jackson.default-property-inclusion: non_null # 忽略 null 字段
  datasource.url: jdbc:h2:mem:sbapp           # 内存库
  jpa.hibernate.ddl-auto: create-drop
  jpa.open-in-view: false
  h2.console.enabled: true
  threads.virtual.enabled: true               # 虚拟线程
  cache.type: simple

app: { name: sb-app, version: 4.0.6, upload-dir: /tmp/uploads, admins: [alice, bob] }

server: { port: 8080, shutdown: graceful }

management:
  endpoints.web.exposure.include: health,info,metrics,prometheus,env,mappings,beans,configprops
  endpoint.health.show-details: when-authorized
  health.mail.enabled: false

springdoc:
  api-docs.path: /v3/api-docs
  swagger-ui.path: /swagger-ui.html

附录 B - 全部 73 个端点

路由前缀 /api/{模块编号}/{主题}/... 全部位于 com.lihaozhe.tutorial.* 包下。

模块 端点 方法 期望码
01 /api/01/hello GET 200
01 /api/01/hello/{name} GET 200
01 /api/01/hello/info GET 200
02 /api/02/config/app GET 200
03 /api/03/profile/info GET 200
04 /api/04/web/query?name=... GET 200
04 /api/04/web/path/{id} GET 200
04 /api/04/web/header GET 200
04 /api/04/web/cookie GET 200/401
04 /api/04/web/matrix/abc;color=red GET 200/401
04 /api/04/web/body POST 200
05 /api/05/users GET/POST 200/201
05 /api/05/users/{id} GET/PUT/PATCH/DELETE 200/204/404
06 /api/06/validation/register POST 200/400
07 /api/07/exception/business GET 400
07 /api/07/exception/runtime GET 500
07 /api/07/exception/missing GET 404
08 /api/08/restclient/github GET 200
09 /api/09/jdbc GET/POST 200/201
09 /api/09/jdbc/{id} GET/PUT/DELETE 200/404
09 /api/09/jdbc/search?keyword=... GET 200
10 /api/10/jpa/users GET/POST 200/201
10 /api/10/jpa/users/{id} GET/PUT/DELETE 200/204/404
10 /api/10/jpa/users/search?keyword=... GET 200
11 /api/11/tx/order GET 200
11 /api/11/tx/rollback GET 500
11 /api/11/tx/query GET 200
12 /api/12/aop/hello GET 200
13 /api/13/cache/{id} GET 200
13 /api/13/cache/{id}?name=... PUT 200
13 /api/13/cache/{id} DELETE 200
14 /api/14/schedule/run GET 200
15 /api/15/async/void GET 200
15 /api/15/async/result GET 200
16 /api/16/logging/{level} GET 200
17 ws://localhost:8080/ws/chat WS -
18 /api/18/file/upload POST 200
18 /api/18/file/downloads POST 200
18 /api/18/file/download/{filename} GET 200/404
19 /api/19/i18n/welcome?name=...&lang=... GET 200
20 /actuator/health GET 200
20 /actuator/info GET 200
20 /actuator/metrics GET 200/401
22 /api/22/secure/admin GET 200/401/403
22 /api/22/secure/user GET 200/401
22 /api/22/secure/self/{name} GET 200/403
24 /api/24/conditional/list GET 200
25 /api/25/event/publish POST 200
26 /api/26/lifecycle/status GET 200
27 /api/27/observability/ping GET 200
27 /api/27/observability/slow GET 200
28 /api/28/mail/send POST 500(演示 SMTP 未配置)
- /swagger-ui.html GET 200
- /h2-console GET 200
- 未匹配路径 GET 404

完整测试运行结果见 `test-result.log`(file:///D:/code/sb-app/test-result.log)。

附录 C - Spring Boot 4 关键变化(vs Spring Boot 3.x)

变化 说明
启动器 spring-boot-starter-webspring-boot-starter-webmvc(名称对齐 reactor 风格)
启动器 spring-boot-starter-aopspring-boot-starter-aspectj
测试 spring-boot-starter-testspring-boot-starter-webmvc-test
客户端 RestClient 成为默认推荐,替代 RestTemplate(仍可用)
校验 @Valid @RequestBodyHandlerMethodValidationException(不再是 MethodArgumentNotValidException
资源 NoResourceFoundException 是 Spring 6+ 处理未匹配路径的默认异常
线程 spring.threads.virtual.enabled=true 默认开启(JDK 21+)
Jakarta 全面切换到 Jakarta EE 11(包名 jakarta.*
Security SecurityFilterChain Bean 函数式配置(WebSecurityConfigurerAdapter 已废弃)
配置 ConfigurationProperties 推荐用 record

相关推荐
千云1 小时前
100w大表0停机回滚:我们为什么放弃Undo Log,选择表名切换?
数据库·后端·mysql
云恒要逆袭1 小时前
Hello World背后的秘密:Java程序是这样运行的
java·后端·程序员
蝎子莱莱爱打怪1 小时前
XZLL-IM干货系列 01|万字拆解分布式 IM 架构:7 个微服务 + 自研 Flutter SDK
java·后端·面试
亦暖筑序2 小时前
Java 8老系统旁路接入AI Gateway:不升级JDK也能用AI
java·spring boot·aigc·企业架构·ai gateway
Elaine3362 小时前
基于Django框架的静态个人名片网站设计
后端·python·django·mvt
道友可好2 小时前
3 个人,100 万行代码,一行都没人写:OpenAI 的 Harness Engineering 实验
前端·人工智能·后端
Yeats_Liao3 小时前
8:Servlet生命周期-Java Web
后端
Soofjan3 小时前
其它(5):Bleve 全文检索
后端
Gopher_HBo3 小时前
Go语言学习笔记(七)并发
后端