19 我的日志切面,让老板直接看到了员工的薪资
19 我的日志切面,让老板直接看到了员工的薪资
周五下午,老板突然把我叫到办公室:"为什么日志里能看到所有人的工资?"我看着生产环境日志里明晃晃的
用户薪资: 25000.00,冷汗瞬间湿透了后背。那一刻我才明白:AOP不是魔法棒,用错了会泄露机密。

一、Spring AOP:从"神奇魔法"到"理解代价"
1.1 我第一次用AOP的"惨痛教训"
我写了一个"完美"的日志切面:
java
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Around("execution(* com.company..*.*(..))")
public Object logAllMethods(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
// 记录入参
Object[] args = joinPoint.getArgs();
log.info("【调用开始】{} 参数: {}", methodName, Arrays.toString(args));
try {
Object result = joinPoint.proceed();
// 记录返回值 - 这就是灾难的开始
log.info("【调用成功】{} 返回值: {}", methodName, result);
return result;
} catch (Exception e) {
log.error("【调用失败】{} 异常: {}", methodName, e.getMessage(), e);
throw e;
}
}
}
看起来很好,对吧?直到我看到生产日志:
text
【调用成功】SalaryService.getSalary(..) 返回值: SalaryDTO(id=123, employeeId=456, amount=25000.00, month=2023-10)
问题:
- 敏感信息泄露:薪资是最高机密,怎么能出现在日志里?
- 日志爆炸:每个方法调用都记录,一天产生100GB日志
- 性能影响:序列化大对象(如包含1000个元素的List)极其耗时
1.2 重构后的安全切面
导师教我如何正确设计切面:

java
@Aspect
@Component
@Slf4j
public class SafeLoggingAspect {
// 1. 使用注解精确控制,而不是拦截所有方法
@Pointcut("@annotation(com.company.annotation.OperationLog)")
public void operationLogPointcut() {}
// 2. 使用SpEL表达式动态获取日志内容
@Around("@annotation(operationLog)")
public Object logOperation(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable {
long startTime = System.currentTimeMillis();
String operation = operationLog.value();
// 只记录必要信息,不记录敏感参数
log.debug("【操作开始】{}", operation);
try {
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
// 3. 使用脱敏工具处理返回值
String resultLog = safeLogResult(result, operationLog.maskFields());
// 4. 根据日志级别动态输出
if (costTime > 1000) {
log.warn("【操作完成-慢查询】{} 耗时: {}ms 结果: {}",
operation, costTime, resultLog);
} else {
log.debug("【操作完成】{} 耗时: {}ms", operation, costTime);
}
return result;
} catch (Exception e) {
long costTime = System.currentTimeMillis() - startTime;
// 5. 区分业务异常和系统异常
if (e instanceof BusinessException) {
log.warn("【操作失败-业务异常】{} 耗时: {}ms 原因: {}",
operation, costTime, e.getMessage());
} else {
log.error("【操作失败-系统异常】{} 耗时: {}ms",
operation, costTime, e);
}
throw e;
}
}
private String safeLogResult(Object result, String[] maskFields) {
if (result == null) {
return "null";
}
// 如果是集合或数组,只记录大小,不记录内容
if (result instanceof Collection) {
return String.format("Collection[size=%d]", ((Collection<?>) result).size());
}
if (result.getClass().isArray()) {
return String.format("Array[length=%d]", Array.getLength(result));
}
// 使用脱敏工具
try {
return MaskUtil.maskFields(result, maskFields);
} catch (Exception e) {
// 脱敏失败时,只记录类型信息
return String.format("%s[masked]", result.getClass().getSimpleName());
}
}
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String value(); // 操作描述
String[] maskFields() default {}; // 需要脱敏的字段
}
// 使用示例
@RestController
@RequestMapping("/salary")
public class SalaryController {
@OperationLog(value = "查询员工薪资", maskFields = {"amount", "bankCardNo"})
@GetMapping("/{employeeId}")
public SalaryDTO getSalary(@PathVariable String employeeId) {
return salaryService.getSalary(employeeId);
}
}
二、Spring Boot自动配置:从"黑盒魔法"到"透明理解"
2.1 那个让我debug三天的配置问题

我的项目突然启动失败:
text
Parameter 0 of method redisTemplate in RedisAutoConfiguration required a single bean, but 2 were found:
- lettuceConnectionFactory
- jedisConnectionFactory
我既没有配置Lettuce,也没有配置Jedis。为什么会这样?
2.2 理解自动配置的原理
导师让我打开spring-boot-autoconfigure包的源码:
java
// RedisAutoConfiguration.java的关键部分
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
// ...
}
}
java
// LettuceConnectionConfiguration.java
@Configuration
@ConditionalOnClass(RedisClient.class)
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
class LettuceConnectionConfiguration {
// 当有RedisClient类,且没有指定client-type或指定为lettuce时生效
}
// JedisConnectionConfiguration.java
@Configuration
@ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "jedis")
class JedisConnectionConfiguration {
// 当有Jedis相关类,且明确指定client-type为jedis时生效
}
问题根源:我的项目中同时引入了Lettuce和Jedis的依赖,Spring Boot无法自动选择。
解决方案:
yaml
# application.yml
spring:
redis:
client-type: lettuce # 明确指定使用lettuce
或者排除不需要的依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2.3 自定义自动配置的实战
有一次,我们需要为所有微服务提供统一的ID生成器(雪花算法)。我写了一个自定义starter:
第一步:创建starter项目结构
text
snowflake-spring-boot-starter/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── company/
│ │ │ └── snowflake/
│ │ │ ├── SnowflakeProperties.java
│ │ │ ├── SnowflakeAutoConfiguration.java
│ │ │ └── IdGenerator.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── spring/
│ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ └── test/
第二步:实现核心代码
java
// 配置属性类
@ConfigurationProperties(prefix = "snowflake")
@Data
public class SnowflakeProperties {
/**
* 工作节点ID (0-31)
*/
private long workerId = 0;
/**
* 数据中心ID (0-31)
*/
private long datacenterId = 0;
/**
* 是否启用
*/
private boolean enabled = true;
/**
* 序列号位数
*/
private long sequenceBits = 12L;
/**
* 工作节点位数
*/
private long workerIdBits = 5L;
/**
* 数据中心位数
*/
private long datacenterIdBits = 5L;
}
// ID生成器
@Component
@ConditionalOnProperty(prefix = "snowflake", name = "enabled", havingValue = "true")
public class IdGenerator {
private final SnowflakeProperties properties;
// 起始时间戳(2023-01-01)
private final long twepoch = 1672531200000L;
private long sequence = 0L;
private long lastTimestamp = -1L;
public IdGenerator(SnowflakeProperties properties) {
this.properties = properties;
validateConfiguration();
}
private void validateConfiguration() {
long maxWorkerId = ~(-1L << properties.getWorkerIdBits());
if (properties.getWorkerId() > maxWorkerId || properties.getWorkerId() < 0) {
throw new IllegalArgumentException(
String.format("workerId不能大于%d或小于0", maxWorkerId));
}
long maxDatacenterId = ~(-1L << properties.getDatacenterIdBits());
if (properties.getDatacenterId() > maxDatacenterId || properties.getDatacenterId() < 0) {
throw new IllegalArgumentException(
String.format("datacenterId不能大于%d或小于0", maxDatacenterId));
}
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("时钟回拨拒绝生成ID。回拨时间: %dms",
lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
long sequenceMask = ~(-1L << properties.getSequenceBits());
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
long timestampLeftShift = properties.getSequenceBits() +
properties.getWorkerIdBits() +
properties.getDatacenterIdBits();
long datacenterIdShift = properties.getSequenceBits() +
properties.getWorkerIdBits();
return ((timestamp - twepoch) << timestampLeftShift)
| (properties.getDatacenterId() << datacenterIdShift)
| (properties.getWorkerId() << properties.getSequenceBits())
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
// 自动配置类
@Configuration
@ConditionalOnClass(IdGenerator.class)
@EnableConfigurationProperties(SnowflakeProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class) // 在数据源配置之后
public class SnowflakeAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(SnowflakeAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "snowflake", name = "enabled", havingValue = "true")
public IdGenerator idGenerator(SnowflakeProperties properties) {
log.info("初始化雪花算法ID生成器,workerId={}, datacenterId={}",
properties.getWorkerId(), properties.getDatacenterId());
return new IdGenerator(properties);
}
// 提供RestTemplate的拦截器,自动为请求添加Trace ID
@Bean
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnMissingBean(name = "traceIdRestTemplateInterceptor")
public ClientHttpRequestInterceptor traceIdRestTemplateInterceptor(IdGenerator idGenerator) {
return (request, body, execution) -> {
request.getHeaders().add("X-Trace-Id", String.valueOf(idGenerator.nextId()));
return execution.execute(request, body);
};
}
}
第三步:配置自动装配
在src/main/resources/META-INF/spring/下创建文件:
text
# org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.company.snowflake.SnowflakeAutoConfiguration
第四步:打包发布
xml
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>snowflake-spring-boot-starter</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>11</java.version>
<spring-boot.version>2.7.15</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot自动配置核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 配置处理器,让IDE能识别配置属性 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
第五步:使用starter
其他项目只需要:
- 引入依赖:
xml
<dependency>
<groupId>com.company</groupId>
<artifactId>snowflake-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
- 配置参数:
yaml
snowflake:
enabled: true
worker-id: 1
datacenter-id: 1
- 直接使用:
java
@Service
public class OrderService {
@Autowired
private IdGenerator idGenerator;
public Order createOrder(OrderDTO dto) {
Order order = new Order();
order.setId(idGenerator.nextId()); // 生成分布式ID
// ...
return order;
}
}
三、Maven高级:从"依赖地狱"到"精准控制"

3.1 我遇到的依赖冲突
项目启动时:
text
java.lang.NoSuchMethodError: org.yaml.snakeyaml.LoaderOptions.setMaxAliasesForCollections(I)V
这是典型的依赖冲突。通过mvn dependency:tree查看:
text
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.15:compile [INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.7.15:compile [INFO] | | \- org.yaml:snakeyaml:jar:1.30:compile [INFO] +- com.thirdparty:some-lib:jar:1.0.0:compile [INFO] | \- org.yaml:snakeyaml:jar:1.25:compile
SnakeYAML有1.30和1.25两个版本,Maven选择了1.25(路径更短?),但这个版本没有setMaxAliasesForCollections方法。
3.2 Maven依赖调解的真相
导师教我理解Maven的依赖调解:
- 最短路径优先:谁离项目近就用谁
- 第一声明优先:路径一样长时,谁先声明就用谁
- 排除传递依赖:可以排除不需要的传递依赖
- 依赖管理:统一管理所有依赖版本
xml
<!-- 解决方案1:排除旧版本 -->
<dependency>
<groupId>com.thirdparty</groupId>
<artifactId>some-lib</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 解决方案2:统一版本管理 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.30</version>
</dependency>
</dependencies>
</dependencyManagement>
3.3 多模块项目的最佳实践
我们有一个大型项目,拆分成多个模块:
text
company-parent/
├── pom.xml (父工程)
├── company-common/ (通用模块)
│ ├── pom.xml
│ └── src/
├── company-domain/ (领域模型)
│ ├── pom.xml
│ └── src/
├── company-repository/ (数据访问)
│ ├── pom.xml
│ └── src/
├── company-service/ (业务逻辑)
│ ├── pom.xml
│ └── src/
├── company-web/ (Web接口)
│ ├── pom.xml
│ └── src/
└── company-batch/ (批处理)
├── pom.xml
└── src/
父pom的关键配置:
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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>company-common</module>
<module>company-domain</module>
<module>company-repository</module>
<module>company-service</module>
<module>company-web</module>
<module>company-batch</module>
</modules>
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一版本管理 -->
<spring-boot.version>2.7.15</spring-boot.version>
<mybatis.version>3.5.13</mybatis.version>
<mysql.version>8.0.33</mysql.version>
<jackson.version>2.14.2</jackson.version>
<lombok.version>1.18.28</lombok.version>
</properties>
<!-- 依赖管理:统一所有子模块的依赖版本 -->
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 其他统一管理的依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 所有子模块共享的构建配置 -->
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip> <!-- 子模块默认不打包为可执行jar -->
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<!-- 所有子模块共享的仓库配置 -->
<repositories>
<repository>
<id>aliyun</id>
<name>Aliyun Maven Repository</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
子模块pom的配置:
xml
<!-- company-service/pom.xml -->
<project>
<parent>
<groupId>com.company</groupId>
<artifactId>company-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>company-service</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- 依赖兄弟模块 -->
<dependency>
<groupId>com.company</groupId>
<artifactId>company-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.company</groupId>
<artifactId>company-repository</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 外部依赖(无需指定版本) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
四、经验总结:进阶之路的必经之痛

4.1 AOP的注意事项
- 性能影响:AOP会增加方法调用的开销,特别是环绕通知
- 异常处理:切面中的异常可能掩盖原始异常
- 代理限制:AOP无法增强私有方法、静态方法、final方法
- 循环依赖:AOP可能引起循环依赖问题
4.2 Spring Boot自动配置的坑
- 条件竞争:多个自动配置类竞争同一个Bean
- 配置覆盖:自定义配置可能被自动配置覆盖
- 启动顺序:自动配置的加载顺序不可控
- 诊断困难:配置不生效时,难以定位原因
4.3 Maven的最佳实践
- 统一版本管理:使用dependencyManagement统一版本
- 模块化设计:合理拆分模块,降低耦合
- 持续集成:配合Jenkins等工具自动化构建
- 依赖分析:定期使用
mvn dependency:analyze分析依赖
结语:从使用者到创造者
学习这些进阶技术后,我最大的变化是:
从"框架能做什么"的思考,转变为"我需要框架做什么"的设计。
当遇到问题时,我不再只是搜索解决方案,而是思考:
- 这个问题是偶发的还是必然的?
- 有没有通用的解决方案?
- 能否设计一个框架或工具来一劳永逸地解决?
更重要的是,我学会了敬畏复杂性。每一个看似简单的"魔法"背后,都是大量的设计和权衡。
记住:真正的技术高手,不是知道多少API,而是理解技术背后的原理和取舍。 只有这样,才能在复杂的业务场景中做出正确的技术选型。
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能