# 优雅实现动态日志记录

最近在做一个企业后台管理系统,需要在首页展示类似下面的「最近活动」时间线:

10:30  张三 提交了请假申请,年假 - 2023-12-01 至 2023-12-03
09:45  李四 完成了加班申请,项目紧急需求处理
09:15  王五 新员工入职,前端开发工程师
08:30  赵六 预约了会议室A,项目讨论会议

经过几轮折腾,终于搞出了一个非常好用、完全通用的解决方案,特此总结

# 最终效果

  • 只需要在 Controller/Service 方法上加一个 @LogActivity(description = "...") 注解
  • 支持任意数量参数(对象、基本类型都行)
  • 支持从对象中提取任意字段(包括嵌套字段)
  • 支持时间自动格式化
  • 自动记录操作人、时间、成功/失败
  • 存到数据库后直接在前台展示「最近活动」

# 1. 自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogActivity {
    String description() default "";  // 如 "{userName} 新增了打卡记录,时间: {time}"
}

# 2. 核心 AOP 切面(完整可复制版)

先引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
import com.base.entity.ActivityLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Aspect
@Component
public class ActivityLogAspect {
    private static final Logger logger = LoggerFactory.getLogger(ActivityLogAspect.class);
//    private final ActivityLogService activityLogService;  // 注入服务,用于存日志

//    public ActivityLogAspect(ActivityLogService activityLogService) {
//        this.activityLogService = activityLogService;
//    }

    // 切点:拦截标注 @LogActivity 的方法,或指定包/类
    @Around("@annotation(logActivity)")  // 灵活配置
    public Object aroundLog(ProceedingJoinPoint joinPoint, LogActivity logActivity) throws Throwable {
        LocalDateTime startTime = LocalDateTime.now();
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();

        // 获取当前用户(假设 Security 已配置)
        String userName = "匿名用户";
//        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//        if (principal instanceof UserDetails) {
//            userName = ((UserDetails) principal).getUsername();
//        }

        try {
            // 执行原方法
            Object result = joinPoint.proceed();

            // 后置:成功记录
            LocalDateTime endTime = LocalDateTime.now();
            String description = generateDescription(logActivity.description(), joinPoint.getArgs(), methodName);
            saveActivityLog(userName, className + "." + methodName, description, startTime, endTime, "成功");

            return result;
        } catch (Throwable e) {
            // 异常记录
            LocalDateTime endTime = LocalDateTime.now();
            saveActivityLog(userName, className + "." + methodName, "操作失败: " + e.getMessage(), startTime, endTime, "失败");
            logger.error("方法执行异常: {}", methodName, e);
            throw e;
        }
    }

    private String generateDescription(String template, Object[] args, String methodName) {
        if (template == null || template.isEmpty()) {
            return methodName + " 操作完成";
        }

        String result = template;

        // 正则匹配所有 {xxx} 形式的占位符
        Pattern pattern = Pattern.compile("\\{([^}]+)\\}");
        Matcher matcher = pattern.matcher(result);

        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String placeholder = matcher.group(1).trim(); // 如: 0.userName、userName、1、2.type
            String replacement = resolvePlaceholder(placeholder, args);
            matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(sb);

        return sb.toString();
    }

    // 核心解析方法:支持以下写法
// {0}              → args[0].toString()
// {0.userName}     → args[0] 的 userName 字段
// {1}              → args[1].toString()
// {2.status}       → args[2] 的 status 字段
// {userName}       → 默认从第0个参数取 userName(最常用)
    private String resolvePlaceholder(String placeholder, Object[] args) {
        try {
            if (placeholder.matches("\\d+")) {
                // 纯数字:{1} 表示第几个参数
                int index = Integer.parseInt(placeholder);
                return args[index] != null ? args[index].toString() : "未知";
            }

            if (placeholder.matches("\\d+\\..+")) {
                // {0.userName} 格式
                String[] parts = placeholder.split("\\.", 2);
                int index = Integer.parseInt(parts[0]);
                String field = parts[1];
                if (index < args.length && args[index] != null) {
                    return getPropertyValue(args[index], field);
                }
            } else {
                // 没写索引:{userName},默认从第0个参数取(99% 的场景都够用)
                if (args.length > 0 && args[0] != null) {
                    return getPropertyValue(args[0], placeholder);
                }
            }
        } catch (Exception e) {
            // 任何异常都不影响主流程
            return "未知";
        }
        return "未知";
    }

    // BeanWrapper 获取属性(支持嵌套、自动格式化时间)
    private String getPropertyValue(Object obj, String propertyPath) {
        try {
            BeanWrapper wrapper = new BeanWrapperImpl(obj);
            Object value = wrapper.getPropertyValue(propertyPath);

            if (value == null) return "未知";
            if (value instanceof LocalDateTime) {
                return ((LocalDateTime) value).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
            }
            if (value instanceof LocalDate) {
                return value.toString();
            }
            return value.toString();
        } catch (Exception e) {
            return "未知";
        }
    }

    // 保存日志(示例:存到数据库)
    private void saveActivityLog(String userName, String method, String description, LocalDateTime start, LocalDateTime end, String status) {
        ActivityLog log = new ActivityLog();
        log.setUserName(userName);
        log.setMethod(method);
        log.setDescription(description);
        log.setStartTime(start);
        log.setEndTime(end);
        log.setStatus(status);
        log.setTimeStr(start.format(DateTimeFormatter.ofPattern("HH:mm")));  // 如 10:30
//        activityLogService.save(log);
        logger.info(log.toString());
        logger.info("活动日志记录: {} - {}", userName, description);
    }
}

# 3. 实际使用示例(随便写都行)

// 1个对象参数(最常见)
@LogActivity(description = "{userName} 提交了{type}请假,{startDate} 至 {endDate}")
public void submitLeave(LeaveRequest req) { ... }

// 2个参数
@LogActivity(description = "{0.userName} {1}了打卡记录")
public void clockIn(AttendanceRecord record, String action) { ... }

// 3个参数
@LogActivity(description = "{0.userName} 使用{2.name}预约了{1}")
public void bookRoom(User user, String purpose, MeetingRoom room) { ... }

// 混合写法(最推荐)
@LogActivity(description = "{userName} {action}了{type}打卡,时间:{time}")
public void handle(AttendanceRecord record, String action) { ... }

# 4. 实体与查询(MyBatis Plus 示例)

CREATE TABLE `activity_log` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_name` VARCHAR(50) NOT NULL COMMENT '用户名,如:张三',
  `method` VARCHAR(200) NOT NULL COMMENT '方法名,如:leaveService.submitLeave',
  `description` VARCHAR(500) DEFAULT NULL COMMENT '描述,如:提交了请假申请',
  `start_time` DATETIME DEFAULT NULL COMMENT '开始时间',
  `end_time` DATETIME DEFAULT NULL COMMENT '结束时间',
  `status` VARCHAR(20) DEFAULT NULL COMMENT '状态:成功/失败',
  `time_str` VARCHAR(20) DEFAULT NULL COMMENT '时间字符串,如:10:30',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
@Data
@TableName("activity_log")
public class ActivityLog {
    private Long id;
    private String userName;  // 如“张三”
    private String method;    // 如“leaveService.submitLeave”
    private String description;  // 如“提交了请假申请,年假 - 2023-12-01 至 2023-12-03”
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private String status;    // 成功/失败
    private String timeStr;   // 时间字符串,如“10:30”
}

// 查询最近10条
public List<ActivityLog> getRecent() {
    return lambdaQuery()
        .eq(ActivityLog::getStatus, "成功")
        .orderByDesc(ActivityLog::getStartTime)
        .last("limit 10")
        .list();
}

# 总结一句话

加上 @LogActivity(description = "这里随便写,想显示什么写什么") 就完事了
以后再也不用手动写日志了,活动时间线直接从数据库拉,爽得一批!

Last Updated: 12/5/2025, 4:23:48 PM