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.yaml 中 management.* |
| 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
启动后:
- API 文档:http://localhost:8080/swagger-ui.html
- H2 控制台:http://localhost:8080/h2-console(JDBC URL:
jdbc:h2:mem:sbapp,用户sa) - 健康检查:http://localhost:8080/actuator/health
- 测试账号:
admin / admin123(ROLE_ADMIN)、user / user123(ROLE_USER)
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中默认启用dev,application-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.yaml中spring.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 |
清除缓存(支持 allEntries、beforeInvocation) |
@Caching |
组合多个缓存注解 |
@CacheConfig |
类级别共享 cacheNames、keyGenerator |
启用 :在 @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 官方推荐
SecurityFilterChainBean(废弃WebSecurityConfigurerAdapter) - 密码编码:必须用
BCryptPasswordEncoder(NoOpPasswordEncoder已废弃) - 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
访问:
- Swagger UI:http://localhost:8080/swagger-ui.html
- OpenAPI JSON:http://localhost:8080/v3/api-docs
注解示例:
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 启动顺序
SpringApplication.run()- 创建ApplicationContextApplicationContextInitializedEventApplicationPreparedEventContextRefreshedEventSmartLifecycle#start(按phase升序)ApplicationStartedEventApplicationRunner/CommandLineRunnerApplicationReadyEvent← 应用真正可服务- 收到 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-web → spring-boot-starter-webmvc(名称对齐 reactor 风格) |
| 启动器 | spring-boot-starter-aop → spring-boot-starter-aspectj |
| 测试 | spring-boot-starter-test → spring-boot-starter-webmvc-test |
| 客户端 | RestClient 成为默认推荐,替代 RestTemplate(仍可用) |
| 校验 | @Valid @RequestBody 抛 HandlerMethodValidationException(不再是 MethodArgumentNotValidException) |
| 资源 | NoResourceFoundException 是 Spring 6+ 处理未匹配路径的默认异常 |
| 线程 | spring.threads.virtual.enabled=true 默认开启(JDK 21+) |
| Jakarta | 全面切换到 Jakarta EE 11(包名 jakarta.*) |
| Security | SecurityFilterChain Bean 函数式配置(WebSecurityConfigurerAdapter 已废弃) |
| 配置 | ConfigurationProperties 推荐用 record |