Spring Boot 项目优雅上线:日志、监控、异常处理最佳实践
导读:代码写完了,测试也过了,但上线后才是真正考验的开始。半夜报警电话响起、用户反馈"页面白屏"、排查问题如大海捞针......这些场景你是否熟悉?
一个成熟的 Spring Boot 应用,不仅要有强大的业务逻辑,更必须具备可观测性(Observability)和健壮性 。本文将从结构化日志 、全链路监控 、全局异常处理三个维度,手把手教你打造"生产级"的 Spring Boot 应用,让上线不再是噩梦,而是从容的交付。
一、日志篇:从"乱码堆"到"数据金矿"
很多开发者的日志习惯是:System.out.println 或者随意打 logger.info("debug..." + obj)。这在开发环境没问题,但在生产环境就是灾难。
1.1 必须使用结构化日志 (JSON Format)
传统文本日志难以被机器自动分析。在生产环境,JSON 格式是标准。
✅ 最佳实践 :引入 logstash-logback-encoder,将日志输出为 JSON。
xml
<!-- pom.xml -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
xml
<!-- logback-spring.xml -->
<configuration>
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<loggerName/>
<threadName/>
<level/>
<message/>
<!-- 关键:添加追踪ID,方便链路追踪 -->
<mdc>
<excludeMdcKeyName>traceId</excludeMdcKeyName>
</mdc>
<pattern>
<pattern>{"traceId": "%X{traceId}"}</pattern>
</pattern>
<stackTrace/>
</providers>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON_CONSOLE"/>
</root>
</configuration>
收益 :ELK (Elasticsearch, Logstash, Kibana) 或 Loki 可以直接解析字段,支持按
traceId、level、className快速检索。
1.2 动态调整日志级别 (无需重启)
生产环境出现 Bug,需要临时开启 DEBUG 日志复现问题,难道要重新打包部署?NO!
✅ 最佳实践:利用 Spring Boot Actuator 动态调整。
xml
<!-- 引入 Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml
# application.yml
management:
endpoints:
web:
exposure:
include: loggers # 只暴露日志控制端点,注意安全!
endpoint:
loggers:
enabled: true
操作示例:
bash
# 查看当前级别
curl http://localhost:8080/actuator/loggers/com.example.service
# 动态调整为 DEBUG
curl -X POST http://localhost:8080/actuator/loggers/com.example.service \
-H 'Content-Type: application/json' \
-d '{"configuredLevel": "DEBUG"}'
1.3 敏感数据脱敏
日志中严禁出现明文密码、手机号、身份证、银行卡号。
✅ 最佳实践:自定义 Converter 或使用脱敏工具类。
typescript
// 简单示例:在打印对象前处理
public class SensitiveInfoUtils {
public static String maskPhone(String phone) {
if (phone == null) return null;
return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");
}
}
// 在 toString() 或 JSON 序列化器中调用
二、监控篇:从"黑盒"到"透明玻璃房"
没有监控的系统就是在裸奔。你需要知道:系统活著吗?慢吗?错了吗?
2.1 核心指标体系 (RED & USE 方法)
-
RED (Request, Error, Duration) :针对微服务接口。
- Rate (QPS):每秒请求数。
- Error Rate:错误率(4xx, 5xx)。
- Duration:响应时间(P99, P95, Avg)。
-
USE (Utilization, Saturation, Errors) :针对基础设施。
- CPU、内存、磁盘 IO、网络带宽。
2.2 集成 Prometheus + Grafana
Spring Boot Actuator 原生支持 Prometheus。
✅ 配置步骤:
-
引入依赖:
xml<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> -
开启端点:
yamlmanagement: endpoints: web: exposure: include: health,info,prometheus,metrics metrics: tags: application: ${spring.application.name} -
自定义业务指标:
java@Component public class CustomMetrics { private final Counter orderCounter; private final Timer orderTimer; public CustomMetrics(MeterRegistry registry) { this.orderCounter = Counter.builder("order.created.total") .description("Total orders created") .register(registry); this.orderTimer = Timer.builder("order.process.time") .description("Time to process an order") .register(registry); } public void recordOrder(long timeMs) { orderCounter.increment(); orderTimer.record(timeMs, TimeUnit.MILLISECONDS); } } -
可视化 :在 Grafana 中导入 Dashboard ID
6756(JVM Micrometer) 或自定义面板。
2.3 健康检查与就绪探针 (K8s 必备)
区分 Liveness (存活) 和 Readiness (就绪)。
yaml
management:
health:
livenessState:
enabled: true
readinessState:
enabled: true
endpoint:
health:
probes:
enabled: true
- /actuator/health/liveness:进程死锁、OOM 时返回 503,K8s 重启容器。
- /actuator/health/readiness:依赖(DB, Redis)未连通时返回 503,K8s 不转发流量。
三、异常处理篇:从"堆栈爆炸"到"友好提示"
直接抛出原始异常给前端是极其不专业的,既暴露系统细节,又让用户困惑。
3.1 统一全局异常处理器 (@RestControllerAdvice)
✅ 最佳实践 :定义统一的响应结构 Result<T> 和全局处理器。
typescript
// 统一响应体
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data, System.currentTimeMillis());
}
public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}
}
// 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常:{}", e.getMessage(), e); // 记录日志,但堆栈可选
return Result.fail(400, e.getMessage());
}
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(err ->
errors.put(err.getField(), err.getDefaultMessage())
);
log.warn("参数校验失败:{}", errors);
return Result.fail(400, "参数校验失败", errors); // 需重载 fail 方法
}
// 兜底:处理未知异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleUnknownException(Exception e, HttpServletRequest request) {
// 关键:记录完整堆栈和请求上下文
String traceId = MDC.get("traceId");
log.error("系统未知异常 [TraceId: {}] [URL: {}]", traceId, request.getRequestURI(), e);
return Result.fail(500, "系统繁忙,请稍后重试 (Error Code: " + generateErrorCode() + ")");
}
private String generateErrorCode() {
return UUID.randomUUID().toString().substring(0, 8);
}
}
3.2 自定义业务异常
不要直接用 RuntimeException,要语义化。
scala
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
// 使用
if (stock < 0) {
throw new BusinessException("库存不足");
}
3.3 异常告警策略
不是所有异常都要打电话报警。
- 忽略:参数校验错误(400),通常是用户问题。
- 警告:业务逻辑错误(如库存不足),记录日志即可。
- 严重 :空指针、数据库连接超时、第三方服务不可用(500),立即触发 PagerDuty/钉钉/企业微信告警。
技巧 :结合 Sentry 或自研告警系统,对
log.error进行聚合,同一错误 1 分钟内只报一次,防止风暴。
四、进阶:链路追踪 (Distributed Tracing)
在微服务架构中,一个请求可能经过 10 个服务。哪里慢了?哪里错了?
✅ 最佳实践 :集成 Micrometer Tracing (Spring Boot 3 默认) + Zipkin/Jaeger/SkyWalking。
-
引入依赖 (以 Zipkin 为例):
xml<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency> -
配置:
yamlmanagement: tracing: sampling: probability: 1.0 # 生产环境建议调低,如 0.1 zipkin: tracing: endpoint: http://zipkin-server:9411/api/v2/spans -
效果 :
每个请求生成唯一的
traceId,贯穿所有服务和日志。在 Zipkin 界面可以看到完整的调用拓扑图和耗时瀑布图。
五、上线检查清单 (Checklist)
在点击"发布"按钮前,请确认:
- 日志:是否已切换为 JSON 格式?敏感字段是否脱敏?
- 级别 :生产环境默认是否为
INFO?是否保留了动态调整能力? - 监控:Prometheus 是否能抓取到指标?Grafana 大盘是否就绪?
- 健康检查 :
/actuator/health是否正常?K8s 探针是否配置正确? - 异常:全局处理器是否捕获了所有 Exception?错误码是否规范?
- 追踪:TraceId 是否打印在日志中?链路追踪系统是否连通?
- 告警:关键指标(CPU > 80%, Error Rate > 1%)是否配置了告警规则?
结语
优雅上线不仅仅是把代码跑起来,而是构建一个可观察、可维护、可信赖的系统。
- 日志是你的眼睛,帮你看见过去;
- 监控是你的仪表盘,帮你把握现在;
- 异常处理是你的安全气囊,帮你抵御意外;
- 链路追踪是你的导航仪,帮你理清复杂关系。
投入时间做好这四点,你将告别"救火式"运维,享受技术带来的掌控感。