news 2026/4/23 15:19:17

java通过日志切面实现新增日志,和前两篇可联通

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
java通过日志切面实现新增日志,和前两篇可联通

注意:

不要把日志逻辑写在 AuthAspect 里,那是做权限的。我们新建一个 LogAspect 专门处理日志。

异步入库:写日志是“辅助业务”,绝不能因为数据库插入慢而阻塞主业务接口的响应速度。

  1. 实体类 (SysOperationLog)

对应你的数据库表结构。

packagecom.yige.entity;importlombok.Data;importjava.util.Date;@DatapublicclassSysOperationLog{privateLongid;privateDateoperateTime;// 操作时间privateStringoperateUser;// 操作用户privateStringoperateType;// 操作类型 (查询/新增/导出...)privateStringmodule;// 模块 (法规库/聊天助手...)privateStringipAddress;// IP地址privateStringstatus;// 状态 (成功/失败)}
  1. 异步日志服务 (LogService)

我们需要一个 Service 来专门负责插入数据库。关键点是加上 @Async 注解。

packagecom.yige.service;importcom.yige.entity.SysOperationLog;importorg.springframework.scheduling.annotation.Async;importorg.springframework.stereotype.Service;// 假设你使用 MyBatis 或 MyBatis-Plus// import com.yige.mapper.SysOperationLogMapper;@ServicepublicclassLogService{// @Autowired// private SysOperationLogMapper logMapper;/** * 异步保存日志,不阻塞主线程 */@Async("taskExecutor")// 需要配置线程池,如果没有配,默认也可以publicvoidsaveLog(SysOperationLoglog){try{System.out.println("【异步日志入库】正在保存日志: "+log);// logMapper.insert(log);// 模拟入库耗时Thread.sleep(50);}catch(Exceptione){System.err.println("日志入库失败: "+e.getMessage());}}}

注意:要在启动类 Application.java 上加 @EnableAsync 开启异步支持。

  1. 日志切面 (LogAspect) —— 核心逻辑

这是最关键的部分。它会拦截请求,分析 URL,提取用户信息,并判断成功还是失败。

package com.yige.aspect;importcom.yige.entity.SysOperationLog;importcom.yige.security.SessionManager;importcom.yige.service.LogService;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjavax.servlet.http.HttpServletRequest;importjava.util.Date;@Aspect@ComponentpublicclassLogAspect{@Autowiredprivate LogService logService;@Autowiredprivate SessionManager sessionManager;// 用来获取当前用户// 拦截所有 controller,排除登录接口(如果登录不记日志)和 WebSocket// 这里的规则和 AuthAspect 类似,但通常登录接口也需要记录日志,看你需求@Pointcut("execution(public * com.yige.controller..*.*(..)) "+"&& !@within(javax.websocket.server.ServerEndpoint)")publicvoidlogPointCut(){}@Around("logPointCut()")public ObjectrecordLog(ProceedingJoinPoint point)throws Throwable{long beginTime=System.currentTimeMillis();// 1. 获取 Request 信息ServletRequestAttributes attributes=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();HttpServletRequest request=attributes!=null?attributes.getRequest():null;Object result=null;String status="成功";String errorMsg=null;try{// 2. 执行目标方法result=point.proceed();}catch(Exception e){status="失败";errorMsg=e.getMessage();throwe;// 异常必须抛出去,不然前端收不到报错}finally{// 3. 无论成功失败,都在 finally 里组装日志if(request!=null){saveSysLog(request,status);}}returnresult;}privatevoidsaveSysLog(HttpServletRequest request,String status){SysOperationLog log=newSysOperationLog();log.setOperateTime(newDate());log.setStatus(status);log.setIpAddress(getIpAddress(request));// --- A. 解析用户 (从 Token 获取) ---String token=request.getHeader("Authorization");String userId="访客/未知";if(token!=null){// 这里需要在 SessionManager 加一个 public String getUserId(String token) 方法// 或者直接访问 sessionManager 里的 map (如果改为 public)String foundUser=sessionManager.getUserIdByToken(token);if(foundUser!=null){userId=foundUser;}}log.setOperateUser(userId);// --- B. 解析模块和操作类型 (核心逻辑) ---String uri=request.getRequestURI();// 例如: /api/repo/saveString method=request.getMethod();// GET, POSTanalyzeModuleAndType(uri,method,log);// --- C. 异步调用 Service 入库 ---logService.saveLog(log);}/** * 根据 URL 前缀判断 模块 和 操作类型 */privatevoidanalyzeModuleAndType(String uri,String method,SysOperationLog log){// 默认值String module="其他模块";String type="未知操作";// 1. 判断模块 (根据一级路径)if(uri.contains("/api/repo")){module="法规知识库";}elseif(uri.contains("/api/chat")){module="AI知识助手";}elseif(uri.contains("/api/device")){module="设备监测";}elseif(uri.contains("/api/user")){module="系统管理";}// 2. 判断操作类型 (根据二级路径 或 请求方法)// 优先根据 URL 关键词判断if(uri.endsWith("/save")||uri.endsWith("/add")||uri.endsWith("/create")){type="新增";}elseif(uri.endsWith("/update")||uri.endsWith("/edit")){type="修改";}elseif(uri.endsWith("/delete")||uri.endsWith("/remove")){type="删除";}elseif(uri.endsWith("/export")||uri.endsWith("/download")){type="导出/下载";}elseif(uri.endsWith("/upload")||uri.endsWith("/import")){type="导入/上传";}elseif(uri.endsWith("/ask")||uri.endsWith("/chat")){type="AI问答";}else{// 如果 URL 看不出,就根据 HTTP 方法兜底if("GET".equals(method))type="查询";elseif("POST".equals(method))type="提交";elseif("DELETE".equals(method))type="删除";elseif("PUT".equals(method))type="更新";}log.setModule(module);log.setOperateType(type);}// 获取真实IP的工具方法private StringgetIpAddress(HttpServletRequest request){String ip=request.getHeader("x-forwarded-for");if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getHeader("Proxy-Client-IP");}if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getHeader("WL-Proxy-Client-IP");}if(ip==null||ip.length()==0||"unknown".equalsIgnoreCase(ip)){ip=request.getRemoteAddr();}returnip!=null&&ip.contains(",")?ip.split(",")[0]:ip;}}
  1. 补充 SessionManager 的支持

因为 LogAspect 需要知道是谁在操作,而 SessionManager 之前是把 Map 设为 private 的。你需要在这个类里加一个查询方法:

在 SessionManager.java 中添加:

/** * 根据 Token 获取用户 ID (用于日志记录等) * 不抛异常,找不到就返回 null */publicStringgetUserIdByToken(Stringtoken){if(token==null)returnnull;returntokenToUserMap.get(token);}

不用在每个 Controller 方法里写日志代码,加个注解或者什么都不用做,AOP 全局拦截。
日志入库是异步的,不影响用户体验。

线程池配置:

package com.yige.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.scheduling.annotation.EnableAsync;importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;importjava.util.concurrent.Executor;importjava.util.concurrent.ThreadPoolExecutor;@Configuration@EnableAsync// 开启异步注解支持publicclassThreadPoolConfig{/** * 定义名为 "taskExecutor" 的线程池 * 专门用于处理 @Async("taskExecutor") 的任务 */@Bean("taskExecutor")public ExecutortaskExecutor(){ThreadPoolTaskExecutor executor=newThreadPoolTaskExecutor();// 1. 核心线程数:平时干活的线程// 对于 IO 密集型任务(如写日志/读库),建议设置为 CPU核数 * 2// 假设服务器是 4核,这里设为 8 或 10 都可以executor.setCorePoolSize(10);// 2. 最大线程数:忙不过来时临时雇佣的线程// 当队列满了之后,会创建新线程直到达到这个值executor.setMaxPoolSize(20);// 3. 队列容量:缓冲池// 重要!日志任务因为不涉及返回结果,可以允许排队。// 设置大一点(比如 500-1000),防止瞬时流量高峰导致丢日志executor.setQueueCapacity(1000);// 4. 线程名称前缀:方便排查日志(报错时能看到是 log-thread-xx)executor.setThreadNamePrefix("async-log-");// 5. 拒绝策略:当队列满了,且线程达到最大值,新任务怎么办?// CALLER_RUNS_POLICY (推荐):由调用者所在的线程(主线程)来执行。// 好处:虽然会稍微拖慢一点主接口的响应速度,但保证了**日志绝对不会丢失**。// 坏处:高并发下可能会阻塞主线程。// 如果你觉得日志丢几条无所谓,想保主业务,可以用 DiscardPolicy (直接丢弃)。executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());// 6. 优雅关闭:应用关闭时,是否等待任务执行完?executor.setWaitForTasksToCompleteOnShutdown(true);// 等待时间(秒),超过这个时间强制销毁executor.setAwaitTerminationSeconds(60);executor.initialize();returnexecutor;}}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 13:49:01

Qt QtWebEngine 白屏的解决方案

公众号:cpp手艺人 Qt QtWebEngine 白屏的解决方案 最近在项目中有同事反馈,软件在开启的瞬间和长时间挂机之后,会出现白屏的现象。 先来看看白屏的常见原因和解决方案 1、QtWebEngine 白屏最常见的 5 大原因和解决方案: 主要原因 解决方式 GPU 加速问题 禁用 GPU、使用…

作者头像 李华
网站建设 2026/4/23 12:18:45

TCU变速箱控制器仿真模型:从代码到现实的传动艺术

TCU变速箱控制器仿真模型-含(设计文档) 乘用车AMTTCU变速箱控制器仿真模型算法模块,含,TCU应用层软件,驱动制动数学模型,电机传动数学模型,车辆数学模型等,在售产品已量产。 含有的功能模块包括…

作者头像 李华
网站建设 2026/4/22 14:48:33

QWebEngine 是什么?与 Chromium 的关系解析

公众号:cpp手艺人 QWebEngine 是什么?与 Chromium 的关系解析 1. 概述:QWebEngine 是什么? QWebEngine 是 Qt 框架中用于嵌入现代 Web 内容的核心模块,自 Qt 5.4(2014年)起正式引入,取代了旧版的 QtWebKit。它基于 Chromium 项目构建,为 Qt 应用程序提供高性能、安…

作者头像 李华
网站建设 2026/4/23 20:59:56

QWebEngine 常用 API 全面梳理

公众号:cpp手艺人 QWebEngine 常用 API 全面梳理(超全版本) Qt WebEngine 基于 Chromium,但提供了 Qt 风格的 API。本文对 QWebEngine 的常用类与 API 进行系统梳理,帮助你快速掌握其开发全景。 1. QWebEngineView(视图层) QWebEngineView 是最常用的 UI 控件,主要…

作者头像 李华
网站建设 2026/4/24 3:01:17

探索 COMSOL 光学与相场模拟的奇妙世界

COMSOL光学模型:随机分布颗粒散射,COMSOL光学仿真模型:光镊/光力模型(包含三个模型,近似算法,张量算法)相场模拟——合金,金属凝固模型,各向异性枝晶生长karma合金凝固模型&#xff…

作者头像 李华
网站建设 2026/4/22 12:32:07

【Linux网络编程】UDP Socket

前言:最近在复习 Linux 网络编程,重点梳理了 UDP 协议的实现细节。虽然 UDP 是无连接、不可靠的协议,但其简单高效的特性在很多场景下(如实时音视频、DNS)依然是首选。从最简单的 Echo Server 出发,逐步重构为支持业务解耦的字典服务器,最后实现一个支持多线程的全双工聊…

作者头像 李华