一、系统概述
1.1 功能简介
若依框架的日志管理系统提供:
- 操作日志:记录业务操作(增删改查、导入导出等)
- 登录日志:记录用户登录/登出
- 异步处理:日志写入不阻塞主流程
- 敏感信息过滤:自动过滤密码等敏感字段
1.2 日志类型
1.2.1 操作日志(SysOperLog)
记录业务操作,包含:
- 操作模块、业务类型、操作人员
- 请求URL、IP、地点
- 请求参数、返回结果
- 操作状态、错误信息
- 执行时间、耗时
1.2.2 登录日志(SysLogininfor)
记录登录信息,包含:
- 用户名、登录状态
- IP、登录地点
- 浏览器、操作系统
- 登录时间、提示消息
二、架构设计
2.1 整体架构图
┌─────────────────────────────────────────────────────────┐
│ Controller层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @Log注解标记需要记录日志的方法 │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ AOP切面层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ LogAspect (操作日志切面) │ │
│ │ - @Before: 记录开始时间 │ │
│ │ - @AfterReturning: 正常返回处理 │ │
│ │ - @AfterThrowing: 异常处理 │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 异步任务管理层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AsyncManager (异步任务管理器) │ │
│ │ - 单例模式 │ │
│ │ - 延迟10ms执行 │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AsyncFactory (异步工厂) │ │
│ │ - recordOper(): 创建操作日志任务 │ │
│ │ - recordLogininfor(): 创建登录日志任务 │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 数据持久层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Service层 → Mapper层 → 数据库 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 技术栈
AOP:AspectJ
异步处理:ScheduledExecutorService
数据持久化:MyBatis
JSON处理:FastJSON2
IP地址解析:AddressUtils
三、核心组件详解
3.1 @Log注解
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
/** 模块 */
public String title() default "";
/** 功能 */
public BusinessType businessType() default BusinessType.OTHER;
/** 操作人类别 */
public OperatorType operatorType() default OperatorType.MANAGE;
/** 是否保存请求的参数 */
public boolean isSaveRequestData() default true;
/** 是否保存响应的参数 */
public boolean isSaveResponseData() default true;
/** 排除指定的请求参数 */
public String[] excludeParamNames() default {};
}
参数说明:
title:操作模块名称
businessType:业务类型(INSERT/UPDATE/DELETE/QUERY/EXPORT等)
operatorType:操作人类别(MANAGE/MOBILE/OTHER)
isSaveRequestData:是否保存请求参数
isSaveResponseData:是否保存响应参数
excludeParamNames:排除的参数名(如密码字段)
3.2 LogAspect(操作日志切面)
核心类:com.construct.framework.aspectj.LogAspect
3.2.1 关键属性
LogAspect.java Lines 54-58
@Before(value = "@annotation(controllerLog)")
public void boBefore(JoinPoint joinPoint, Log controllerLog)
{
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
EXCLUDE_PROPERTIES:默认排除的敏感字段(password、oldPassword等)TIME_THREADLOCAL:ThreadLocal,记录方法开始时间
3.2.2 核心方法
boBefore():方法执行前
- 记录开始时间到
ThreadLocal
doAfterReturning():方法正常返回后
- 调用
handleLog()处理日志
doAfterThrowing():方法抛出异常后
- 调用
handleLog(),传入异常信息
handleLog():统一日志处理
LogAspect.java Lines 83-130
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult){
try {
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr();
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
if (loginUser != null)
{
operLog.setOperName(loginUser.getUser().getNickName());
}
if (e != null)
{
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 设置消耗时间
operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
}
catch (Exception exp)
{
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
finally
{
TIME_THREADLOCAL.remove();
}
}
处理流程:
(1) 获取当前登录用户
(2) 创建SysOperLog对象
(3) 设置IP、URL、操作人
(4) 处理异常信息(如有)
(5) 设置方法名、请求方式
(6) 解析注解参数
(7) 计算耗时
(8) 异步保存日志
getControllerMethodDescription():解析注解参数
LogAspect.java ines 139-158
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
{
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData())
{
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog, log.excludeParamNames());
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
{
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
setRequestValue():获取并处理请求参数
LogAspect.java Lines 166-180
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception
{
Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
String requestMethod = operLog.getRequestMethod();
if (StringUtils.isEmpty(paramsMap)
&& (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)))
{
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
}
else
{
operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
}
}
isFilterObject():过滤不需要记录的对象
LogAspect.java Lines 222-249
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o)
{
Class<?> clazz = o.getClass();
if (clazz.isArray())
{
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
}
else if (Collection.class.isAssignableFrom(clazz))
{
Collection collection = (Collection) o;
for (Object value : collection)
{
return value instanceof MultipartFile;
}
}
else if (Map.class.isAssignableFrom(clazz))
{
Map map = (Map) o;
for (Object value : map.entrySet())
{
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
过滤对象:
MultipartFile(文件上传)
HttpServletRequest、HttpServletResponse
BindingResult(参数校验结果)
3.3 AsyncManager(异步任务管理器)
AsyncManager.java Lines 15-56
public class AsyncManager
{
/**
* 操作延迟10毫秒
*/
private final int OPERATE_DELAY_TIME = 10;
/**
* 异步操作任务调度线程池
*/
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
/**
* 单例模式
*/
private AsyncManager(){}
private static AsyncManager me = new AsyncManager();
public static AsyncManager me()
{
return me;
}
/**
* 执行任务
*
* @param task 任务
*/
public void execute(TimerTask task)
{
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}
/**
* 停止任务线程池
*/
public void shutdown()
{
Threads.shutdownAndAwaitTermination(executor);
}
}
特点:
- 单例模式
- 延迟
10ms执行,避免影响主流程
- 使用
ScheduledExecutorService执行任务
3.4 AsyncFactory(异步工厂)
AsyncFactory.java Lines 99-111
public static TimerTask recordOper(final SysOperLog operLog)
{
return new TimerTask()
{
@Override
public void run()
{
// 远程查询操作地点
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
}
};
}
功能:
recordOper():创建操作日志任务- 根据IP查询操作地点
- 调用
Service保存日志
recordLogininfor():创建登录日志任务- 解析
User-Agent获取浏览器和操作系统 - 根据IP查询登录地点
- 保存登录日志
- 解析
3.5 线程池配置
ThreadPoolConfig.java Lines 45-62
/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService()
{
return new ScheduledThreadPoolExecutor(corePoolSize,
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy())
{
@Override
protected void afterExecute(Runnable r, Throwable t)
{
super.afterExecute(r, t);
Threads.printException(r, t);
}
};
}
配置参数:
- 核心线程数:50
- 最大线程数:200
- 队列容量:1000
- 线程命名:
schedule-pool-%d - 守护线程:是
- 拒绝策略:
CallerRunsPolicy
3.6 实体类
3.6.1 SysOperLog(操作日志实体)
主要字段:
operId:日志主键title:操作模块businessType:业务类型(0其它 1新增 2修改 3删除等)method:请求方法(类名.方法名)requestMethod:请求方式(GET/POST等)operatorType:操作类别(0其它 1后台用户 2手机端用户)operName:操作人员operUrl:请求URLoperIp:操作IPoperLocation:操作地点operParam:请求参数(JSON,最大2000字符)jsonResult:返回参数(JSON,最大2000字符)status:操作状态(0正常 1异常)errorMsg:错误消息(最大2000字符)operTime:操作时间costTime:消耗时间(毫秒)
四、实现流程
4.1 操作日志记录流程
1. Controller方法执行
↓
2. @Log注解触发AOP切面
↓
3. LogAspect.boBefore()
- 记录开始时间到ThreadLocal
↓
4. 业务方法执行
↓
5. LogAspect.doAfterReturning() 或 doAfterThrowing()
↓
6. LogAspect.handleLog()
- 获取用户信息
- 创建SysOperLog对象
- 设置基本信息(IP、URL、方法名等)
- 解析@Log注解参数
- 处理请求参数和响应结果
- 计算执行耗时
↓
7. AsyncManager.execute()
- 延迟10ms执行
↓
8. AsyncFactory.recordOper()
- 根据IP查询操作地点
- 调用Service保存日志
↓
9. SysOperLogService.insertOperlog()
- 保存到数据库
4.2 登录日志记录流程
1. 用户登录/登出
↓
2. 调用AsyncManager.execute(AsyncFactory.recordLogininfor(...))
↓
3. AsyncFactory.recordLogininfor()
- 解析User-Agent获取浏览器和操作系统
- 获取IP地址
- 根据IP查询登录地点
- 创建SysLogininfor对象
↓
4. SysLogininforService.insertLogininfor()
- 保存到数据库
4.3 参数处理流程
1. 检查是否需要保存请求参数(isSaveRequestData)
↓
2. 获取请求参数
- GET请求:从request.getParameterMap()获取
- POST/PUT请求:从方法参数获取
↓
3. 过滤敏感字段
- 默认过滤:password、oldPassword、newPassword、confirmPassword
- 注解指定过滤:excludeParamNames
↓
4. 过滤不需要的对象
- MultipartFile、HttpServletRequest、HttpServletResponse、BindingResult
↓
5. 转换为JSON字符串(最大2000字符)
↓
6. 保存到operParam字段
五、使用指南
5.1 基本使用
在Controller方法上添加@Log注解:
@RestController
@RequestMapping("/system/user")
public class SysUserController {
@Log(title = "用户管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysUser user) {
// 业务逻辑
return AjaxResult.success();
}
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysUser user) {
// 业务逻辑
return AjaxResult.success();
}
@Log(title = "用户管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds) {
// 业务逻辑
return AjaxResult.success();
}
@Log(title = "用户管理", businessType = BusinessType.QUERY)
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
// 业务逻辑
return getDataTable(list);
}
}
5.2 高级用法
5.2.1 排除敏感参数
@Log(
title = "用户管理",
businessType = BusinessType.UPDATE,
excludeParamNames = {"password", "secretKey"}
)
@PutMapping
public AjaxResult updateUser(@RequestBody SysUser user) {
// 业务逻辑
}
5.2.2 不保存请求参数
@Log(
title = "用户管理",
businessType = BusinessType.QUERY,
isSaveRequestData = false
)
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
// 业务逻辑
}
5.2.3 不保存响应结果
@Log(
title = "用户管理",
businessType = BusinessType.QUERY,
isSaveResponseData = false
)
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
// 业务逻辑
}
5.2.4 指定操作人类别
@Log(
title = "移动端用户管理",
businessType = BusinessType.INSERT,
operatorType = OperatorType.MOBILE
)
@PostMapping
public AjaxResult add(@RequestBody SysUser user) {
// 业务逻辑
}
5.3 业务类型枚举
public enum BusinessType {
OTHER, // 其它
INSERT, // 新增
UPDATE, // 修改
DELETE, // 删除
GRANT, // 授权
EXPORT, // 导出
IMPORT, // 导入
FORCE, // 强退
GENCODE, // 生成代码
CLEAN, // 清空数据
QUERY // 查询
}
5.4 操作人类别枚举
public enum OperatorType {
OTHER, // 其它
MANAGE, // 后台用户
MOBILE // 手机端用户
}
六、技术要点
6.1 AOP切面技术
- 使用
@Aspect和@Component声明切面 - 使用
@Before、@AfterReturning、@AfterThrowing定义切点 - 切点表达式:
@annotation(controllerLog),拦截带@Log注解的方法
6.2 ThreadLocal使用
- 使用
NamedThreadLocal记录方法开始时间 - 线程安全,避免并发问题
- 在
finally中清理,防止内存泄漏
6.3 异步处理
- 使用
ScheduledExecutorService实现异步 - 延迟
10ms执行,不阻塞主流程 - 单例模式管理异步任务
6.4 敏感信息过滤
- 默认过滤密码相关字段
- 支持注解指定过滤字段
- 使用
PropertyPreExcludeFilter实现JSON序列化过滤
6.5 IP地址解析
- 使用
IpUtils.getIpAddr()获取真实IP - 使用
AddressUtils.getRealAddressByIP()查询地理位置 - 支持代理服务器场景
6.6 参数长度限制
- 请求参数:最大2000字符
- 响应结果:最大2000字符
- 错误消息:最大2000字符
- 使用
StringUtils.substring()截断
6.7 异常处理
- 切面捕获异常,不影响业务
- 记录异常信息到日志
- 异常时设置
status为FAIL,记录errorMsg
七、扩展说明
7.1 ControllerLogsAspect(调试日志切面)
除了LogAspect,还有一个ControllerLogsAspect用于开发调试:
@Component
@Aspect
@Order(1)
@Slf4j
public class ControllerLogsAspect {
@Pointcut("execution(public * com.construct.*.controller..*.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void doBeforeInServiceLayer(JoinPoint joinPoint) {
// 记录开始时间
}
@AfterReturning(pointcut = "pointCut()", returning = "returnValue")
public void log(JoinPoint joinPoint, Object returnValue) {
// 打印调试日志
}
}
功能:
- 拦截所有
Controller方法 - 记录请求URL、参数、响应结果、处理时间
- 仅非生产环境输出日志
- 排除静态资源请求
7.2 业务日志扩展
系统还提供了业务日志扩展(如PubOperateLog、DmsOperateLog),用于记录业务数据变更:特点:
- 记录字段级别的变更(修改前/修改后)
- 支持业务ID关联
- 支持操作类型(创建/编辑)
- 支持操作详情记录
7.3 日志查询和管理
系统提供了日志查询和管理功能:
- 操作日志查询:
/monitor/operlog/list - 登录日志查询:
/monitor/logininfor/list - 日志导出:支持Excel导出
- 日志删除:支持批量删除和清空
八、总结
8.1 设计优势
- 非侵入:通过注解实现,不影响业务代码
- 异步处理:日志写入不阻塞主流程
- 灵活配置:支持多种配置选项
- 安全过滤:自动过滤敏感信息
- 异常处理:完善的异常处理机制
- 性能优化:延迟执行、线程池管理
8.2 适用场景
- 操作审计:记录用户操作
- 问题排查:通过日志定位问题
- 性能监控:记录方法执行时间
- 安全审计:记录登录和操作信息
8.3 注意事项
- 日志表会持续增长,需要定期清理
- 异步执行可能丢失日志(极端情况)
- 参数长度限制可能截断重要信息
- IP地址解析依赖外部服务,可能影响性能
- 生产环境建议关闭调试日志切面
附录:相关文件清单
核心文件
LogAspect.java- 操作日志切面ControllerLogsAspect.java- 控制器日志切面(调试用)Log.java- 日志注解定义AsyncManager.java- 异步任务管理器AsyncFactory.java- 异步工厂ThreadPoolConfig.java- 线程池配置
实体类
SysOperLog.java- 操作日志实体SysLogininfor.java- 登录日志实体
服务层
ISysOperLogService.java- 操作日志服务接口SysOperLogServiceImpl.java- 操作日志服务实现ISysLogininforService.java- 登录日志服务接口SysLogininforServiceImpl.java- 登录日志服务实现
数据层
SysOperLogMapper.java- 操作日志Mapper接口SysOperLogMapper.xml- 操作日志Mapper XMLSysLogininforMapper.java- 登录日志Mapper接口SysLogininforMapper.xml- 登录日志Mapper XML
控制器
SysOperlogController.java- 操作日志管理控制器SysLogininforController.java- 登录日志管理控制器