SpringBoot(三):配置管理与生产实践
SpringBoot(三):配置管理与生产实践
导语:Spring Boot 的"最后一公里"是配置与交付。本篇讲清配置文件的位置与优先级(含 2.4 的 Config Data API 变化)、
Profile多环境、@Value与@ConfigurationProperties的取舍,再落到生产必备的 Actuator 与安全、全局异常、参数校验、日志、跨域、可执行 jar 原理、优雅停机,共 13 题。
一、配置加载与多环境
1. Spring Boot 的配置文件加载位置和顺序是怎样的?
答: 默认 application(可通过 spring.config.name 改)会从以下位置全部加载(2.4+ Config Data API):
classpath:/ (jar/classes 根)
classpath:/config/ (jar/classes 下 config 目录)
file:./ (当前目录)
file:./config/ (当前目录 config 子目录)
file:./config/*/ (当前目录 config 下任意子目录,2.4+)规则:
- 同一位置内,
-{profile}专属文件 > 非专属文件; - 不同位置之间,后者覆盖前者(
classpath:/→classpath:/config/→file:./→file:./config/,即"外 > 内、config 子目录 > 同级、当前目录 > jar 内"); .properties与.yml同时存在时,.properties优先(YAML 加载在后面);- 不会"只加载一个"——所有找到的都会被加载,按优先级覆盖合并(List/Map 类型还会做合并而非整体替换)。
2.4 之后的新增能力:
spring:
config:
import: # 显式导入其他配置(Config Server、Vault、其他文件)
- optional:configtree:/etc/app/
- nacos:app.yaml注意重大变化:Boot 2.4 起移除了
spring.profiles的include语义、重写了加载顺序(Config Data API),并规定spring.profiles.active不能写在 profile-specific 文件里(会报InvalidConfigDataPropertyException)——这是升级 2.4+ 时最常见的启动报错。
2. 外部化配置的完整优先级(从高到低)是什么?
答: 记住"命令行 > 环境变量 > jar 外配置 > jar 内配置 > 默认"这条主线,完整顺序如下:
| 优先级 | 来源 |
|---|---|
| 1(最高) | Devtools 全局配置(~/.spring-boot-devtools.properties) |
| 2 | 测试相关:@TestPropertySource、@SpringBootTest(properties) |
| 3 | 命令行参数(--server.port=8081,Spring Boot 会把它注册为最高优先级的 PropertySource) |
| 4 | SPRING_APPLICATION_JSON |
| 5 | ServletConfig / ServletContext 初始化参数 |
| 6 | JNDI(java:comp/env) |
| 7 | Java 系统属性(-Dkey=value) |
| 8 | OS 环境变量 |
| 9 | random.* |
| 10 | jar 外 application-{profile}.yml / application-{profile}.properties |
| 11 | jar 外 application.yml / application.properties |
| 12 | jar 内 application-{profile}.* |
| 13 | jar 内 application.* |
| 14 | @PropertySource |
| 15(最低) | SpringApplication.setDefaultProperties / @Bean 中 properties |
实践结论:
- 改配置不改包:外部化
application-prod.yml或环境变量覆盖即可; - 容器化环境优先用环境变量/ConfigMap(
SPRING_DATASOURCE_URL这种 relaxed binding 形式),避免把配置打进镜像; - 命令行参数适合临时排障,但不要写进启动脚本(会覆盖一切,导致配置中心失效)。
3. Profile 多环境怎么用?激活方式有哪些?
答:
# application.yml
spring:
profiles:
active: dev # 或在启动时指定 --spring.profiles.active=prod
group:
prod: [ prod-db, prod-mq ] # 一个 profile 激活一组
---
# application-prod.yml 也可以写在同一个文件里(多文档 + 条件)
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://...激活方式(优先级由高到低):
- 命令行
--spring.profiles.active=prod - Java 系统属性
-Dspring.profiles.active=prod - OS 环境变量
SPRING_PROFILES_ACTIVE=prod - 配置文件中的
spring.profiles.active SpringApplication.setAdditionalProfiles(...)@ActiveProfiles(测试)
用法要点:
@Profile("prod")控制 Bean 是否注册;application-{profile}.*控制配置值;两者互补。- 不要用 profile 承载"功能开关"——功能开关应该用
@ConditionalOnProperty或配置中心的动态开关(支持运行时切换),而 profile 是启动时确定的。 - 默认 profile 是
default;未激活任何 profile 时只加载application.*。
4. @Value 和 @ConfigurationProperties 有什么区别?怎么选?
答:
| 维度 | @Value | @ConfigurationProperties |
|---|---|---|
| 粒度 | 单个属性逐一声明 | 一组同前缀属性批量绑定到 Bean |
| 表达式 | 支持 SpEL(#{...})、${...} | 不支持 SpEL,但支持松绑定(relaxed binding) |
| 松绑定 | 不支持(server.port 必须写全) | 支持(server.port / serverPort / SERVER_PORT 都能绑) |
| 校验 | 需配合 @Validated 于类上 | 支持 JSR-303 校验(@Validated + @NotBlank 等) |
| 复杂结构 | 集合/嵌套对象写起来很痛苦 | 天然支持 List/Map/嵌套对象、构造器绑定 |
| 元数据 | 无 | 配 spring-boot-configuration-processor 生成 IDE 提示元数据 |
| 动态刷新 | 需 @RefreshScope | 需 @RefreshScope + 重新绑定(@ConfigurationProperties 的 @Bean 方式刷新更自然) |
@ConfigurationProperties(prefix = "demo")
@Validated
public class DemoProperties {
@NotBlank private String prefix;
private int timeout = 3000; // 默认值
private List<String> hosts = List.of(); // 集合
private final Nested nested; // 构造器绑定(Spring Boot 3 推荐)
}选型结论:成组的结构化配置一律用
@ConfigurationProperties;@Value只适合"零散的单个值 / 需要 SpEL 表达式"的场景(如@Value("#{systemProperties['os.name']}"))。不要在业务类里散落@Value——那是配置分散、难以校验的根源。
5. 配置能热更新吗?动态配置中心怎么接入?
答:
- 本地配置文件:默认不支持热更新(重启才生效);
@RefreshScope(Spring Cloud Context):被标注的 Bean 在ContextRefresher触发刷新时销毁重建(懒加载代理),从而拿到新配置;- Nacos / Apollo / Config Server:都是"配置中心 + 客户端监听"模式——Nacos 用长轮询把变更推给客户端,客户端发布
RefreshEvent,@RefreshScope与@ConfigurationProperties完成重新绑定(详见《SpringCloud(三)》); - 注意:
@RefreshScope只能刷新被它管理的 Bean,静态字段、@Value在非 refresh Bean 中、以及已初始化的连接池通常不会自动更新,需要显式写@EventListener(RefreshScopeRefreshedEvent.class)做重建。
生产建议:配置中心的推送不等于"安全生效"——对连接池、线程池、限流阈值这类参数,必须定义"变更→校验→灰度→回滚"的流程,避免一次误改引发全站故障。
二、生产级能力
6. Actuator 有哪些常用端点?生产上如何保证安全?
答: Actuator 提供生产级监控与运维端点:
| 端点 | 作用 |
|---|---|
/actuator/health | 健康检查(可聚合 DB/Redis/MQ/磁盘),探针的数据来源 |
/actuator/info | 应用信息(版本、构建时间) |
/actuator/metrics | JVM、HTTP、线程池、DataSource 指标(Micrometer) |
/actuator/prometheus | Prometheus 抓取格式 |
/actuator/env | 环境属性与来源(排障神器,也最敏感) |
/actuator/beans | 容器中的 Bean |
/actuator/mappings | URL ↔ Controller 映射 |
/actuator/conditions | 自动配置条件评估结果 |
/actuator/loggers | 运行时查看/修改日志级别 |
/actuator/refresh | 刷新 @RefreshScope 配置 |
/actuator/shutdown | 关闭应用(默认关闭) |
默认只暴露 health 和 info,其余需显式开启:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus # 按需最小暴露
endpoint:
health:
show-details: when_authorized # 不泄露内部细节
server:
port: 8090 # 独立管理端口,不对外安全加固(生产必做):
- 最小暴露:只开必要端点,绝不
include: "*"; - 独立管理端口 + 内网/安全组隔离,不暴露到公网;
- 用 Spring Security 保护管理端点(或网关统一鉴权);
show-details不要设为always(会泄露 DB 地址、磁盘路径);- 区分 liveness 与 readiness:
management.endpoint.health.probes.enabled=true,就绪探针要包含下游依赖,存活探针不要依赖下游(否则下游抖动会导致 Pod 被反复重启)。
易错点:
/actuator暴露在公网等于把"环境变量、Bean 列表、配置来源"全部公开——这是真实存在的重大安全事故来源。
7. Spring Boot 怎么做全局异常处理与统一响应?
答: 用 @RestControllerAdvice + @ExceptionHandler(原理见《Spring(五)》第 12 题),生产上建议约定"统一响应结构 + 统一错误码":
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> biz(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); }
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> invalid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage()).collect(Collectors.joining("; "));
return Result.fail(ErrorCode.PARAM_INVALID, msg);
}
@ExceptionHandler(Exception.class)
public Result<Void> unknown(Exception e) {
log.error("unexpected error", e); // 打印堆栈,但不返回给前端
return Result.fail(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍后重试");
}
}实践要点:
- 对外只返回脱敏的错误信息,堆栈只进日志(避免泄露内部结构/SQL);
- 与 Actuator 的 health 区分开:"异常"是业务语义,"健康"是运行时状态;
- 错误码要可枚举、可文档化(配合 OpenAPI),否则前端无法针对性处理;
- 注意
@ExceptionHandler覆盖不到 Filter 阶段与容器级错误(404/500 容器页)——那部分由ErrorController处理(Boot 默认BasicErrorController),可自定义ErrorAttributes统一格式。
8. Spring Boot 怎么统一做参数校验?
答:
@PostMapping("/orders")
public Result<OrderVO> create(@Validated @RequestBody OrderCreateForm form) { ... }
@GetMapping("/orders")
public Result<Page<OrderVO>> page(@Validated OrderQuery query) { ... } // Query 对象绑定校验- 引入
spring-boot-starter-validation(Boot 2.3+ 已不默认包含); - 校验注解来自 Jakarta Bean Validation(
@NotNull、@Size、@Min、@Pattern、@Email…),实现默认 Hibernate Validator; - 异常统一在
@RestControllerAdvice收敛:@RequestBody失败抛MethodArgumentNotValidException,@RequestParam/@PathVariable失败抛ConstraintViolationException; @Validatedvs@Valid:@Validated是 Spring 变体,支持分组校验(@GroupSequence,如"创建时不校验 id、更新时必填 id")和类级/方法级校验;- 分组校验是加分点:不同业务入口用不同
groups,避免"一套 DTO 到处用"导致的校验规则混乱。
9. spring-boot-devtools 的原理是什么?生产能用吗?
答:
- 原理:使用两个类加载器——
base类加载器加载不变的第三方依赖,restart类加载器加载你自己的类;当类路径变化时只丢弃并重建 restart 类加载器,因此重启比重启 JVM 快得多("快速重启"而非"热替换")。 - 附带能力:默认关闭模板缓存(
spring.thymeleaf.cache=false)、配置LiveReload、HMR 等。 - 注意:
- 生产禁用:
devtools打包时会被自动排除(repackage阶段),但要确保不要手动打进镜像; - 它不是真正的字节码热替换(如 JRebel),修改方法体也需要重启上下文;
- 与某些 Agent、远程调试、
spring-boot-devtools.properties(全局配置)可能冲突。
- 生产禁用:
实践:本地开发用它提升效率;CI/生产环境不引入,容器里也不要做 classpath 挂载导致误触发重启。
10. Spring Boot 的日志如何管理?
答:
- 默认使用 Logback + SLF4J,
spring-boot-starter-logging自动装配;换 Log4j2 需排除 starter-logging。 - 配置方式:
logging: level: root: INFO com.demo: DEBUG file: name: /var/log/app/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 15 pattern: console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" - 复杂配置用
logback-spring.xml(注意是-spring后缀,才能用<springProfile>、<springProperty>):<springProfile name="prod"> <root level="INFO"><appender-ref ref="FILE"/></root> </springProfile> - 实践要点:
- 日志格式要包含 traceId(配合链路追踪 MDC),否则微服务排障无从下手;
- 容器化写到 stdout(由平台收集),同时可保留文件输出;
logging.level可运行时用/actuator/loggers动态调整(排障利器);- 日志轮转与磁盘保护(max-history、total-size-cap),防止打满磁盘;
- 避免在循环里打日志/打印大对象——日志 IO 是常见性能损耗源(见《高性能(二)》)。
11. Spring Boot 如何配置跨域?
答: 三种方式(原理见《Spring(五)》第 13 题):
// ① 注解:类/方法级
@CrossOrigin(origins = "https://a.com", allowCredentials = "true")
// ② 全局:WebMvcConfigurer
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("https://*.demo.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true).maxAge(3600);
}
}
// ③ Filter:CorsFilter(可处理 OPTIONS 预检,优先级最高)生产建议:
- 微服务架构下统一在网关处理 CORS(Gateway 的
CorsWebFilter/ 全局配置),避免各服务规则不一致导致的"预检 403"; allowCredentials=true时不能用allowedOrigins("*"),必须用allowedOriginPatterns或回显具体域名;- 别为了图省事全放开——CORS 放开意味着任意站点可携带用户凭证调你的接口(配合 CSRF 就是完整攻击链),生产应结合鉴权(Token/CSRF)一起防护。
12. Spring Boot 的可执行 jar 是怎么做到"能直接 java -jar 运行"的?
答: 靠 spring-boot-maven-plugin 的 repackage 目标 + 自定义类加载器:
app.jar
├── META-INF/MANIFEST.MF
│ Main-Class: org.springframework.boot.loader.launch.JarLauncher (Boot 3.2+)
│ Start-Class: com.demo.Application (真正的业务主类)
├── BOOT-INF/classes/ ← 应用自己的类和 resources
├── BOOT-INF/lib/ ← 所有依赖 jar(jar in jar,非 fat jar 解压)
└── org/springframework/boot/loader/... ← 启动器与自定义类加载器关键点:
- 标准 JDK 无法从嵌套 jar 加载类,所以 Boot 自己实现
JarLauncher+LaunchedURLClassLoader来读取BOOT-INF/lib下的嵌套 jar; Main-Class是JarLauncher(不是你的主类),它负责启类加载器再调用Start-Class;- Boot 3.2 起重构了 loader(
spring-boot-loader基于ZipEntry索引,启动更快、支持spring-boot-jarmode的layertools分层); - 分层(Layered jar):把 jar 拆成
dependencies/spring-boot-loader/snapshot-dependencies/application,让 Docker 镜像层可复用——依赖不变时只重建最上层,镜像推送量大幅减少(K8s 拉取更快)。
其他部署方式:
- 传统 war 部署:
<packaging>war</packaging>+ 继承SpringBootServletInitializer(需排除内嵌容器,较少用); - Docker/OCI:
bootBuildImage(Buildpacks)或 Dockerfile 多阶段构建 +layertools; - Native Image:
native-maven-plugin编译为本地可执行文件。
13. 生产落地 Spring Boot 有哪些必做项?
答: 一份可直接对照的"上线检查清单":
| 类别 | 必做项 |
|---|---|
| 可观测 | 暴露 health/metrics/prometheus;接入 Prometheus + Grafana;日志带 traceId;关键业务指标埋点 |
| 健康与探针 | 区分 liveness / readiness;readiness 校验下游依赖;探针路径与端口正确 |
| 优雅停机 | server.shutdown=graceful + timeout-per-shutdown-phase;K8s preStop + terminationGracePeriodSeconds 对齐 |
| 线程池与容器 | 按业务隔离线程池、有界队列 + 明确拒绝策略;-XX:MaxRAMPercentage + -XX:ActiveProcessorCount(见《高性能(二)》) |
| 超时与重试 | 每个远程调用(DB/Redis/HTTP/MQ)显式设超时;重试有上限与退避(见《高可用(一)》) |
| 配置管理 | 配置外部化(环境变量/ConfigMap/配置中心);敏感信息加密;不使用 -- 覆盖配置中心 |
| 安全 | Actuator 最小暴露 + 独立端口;关闭生产环境 devtools;依赖漏洞扫描(SCA);CORS 收敛 |
| 容错 | 关键依赖配熔断/限流/降级预案(Sentinel/Resilience4J),并演练 |
| 发布 | 分层/多阶段镜像构建;滚动发布 + 快速回滚;每次发布只改一个变量,避免"配置+代码"同时变 |
本章小结:配置的核心是「优先级要清楚、来源要外部化、变更要可控」;生产能力的核心是「可观测 + 可优雅退出 + 有超时和隔离」——这两句话能覆盖 Spring Boot 工程实践类问题的大部分追问。
