Java基础架构设计(四)| 通用响应与异常处理(单体/分布式通用增强方案)

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日志检索优化(索引设计+查询优化))
  • 四、部署指南&问题排查(通用适配)
    • [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

验证方式

  1. 发起请求,查看SLS控制台和本地日志文件,验证日志是否双写成功;
  2. 模拟ERROR异常(如空指针、业务异常),查看钉钉群是否收到告警消息(含traceId/bizId);
  3. 7天后检查本地日志目录,验证超过7天的日志是否已压缩并上传OSS,本地仅保留最近7天日志;
  4. 90天后检查OSS归档目录,验证过期日志是否自动清理。

场景2:安全合规(金融/政务项目必选)

  • 核心目标:敏感信息脱敏、审计日志不可篡改、满足等保三级要求(解决单体/分布式数据安全风险)
  • 核心实现:注解式脱敏(AOP)+ 审计日志加密 + 数据库/文件只读控制
  • 依赖说明
    • 基础依赖:单体方案的审计日志注解@AuditLogsys_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天);
  • 仅授权只读账号给开发/运维人员,禁止删除权限。

验证方式

  1. 实体类字段添加注解:@Sensitive(type = "phone")@Sensitive(type = "name", rule = "prefix=1,suffix=0"),打印日志时验证是否脱敏;
  2. 查看sys_biz_log表,验证content字段为加密字符串,通过SensitiveUtils.decryptAuditLog可解密;
  3. 手动更新sys_biz_log表数据,验证is_modified字段自动置为1,查询视图v_sys_biz_log无法看到该记录;
  4. 尝试修改本地日志文件(应用用户权限),验证提示"权限不足"。

场景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

验证方式

  1. 应用启动后,在Nacos控制台修改logging.level.com.example为DEBUG,验证应用即时输出DEBUG级别日志(无需重启);
  2. 在Nacos控制台新增脱敏规则address=5,0,标记实体类address字段@Sensitive(type = "address"),验证地址脱敏为"北京市海淀区****";
  3. 在Nacos控制台新增错误码说明630=地址不存在,通过dynamicConfig.getErrorCodeDesc()获取,验证能即时获取到新说明;
  4. 修改告警阈值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;
    }
}

验证方式

  1. 查看数据库,验证sys_log表是否按月份自动路由到对应分表(如2025年1月日志写入sys_log_202501);
  2. 模拟千万级日志写入ES,调用searchLogByTraceId方法,验证响应时间≤1秒;
  3. 执行多条件组合查询(traceId+serviceName+时间范围),验证结果精准且响应快速;
  4. 查看ES索引分片状态,验证数据均匀分布在各个分片,无单点压力。

四、部署指南&问题排查(通用适配)

4.1 部署步骤(按场景细化,兼容单体/分布式)

4.1.1 高可用优化部署(30分钟)

  1. 准备阿里云SLS/OSS资源,获取AK/SK(生产环境存入环境变量);
  2. 部署钉钉机器人,获取Webhook和密钥(Nacos配置);
  3. 上传日志归档脚本到服务器,配置crontab定时任务;
  4. 修改logback-spring.xml,添加SLS和钉钉告警Appender;
  5. 重启应用(仅首次部署需重启,后续配置变更通过Nacos热更新)。

4.1.2 安全合规部署(60分钟)

  1. 集成脱敏注解、工具类、AOP切面到项目(无需修改业务代码);
  2. 初始化AES加密密钥,存入环境变量AUDIT_LOG_AES_SECRET
  3. 执行SQL脚本,给sys_biz_log表添加字段和触发器;
  4. 配置本地日志文件权限(root可写,应用用户只读);
  5. 在SLS控制台开启日志不可删除策略;
  6. 验证脱敏、加密、不可篡改效果(无需重启应用)。

4.1.3 动态配置部署(30分钟)

  1. 复用分布式方案的Nacos配置中心(单体项目需单独部署Nacos);
  2. 在Nacos创建配置文件optimization-core-demo-prod.yaml,填入动态配置;
  3. 项目添加Nacos Config依赖和bootstrap.yml配置;
  4. 集成动态配置Bean、规则解析类、配置监听器;
  5. 启动应用,修改Nacos配置,验证热更新效果。

4.1.4 日志检索优化部署(60分钟)

  1. 集成Sharding-JDBC依赖,配置分表规则;
  2. 执行分表初始化SQL脚本,提前创建全年分表;
  3. 复用分布式方案的ELK集群(单体项目需部署ELK);
  4. 执行ES索引模板创建脚本,优化分词策略;
  5. 修改logback-spring.xml,添加ES输出;
  6. 集成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编码
相关推荐
wheelmouse77882 小时前
Java工程师Python实战教程:通过MCP服务器掌握Python核心语法
java·服务器·python
nix.gnehc2 小时前
Spring AI/Spring AI Alibaba简介
java·人工智能·spring·ai
任子菲阳2 小时前
学JavaWeb第三天——Maven
java·maven·intellij-idea
wadesir2 小时前
Java消息队列入门指南(RabbitMQ与Spring Boot实战教程)
java·rabbitmq·java-rabbitmq
世转神风-2 小时前
qt-初步编译运行报错-When executing step “Make“-无法启动进程“make“
开发语言·qt
ghgxm5202 小时前
EXCEL使用VBA代码实现按条件查询数据库--简单实用
开发语言·数据仓库·笔记·excel·数据库开发
一只懒鱼a2 小时前
SpringBoot整合canal实现数据一致性
java·运维·spring boot
..空空的人2 小时前
C++基于protobuf实现仿RabbitMQ消息队列---服务器模块认识1
服务器·开发语言·c++·分布式·rabbitmq·protobuf
Hello.Reader2 小时前
Flink SQL 新特性Materialized Table 一文讲透(数据新鲜度驱动的自动刷新管道)
java·sql·flink