SpringBoot开发中那些容易忽略的配置细节

SpringBoot开发中那些容易忽略的配置细节

你盯着屏幕上那个“已启动成功”的日志,心想一切正常。可当第一个请求过来,接口返回的日期时间格式不对;第二个请求触发了一个诡异的空指针;第三个请求直接把内存撑爆了。你翻遍所有业务代码,找不到任何逻辑错误——这时候,大概率是某个你根本没正眼瞧过的配置文件在暗处作祟。

SpringBoot号称“约定优于配置”,但那份“约定”其实是一张细密的网,任何一处疏漏都会让整个应用行为失常。下面这十一个配置盲区,我敢说你至少遇到过其中三个。

那个被你忽略的bootstrap.yml到底干了什么?

大多数开发者只认识application.yml,却很少会在意bootstrap.yml。实际上,Spring Cloud 环境下 bootstrap.yml 的优先级高于 application.yml,它负责加载外部配置源,比如配置中心、加密密钥。如果你把数据库密码直接写在 application.yml 里,而配置中心又在 bootstrap 阶段才初始化——密码根本不会生效。

更隐蔽的是,bootstrap.yml 中的spring.application.name决定了后续所有配置的命名空间。改了这个名字,连接 Consul、Nacos 时可能全盘失效。另一个常见陷阱:bootstrap.yml 里的server.port不会被识别,要改端口必须写在 application.yml 里。

配置文件加载顺序:你以为的“覆盖”可能根本没发生

SpringBoot 加载配置的规则是一个多层塔:依次从file:./config/file:./classpath:/config/classpath:/读取。但你部署时如果把 jar 包和外部 config 目录放在一起,优先级确实最高——问题是很多人把外部配置放在application-{profile}.yml里,而 Profile 文件只在特定情况下才被读取

举个例子:你在 jar 包同级目录下放了config/application-prod.yml,但启动命令里只写了java -jar app.jar没加--spring.profiles.active=prod,那么 prod 配置永远不生效。Profile 激活不是通过文件存在自动触发的,必须显式指定。另一个冷知识:@PropertySource注解无法作用于 YAML 文件,你如果用它指向一个 .yml,整个应用会静默失败。

@ConfigurationProperties@Value:谁比谁更坑?

@Value用起来简单,但它有两个致命弱点:不支持复杂的嵌套结构,比如user.addresses[0].city这种;也不支持松散绑定,你在配置里写user-name,@Value 必须用@Value("${user-name}")一字不差。而@ConfigurationProperties能自动把user-name映射到userName字段上。

更隐蔽的是,@ConfigurationProperties 默认不会校验。如果你在配置里写了个字符串给server.port,Spring 会静默地把它转成 int,遇到非数字直接抛异常。加上@Validated注解才能触发校验。还有,@ConfigurationProperties类的属性必须有 setter 方法,否则注入失败,很多人在 Kotlin 或 Lombok 环境下忘记生成 setter,结果属性全为 null,查半天。

日志配置:你还在用logging.level.root=INFO就完事了?

生产环境中只靠一个 INFO 级别什么也查不到。但问题是,SpringBoot 默认的日志是 Logback,它只在classpath:logback-spring.xml存在时才覆盖默认配置。如果你直接在 application.yml 里写logging.pattern.console=...,它只能影响控制台输出,文件日志的模式根本不受影响。

还有一个容易踩的雷:日志文件的滚动策略。默认按10MB滚动,但如果你的应用一天产生 20GB日志,10MB 切一次会疯狂产生小文件,磁盘 IO 直接打满。你应该在配置里明确指定logging.logback.rollingpolicy.max-history=7logging.logback.rollingpolicy.total-size-cap=5GB。不配置?日志目录可能在几天内撑爆磁盘。

数据库连接池:HikariCP 的默认值真的适合你吗?

SpringBoot 2.x 默认用 HikariCP,官方说它是“最快连接池”,但默认的maximum-pool-size=10对于高并发业务完全是灾难。如果你的应用有 20 个线程同时访问数据库,10 个连接瞬间用完,剩余的请求全在排队。而排队时间默认为 30 秒(connection-timeout=30000),用户等半分钟才看到超时错误。

更隐蔽的是连接泄漏。HikariCP 默认不主动检测泄漏,需要设置leak-detection-threshold=2000(毫秒),如果一个连接被占用超过 2 秒没归还,就打印警告。很多团队直到线上出现“连接不可用”错误,才想起打开这个开关。另一个坑:idle-timeout默认 10 分钟,如果你的数据库有连接空闲超时(比如 MySQL 的 wait_timeout=8小时),HikariCP 的max-lifetime要设置比数据库的超时短一点,否则数据库干掉空闲连接后,连接池还傻傻地认为连接是好的。

跨域配置:前端报跨域时,别只怪浏览器

大部分新手用@CrossOrigin在 Controller 上加注解,但如果你用了 Spring Security,跨域配置的优先级必须高于 Security 过滤器。正确做法是注册一个CorsConfigurationSourceBean,或者实现WebMvcConfigureraddCorsMappings。否则 Security 的 CSRF 保护会先拦截 OPTIONS 预检请求,直接返回 403。

另外,跨域配置中的allowedOrigins不能写""并同时允许携带凭证(allowCredentials=true),这是浏览器的安全限制。很多人在本地测试时写""加上allowCredentials=true,结果 Chrome 直接报错,然后跑去查前端配置。

Jackson 序列化:你以为@JsonFormat能搞定一切?

时间格式是表面问题,更深的是全局时间格式化配置必须在application.yml里写spring.jackson.date-format=yyyy-MM-dd HH:mm:ss,但如果你同时用了@JsonFormat注解,注解优先级更高。问题在于,很多人都不知道 Jackson 还有timezone设置,默认取服务器本地时区。如果你的服务器是 UTC,数据库存的是北京时间,前端看到的永远是减了8小时的时间。

另一个容易爆炸的点是BigDecimal 的序列化。默认情况下,0.00会被输出为0.00.000输出为0.0。要保留尾随零,必须配置spring.jackson.serialization.write-bigdecimal-as-plain=true。更离谱的是Jackson 对未知字段的处理:默认抛出异常,如果你返回的实体类少了某个字段,前端调用直接报 500。加上spring.jackson.deserialization.fail-on-unknown-properties=false才能优雅忽略。

异步任务配置:@Async 为什么没生效?

你给方法加了@Async,以为能自动异步执行,结果发现还是同步阻塞。最常见的错误是:调用 Async 方法的代码必须来自另一个 Bean,因为 Spring 的 AOP 原理是基于代理的,从同一个类内部调用,代理不生效。解决办法是注入自己,或者把异步方法提取到另一个 Service 里。

更隐蔽的是自定义线程池@EnableAsync默认使用SimpleAsyncTaskExecutor,它每次任务都新建一个线程,高并发下线程数暴增,直接 OOM。你需要在配置类里定义一个Executor类型的 Bean,并设置corePoolSizemaxPoolSize另一个被忽略的点是@Async方法返回值必须为voidFuture,如果你返回普通对象,Spring 会直接返回 null。

健康检查:为什么 /actuator/health 返回 404?

你在 pom.xml 里加了spring-boot-starter-actuator,然后访问/actuator/health,却得到 404。原因很简单:从 SpringBoot 2.x 开始,Actuator 默认只暴露 health 和 info 两个端点,且路径是/actuator/health,但很多人忘了写/actuator。另一个坑是:如果你配置了server.servlet.context-path=/demo,那么端点路径会变成/demo/actuator/health

健康检查还有更细的配置:management.endpoint.health.show-details=when_authorized,不配置的话默认只返回 "UP" 或 "DOWN",没有详细信息。对于 Kubernetes 的存活探针和就绪探针,你得分别配置liveness-state.enabled=truereadiness-state.enabled=true,否则探针只能判断应用是否启动,不能判断是否真正就绪。

配置加密:你把数据库密码明文写在配置文件里了吗?

别觉得内网就安全。任何人能接触到运维机器或代码仓库,就能拿到你的所有明文密码。Spring Cloud Config 支持对称加密,只需在 bootstrap.yml 里设置encrypt.key即可。但有个细节:加密值必须用前缀{cipher}开头,否则 Spring 不会解密。而且你要先通过/encrypt端点生成密文,再粘贴到配置文件。

更推荐的方案是使用Jasypt Spring Boot Starter,它能在任意配置属性上解密。但Jasypt 默认使用的加密算法是 PBEWithMD5AndDES,这个算法已经被认为是弱密码。你应该改成PBEWithHMACSHA512AndAES_256,同时设置一个强密码。另一个容易被忽略的点:解密过程发生在 Spring 初始化 Bean 之前,如果你在 @PostConstruct 里用 @Value 获取密码,但解密器还没加载完,得到的还是密文。

多环境配置:你还在手动改 application.yml 吗?

很多团队靠复制粘贴多个 application-{profile}.yml 来管理环境,但不同环境之间高度重复的配置(比如相同的 Redis 超时时间)会导致维护噩梦。正确的做法是利用Spring Boot 的 Profile 分组,在spring.profiles.group里组合多个 Profile,或者用spring.profiles.include引入公共配置。

最容易被忽略的是--spring.profiles.active--spring.profiles.include的区别:active 会覆盖默认的 default Profile,而 include 会追加。如果你在application.yml里定义了spring.profiles.include=db,cache,然后启动时又设置了--spring.profiles.active=prod,那么只有 prod 配置文件被加载,db 和 cache 都没了。解决办法是把公共的 profile 通过spring.profiles.default设置,或者用 Maven/Gradle 的 profile 在打包时就内置进去

自动配置类排除:为什么你明明加了 starter 却不生效?

你引入了spring-boot-starter-data-redis,也写了 Redis 配置,但注入RedisTemplate时报错 “No qualifying bean of type”。原因很可能是其他自动配置类冲突了。SpringBoot 的@SpringBootApplication注解默认会扫描@EnableAutoConfiguration,你可以通过exclude属性排除掉不需要的自动配置。

更常见的是自定义 Starter 的自动配置不生效。SpringBoot 会扫描META-INF/spring.factories文件,但你如果漏掉了org.springframework.boot.autoconfigure.EnableAutoConfiguration那一行,或者写错了全限定名,你的配置类就永远不会被加载。而且SpringBoot 2.7 之后推荐使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,旧的spring.factories方式在 3.x 里被废弃——很多人升级到 SpringBoot 3.x 后,自定义自动配置全部失效,就是因为没改这个文件。

结尾

修改一个配置的成本几乎为零,但找到问题配置的时间可能是一整天。你永远不知道下一个线上事故是因为漏写了一个spring.jackson.default-property-inclusion=non_null,还是因为多写了一个spring.jpa.open-in-view=true(这个默认开启的配置会导致请求过程中数据库连接一直被持有,高并发下直接拖垮连接池)。

建议你花两个小时把项目中所有的配置文件梳理一遍,对照本文提到的每个细节逐一检查。那些被忽略的配置,往往就是系统脆弱性的源头