# 优雅实现动态日志记录
最近在做一个企业后台管理系统,需要在首页展示类似下面的「最近活动」时间线:
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 = "这里随便写,想显示什么写什么") 就完事了
以后再也不用手动写日志了,活动时间线直接从数据库拉,爽得一批!