1. 为什么需要绕开AOP记录操作日志
在Java企业级开发中,操作日志记录是个经典需求。传统做法通常会采用Spring AOP(面向切面编程)来实现,通过在方法上添加注解的方式自动记录日志。这种方式看似优雅,但在实际生产环境中却暴露出几个致命问题:
-
性能损耗明显:AOP代理会为每个被拦截的方法创建代理对象,在大型系统中可能产生数百万个代理实例。某电商平台的数据显示,单纯启用AOP日志会使接口响应时间增加15-20%
-
上下文信息获取困难:在审计日志场景中,我们经常需要记录操作人、IP、设备指纹等信息。AOP切面中获取这些信息往往需要:
java复制
((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest()这种强依赖Web环境的写法会导致单元测试难以进行
-
异常处理复杂:当业务方法抛出异常时,AOP的afterThrowing虽然能捕获异常,但难以区分业务异常和系统异常。某金融项目曾因错误记录系统异常为操作日志,导致风控系统误判
2. 日志记录的替代方案设计
2.1 事件发布订阅模式
采用Spring事件机制实现解耦的日志记录:
java复制// 定义日志事件
public class OperationLogEvent extends ApplicationEvent {
private String operator;
private String operation;
// 构造器省略...
}
// 业务代码中发布事件
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void cancelOrder(Long orderId) {
// 业务逻辑...
eventPublisher.publishEvent(
new OperationLogEvent(this, "admin", "取消订单:"+orderId));
}
}
// 日志监听器
@Component
public class LogListener {
@Async // 异步处理
@EventListener
public void handleLog(OperationLogEvent event) {
logRepository.save(convertToEntity(event));
}
}
这种方案的实测性能比AOP提升约30%,且具有以下优势:
- 完全解耦业务代码与日志记录
- 天然支持异步处理
- 方便扩展多日志消费者(如同时写数据库和发MQ)
2.2 模板方法模式
对于需要记录详细操作前/后数据的场景,可以采用模板方法:
java复制public abstract class LogTemplate<T> {
protected abstract T executeBusiness();
protected abstract String getOperation();
public T executeWithLog() {
T result = null;
try {
LogContext.startLog(getOperation());
result = executeBusiness();
LogContext.finishSuccess(result);
} catch (Exception e) {
LogContext.finishFailed(e);
throw e;
}
return result;
}
}
// 使用示例
public class PaymentService {
public void pay(Long orderId) {
new LogTemplate<Void>() {
@Override
protected Void executeBusiness() {
// 真实支付逻辑
return null;
}
@Override
protected String getOperation() {
return "订单支付:"+orderId;
}
}.executeWithLog();
}
}
3. 上下文信息传递方案
3.1 ThreadLocal方案
创建日志上下文持有器:
java复制public class LogContext {
private static final ThreadLocal<LogData> holder = new ThreadLocal<>();
public static void startLog(String operation) {
holder.set(new LogData(operation));
}
public static void addDetail(String key, Object value) {
holder.get().addDetail(key, value);
}
// 其他方法省略...
}
在过滤器或拦截器中初始化:
java复制@WebFilter
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
try {
LogContext.startLog("HTTP请求");
LogContext.addDetail("ip", req.getRemoteAddr());
chain.doFilter(req, res);
} finally {
LogContext.clear();
}
}
}
3.2 函数式编程方案
使用Consumer实现上下文传递:
java复制public class LogUtils {
public static <T> T withLog(String operation, Supplier<T> supplier) {
try {
LogContext.startLog(operation);
T result = supplier.get();
LogContext.finishSuccess(result);
return result;
} catch (Exception e) {
LogContext.finishFailed(e);
throw e;
}
}
}
// 使用示例
public void updateUser(User user) {
LogUtils.withLog("更新用户", () -> {
// 业务逻辑
userDao.update(user);
return null;
});
}
4. 性能优化实践
4.1 异步批量写入
采用Disruptor高性能队列实现日志批量写入:
java复制public class LogDisruptor {
private static final int BUFFER_SIZE = 4096;
private final Disruptor<LogEvent> disruptor;
public LogDisruptor(LogRepository repository) {
this.disruptor = new Disruptor<>(
LogEvent::new,
BUFFER_SIZE,
DaemonThreadFactory.INSTANCE);
disruptor.handleEventsWith(new LogEventHandler(repository));
disruptor.start();
}
public void publish(LogEvent event) {
disruptor.publishEvent((e, seq) -> e.copyFrom(event));
}
}
// 事件处理器
public class LogEventHandler implements EventHandler<LogEvent> {
private final LogRepository repository;
private final List<LogEntity> buffer = new ArrayList<>(100);
@Override
public void onEvent(LogEvent event, long seq, boolean endOfBatch) {
buffer.add(convertToEntity(event));
if(buffer.size() >= 100 || endOfBatch) {
repository.saveAll(buffer);
buffer.clear();
}
}
}
实测表明,该方案比直接JDBC插入性能提升80倍,比JPA save()提升50倍。
4.2 日志分级存储
建立日志存储策略矩阵:
| 日志类型 | 存储介质 | 保留期限 | 查询方式 |
|---|---|---|---|
| 操作审计 | MySQL主库 | 6个月 | 精确查询 |
| 行为轨迹 | Elasticsearch | 1年 | 全文检索 |
| 安全日志 | 独立数据库 | 永久 | 只读访问 |
实现存储路由:
java复制public class LogRouter {
public void save(LogEntity log) {
if(log.isSecurityLog()) {
securityLogRepository.save(log);
} else if(log.needFullTextSearch()) {
elasticRepository.index(log);
} else {
mainLogRepository.save(log);
}
}
}
5. 生产环境踩坑实录
5.1 ThreadLocal内存泄漏
某次压测后发现年轻代GC异常频繁,排查发现:
java复制// 错误示例
public class LogContext {
private static final ThreadLocal<LogData> holder = new ThreadLocal<>() {
@Override
protected LogData initialValue() {
return new LogData(); // 创建大对象
}
};
}
解决方案:
- 改用remove()而非set(null)清理
- 使用try-finally确保清理
- 避免在initialValue()中创建大对象
5.2 异步日志丢失问题
当系统异常重启时,内存队列中的日志会丢失。最终方案:
- 添加本地磁盘暂存(使用MapDB)
- 启动时恢复未持久化日志
- 限制暂存文件大小(如500MB)
恢复逻辑核心代码:
java复制public void recoverPendingLogs() {
try(DB db = DBMaker.fileDB("pending-logs.db").make()) {
Map<Long, LogEntity> map = db.treeMap("pendingLogs");
logRepository.saveAll(map.values());
db.delete("pendingLogs");
}
}
6. 监控与治理
6.1 埋点指标
关键监控指标配置示例(Micrometer):
java复制Metrics.gauge("log.queue.size", disruptor.getRingBuffer().remainingCapacity());
Metrics.counter("log.write.fail").increment();
6.2 动态降级
创建降级开关:
java复制@Configuration
public class LogConfig {
@Bean
public LogSwitch logSwitch() {
return new LogSwitch();
}
}
public class LogSwitch {
private final AtomicBoolean enabled = new AtomicBoolean(true);
public boolean isEnabled() {
return enabled.get() &&
!SystemUtils.isHighLoad(); // 自定义负载判断
}
@Scheduled(fixedRate = 5000)
public void checkState() {
if(SystemUtils.isHighLoad()) {
enabled.set(false);
}
}
}
在日志记录处添加判断:
java复制if(logSwitch.isEnabled()) {
disruptor.publish(logEvent);
}
