Java基础架构设计(四)| 通用响应与异常处理(单体/分布式通用增强方案)
- 一、前置说明
-
- [1.1 核心定位](#1.1 核心定位)
- [1.2 与核心方案的协同关系](#1.2 与核心方案的协同关系)
- [1.3 快速匹配指南](#1.3 快速匹配指南)
- [1.4 术语说明(补充分享优化专属概念)](#1.4 术语说明(补充分享优化专属概念))
- 二、版本兼容&依赖总表(优化场景专属依赖)
-
- [2.1 核心依赖清单(在原有基础上新增)](#2.1 核心依赖清单(在原有基础上新增))
- [2.2 版本兼容说明(确保与核心方案无冲突)](#2.2 版本兼容说明(确保与核心方案无冲突))
- 三、通用增强场景(单体/分布式均可复用)
-
- 场景1:高可用优化(中大型项目建议)
-
- [1. 日志双写配置(logback-spring.xml 扩展,兼容原有ELK输出)](#1. 日志双写配置(logback-spring.xml 扩展,兼容原有ELK输出))
- [2. 日志长期归档到OSS(Shell脚本+定时任务,兼容单体/分布式)](#2. 日志长期归档到OSS(Shell脚本+定时任务,兼容单体/分布式))
- [3. 定时任务配置(crontab,服务器层面部署)](#3. 定时任务配置(crontab,服务器层面部署))
- 场景2:安全合规(金融/政务项目必选)
-
- [1. 敏感信息脱敏注解(Sensitive.java,无侵入式设计)](#1. 敏感信息脱敏注解(Sensitive.java,无侵入式设计))
- [2. 脱敏工具类(SensitiveUtils.java,兼容多场景脱敏)](#2. 脱敏工具类(SensitiveUtils.java,兼容多场景脱敏))
- [3. 脱敏AOP切面(SensitiveAspect.java,自动拦截日志打印)](#3. 脱敏AOP切面(SensitiveAspect.java,自动拦截日志打印))
- [4. 审计日志加密切面(AuditLogEncryptAspect.java,复用单体@AuditLog注解)](#4. 审计日志加密切面(AuditLogEncryptAspect.java,复用单体@AuditLog注解))
- [5. 不可篡改配置(数据库+文件层面,生产环境必配)](#5. 不可篡改配置(数据库+文件层面,生产环境必配))
- 场景3:动态配置(频繁调整配置项目)
-
- [1. 基础配置(bootstrap.yml,复用分布式方案的Nacos)](#1. 基础配置(bootstrap.yml,复用分布式方案的Nacos))
- [2. 动态配置Bean(DynamicConfig.java,支持热更新)](#2. 动态配置Bean(DynamicConfig.java,支持热更新))
- [3. 动态日志级别调整(DynamicLogLevelConfig.java)](#3. 动态日志级别调整(DynamicLogLevelConfig.java))
- [4. 动态脱敏规则解析(DynamicSensitiveRuleConfig.java)](#4. 动态脱敏规则解析(DynamicSensitiveRuleConfig.java))
- [5. Nacos配置监听(ConfigListener.java,配置变更触发更新)](#5. Nacos配置监听(ConfigListener.java,配置变更触发更新))
- [6. Nacos配置示例(optimization-core-demo-prod.yaml)](#6. Nacos配置示例(optimization-core-demo-prod.yaml))
- 场景4:日志检索优化(日志量极大项目)
-
- [1. 数据库日志分表配置(application.yml,单体/分布式均适用)](#1. 数据库日志分表配置(application.yml,单体/分布式均适用))
- [2. 分表初始化SQL脚本(sys_log分表创建)](#2. 分表初始化SQL脚本(sys_log分表创建))
- [3. ES日志检索优化(索引设计+查询优化)](#3. ES日志检索优化(索引设计+查询优化))
-
- (1)ES索引创建脚本(优化分词策略)
- [(2)ES日志输出配置(logback-spring.xml 扩展)](#(2)ES日志输出配置(logback-spring.xml 扩展))
- (3)ES日志检索服务(EsLogSearchService.java,优化查询性能)
- 四、部署指南&问题排查(通用适配)
-
- [4.1 部署步骤(按场景细化,兼容单体/分布式)](#4.1 部署步骤(按场景细化,兼容单体/分布式))
-
- [4.1.1 高可用优化部署(30分钟)](#4.1.1 高可用优化部署(30分钟))
- [4.1.2 安全合规部署(60分钟)](#4.1.2 安全合规部署(60分钟))
- [4.1.3 动态配置部署(30分钟)](#4.1.3 动态配置部署(30分钟))
- [4.1.4 日志检索优化部署(60分钟)](#4.1.4 日志检索优化部署(60分钟))
- [4.2 生产环境必查项](#4.2 生产环境必查项)
-
- [4.2.1 安全配置](#4.2.1 安全配置)
- [4.2.2 性能优化](#4.2.2 性能优化)
- [4.2.3 监控告警](#4.2.3 监控告警)
- [4.3 常见问题排查](#4.3 常见问题排查)
一、前置说明
1.1 核心定位
- 通用适配:100% 兼容前文「单体核心方案」与「分布式扩展方案」,基于已有响应统一、链路追踪、日志规范能力做增强,不修改原有核心代码,无侵入式集成
- 按需选择:4个优化场景独立解耦,可根据项目规模(中大型/金融/日志量大)和业务需求(合规/高可用/动态配置)灵活组合,避免过度设计
- 生产级落地:遵循大厂技术规范,覆盖高可用、安全合规、动态配置、性能优化四大核心诉求,满足中大型项目及金融/政务场景的工业级要求
1.2 与核心方案的协同关系
| 核心方案 | 核心能力 | 增强方案价值(本方案) |
|---|---|---|
| 单体核心方案 | 响应统一、异常收口、日志规范、链路追踪基础 | 叠加高可用保障、安全合规、动态配置,提升单体项目稳定性和扩展性 |
| 分布式扩展方案 | 跨服务链路追踪、日志集中、异常统一、熔断降级 | 强化分布式场景下的日志检索性能、敏感信息防护、配置热更新能力 |
1.3 快速匹配指南
| 项目类型/核心需求 | 推荐优化场景 | 核心收益 | 落地成本(人天) |
|---|---|---|---|
| 中大型项目(高可用要求) | 场景1:高可用优化(日志双写+告警+归档) | 日志零丢失、异常即时感知、存储成本可控 | 1 |
| 金融/政务项目(合规要求) | 场景2:安全合规(脱敏+不可篡改+加密) | 满足等保三级、敏感信息防护、审计日志可追溯 | 1.5 |
| 频繁调整配置(运维需求) | 场景3:动态配置(日志级别+规则热更新) | 无需重启应用、配置变更即时生效、运维效率提升 | 1 |
| 日志量极大(千万级/亿级) | 场景4:日志检索优化(分表+ES索引) | 检索秒级响应、单表数据量可控、存储成本降低 | 2 |
1.4 术语说明(补充分享优化专属概念)
- 日志双写:同一日志同时输出到「本地文件+云日志(SLS/CLS)」,避免单存储介质故障导致日志丢失
- 注解式脱敏:通过
@Sensitive注解标记敏感字段,AOP自动完成脱敏,无需修改业务代码 - 配置热更新:基于Nacos配置中心,修改配置后即时生效,无需重启应用(依赖分布式方案的Nacos基础)
- 日志分表分库:
sys_log按月份分表(如sys_log_202501),避免单表数据量过大导致查询缓慢 - 不可篡改日志:审计日志写入后通过数据库触发器+文件权限控制标记为只读,防止非法篡改
- 分词索引:ES中对日志关键字(如traceId、bizId)创建keyword类型索引,提升精确查询性能
二、版本兼容&依赖总表(优化场景专属依赖)
2.1 核心依赖清单(在原有基础上新增)
xml
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 继承原有核心依赖(单体/分布式) -->
<parent>
<groupId>com.example</groupId>
<artifactId>distributed-core-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>optimization-core-demo</artifactId>
<name>optimization-core-demo</name>
<description>单体/分布式通用增强方案</description>
<properties>
<!-- 云服务依赖版本(对齐分布式方案的中间件版本) -->
<aliyun-sls.version>0.1.28</aliyun-sls.version>
<aliyun-oss.version>3.15.1</aliyun-oss.version>
<!-- 分表插件版本(兼容Spring Boot2.7.x) -->
<sharding-jdbc.version>4.1.1</sharding-jdbc.version>
<!-- 动态配置依赖(复用分布式方案的Nacos版本) -->
<spring-cloud-alibaba-nacos-config.version>2021.0.4.0</spring-cloud-alibaba-nacos-config.version>
<!-- 加密依赖(与Spring Security兼容) -->
<spring-security-crypto.version>5.7.6</spring-security-crypto.version>
</properties>
<dependencies>
<!-- 1. 高可用优化依赖(云日志+OSS归档+钉钉告警) -->
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>logback-appender</artifactId>
<version>${aliyun-sls.version}</version> <!-- 阿里云SLS日志(分布式方案ELK的补充) -->
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-oss.version}</version> <!-- 阿里云OSS长期归档 -->
</dependency>
<dependency>
<groupId>com.github.lybgeek</groupId>
<artifactId>logback-dingtalk-appender</artifactId>
<version>1.0.0</version> <!-- 日志ERROR级别钉钉告警 -->
</dependency>
<!-- 2. 安全合规依赖(脱敏+加密) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>${spring-security-crypto.version}</version> <!-- 加密工具(无侵入) -->
</dependency>
<!-- 3. 动态配置依赖(复用分布式方案的Nacos Config) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${spring-cloud-alibaba-nacos-config.version}</version>
</dependency>
<!-- 4. 日志检索优化依赖(分表+ES检索) -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-jdbc.version}</version> <!-- 日志分表插件 -->
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.0</version> <!-- ES检索客户端(对齐分布式方案ELK版本) -->
</dependency>
</dependencies>
</project>
2.2 版本兼容说明(确保与核心方案无冲突)
| 优化场景 | 核心依赖 | 适配版本要求 | 与核心方案的关联 |
|---|---|---|---|
| 高可用优化 | SLS/OSS/钉钉告警 | Spring Boot2.7.x + Logback1.2.x | 复用单体方案的日志基础配置 |
| 安全合规 | Spring Security Crypto | 与Spring Boot2.7.x兼容 | 复用单体方案的审计日志注解@AuditLog |
| 动态配置 | Nacos Config | Spring Cloud Alibaba2021.0.4.0+ | 复用分布式方案的Nacos配置中心 |
| 日志检索优化 | Sharding-JDBC/Elasticsearch | Sharding-JDBC4.1.x + Elasticsearch7.17.x | 复用分布式方案的ELK集中存储 |
三、通用增强场景(单体/分布式均可复用)
场景1:高可用优化(中大型项目建议)
- 核心目标:日志零丢失、异常即时感知、过期日志安全归档,提升系统稳定性(解决单体/分布式日志存储单点风险)
- 核心实现:日志双写(本地+SLS)+ 异常分级告警(钉钉)+ 日志长期归档(OSS)
- 依赖说明
- 基础依赖:单体方案的日志规范(logback配置、本地日志存储)、分布式方案的ELK集中存储
- 新增依赖:阿里云SLS/OSS SDK、钉钉告警插件(无业务侵入)
1. 日志双写配置(logback-spring.xml 扩展,兼容原有ELK输出)
xml
<!-- 1. 阿里云SLS日志输出(双写之一:云存储,避免本地磁盘故障) -->
<appender name="SLS-APPENDER" class="com.aliyun.openservices.log.logback.LoghubAppender">
<endpoint>cn-hangzhou.log.aliyuncs.com</endpoint>
<projectName>your-sls-project</projectName> <!-- 你的SLS项目名(从Nacos配置) -->
<logStoreName>your-logstore</logStoreName> <!-- 你的SLS日志库名 -->
<accessKeyId>${aliyun.accessKeyId}</accessKeyId> <!-- 从环境变量注入,避免硬编码 -->
<accessKeySecret>${aliyun.accessKeySecret}</accessKeySecret>
<topic>${spring.application.name}</topic> <!-- 服务名作为topic(分布式场景区分服务) -->
<source>${HOSTNAME}</source> <!-- 主机名(定位部署节点) -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"appName":"${spring.application.name}","environment":"${spring.profiles.active}"}</customFields>
<includeMdcKeyName>traceId</includeMdcKeyName> <!-- 复用分布式方案的链路追踪字段 -->
<includeMdcKeyName>bizId</includeMdcKeyName>
</encoder>
<!-- 批量发送优化(高并发场景) -->
<batchSize>1024</batchSize>
<batchWaitSeconds>3</batchWaitSeconds>
<!-- 失败重试配置 -->
<retryTimes>3</retryTimes>
<retryIntervalMilliseconds>1000</retryIntervalMilliseconds>
</appender>
<!-- 2. 钉钉异常告警(ERROR级别触发,含链路信息) -->
<appender name="DINGTALK-ALERT" class="com.github.lybgeek.logback.dingtalk.DingTalkAppender">
<webhook>${dingtalk.webhook}</webhook> <!-- 钉钉机器人Webhook(Nacos配置) -->
<secret>${dingtalk.secret}</secret> <!-- 钉钉机器人密钥(可选,防刷) -->
<keyword>日志告警</keyword>
<threshold>ERROR</threshold> <!-- 仅ERROR级别触发,避免刷屏 -->
<encoder>
<pattern>
【%d{yyyy-MM-dd HH:mm:ss.SSS}】
应用名:${spring.application.name}
环境:${spring.profiles.active}
级别:%p
traceId:%X{traceId} <!-- 链路追踪ID,快速定位问题 -->
bizId:%X{bizId} <!-- 业务ID,关联具体业务场景 -->
消息:%m
异常:%ex
</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 限流防刷屏(60秒最多10条) -->
<rateLimit>true</rateLimit>
<rateLimitPeriod>60</rateLimitPeriod>
<rateLimitCount>10</rateLimitCount>
</appender>
<!-- 3. 根日志配置(保留原有输出,新增双写和告警) -->
<root level="INFO">
<appender-ref ref="LOGSTASH"/> <!-- 原有ELK输出(分布式场景)/本地文件(单体场景) -->
<appender-ref ref="SLS-APPENDER"/> <!-- SLS双写(高可用保障) -->
<appender-ref ref="DINGTALK-ALERT"/> <!-- 钉钉告警(异常即时感知) -->
<springProfile name="dev">
<appender-ref ref="CONSOLE"/> <!-- 开发环境控制台输出 -->
</springProfile>
</root>
2. 日志长期归档到OSS(Shell脚本+定时任务,兼容单体/分布式)
bash
#!/bin/bash
# 日志归档到OSS脚本(log-archive-oss.sh)
# 配置参数(生产环境从Nacos配置中心读取,此处简化)
APP_NAME="optimization-core-demo"
LOG_HOME="/home/admin/logs/${APP_NAME}"
OSS_BUCKET="your-oss-bucket" # OSS归档桶名
OSS_ENDPOINT="oss-cn-hangzhou.aliyuncs.com"
ACCESS_KEY="${aliyun.accessKeyId}" # 环境变量注入
ACCESS_SECRET="${aliyun.accessKeySecret}"
ARCHIVE_DAYS=7 # 本地保留7天日志,超过归档到OSS
DELETE_DAYS=90 # OSS归档日志保留90天,过期自动删除
# 确保归档目录存在
mkdir -p ${LOG_HOME}/archive-temp
# 归档7天前的日志(压缩后上传OSS)
find ${LOG_HOME} -name "*.log" -type f -mtime +${ARCHIVE_DAYS} -print0 | while IFS= read -r -d '' log_file; do
# 压缩日志(保留原文件,上传成功后删除)
gzip -c ${log_file} > ${log_file}.gz
compressed_file="${log_file}.gz"
# 上传到OSS(按日期+服务名目录存储,分布式场景区分服务)
ossutil64 cp -u ${compressed_file} oss://${OSS_BUCKET}/log-archive/${APP_NAME}/$(date -d "-${ARCHIVE_DAYS} days" +%Y%m%d)/ \
--endpoint ${OSS_ENDPOINT} --access-key-id ${ACCESS_KEY} --access-key-secret ${ACCESS_SECRET}
# 上传成功后删除本地压缩文件
if [ $? -eq 0 ]; then
rm -f ${compressed_file}
echo "[$(date +'%Y-%m-%d %H:%M:%S')] 归档成功:${compressed_file}" >> ${LOG_HOME}/archive.log
else
echo "[$(date +'%Y-%m-%d %H:%M:%S')] 归档失败:${compressed_file}" >> ${LOG_HOME}/archive.error.log
fi
done
# 删除90天前的OSS归档日志(节省存储成本)
ossutil64 rm -r -f oss://${OSS_BUCKET}/log-archive/${APP_NAME}/$(date -d "-${DELETE_DAYS} days" +%Y%m%d)/ \
--endpoint ${OSS_ENDPOINT} --access-key-id ${ACCESS_KEY} --access-key-secret ${ACCESS_SECRET}
3. 定时任务配置(crontab,服务器层面部署)
bash
# 编辑定时任务(root用户执行)
crontab -e
# 添加配置:每日凌晨2点(低峰期)执行归档脚本
0 2 * * * /bin/bash /home/admin/scripts/log-archive-oss.sh >> /home/admin/scripts/archive-cron.log 2>&1
验证方式
- 发起请求,查看SLS控制台和本地日志文件,验证日志是否双写成功;
- 模拟ERROR异常(如空指针、业务异常),查看钉钉群是否收到告警消息(含traceId/bizId);
- 7天后检查本地日志目录,验证超过7天的日志是否已压缩并上传OSS,本地仅保留最近7天日志;
- 90天后检查OSS归档目录,验证过期日志是否自动清理。
场景2:安全合规(金融/政务项目必选)
- 核心目标:敏感信息脱敏、审计日志不可篡改、满足等保三级要求(解决单体/分布式数据安全风险)
- 核心实现:注解式脱敏(AOP)+ 审计日志加密 + 数据库/文件只读控制
- 依赖说明
- 基础依赖:单体方案的审计日志注解
@AuditLog、sys_biz_log表、日志工具类LogUtils - 新增依赖:Spring Security Crypto(加密)、AOP切面(无业务侵入)
- 基础依赖:单体方案的审计日志注解
1. 敏感信息脱敏注解(Sensitive.java,无侵入式设计)
java
package com.example.optimization.annotation;
import java.lang.annotation.*;
/**
* 敏感信息脱敏注解(标记在实体类字段上,无需修改业务代码)
* 支持类型:phone(手机号)、idCard(身份证)、bankCard(银行卡)、email(邮箱)、name(姓名)
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
/** 脱敏类型(必填) */
String type();
/** 自定义脱敏规则(可选,如姓名脱敏保留1个字符:prefix=1,suffix=0) */
String rule() default "";
}
2. 脱敏工具类(SensitiveUtils.java,兼容多场景脱敏)
java
package com.example.optimization.util;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.encrypt.AesBytesEncryptor;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 敏感信息脱敏+审计日志加密工具类(生产环境密钥从Nacos配置中心获取)
*/
public class SensitiveUtils {
// AES加密密钥(用于审计日志加密,防止篡改)
private static final String AES_SECRET = System.getenv("AUDIT_LOG_AES_SECRET");
private static final AesBytesEncryptor ENCRYPTOR = new AesBytesEncryptor(
AES_SECRET.getBytes(StandardCharsets.UTF_8), KeyGenerators.string().generateKey());
/**
* 脱敏入口方法(根据注解类型自动选择脱敏规则)
*/
public static String desensitize(String value, String type, String rule) {
if (value == null || value.isEmpty()) {
return value;
}
// 优先使用自定义规则,无则使用默认规则
if (StringUtils.hasText(rule)) {
return desensitizeByRule(value, rule);
}
// 默认脱敏规则
return switch (type) {
case "phone" -> desensitizePhone(value);
case "idCard" -> desensitizeIdCard(value);
case "bankCard" -> desensitizeBankCard(value);
case "email" -> desensitizeEmail(value);
case "name" -> desensitizeName(value);
default -> value;
};
}
/**
* 手机号脱敏:138****1234
*/
private static String desensitizePhone(String phone) {
if (phone.length() != 11) {
return phone;
}
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
/**
* 身份证脱敏:110101****1234(18位)/1101****1234(15位)
*/
private static String desensitizeIdCard(String idCard) {
if (idCard.length() == 18) {
return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
} else if (idCard.length() == 15) {
return idCard.replaceAll("(\\d{4})\\d{7}(\\d{4})", "$1*******$2");
}
return idCard;
}
/**
* 银行卡脱敏:6222****6666(保留前4后4)
*/
private static String desensitizeBankCard(String bankCard) {
if (bankCard.length() < 10) {
return bankCard;
}
return bankCard.replaceAll("(\\d{4})\\d+(\\d{4})", "$1****$2");
}
/**
* 邮箱脱敏:a****@163.com(保留前缀1位)
*/
private static String desensitizeEmail(String email) {
String[] parts = email.split("@");
if (parts.length != 2) {
return email;
}
return parts[0].substring(0, 1) + "****@" + parts[1];
}
/**
* 姓名脱敏:张**(双字)/李*(单字)
*/
private static String desensitizeName(String name) {
if (name.length() == 1) {
return name + "*";
} else if (name.length() == 2) {
return name.substring(0, 1) + "**";
} else {
return name.substring(0, 1) + "***" + name.substring(name.length() - 1);
}
}
/**
* 自定义脱敏规则(如prefix=1,suffix=0:保留前1后0,中间用*填充)
*/
private static String desensitizeByRule(String value, String rule) {
Map<String, Integer> ruleMap = parseRule(rule);
int prefix = ruleMap.getOrDefault("prefix", 0);
int suffix = ruleMap.getOrDefault("suffix", 0);
if (prefix + suffix >= value.length()) {
return value;
}
StringBuilder sb = new StringBuilder();
sb.append(value.substring(0, prefix));
sb.append("*".repeat(value.length() - prefix - suffix));
sb.append(value.substring(value.length() - suffix));
return sb.toString();
}
/**
* 解析自定义规则(格式:prefix=1,suffix=0)
*/
private static Map<String, Integer> parseRule(String rule) {
Map<String, Integer> ruleMap = new HashMap<>();
String[] parts = rule.split(",");
for (String part : parts) {
String[] keyValue = part.split("=");
if (keyValue.length == 2) {
ruleMap.put(keyValue[0], Integer.parseInt(keyValue[1]));
}
}
return ruleMap;
}
/**
* 审计日志加密(存入数据库前调用,防止篡改)
*/
public static String encryptAuditLog(String content) {
byte[] encrypted = ENCRYPTOR.encrypt(content.getBytes(StandardCharsets.UTF_8));
return new String(Hex.encode(encrypted), StandardCharsets.UTF_8);
}
/**
* 审计日志解密(查询时调用,仅授权用户可解密)
*/
public static String decryptAuditLog(String encryptedContent) {
byte[] decrypted = ENCRYPTOR.decrypt(Hex.decode(encryptedContent.getBytes(StandardCharsets.UTF_8)));
return new String(decrypted, StandardCharsets.UTF_8);
}
}
3. 脱敏AOP切面(SensitiveAspect.java,自动拦截日志打印)
java
package com.example.optimization.aspect;
import com.example.optimization.annotation.Sensitive;
import com.example.optimization.util.SensitiveUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
/**
* 脱敏AOP切面(拦截日志打印方法,自动脱敏@Sensitive标记的字段)
* 切入点:单体/分布式方案中的LogUtils日志打印方法
*/
@Aspect
@Component
public class SensitiveAspect {
// 切入点:LogUtils的所有日志打印方法(info/error/debug等)
@Around("execution(* com.example.monomer.core.util.LogUtils.*(..))")
public Object aroundLogPrint(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return joinPoint.proceed();
}
// 对每个日志参数进行脱敏处理(递归处理对象/集合)
for (int i = 0; i < args.length; i++) {
args[i] = desensitizeObject(args[i]);
}
// 执行原日志打印方法
return joinPoint.proceed(args);
}
/**
* 递归脱敏对象中的@Sensitive字段(支持嵌套对象、集合)
*/
private Object desensitizeObject(Object obj) {
if (obj == null || obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
return obj;
}
// 处理集合(List/Set)
if (obj instanceof Iterable<?>) {
((Iterable<?>) obj).forEach(this::desensitizeObject);
return obj;
}
// 处理数组
if (obj.getClass().isArray()) {
Object[] array = (Object[]) obj;
for (Object item : array) {
desensitizeObject(item);
}
return obj;
}
// 处理普通对象(反射获取@Sensitive字段)
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Sensitive.class)) {
field.setAccessible(true);
Sensitive sensitive = field.getAnnotation(Sensitive.class);
try {
Object value = field.get(obj);
if (value != null && value instanceof String) {
// 调用脱敏工具类进行脱敏
String desensitizedValue = SensitiveUtils.desensitize(
(String) value, sensitive.type(), sensitive.rule()
);
field.set(obj, desensitizedValue);
}
} catch (IllegalAccessException e) {
com.example.monomer.core.util.LogUtils.error("脱敏失败:field={}", field.getName(), e);
}
}
}
return obj;
}
}
4. 审计日志加密切面(AuditLogEncryptAspect.java,复用单体@AuditLog注解)
java
package com.example.optimization.aspect;
import com.example.monomer.core.annotation.AuditLog;
import com.example.optimization.util.SensitiveUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
/**
* 审计日志加密切面(写入sys_biz_log前加密,防止篡改)
* 依赖单体方案的@AuditLog注解和sys_biz_log表
*/
@Component
@Aspect
public class AuditLogEncryptAspect {
// 切入点:所有标记@AuditLog的方法
@Pointcut("@annotation(auditLog)")
public void auditLogPointcut(AuditLog auditLog) {}
@Before("auditLogPointcut(auditLog)")
public void beforeAuditLog(JoinPoint joinPoint, AuditLog auditLog) {
// 获取脱敏后的参数(已通过SensitiveAspect脱敏)
Object[] args = joinPoint.getArgs();
String content = com.alibaba.fastjson.JSONObject.toJSONString(args);
// 加密内容(存入数据库,仅授权用户可解密)
String encryptedContent = SensitiveUtils.encryptAuditLog(content);
// 存入MDC,供logback入库到sys_biz_log表
MDC.put("encryptedContent", encryptedContent);
}
}
5. 不可篡改配置(数据库+文件层面,生产环境必配)
(1)数据库层面(sys_biz_log表增强)
sql
-- 1. 给sys_biz_log表添加is_modified字段(标记是否被篡改)
ALTER TABLE sys_biz_log ADD COLUMN is_modified TINYINT DEFAULT 0 COMMENT '是否被篡改:0-否,1-是';
-- 2. 创建更新触发器(更新时自动置为1)
DELIMITER //
CREATE TRIGGER trg_sys_biz_log_update
BEFORE UPDATE ON sys_biz_log
FOR EACH ROW
BEGIN
SET NEW.is_modified = 1;
END //
DELIMITER ;
-- 3. 创建删除触发器(删除时记录日志,实际项目中可禁止删除)
DELIMITER //
CREATE TRIGGER trg_sys_biz_log_delete
BEFORE DELETE ON sys_biz_log
FOR EACH ROW
BEGIN
INSERT INTO sys_biz_log_delete_log (log_id, delete_time, operator)
VALUES (OLD.id, NOW(), CURRENT_USER());
END //
DELIMITER ;
-- 4. 创建查询视图(过滤被篡改的记录)
CREATE VIEW v_sys_biz_log AS
SELECT * FROM sys_biz_log WHERE is_modified = 0;
(2)文件层面(本地日志只读控制)
bash
# 1. 设置日志文件目录权限(仅root可写,应用用户只读)
chown -R root:admin /home/admin/logs
chmod -R 755 /home/admin/logs
# 2. 日志文件生成后自动设置为只读(通过logback配置)
# 在logback-spring.xml中添加文件滚动配置
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/home/admin/logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/home/admin/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%p] %logger{50} - %msg%n</pattern>
</encoder>
<!-- 日志文件滚动后设置为只读 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>100MB</maxFileSize>
</triggeringPolicy>
<postFileRotationProcessors>
<processor class="com.example.optimization.logback.ReadOnlyFileProcessor"/>
</postFileRotationProcessors>
</appender>
(3)云日志层面(SLS日志不可删除)
- 登录阿里云SLS控制台,进入日志库配置;
- 开启"日志不可删除"策略,设置保留时间(如90天);
- 仅授权只读账号给开发/运维人员,禁止删除权限。
验证方式
- 实体类字段添加注解:
@Sensitive(type = "phone")或@Sensitive(type = "name", rule = "prefix=1,suffix=0"),打印日志时验证是否脱敏; - 查看
sys_biz_log表,验证content字段为加密字符串,通过SensitiveUtils.decryptAuditLog可解密; - 手动更新
sys_biz_log表数据,验证is_modified字段自动置为1,查询视图v_sys_biz_log无法看到该记录; - 尝试修改本地日志文件(应用用户权限),验证提示"权限不足"。
场景3:动态配置(频繁调整配置项目)
- 核心目标:日志级别、脱敏规则、错误码说明热更新,无需重启应用(解决单体/分布式配置变更重启痛点)
- 核心实现:Nacos配置中心+@RefreshScope动态Bean+配置监听
- 依赖说明
- 基础依赖:单体方案的错误码枚举
BizErrorCodeEnum、日志工具类LogUtils;分布式方案的Nacos配置中心 - 新增依赖:Spring Cloud Nacos Config(动态配置核心)
- 基础依赖:单体方案的错误码枚举
1. 基础配置(bootstrap.yml,复用分布式方案的Nacos)
yaml
spring:
application:
name: optimization-core-demo
cloud:
nacos:
config:
server-addr: localhost:8848 # Nacos地址(分布式方案已部署)
file-extension: yaml # 配置文件格式
namespace: your-namespace # 与分布式方案共用命名空间
group: DEFAULT_GROUP # 配置分组
refresh-enabled: true # 启用自动刷新
profiles:
active: prod
2. 动态配置Bean(DynamicConfig.java,支持热更新)
java
package com.example.optimization.config;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
/**
* 动态配置Bean(@RefreshScope注解实现热更新,无需重启应用)
* 配置存储在Nacos:optimization-core-demo-prod.yaml
*/
@Component
@RefreshScope
public class DynamicConfig {
// 1. 动态日志级别(默认INFO,支持DEBUG/INFO/WARN/ERROR)
@Value("${logging.level.com.example:INFO}")
private String logLevel;
// 2. 动态脱敏规则(格式:type=prefix,suffix;...,支持新增脱敏类型)
@Value("${sensitive.rules:phone=3,4;idCard=6,4;bankCard=4,4;email=1,0}")
private String sensitiveRules;
// 3. 动态错误码说明(格式:code=desc;...,新增错误码无需改代码)
@Value("${error.code.desc:601=用户不存在;602=密码错误;610=订单不存在;620=库存不足}")
private String errorCodeDesc;
// 4. 动态告警阈值(如ERROR日志每分钟最多5条)
@Value("${alert.threshold.error-log:5}")
private int errorLogThreshold;
// Getter方法(无Setter,通过Nacos更新)
public String getLogLevel() {
return logLevel;
}
public String getSensitiveRules() {
return sensitiveRules;
}
public String getErrorCodeDesc() {
return errorCodeDesc;
}
public int getErrorLogThreshold() {
return errorLogThreshold;
}
}
3. 动态日志级别调整(DynamicLogLevelConfig.java)
java
package com.example.optimization.config;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
/**
* 动态日志级别调整(Nacos配置变更后即时生效)
* 依赖单体/分布式方案的Logback日志配置
*/
@Component
@RefreshScope
public class DynamicLogLevelConfig {
@Autowired
private DynamicConfig dynamicConfig;
// 日志级别映射(SLF4J→Logback)
private static final Map<String, Level> LEVEL_MAP = new HashMap<>();
static {
LEVEL_MAP.put("DEBUG", Level.DEBUG);
LEVEL_MAP.put("INFO", Level.INFO);
LEVEL_MAP.put("WARN", Level.WARN);
LEVEL_MAP.put("ERROR", Level.ERROR);
LEVEL_MAP.put("OFF", Level.OFF);
}
/**
* 初始化日志级别(应用启动时执行)
*/
@PostConstruct
public void initLogLevel() {
updateLogLevel();
}
/**
* 配置变更后更新日志级别(Nacos配置监听触发)
*/
public void updateLogLevel() {
String logLevel = dynamicConfig.getLogLevel().toUpperCase();
Level targetLevel = LEVEL_MAP.getOrDefault(logLevel, Level.INFO);
// 更新项目包下的日志级别(com.example为根包)
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.getLogger("com.example").setLevel(targetLevel);
// 打印日志级别变更日志(仅INFO级别可见)
com.example.monomer.core.util.LogUtils.info("动态调整日志级别:{} → {}",
loggerContext.getLogger("com.example").getLevel(), targetLevel);
}
}
4. 动态脱敏规则解析(DynamicSensitiveRuleConfig.java)
java
package com.example.optimization.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
/**
* 动态脱敏规则解析(Nacos配置变更后即时生效,无需改代码)
*/
@Component
@RefreshScope
public class DynamicSensitiveRuleConfig {
@Autowired
private DynamicConfig dynamicConfig;
// 脱敏规则缓存(key:脱敏类型,value:{prefix:xxx, suffix:xxx})
private Map<String, Map<String, Integer>> sensitiveRuleCache = new HashMap<>();
/**
* 初始化规则(应用启动时执行)
*/
@PostConstruct
public void initRule() {
parseSensitiveRules();
}
/**
* 解析Nacos配置的脱敏规则
*/
public void parseSensitiveRules() {
String rules = dynamicConfig.getSensitiveRules();
if (!StringUtils.hasText(rules)) {
return;
}
Map<String, Map<String, Integer>> newRuleCache = new HashMap<>();
String[] ruleArray = rules.split(";");
for (String rule : ruleArray) {
String[] typeAndRule = rule.split("=");
if (typeAndRule.length != 2) {
continue;
}
String type = typeAndRule[0].trim();
String[] prefixSuffix = typeAndRule[1].split(",");
if (prefixSuffix.length != 2) {
continue;
}
Map<String, Integer> detailRule = new HashMap<>();
detailRule.put("prefix", Integer.parseInt(prefixSuffix[0].trim()));
detailRule.put("suffix", Integer.parseInt(prefixSuffix[1].trim()));
newRuleCache.put(type, detailRule);
}
// 更新缓存
this.sensitiveRuleCache = newRuleCache;
com.example.monomer.core.util.LogUtils.info("动态更新脱敏规则:{}", newRuleCache);
}
/**
* 获取脱敏规则(供脱敏工具类使用)
*/
public Map<String, Integer> getSensitiveRule(String type) {
return sensitiveRuleCache.getOrDefault(type, new HashMap<>());
}
}
5. Nacos配置监听(ConfigListener.java,配置变更触发更新)
java
package com.example.optimization.listener;
import com.alibaba.nacos.api.config.annotation.NacosConfigListener;
import com.example.optimization.config.DynamicConfig;
import com.example.optimization.config.DynamicLogLevelConfig;
import com.example.optimization.config.DynamicSensitiveRuleConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Nacos配置监听(配置变更时触发对应组件更新)
*/
@Component
public class ConfigListener {
@Autowired
private DynamicLogLevelConfig dynamicLogLevelConfig;
@Autowired
private DynamicSensitiveRuleConfig dynamicSensitiveRuleConfig;
/**
* 监听核心配置变更(dataId与bootstrap.yml一致)
*/
@NacosConfigListener(dataId = "${spring.application.name}-${spring.profiles.active}.yaml", group = "DEFAULT_GROUP")
public void onConfigChange(String configContent) {
com.example.monomer.core.util.LogUtils.info("Nacos配置变更,内容:{}", configContent);
// 1. 更新日志级别
dynamicLogLevelConfig.updateLogLevel();
// 2. 更新脱敏规则
dynamicSensitiveRuleConfig.parseSensitiveRules();
// 3. 其他配置更新(如错误码说明,可在此扩展)
}
}
6. Nacos配置示例(optimization-core-demo-prod.yaml)
yaml
# 动态日志级别
logging:
level:
com.example: DEBUG
# 动态脱敏规则(新增类型无需改代码)
sensitive:
rules: phone=3,4;idCard=6,4;bankCard=4,4;email=1,0;address=5,0
# 动态错误码说明(新增错误码无需改代码)
error:
code:
desc: 601=用户不存在;602=密码错误;610=订单不存在;620=库存不足;630=地址不存在
# 动态告警阈值
alert:
threshold:
error-log: 5
验证方式
- 应用启动后,在Nacos控制台修改
logging.level.com.example为DEBUG,验证应用即时输出DEBUG级别日志(无需重启); - 在Nacos控制台新增脱敏规则
address=5,0,标记实体类address字段@Sensitive(type = "address"),验证地址脱敏为"北京市海淀区****"; - 在Nacos控制台新增错误码说明
630=地址不存在,通过dynamicConfig.getErrorCodeDesc()获取,验证能即时获取到新说明; - 修改告警阈值
alert.threshold.error-log=3,验证ERROR日志每分钟超过3条时触发限流(需扩展告警逻辑)。
场景4:日志检索优化(日志量极大项目)
- 核心目标:解决千万级/亿级日志检索缓慢问题,实现秒级响应、存储成本可控(适配分布式日志集中场景)
- 核心实现:Sharding-JDBC分表(数据库日志)+ ES分词索引(集中日志)+ 检索优化
- 依赖说明
- 基础依赖:单体方案的
sys_log表;分布式方案的ELK集中存储 - 新增依赖:Sharding-JDBC(分表)、Elasticsearch Rest High Level Client(检索)
- 基础依赖:单体方案的
1. 数据库日志分表配置(application.yml,单体/分布式均适用)
yaml
spring:
shardingsphere:
datasource:
names: ds0 # 数据源名称(多数据源时可扩展ds1、ds2)
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/monomer_core?useSSL=false&serverTimezone=Asia/Shanghai
username: ${DB_USERNAME} # 环境变量注入
password: ${DB_PASSWORD}
rules:
sharding:
tables:
sys_log: # 需要分表的表名(单体方案的系统日志表)
actual-data-nodes: ds0.sys_log_${202501..202512} # 2025年1-12月分表(提前创建)
table-strategy:
standard:
sharding-column: create_time # 分表字段(日志创建时间)
sharding-algorithm-name: sys_log_inline # 分表算法
key-generator:
column: id # 主键字段
type: SNOWFLAKE # 雪花算法生成主键(避免分表主键冲突)
sharding-algorithms:
sys_log_inline:
type: INLINE
props:
algorithm-expression: sys_log_${create_time.format('yyyyMM')} # 分表格式:sys_log_202501
props:
sql:
show: false # 生产环境关闭SQL打印
2. 分表初始化SQL脚本(sys_log分表创建)
sql
-- 创建2025年1-12月分表(可通过脚本批量生成)
CREATE TABLE sys_log_202501 LIKE sys_log;
CREATE TABLE sys_log_202502 LIKE sys_log;
CREATE TABLE sys_log_202503 LIKE sys_log;
-- ... 省略其他月份分表 ...
CREATE TABLE sys_log_202512 LIKE sys_log;
-- 给每个分表添加索引(优化查询性能)
ALTER TABLE sys_log_202501 ADD INDEX idx_trace_id (trace_id);
ALTER TABLE sys_log_202501 ADD INDEX idx_create_time (create_time);
-- ... 其他分表同理 ...
3. ES日志检索优化(索引设计+查询优化)
(1)ES索引创建脚本(优化分词策略)
bash
# 创建日志索引模板(distributed-log-template.json)
curl -X PUT "http://localhost:9200/_index_template/distributed-log-template" -H "Content-Type: application/json" -d '{
"index_patterns": ["distributed-log-*"],
"template": {
"mappings": {
"properties": {
"traceId": {
"type": "keyword" // 精确匹配,不分词(优化traceId查询)
},
"bizId": {
"type": "keyword" // 精确匹配,不分词
},
"operatorId": {
"type": "keyword"
},
"serviceName": {
"type": "keyword" // 服务名精确匹配
},
"level": {
"type": "keyword"
},
"message": {
"type": "text",
"analyzer": "ik_max_word", // 中文分词(优化日志内容模糊查询)
"search_analyzer": "ik_smart"
},
"exception": {
"type": "text",
"analyzer": "ik_max_word"
},
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss.SSS"
},
"environment": {
"type": "keyword"
}
}
},
"settings": {
"number_of_shards": 3, // 分片数(根据ES节点数调整)
"number_of_replicas": 1, // 副本数(高可用)
"index.query.bool.max_clause_count": 4096 // 提高查询条件上限
}
},
"priority": 10,
"version": 1
}'
(2)ES日志输出配置(logback-spring.xml 扩展)
xml
<!-- ES日志输出(替代数据库分表存储,日志量大时推荐) -->
<appender name="ELASTICSEARCH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>localhost:9200</destination> <!-- ES集群地址(分布式场景) -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"appName":"${spring.application.name}","logType":"sys_log"}</customFields>
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>bizId</includeMdcKeyName>
<includeMdcKeyName>operatorId</includeMdcKeyName>
<timestampPattern>yyyy-MM-dd HH:mm:ss.SSS</timestampPattern>
<!-- 忽略冗余字段,减小ES存储压力 -->
<excludeMdcKeyName>requestId</excludeMdcKeyName>
<excludeMdcKeyName>sessionId</excludeMdcKeyName>
</encoder>
<!-- 连接策略(失败重试) -->
<connectionStrategy>RETRY_ON_FAILURE</connectionStrategy>
<reconnectionDelay>5000</reconnectionDelay>
<!-- 批量发送优化 -->
<batchSize>1024</batchSize>
<batchDelay>1000</batchDelay>
</appender>
<!-- 根日志添加ES输出(日志量大时禁用数据库存储) -->
<root level="INFO">
<appender-ref ref="ELASTICSEARCH"/> <!-- ES集中存储(优先) -->
<springProfile name="dev">
<appender-ref ref="CONSOLE"/>
</springProfile>
</root>
(3)ES日志检索服务(EsLogSearchService.java,优化查询性能)
java
package com.example.optimization.service;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* ES日志检索服务(亿级日志秒级响应优化)
*/
@Service
public class EsLogSearchService {
@Autowired
private RestHighLevelClient esClient;
/**
* 按traceId检索全链路日志(分布式场景核心查询)
*/
public List<Map<String, Object>> searchLogByTraceId(String traceId, int pageNum, int pageSize) throws IOException {
// 构建查询请求(索引模式:distributed-log-*)
SearchRequest searchRequest = new SearchRequest("distributed-log-*");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 精确匹配traceId(keyword字段,不分词,性能最优)
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("mdc.traceId.keyword", traceId));
// 分页配置(from=偏移量,size=每页条数)
sourceBuilder.query(boolQuery)
.from((pageNum - 1) * pageSize)
.size(pageSize)
.sort("@timestamp", SortOrder.ASC); // 按时间升序排列(链路顺序)
// 限制返回字段(只返回需要的字段,提高响应速度)
sourceBuilder.fetchSource(new String[]{"timestamp", "serviceName", "level", "message", "mdc.traceId", "mdc.bizId"}, null);
searchRequest.source(sourceBuilder);
// 执行查询
SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果
List<Map<String, Object>> result = new ArrayList<>();
response.getHits().forEach(hit -> {
result.add(hit.getSourceAsMap());
});
return result;
}
/**
* 多条件组合查询(traceId+服务名+时间范围)
*/
public List<Map<String, Object>> searchLogByMultiCondition(
String traceId, String serviceName, String startTime, String endTime, int pageNum, int pageSize) throws IOException {
SearchRequest searchRequest = new SearchRequest("distributed-log-*");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 精确匹配traceId
if (traceId != null && !traceId.isEmpty()) {
boolQuery.must(QueryBuilders.termQuery("mdc.traceId.keyword", traceId));
}
// 精确匹配服务名
if (serviceName != null && !serviceName.isEmpty()) {
boolQuery.must(QueryBuilders.termQuery("serviceName.keyword", serviceName));
}
// 时间范围查询
if (startTime != null && !startTime.isEmpty() && endTime != null && !endTime.isEmpty()) {
boolQuery.must(QueryBuilders.rangeQuery("timestamp")
.gte(startTime)
.lte(endTime)
.format("yyyy-MM-dd HH:mm:ss"));
}
sourceBuilder.query(boolQuery)
.from((pageNum - 1) * pageSize)
.size(pageSize)
.sort("@timestamp", SortOrder.ASC);
searchRequest.source(sourceBuilder);
SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
List<Map<String, Object>> result = new ArrayList<>();
response.getHits().forEach(hit -> {
result.add(hit.getSourceAsMap());
});
return result;
}
}
验证方式
- 查看数据库,验证
sys_log表是否按月份自动路由到对应分表(如2025年1月日志写入sys_log_202501); - 模拟千万级日志写入ES,调用
searchLogByTraceId方法,验证响应时间≤1秒; - 执行多条件组合查询(traceId+serviceName+时间范围),验证结果精准且响应快速;
- 查看ES索引分片状态,验证数据均匀分布在各个分片,无单点压力。
四、部署指南&问题排查(通用适配)
4.1 部署步骤(按场景细化,兼容单体/分布式)
4.1.1 高可用优化部署(30分钟)
- 准备阿里云SLS/OSS资源,获取AK/SK(生产环境存入环境变量);
- 部署钉钉机器人,获取Webhook和密钥(Nacos配置);
- 上传日志归档脚本到服务器,配置crontab定时任务;
- 修改logback-spring.xml,添加SLS和钉钉告警Appender;
- 重启应用(仅首次部署需重启,后续配置变更通过Nacos热更新)。
4.1.2 安全合规部署(60分钟)
- 集成脱敏注解、工具类、AOP切面到项目(无需修改业务代码);
- 初始化AES加密密钥,存入环境变量
AUDIT_LOG_AES_SECRET; - 执行SQL脚本,给
sys_biz_log表添加字段和触发器; - 配置本地日志文件权限(root可写,应用用户只读);
- 在SLS控制台开启日志不可删除策略;
- 验证脱敏、加密、不可篡改效果(无需重启应用)。
4.1.3 动态配置部署(30分钟)
- 复用分布式方案的Nacos配置中心(单体项目需单独部署Nacos);
- 在Nacos创建配置文件
optimization-core-demo-prod.yaml,填入动态配置; - 项目添加Nacos Config依赖和bootstrap.yml配置;
- 集成动态配置Bean、规则解析类、配置监听器;
- 启动应用,修改Nacos配置,验证热更新效果。
4.1.4 日志检索优化部署(60分钟)
- 集成Sharding-JDBC依赖,配置分表规则;
- 执行分表初始化SQL脚本,提前创建全年分表;
- 复用分布式方案的ELK集群(单体项目需部署ELK);
- 执行ES索引模板创建脚本,优化分词策略;
- 修改logback-spring.xml,添加ES输出;
- 集成ES检索服务,压测验证亿级日志检索性能。
4.2 生产环境必查项
4.2.1 安全配置
- 敏感信息:AK/SK、加密密钥、数据库密码等通过环境变量或Nacos配置中心注入,禁止硬编码;
- 权限控制:ES/SLS/Nacos仅开放内网访问,配置最小权限账号;
- 数据加密:审计日志加密存储,脱敏规则严格按业务要求配置(如金融场景需符合PCI DSS规范)。
4.2.2 性能优化
- 分表策略:分表字段选择高频查询字段(如create_time),提前创建分表避免运行时创建开销;
- ES优化:分片数=ES节点数×2,副本数=1,定期清理过期索引;
- 日志输出:禁用不必要的日志字段(如请求头、响应体),批量发送日志减少IO开销;
- 定时任务:归档脚本在低峰期执行,避免影响业务。
4.2.3 监控告警
- 中间件监控:监控ES/SLS/OSS/Nacos的CPU、内存、磁盘使用率,设置阈值告警;
- 日志监控:SLS日志ERROR量突增、ES检索响应时间超过500ms告警;
- 安全监控:监控
sys_biz_log表的is_modified字段,出现1时触发告警(可能存在篡改); - 配置监控:Nacos配置变更记录审计,敏感配置(如脱敏规则)变更触发通知。
4.3 常见问题排查
| 问题现象 | 排查方向 | 解决方案 |
|---|---|---|
| 钉钉告警未触发 | 1. Webhook/密钥错误;2. 日志级别未到ERROR;3. 限流触发;4. 网络不通 | 1. 核对钉钉机器人配置,测试Webhook连通性;2. 模拟ERROR异常;3. 调整限流参数;4. 检查服务器网络是否能访问钉钉API |
| 脱敏注解不生效 | 1. AOP切面未扫描;2. 字段类型非String;3. 脱敏类型错误;4. 切入点配置错误 | 1. 检查切面@Component注解和包扫描范围;2. 仅支持String字段;3. 确认type为phone/idCard等;4. 核对切入点是否为LogUtils的日志方法 |
| 动态配置不刷新 | 1. 未加@RefreshScope;2. Nacos配置未发布;3. 配置键名不匹配;4. 缓存未更新 | 1. 动态Bean添加@RefreshScope;2. Nacos修改后点击"发布";3. 核对@Value键名与Nacos配置一致;4. 检查配置监听器是否触发 |
| ES检索缓慢 | 1. 未创建keyword索引;2. 查询条件未用精确匹配;3. ES分片不均;4. 日志字段过多 | 1. 确认traceId/bizId等字段为keyword类型;2. 精确匹配用termQuery+keyword;3. 重新分配分片;4. 禁用冗余字段输出 |
| 分表未自动路由 | 1. 分表算法错误;2. 分表字段格式不匹配;3. 未提前创建分表;4. Sharding-JDBC依赖缺失 | 1. 核对sharding-algorithm-expression;2. 确认create_time格式为yyyy-MM-dd;3. 执行分表初始化脚本;4. 核对Sharding-JDBC依赖 |
| 审计日志解密失败 | 1. AES密钥不一致;2. 加密内容被篡改;3. 字符编码错误 | 1. 确保加密和解密使用同一密钥;2. 检查sys_biz_log表is_modified字段;3. 统一使用UTF-8编码 |