Spring Boot
Spring Boot 最容易被低估的地方,是它看起来太容易启动了。
mvn spring-boot:run
java -jar target/app.jar这两条命令能把服务跑起来,但生产环境不会只问“能不能启动”。它还会继续追问:
启动时到底加载了哪些配置?
profile 是谁指定的?
日志为什么没有写到文件?
异常响应能不能被前端稳定解析?
健康检查能不能区分进程活着和服务可接流量?
打出来的 jar 为什么能 java -jar?
上线后配置、日志、监控、回滚边界谁负责?这篇文章按一个后端服务从开发到上线的基础链路来讲 Spring Boot。少讲口号,多讲命令、配置、现象、原因和验证方式。
本文范围
本文覆盖:
- Spring Boot 应用启动入口、启动流程和自动配置边界。
SpringApplication.run到ApplicationReadyEvent的完整主链路。Environment、ConfigData、PropertySource、Profile 和Binder如何共同决定最终配置。ApplicationContext、BeanDefinition、refresh、BeanFactoryPostProcessor、BeanPostProcessor在启动期各自负责什么。- Maven 最小工程、启动、打包、可执行 jar 验证。
- 外部配置、Profile、环境变量、命令行参数的使用方式。
- 日志配置、滚动策略、结构化日志和排障入口。
- 基础异常处理、ProblemDetail、统一错误响应边界。
- Actuator 健康检查、liveness/readiness、生产暴露策略。
- 自动配置条件报告、starter 当前注册方式、Bean 冲突和配置绑定失败的反推路径。
- 从开发机到上线前的检查清单、排障手册和落地工程深水区。
本文不展开:
- systemd、Nginx、Docker、K8S 的完整部署运维长篇。
- Spring MVC Controller、参数绑定、接口版本和过滤器/拦截器细节;这些放到 Spring MVC / Web 接口开发文章。
- Spring Cloud Alibaba、Nacos、Gateway、Sentinel 等微服务治理。
- 数据库、缓存、MQ 的业务建模和性能优化。
版本边界先说清楚:写作日期是 2026-07-10。本文以 Spring Boot 4.1.0、Spring Framework 7.0.8、Java 17+ 为基准。Spring Boot 4.1 官方系统要求里,Java 最低版本是 17,并兼容到 Java 26;如果你的项目还在 Spring Boot 2.x / Java 8,配置思想可以参考,但依赖坐标、Jakarta 包名、JVM 参数、starter 拆分和 Actuator 行为必须回到对应版本文档确认。
前置条件
开发机先确认这些命令可用:
java -version
mvn -version
curl --version建议基线:
JDK:17 或更高
Maven:3.6.3 或更高
Spring Boot:4.1.0
系统:Windows、macOS、Linux 都可以开发;上线前要在目标运行环境再验证一遍企业内网或离线环境要提前准备:
内网 Maven 仓库或制品库
JDK 安装包
构建机 settings.xml
依赖和 Maven 插件缓存
上线环境的外部配置文件或配置中心不要默认每台机器都能访问公网仓库。Spring Boot 项目本身很好启动,真正拖慢交付的往往是依赖没闭环、配置没人管、日志找不到。
一、先建一个能跑的最小工程
如果你用 Spring Initializr 创建项目,选择 Spring Web MVC、Actuator、Validation 即可。这里直接给 Maven 版本,便于你看清楚依赖边界。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0</version>
<name>order-service</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>这里要注意两点。
第一,Spring Boot 4.x 可以显式选择 spring-boot-starter-webmvc 来表达 Servlet MVC 应用;官方 Web 总览里仍把 spring-boot-starter-web 作为多数 Web 应用快速入口。团队要统一基线,不要在同一个服务里随手混入 MVC、WebFlux、gRPC、Servlet starter。升级老项目时也不要只改版本号,要按官方迁移说明确认 starter、Jakarta 包名、Servlet 基线和第三方库兼容性。
第二,spring-boot-maven-plugin 不是摆设。它负责把 Maven 普通 jar 重新打成 Spring Boot 可执行归档,否则你很可能会遇到:
no main manifest attribute
Unable to access jarfile
ClassNotFoundException启动类保持朴素:
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}先加一个最小接口,验证服务真的能处理 HTTP 请求:
package com.example.order;
import java.time.Instant;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class PingController {
@GetMapping("/ping")
Map<String, Object> ping() {
return Map.of(
"status", "ok",
"time", Instant.now().toString()
);
}
}运行:
mvn spring-boot:run另开一个终端验证:
curl -i http://127.0.0.1:8080/ping你应该看到 200 响应和一段 JSON。这里 200 比页面漂亮不漂亮更重要,因为它证明了:嵌入式 Servlet 容器启动了,DispatcherServlet 注册了,Controller 被扫描到了。
如果访问失败,先查三件事:
java -version
ss -lntp | grep ':8080'
curl -v http://127.0.0.1:8080/pingWindows 上没有 ss 时,可以用:
netstat -ano | findstr :8080二、启动流程先抓主线
Spring Boot 启动不是一句 run 就结束了。日常排障不需要背完整源码,但要知道几条关键链路。
这张图用来排障很够了。再往源码主线压一层,可以这样记:
SpringApplication.run
-> 推断 WebApplicationType
-> 准备 Environment
-> 运行 ConfigData 加载 application.yml / profile 配置
-> 创建 ApplicationContext
-> 加载启动类和自动配置 BeanDefinition
-> refresh()
-> 执行 BeanFactoryPostProcessor
-> 注册 BeanPostProcessor
-> 实例化非懒加载单例 Bean
-> 启动内嵌 WebServer
-> 发布 ApplicationStartedEvent
-> 调用 ApplicationRunner / CommandLineRunner
-> 发布 ApplicationReadyEvent这条链路里有几个对象要分清。
WebApplicationType 决定应用类型。classpath 上有 Servlet MVC 相关类时通常是 SERVLET,WebFlux 应用是 REACTIVE,纯任务进程可以是 NONE。如果一个批处理任务误引入 Web starter,进程会启动 Web 容器并占端口;如果一个 Web 服务被强行设成 NONE,Controller 再正确也不会对外提供 HTTP。
需要强制非 Web 应用时,可以这样写:
package com.example.order;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
public class OrderJobApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(OrderApplication.class)
.web(WebApplicationType.NONE)
.run(args);
}
}Environment 负责保存最终配置视图,里面是一组有顺序的 PropertySource。ConfigData 负责把 application.yml、application-prod.yml、外部配置目录、spring.config.import 等配置数据加载进来。后面的 Binder 再把这些字符串值绑定到 @ConfigurationProperties 对象、自动配置属性类和部分框架组件里。
ApplicationContext 是容器本体。启动类、扫描出来的组件、自动配置导入的类,最终都会变成 BeanDefinition。refresh() 是容器真正开机的主方法:先让 BeanFactoryPostProcessor 有机会改 Bean 定义,再注册 BeanPostProcessor,最后创建非懒加载单例 Bean。很多“Bean 冲突”“事务代理没生效”“配置类没被识别”的问题,都发生在这个阶段。
内嵌 WebServer 不是在 main 方法里直接 new 出来的。Servlet 应用通常由 ServletWebServerApplicationContext 在 refresh() 期间创建和启动 WebServer,然后发布 WebServerInitializedEvent。这也是为什么端口占用会导致上下文启动失败,而不是等到第一个请求进来才失败。
ApplicationRunner 和 CommandLineRunner 在 ApplicationStartedEvent 之后、ApplicationReadyEvent 之前执行。它们适合做启动参数解析、必须完成的本地初始化和短任务,不适合放无上限远程调用、全量缓存预热和死循环。只要 runner 卡住,readiness 就不会进入可接流量状态。
例如日志里报配置没加载,要优先看 Environment 和配置文件顺序。Controller 404,要看组件扫描和 Web MVC 自动配置。启动很慢,要看 Bean 初始化、数据库连接、启动任务。健康检查一直不 ready,要看 ApplicationReadyEvent 前后做了什么。
源码定位时不要在 IDE 里无头苍蝇一样全局搜。先按阶段定位这些入口:
| 现象 | 先看入口 | 你要确认什么 |
|---|---|---|
| 启动参数、Banner、事件监听顺序异常 | SpringApplication#run、SpringApplicationRunListeners | 参数、监听器、启动事件是否按预期执行 |
application-prod.yml 没生效 | ConfigDataEnvironmentPostProcessor、ConfigDataEnvironment | 配置文件、profile、spring.config.import 是否进入 Environment |
@ConfigurationProperties 绑定成默认值 | Binder、ConfigurationPropertiesBindingPostProcessor | 属性前缀、类型转换、松散绑定和校验是否命中 |
| 自动配置没生效或抢 Bean | AutoConfigurationImportSelector、AutoConfiguration.imports、ConditionEvaluationReport | 自动配置类是否导入,条件为什么匹配或不匹配 |
| Web 端口启动失败 | ServletWebServerApplicationContext#onRefresh、createWebServer | 内嵌容器是否创建、端口是否占用、Servlet 组件是否注册 |
| Runner 卡住 readiness | callRunners、ApplicationRunner、CommandLineRunner | 启动任务是否有远程调用、死循环或全量预热 |
验证方式:启动时加 --debug 看条件报告;有 Actuator 时打开 conditions 和 startup,把“猜测哪个自动配置没生效”变成“哪个条件没有匹配”。如果你连入口类都找不到,先确认当前依赖是不是 Boot 4.x;老项目里 spring.factories 和当前 AutoConfiguration.imports 的差异会直接影响排查路径。
@SpringBootApplication 是组合注解,主要包含三件事:
@SpringBootConfiguration:声明启动类也是配置类
@EnableAutoConfiguration:按 classpath 和条件注解导入自动配置
@ComponentScan:从启动类所在包及子包扫描组件项目里最常见的扫描坑是:启动类放在了过深的子包。
推荐包结构:
com.example.order
OrderApplication
controller
service
repository
config不要这样:
com.example.order.bootstrap
OrderApplication
com.example.order.controller
OrderController如果启动类在 bootstrap 子包,默认扫描不到兄弟包 controller。你可以用 scanBasePackages 修,但更推荐把启动类放到根包。
自动配置的核心判断可以这样理解:
classpath 上有相关依赖
配置项满足条件
用户没有自己定义同类 Bean
自动配置类才会生效Spring Boot 4.x 的自动配置导入主要看 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。老资料里经常提 spring.factories,这对理解历史有帮助,但写新 starter 或排查 Boot 4 项目时,要以当前官方机制为准。
一个最小自动配置类大概长这样:
package com.example.order.starter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(OrderClient.class)
public class OrderClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
OrderClient orderClient() {
return new OrderClient();
}
}对应的注册文件是:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容只写类名:
com.example.order.starter.OrderClientAutoConfiguration这几个条件注解决定了自动配置的边界:
@ConditionalOnClass:classpath 有某个类才启用
@ConditionalOnMissingBean:用户没有自己声明同类 Bean 才兜底
@ConditionalOnProperty:配置项满足指定值才启用
@ConditionalOnWebApplication:只在 Web 应用类型下启用所以排查自动配置,不能只问“starter 引了没有”,还要问:依赖类在不在、配置值是什么、用户是否声明了自己的 Bean、当前应用类型是不是它期望的类型。
如果想看自动配置报告,临时加:
java -jar target/order-service-1.0.0.jar --debug或者在配置里打开:
debug: true启动日志会输出哪些自动配置命中、哪些没命中。生产不要长期打开,日志会很吵。
引入 Actuator 后,也可以通过条件报告看自动配置命中原因:
curl -s http://127.0.0.1:8080/actuator/conditions这个端点可能暴露类名、配置名和依赖结构,生产只适合放在受控内网或排障通道里。
三、配置体系要先定规矩
Spring Boot 的配置很强,也很容易乱。先记一个原则:同一个配置只能有一个最终负责人。
最小 application.yml:
spring:
application:
name: order-service
server:
port: 8080
shutdown: graceful
logging:
level:
root: INFO
com.example.order: INFO
file:
name: ${LOG_DIR:./logs}/order-service.log
management:
endpoints:
web:
exposure:
include: health,info,metrics,loggers
endpoint:
health:
probes:
enabled: true
show-details: never这里写了几类基础配置:
spring.application.name:服务名,日志、指标、链路追踪都会用
server.port:HTTP 端口
server.shutdown:开启优雅停机
logging.file.name:日志文件路径
management.endpoints.web.exposure.include:明确暴露哪些 Actuator 端点
management.endpoint.health.probes.enabled:开启 liveness / readiness 探针配置进入应用,大体分三步:
ConfigData 找到配置文件
PropertySource 按优先级放入 Environment
Binder 把最终值绑定到对象把配置链路画出来,会更容易定位“为什么我改了配置但没生效”:
这张图的排障用法很直接:先看配置有没有被 ConfigData 加载,再看是否被后面的 PropertySource 覆盖,最后看 Binder 是否绑定到目标对象。/actuator/env 和 /actuator/configprops 正是用来验证这两段的。
常见覆盖顺序要记住这些高频项:
jar 内 application.yml
jar 内 application-{profile}.yml
jar 外 application.yml
jar 外 application-{profile}.yml
环境变量
Java System properties
SPRING_APPLICATION_JSON
命令行参数
测试属性后面的来源可以覆盖前面的来源。也就是说,application-prod.yml 写了 server.port=8080,启动命令里传了 --server.port=9000,最终端口就是 9000。这个设计方便临时验证,也最容易制造“配置漂移”。生产上关键配置不要长期靠命令行覆盖,除非发布系统能记录审计。
外部配置建议优先使用追加路径,而不是替换默认搜索路径:
java -jar target/order-service-1.0.0.jar \
--spring.profiles.active=prod \
--spring.config.additional-location=optional:file:./config/spring.config.location 会改变搜索位置,容易把 jar 内默认配置一起绕开;spring.config.additional-location 更适合在默认配置之外补生产差异。路径不存在但允许启动时,加 optional:,否则 Spring Boot 会因为找不到配置位置直接失败。
容器平台和密钥目录可以用配置树:
spring:
config:
import: "optional:configtree:/run/secrets/"如果目录里有一个文件叫 db.password,文件内容就会成为 db.password 配置值。这个方式比把长密码塞进环境变量更容易做权限和轮转。
生产配置不要把密码写死:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}启动时传环境变量:
export DB_URL='jdbc:mysql://db.example.com:3306/order_db'
export DB_USERNAME='order_app'
export DB_PASSWORD='change-me'
java -jar target/order-service-1.0.0.jar --spring.profiles.active=prod如果服务由 systemd、Docker、K8S 启动,环境变量要写到对应平台的配置里,不能指望它继承你当前终端里的 export。
读取配置时,少量值可以用 @Value:
@Value("${spring.application.name}")
private String applicationName;一组配置推荐用 @ConfigurationProperties:
package com.example.order.config;
import jakarta.validation.constraints.Min;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "order.feature")
public record OrderFeatureProperties(
boolean idempotencyEnabled,
@Min(1)
int submitTimeoutSeconds
) {
}注册:
package com.example.order.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(OrderFeatureProperties.class)
class OrderFeatureConfig {
}配置:
order:
feature:
idempotency-enabled: true
submit-timeout-seconds: 3Binder 绑定时支持宽松命名。下面这些名字会尽量绑定到同一个属性:
order.feature.submit-timeout-seconds
order.feature.submitTimeoutSeconds
ORDER_FEATURE_SUBMIT_TIMEOUT_SECONDS但不要因此放纵命名。仓库里的 YAML 统一用 kebab-case,环境变量统一用大写下划线,减少跨平台和团队协作时的误解。配置对象加上校验后,非法值会在启动期失败,比带着错误配置跑到业务请求里再炸要好排查得多。
验证配置是否绑定成功,可以写一个很小的测试:
package com.example.order.config;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(properties = {
"order.feature.idempotency-enabled=true",
"order.feature.submit-timeout-seconds=5"
})
class OrderFeaturePropertiesTest {
@Autowired
OrderFeatureProperties properties;
@Test
void bindProperties() {
assertThat(properties.idempotencyEnabled()).isTrue();
assertThat(properties.submitTimeoutSeconds()).isEqualTo(5);
}
}运行:
mvn -q -Dtest=OrderFeaturePropertiesTest test配置未生效时,按这个顺序查:
ps -ef | grep '[j]ava'
grep -i "profile" logs/order-service.log
env | grep -E 'SPRING|DB|LOG|ORDER'如果启用了 Actuator,并且端点只在内网或本机可访问,可以临时看:
curl -s http://127.0.0.1:8080/actuator/env | head
curl -s http://127.0.0.1:8080/actuator/configprops | head/actuator/env 和 /actuator/configprops 可能暴露敏感配置、类名和配置结构,生产不要对公网开放。排障时看完就收回暴露范围。
四、Profile 只解决环境差异
Profile 不是功能开关仓库。它只适合表达环境差异,比如 dev、test、prod。
公共配置:
spring:
application:
name: order-service
server:
port: 8080开发配置 application-dev.yml:
logging:
level:
com.example.order: DEBUG
order:
feature:
idempotency-enabled: false生产配置 application-prod.yml:
logging:
level:
root: INFO
com.example.order: INFO
order:
feature:
idempotency-enabled: true启动开发环境:
mvn spring-boot:run -Dspring-boot.run.profiles=dev启动生产环境:
java -jar target/order-service-1.0.0.jar --spring.profiles.active=prod也可以用环境变量:
SPRING_PROFILES_ACTIVE=prod java -jar target/order-service-1.0.0.jar验证 profile:
curl -s http://127.0.0.1:8080/actuator/env | grep -i profiles没有暴露 env 端点时,看启动日志即可,Spring Boot 会打印 active profiles。
这里有几个生产坑:
不要在 application.yml 写 spring.profiles.active=prod 后提交仓库
不要让一个服务同时激活 dev,prod 两个 profile
不要用 profile 表达灰度用户、按钮开关、租户差异
不要让配置中心、本地文件、环境变量三方都能改同一个关键配置功能开关更适合放配置中心、数据库或专门的 feature flag 系统,并且要有审计和回滚。
五、日志要服务排障
Spring Boot 默认使用 Logback,开发阶段控制台日志已经够用。生产要补三件事:
日志级别明确
文件滚动明确
请求 traceId 明确基础配置:
logging:
level:
root: INFO
com.example.order: INFO
org.springframework.web: WARN
file:
name: ${LOG_DIR:./logs}/order-service.log
logback:
rollingpolicy:
max-file-size: 100MB
max-history: 14
total-size-cap: 2GB本机验证:
mkdir -p logs
mvn spring-boot:run
tail -f logs/order-service.log如果没有文件日志,先查:
ls -ld logs
env | grep LOG_DIR
grep -i "logback" logs/order-service.log线上排障经常需要临时调日志级别。Actuator 暴露 loggers 后可以这样看:
curl -s http://127.0.0.1:8080/actuator/loggers/com.example.order临时改成 DEBUG:
curl -X POST http://127.0.0.1:8080/actuator/loggers/com.example.order \
-H 'Content-Type: application/json' \
-d '{"configuredLevel":"DEBUG"}'恢复:
curl -X POST http://127.0.0.1:8080/actuator/loggers/com.example.order \
-H 'Content-Type: application/json' \
-d '{"configuredLevel":"INFO"}'这里要注意,loggers 端点不应该对公网开放。它能改变运行时行为,至少要限制网络、鉴权和审计。
如果团队做集中日志,Spring Boot 4 支持结构化日志配置。示例:
logging:
structured:
format:
console: ecs是否启用结构化日志,要看日志平台是否能解析。不要为了“看起来先进”把所有团队工具链打乱。
生产日志建议:
INFO 记录关键业务动作和外部调用摘要
WARN 记录可恢复异常、降级、重试
ERROR 记录真正需要处理的失败,必须带错误码、traceId、关键上下文
不要在日志里打印密码、token、身份证、手机号全量、银行卡号
不要在高 QPS 接口里打印大对象 JSON六、异常处理先稳定协议
异常处理不是为了把所有错误都变成 200。接口错误应该让调用方稳定理解:
HTTP 状态码表达协议层结果
业务错误码表达业务原因
message 给人看
traceId 用来排障Spring Framework 已经提供 ProblemDetail 和 ErrorResponse 体系,适合做标准错误响应。先打开 MVC Problem Details:
spring:
mvc:
problemdetails:
enabled: true定义业务异常:
package com.example.order.error;
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String code() {
return code;
}
}全局处理:
package com.example.order.error;
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
ProblemDetail handleBusiness(BusinessException ex) {
ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
detail.setTitle("业务规则校验失败");
detail.setType(URI.create("https://example.com/problems/business-error"));
detail.setProperty("code", ex.code());
return detail;
}
}测试:
curl -i http://127.0.0.1:8080/some-error你要看到的是稳定结构,而不是一整页 HTML Whitelabel Error Page。
这里先讲基础边界:Spring Boot 层负责打开错误响应能力、统一日志和全局异常入口。更细的 Controller 参数校验、MethodArgumentNotValidException、HandlerMethodValidationException、接口错误码和版本兼容,放到 Spring MVC / Web 接口开发文章里展开。
生产建议:
404、405、415 不要伪装成业务成功
业务异常不要打印满屏堆栈,记录摘要和 traceId 即可
未知异常必须 ERROR 并保留堆栈
错误响应不要暴露 SQL、内网地址、类名、堆栈和配置路径七、Actuator 要会用也要会收
Actuator 是生产可观测的入口之一,不是“把所有端点暴露出来给人看”。
端点暴露要先画清边界,再决定开哪些:
health、info、metrics 和 prometheus 可以作为稳定观测入口;env、beans、configprops、mappings、conditions 更像手术刀,适合受控排障,不适合长期裸露。
依赖已经在前面加过:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>推荐先显式配置:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers
endpoint:
health:
probes:
enabled: true
show-details: never
info:
env:
enabled: true
info:
app:
name: ${spring.application.name}
version: ${APP_VERSION:local}验证:
curl -i http://127.0.0.1:8080/actuator/health
curl -i http://127.0.0.1:8080/actuator/info
curl -i http://127.0.0.1:8080/actuator/health/liveness
curl -i http://127.0.0.1:8080/actuator/health/readiness正常时:
{"status":"UP"}理解 liveness 和 readiness:
liveness:进程是不是活着,通常不要依赖外部数据库
readiness:服务是否可以接流量,可以考虑关键依赖状态这个区别非常重要。假如 liveness 因数据库短暂抖动返回 DOWN,容器平台可能不断重启应用,结果把本来能恢复的数据库抖动扩大成服务雪崩。
排查 Spring Boot 自身问题时,这几个端点最常用:
conditions:看自动配置命中和未命中的条件
configprops:看 @ConfigurationProperties 绑定后的结构
env:看 Environment 里的最终配置和来源
beans:看容器里最终有哪些 Bean
mappings:看请求路径是否真的注册
loggers:看和临时修改日志级别
metrics / prometheus:看运行指标临时打开诊断端点时,要显式加到暴露列表:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers,conditions,configprops,mappings验证:
curl -s http://127.0.0.1:8080/actuator/conditions | head
curl -s http://127.0.0.1:8080/actuator/configprops | head
curl -s http://127.0.0.1:8080/actuator/mappings | head如果管理端口和业务端口分开,探针可能只证明管理端口活着,不代表业务端口能接连接。需要让 liveness/readiness 也挂到主端口时,可以配置:
management:
endpoint:
health:
probes:
add-additional-paths: true这样主端口会额外提供 /livez 和 /readyz。这不是必须项,但在管理端口与业务端口隔离的环境里很有价值。
生产暴露策略:
公网:不要暴露敏感 Actuator 端点
内网:按需暴露 health、info、metrics、prometheus
本机:需要排障时临时访问 env、beans、configprops、loggers
权限:通过网关、网络策略或 Spring Security 控制
审计:运行时修改日志级别要留记录Boot 4 里要区分两个动作:exposure 决定端点是否通过 HTTP/JMX 暴露,access 决定端点允许什么级别的访问。只写 management.endpoints.web.exposure.include 只是把门挂出来,不等于把访问权限治理好了。需要白名单式排障时,可以把默认 access 收紧,再逐个放开:
management:
endpoints:
access:
default: none
web:
exposure:
include: health,info,loggers
endpoint:
health:
access: read-only
info:
access: read-only
loggers:
access: read-onlyloggers 如果要临时改日志级别,需要确认当前 access 是否允许写操作;排障结束后要把它收回。更强的全局兜底是 management.endpoints.access.max-permitted=read-only,它会把端点访问上限压成只读,适合生产默认配置。常见坑是只在网关上挡住 Actuator,却让应用内端点本身仍处于宽松状态;一旦管理端口、内网 ACL 或 sidecar 配置变动,风险会被放大。
如果 /actuator/health 404,按顺序查:
mvn -q dependency:tree | grep actuator
grep -R "management.endpoints" -n src/main/resources
curl -i http://127.0.0.1:8080/actuator常见原因:
没有引入 actuator starter
配置了 context-path 或 management base-path
端点没有暴露
被安全配置拦截
访问路径经过 Nginx 后被改写八、打包和可执行 jar 验证
构建:
mvn -B clean verify如果测试在流水线前面已经跑过,临时打包:
mvn -B clean package -DskipTests看产物:
ls -lh target/*.jar验证是不是 Spring Boot 可执行 jar:
jar tf target/order-service-1.0.0.jar | head -n 30正常能看到:
META-INF/
BOOT-INF/classes/
BOOT-INF/lib/
org/springframework/boot/loader/看启动清单:
unzip -p target/order-service-1.0.0.jar META-INF/MANIFEST.MF正常会看到类似:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.order.OrderApplication不同版本 loader 类名可能不同,不要死背类名。关键是:Main-Class 指向 Spring Boot loader,Start-Class 指向你的启动类。
这个 jar 能 java -jar,靠的不是 JDK 原生 classpath 魔法,而是 Spring Boot loader。大概过程是:
JVM 读取 MANIFEST.MF
执行 Main-Class 指向的 JarLauncher
JarLauncher 建立能读取嵌套 jar 的 ClassLoader
加载 BOOT-INF/classes 和 BOOT-INF/lib
再调用 Start-Class 里的 main 方法所以 Spring Boot 可执行 jar 和普通 jar 有几个边界:
应用 class 在 BOOT-INF/classes
依赖 jar 在 BOOT-INF/lib
嵌套 jar 不需要先解压到磁盘
部分手写 ClassLoader 或 SystemClassLoader 取资源的老代码可能不兼容如果某个三方库在线上 java -jar 方式下找不到资源,而 IDE 里正常,优先怀疑它是否绕开了线程上下文类加载器。大多数框架会用 Thread.currentThread().getContextClassLoader(),老代码如果直接依赖系统类加载器,就可能在嵌套 jar 场景翻车。
本地按生产方式跑一次:
java -Xms256m -Xmx256m \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Asia/Shanghai \
-jar target/order-service-1.0.0.jar \
--spring.profiles.active=prod \
--server.port=8080参数位置别放错:
JVM 参数放在 -jar 前面
Spring Boot 应用参数放在 jar 后面验活:
curl -fsS http://127.0.0.1:8080/ping
curl -fsS http://127.0.0.1:8080/actuator/health记录校验值:
sha256sum target/order-service-1.0.0.jar上线后排查“线上跑的是不是这个包”,这个值比口头确认可靠得多。
九、从开发到上线的基础链路
一个 Spring Boot 服务,最小上线链路可以收敛成这样:
本地启动 -> 单元测试 -> 打包 -> jar 验证 -> 外部配置 -> 生产参数 -> 健康检查 -> 日志确认 -> 版本确认 -> 回滚预案用一张图把这条链路变成发布检查顺序:
这张图的价值在于把“能启动”拆成可验证证据:包结构、启动参数、外部配置、端口、健康检查、日志、版本和回滚预案都要能被命令证明。
本地启动:
mvn spring-boot:run -Dspring-boot.run.profiles=dev测试:
mvn -B test打包:
mvn -B clean verify生产参数模板:
JAVA_OPTS="
-Xms512m
-Xmx512m
-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/order-service/logs
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/opt/apps/order-service/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m
"
APP_OPTS="
--spring.profiles.active=prod
--spring.config.additional-location=file:/opt/apps/order-service/config/
--logging.file.name=/opt/apps/order-service/logs/order-service.log
"
java $JAVA_OPTS -jar /opt/apps/order-service/current/order-service.jar $APP_OPTS这段命令没有展开 systemd 或 Docker,只表达后端开发必须交清楚的运行边界:
JVM 内存多少
时区和编码是什么
OOM 现场放哪里
GC 日志放哪里
profile 是什么
外部配置从哪里读
应用日志写哪里这些边界没讲清楚,后面交给任何部署平台都会出问题。
可运行实验矩阵
Spring Boot 的很多问题不能靠背概念解决,最好用一组小实验把“配置、启动、自动配置、健康检查、打包”跑出证据。下面这组实验建议放进团队样板工程,升级 Spring Boot、换 JDK、改 starter 或改发布脚本时都跑一遍。
| 实验 | 命令或配置 | 你应该看到什么 | 排查价值 |
|---|---|---|---|
| Profile 覆盖 | java -jar app.jar --spring.profiles.active=prod | 日志出现 active profile,配置切到 prod | 验证环境选择不是靠猜 |
| 命令行覆盖端口 | java -jar app.jar --server.port=9090 | 9090 监听,8080 不再监听 | 验证参数优先级 |
| 外部配置追加 | --spring.config.additional-location=optional:file:./config/ | 外部 YAML 覆盖 jar 内默认值 | 验证发布配置路径 |
| 配置树密钥 | spring.config.import=optional:configtree:/run/secrets/ | 文件名变配置键,文件内容变配置值 | 验证密钥挂载方式 |
| 自动配置条件 | java -jar app.jar --debug 或 /actuator/conditions | 能看到命中和未命中的自动配置条件 | 反推 starter 为什么生效或失效 |
| 配置绑定 | /actuator/configprops | @ConfigurationProperties 的最终结构可见 | 反推 Binder 和环境变量命名问题 |
| WebServer 启动 | `ss -lntp | grep ':8080'` | Java 进程监听业务端口 |
| readiness 语义 | /actuator/health/readiness | runner 完成后才进入可接流量状态 | 验证启动任务是否卡流量 |
| liveness 附加路径 | management.endpoint.health.probes.add-additional-paths=true | 主端口出现 /livez、/readyz | 避免只验证管理端口 |
| 可执行 jar | unzip -p app.jar META-INF/MANIFEST.MF | Main-Class 是 loader,Start-Class 是业务启动类 | 验证 java -jar 产物正确 |
| 日志滚动 | 压测或临时调 DEBUG 后看 logs/ | 文件按大小和历史周期滚动 | 防止日志爆盘 |
这些实验不要求每次都全量人工执行。更实际的做法是:CI 固定跑 mvn -B clean verify 和少量 Mock/集成测试;发布前由脚本跑 jar manifest、健康检查、端口监听、配置路径和 Actuator 基础端点;升级前再额外打开 --debug 和 startup/conditions 做差异检查。
十、常见排障手册
1. 启动类找不到
现象:
no main manifest attribute
ClassNotFoundException检查:
unzip -p target/order-service-1.0.0.jar META-INF/MANIFEST.MF
jar tf target/order-service-1.0.0.jar | head原因:
没有执行 spring-boot-maven-plugin repackage
拿错了 .original jar
启动类包路径被改坏修复:
mvn -B clean package
java -jar target/order-service-1.0.0.jar2. JDK 版本不匹配
现象:
UnsupportedClassVersionError检查:
java -version
mvn -version原因:编译用的 JDK 比运行环境高,或者 Spring Boot 版本要求的最低 Java 版本没有满足。
修复:统一构建机、开发机、运行机的 Java 基线。Spring Boot 4.1 至少 Java 17。
3. 端口被占用
现象:
Port 8080 was already in use检查:
ss -lntp | grep ':8080'
lsof -i :8080修复:
java -jar target/order-service-1.0.0.jar --server.port=8081或者停掉旧进程。生产上不要随手换端口,端口会影响 Nginx、健康检查、防火墙和发布脚本。
4. Profile 不生效
现象:
连错数据库
日志级别不对
本地配置覆盖了生产配置检查:
ps -ef | grep '[j]ava'
grep -i "active profiles" logs/order-service.log
env | grep SPRING_PROFILES_ACTIVE修复:
java -jar target/order-service-1.0.0.jar --spring.profiles.active=prod如果由部署平台启动,要去平台配置里查变量,不要只看当前终端。
5. 配置读取不到
现象:
Could not resolve placeholder
Failed to configure a DataSource检查:
grep -R "DB_URL\|spring.config" -n .
env | grep DB_修复思路:
确认外部配置文件路径
确认环境变量名称
确认 profile 文件是否被激活
确认配置中心是否覆盖
确认配置项是否拼错6. Actuator 健康检查 DOWN
检查:
curl -i http://127.0.0.1:8080/actuator/health如果显示数据库 DOWN,先不要重启应用,先查依赖:
nc -vz db.example.com 3306如果 readiness DOWN、liveness UP,说明进程活着但暂时不适合接流量。这个状态比简单重启更有信息量。
7. 日志找不到
检查:
ls -ld logs
find . -name "*.log" -type f
env | grep LOG_DIR原因:
日志目录不存在
运行用户没有写权限
LOG_DIR 没传进去
Logback 配置没有加载
服务启动早期已经失败修复:先让日志写到一个明确目录,再接入日志采集。
8. 自动配置没有生效
现象:
引了 starter,但 Bean 没有出现
用户自定义 Bean 没有覆盖默认 Bean
配置项改了,行为没有变化先开条件报告:
java -jar target/order-service-1.0.0.jar --debug
curl -s http://127.0.0.1:8080/actuator/conditions | head再看依赖:
mvn -q dependency:tree常见原因:
classpath 缺少触发自动配置的类
配置项名称拼错或 profile 没激活
用户已经声明了同类型 Bean,@ConditionalOnMissingBean 不再生效
WebApplicationType 不符合自动配置条件
自定义 starter 还停留在旧的 spring.factories 写法修复思路:
先用 conditions 找未命中的条件
再用 configprops/env 查最终配置值
最后看 beans 确认容器里到底有什么9. Bean 冲突或覆盖失败
现象:
expected single matching bean but found 2
The bean could not be registered
业务 Bean 被框架默认 Bean 覆盖检查:
curl -s http://127.0.0.1:8080/actuator/beans | head
mvn -q dependency:tree | grep starter原因反推:
两个 starter 同时提供了同类 Bean
业务代码和自动配置都声明了同名 Bean
@Primary、@Qualifier、@ConditionalOnMissingBean 边界没有定清楚
依赖升级后自动配置类拆分,原来的排除规则失效生产建议:
业务核心 Bean 不要依赖默认覆盖顺序
公共 starter 必须给 Bean 命名、条件和配置开关
不要全局打开 allow-bean-definition-overriding 来掩盖冲突
升级后先跑 conditions 和 beans 差异检查10. 启动慢或 readiness 一直不通过
现象:
日志显示 Tomcat 已启动,但 readiness 仍然 DOWN
ApplicationReadyEvent 迟迟没有出现
服务上线后长时间不接流量检查启动耗时:
grep -iE "started|ready|runner|initialized" logs/order-service.log
curl -i http://127.0.0.1:8080/actuator/health/readiness重点看这些位置:
Bean 初始化是否连外部数据库、Redis、MQ
ApplicationRunner / CommandLineRunner 是否同步阻塞
@PostConstruct 是否做了长任务
缓存预热是否没有超时和降级
数据库连接池初始化是否等太久修复原则:
必须完成才能接流量的任务,失败就 fail fast
可以延后完成的任务,放后台并让 readiness 尽快表达真实状态
所有外部调用必须有超时
启动期任务要有耗时日志11. 配置绑定失败
现象:
BindException
ConfigurationPropertiesBindException
Could not bind properties检查:
env | grep -E 'ORDER|SPRING'
curl -s http://127.0.0.1:8080/actuator/configprops | head
curl -s http://127.0.0.1:8080/actuator/env | grep -i "order.feature"常见原因:
环境变量命名没有按大写下划线
YAML 缩进错误
数字、Duration、DataSize 单位写错
record 构造参数和配置名对不上
校验注解拒绝了非法值生产建议:配置类全部用 @ConfigurationProperties 管起来,关键配置加校验,测试里覆盖最小可用值和非法值。不要让 @Value 散落在业务代码里,否则配置治理会变成全文搜索。
十一、生产化建议
版本治理:
明确 Spring Boot、Spring Framework、JDK、Maven 插件版本
升级前先读 release notes 和迁移指南
不要让父 pom、BOM、starter 版本互相打架配置治理:
生产密码不进 Git
每个关键配置只有一个最终来源
profile 只表达环境,不表达业务开关
配置变更要可审计、可回滚日志治理:
必须有 traceId 或 requestId
必须有滚动策略和保留策略
禁止打印敏感数据
ERROR 日志要能指向处理动作健康检查治理:
liveness 不要绑定外部依赖
readiness 可以反映关键依赖是否可用
健康检查路径要被发布平台、负载均衡和监控统一使用启动治理:
不要在 ApplicationRunner 里做不可控长任务
启动任务失败要明确 fail fast 还是降级
外部依赖连接超时要有上限打包治理:
CI 使用 mvn -B clean verify
产物记录版本、commit、sha256
上线前用 java -jar 本地验证一次
回滚包保留至少一个稳定版本十二、落地工程深水区
1. 配置来源太多会拖垮排障
一个服务如果同时允许本地 YAML、环境变量、命令行、配置中心、平台变量、启动脚本都改数据库地址,事故现场很难判断最终值是谁覆盖了谁。
落地约束:
配置中心负责业务动态配置
环境变量负责密钥和平台注入
外部 YAML 负责单机或离线交付
命令行参数只用于临时验证上线检查:
ps -ef | grep '[j]ava'
env | grep -E 'SPRING|DB|REDIS|MQ'
curl -s http://127.0.0.1:8080/actuator/health2. 启动任务会影响 readiness
有些项目启动时加载缓存、同步字典、扫描文件、调用远程接口。数据量小时没问题,生产数据一大,服务五分钟都不 ready。
判断标准:
必须完成才能接流量的任务:放启动流程,失败就 fail fast
可以稍后完成的任务:异步后台执行,readiness 不要长期阻塞
依赖外部服务的任务:必须有超时、重试上限和降级方案3. 日志增长会悄悄吃掉磁盘
开发时 DEBUG 很舒服,生产忘了关就是磁盘炸弹。
检查:
du -sh logs
find logs -name "*.log" -mtime +14配置约束:
logging:
logback:
rollingpolicy:
max-file-size: 100MB
max-history: 14
total-size-cap: 2GB4. 健康检查设计不好会制造雪崩
如果 liveness 依赖数据库,数据库一抖动,所有应用被平台重启;应用越重启,连接风暴越大。
建议:
liveness 只证明进程和核心线程活着
readiness 证明服务能接业务流量
数据库、Redis、MQ 的 DOWN 要影响 readiness,不要轻易影响 liveness5. 异常响应不稳定会拖垮前后端协作
前端、移动端、第三方调用方最怕同一个错误一会儿 HTML、一会儿 JSON、一会儿 200、一会儿 500。
约束:
协议错误用 HTTP 状态码
业务错误用稳定 code
响应里带 traceId
错误码要有归属人和文档6. 升级不是只改版本号
Spring Boot 2 到 3 已经从 javax.* 迁到 jakarta.*,Spring Boot 4 又有 starter 和模块边界变化。升级前要先跑:
mvn -B -DskipTests=false test
mvn -B dependency:tree重点查:
第三方 starter 是否兼容
Servlet 容器版本是否兼容
Jakarta 包名是否混用
自定义自动配置是否还生效
Actuator 端点和安全配置是否变化十三、命令速查
# 开发启动
mvn spring-boot:run
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# 测试和打包
mvn -B test
mvn -B clean verify
mvn -B clean package -DskipTests
# 可执行 jar 验证
jar tf target/order-service-1.0.0.jar | head
unzip -p target/order-service-1.0.0.jar META-INF/MANIFEST.MF
sha256sum target/order-service-1.0.0.jar
# 生产方式本机验证
java -Xms256m -Xmx256m -jar target/order-service-1.0.0.jar --spring.profiles.active=prod
# 端口和健康检查
ss -lntp | grep ':8080'
curl -fsS http://127.0.0.1:8080/ping
curl -fsS http://127.0.0.1:8080/actuator/health
curl -fsS http://127.0.0.1:8080/actuator/health/liveness
curl -fsS http://127.0.0.1:8080/actuator/health/readiness
# 自动配置和配置绑定诊断
java -jar target/order-service-1.0.0.jar --debug
curl -s http://127.0.0.1:8080/actuator/conditions | head
curl -s http://127.0.0.1:8080/actuator/configprops | head
curl -s http://127.0.0.1:8080/actuator/beans | head
mvn -q dependency:tree
# 日志
tail -f logs/order-service.log
grep -iE "exception|error|failed|timeout" logs/order-service.log
# 依赖闭环
mvn -B dependency:go-offline
mvn -o -B test十四、上线前检查清单
[ ] JDK 版本满足 Spring Boot 版本要求
[ ] 启动类放在根包,组件扫描范围正确
[ ] Maven 构建能在 CI 里 mvn -B clean verify 通过
[ ] jar 是 Spring Boot 可执行 jar,MANIFEST 正常
[ ] profile 激活方式明确
[ ] 外部配置路径明确,敏感信息不进 Git
[ ] `@ConfigurationProperties` 有命名规范、校验和测试
[ ] 日志文件、滚动策略、保留周期明确
[ ] Actuator health / info / metrics 暴露范围明确
[ ] Actuator exposure、access、max-permitted 的默认值和临时排障开关明确
[ ] liveness/readiness 语义明确
[ ] conditions / configprops / beans 只在受控排障通道开放
[ ] 全局异常响应是 JSON / ProblemDetail,不暴露堆栈
[ ] JVM 参数包含编码、时区、OOM dump、GC 日志
[ ] ApplicationRunner / CommandLineRunner 没有无上限阻塞任务
[ ] 版本号、commit、sha256 可追踪
[ ] 回滚包和回滚命令已准备十五、官方文档入口
| 主题 | 官方文档 |
|---|---|
| Spring Boot 系统要求 | Spring Boot System Requirements |
| SpringApplication 启动与事件 | SpringApplication |
| 外部配置 | Externalized Configuration |
| Profile | Profiles |
| 自动配置 | Auto-configuration |
| 自定义自动配置 | Creating Your Own Auto-configuration |
| Logging | Logging |
| Actuator Endpoints | Actuator Endpoints |
| Actuator Health | Health Information |
| Actuator Endpoint Access | Controlling Access to Endpoints |
| Maven 插件 | Spring Boot Maven Plugin |
| 可执行归档 | Executable Jar Format |
| 可执行 jar 启动器 | Launching Executable Jars |
| Spring MVC 自动配置 | Spring MVC Auto-configuration |
| Error Responses | Spring MVC Error Responses |
| Spring Boot 4 迁移 | Spring Boot 4.0 Migration Guide |
最后给一个很朴素的落地原则:Spring Boot 的价值不是少写几行 XML,而是让服务的启动、配置、日志、健康、异常和打包都有统一入口。只要这些入口可验证,后面接 systemd、Docker、K8S 或 CI/CD 才不会变成猜谜。
