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

相关推荐
颜酱16 分钟前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱17 分钟前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康2 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅2 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries2 小时前
文件上传系统
后端
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端