一、需求描述
如何优雅地记录用户操作日志?
网站后台,功能开发完成后,新增了一个需求,即需要记录用户的各种操作记录。
由于是在开发后期,如果针对每一个功能都去添加一段记录日志的代码,工作量较大、代码侵入性太强,因此采用AOP+注解的方式实现。可读性大大提高,且便于维护和扩展。
AOP:面向切面编程,在不修改现有逻辑代码的情况下,增强功能,恰好体现了spring的理念:无入侵式
自定义注解:当被注解的方法执行时,可以通过切面逻辑捕获操作信息,并将这些信息存储到数据库或写入日志文件
二、代码实现
1.自定义注解
如下自定义一个注解,包含两个属性。这两个属性会在接口方法处使用@AdminLog赋值,并在aop中获取并记录到数据库
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解:记录后台用户操作日志
* @author sxd
*/
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface AdminLog {
String value() default "";
int type() default 0; // 操作日志类型(1查询,2添加,3修改,4删除,5导入,6导出)
}
2.aop配置
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.common.api.dto.LogDTO;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.base.service.BaseCommonService;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
/**
* Aop:自定义注解:记录后台用户操作日志
*
* @author: sxd
* @date: 2025-05-23 16:50
**/
@Aspect
@Component
public class AdminLogAop {
@Resource
private BaseCommonService baseCommonService;
// 使用自定义的注解作为切入点
@Pointcut("@annotation(org.jeecg.modules.aop.AdminLog)")
public void adminLogPointcut() {
}
@Around("adminLogPointcut()")
public Object aroundAdminLogPointcut(ProceedingJoinPoint pjp) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = pjp.proceed();
long costTime = System.currentTimeMillis() - beginTime;
// 获取request
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
// 注解中的信息
String message = method.getAnnotation(AdminLog.class).value();
int operateType = method.getAnnotation(AdminLog.class).type();
// 类名
String className = pjp.getTarget().getClass().getName();
// 方法名
String methodName = signature.getName();
// 请求参数
Object[] args = pjp.getArgs();
StringBuilder params = new StringBuilder();
for (Object arg : args) {
if (arg instanceof MultipartFile) {
params.append(" ").append(((MultipartFile) arg).getOriginalFilename());
} else if (arg instanceof StandardMultipartHttpServletRequest) {
StandardMultipartHttpServletRequest request1 = (StandardMultipartHttpServletRequest) arg;
for (MultipartFile file : request1.getFileMap().values()) {
params.append(" ").append(file.getOriginalFilename());
}
} else {
// arg的部分类型无需处理,因此屏蔽报错
try {
params.append(" ").append(JSONObject.toJSONString(arg));
} catch (Exception e) {
// nothing to do
}
}
}
// LogDTO 根据实际情况保存需要的信息即可
LogDTO dto = new LogDTO();
dto.setLogType(0);
dto.setLogContent(message);
dto.setOperateType(operateType);
dto.setMethod(className + "." + methodName);
dto.setRequestParam(params.toString());
//设置IP地址
dto.setIp(IpUtils.getIpAddr(request));
//获取登录用户信息
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
if (sysUser != null) {
dto.setUserid(sysUser.getUsername());
dto.setUsername(sysUser.getRealname());
}
dto.setCostTime(costTime);
dto.setCreateTime(new Date());
//保存系统日志
baseCommonService.addLog(dto);
return result;
}
}
3.日志记录类
aop中的baseCommonService.addLog(dto);是为了将日志记录到数据库,根据实际情况保存需要的信息即可
方法如下:
@Override
public void addLog(LogDTO logDTO) {
log.info("addLog-received:{}", JSONObject.toJSONString(logDTO));
if(oConvertUtils.isEmpty(logDTO.getId())){
logDTO.setId(String.valueOf(IdWorker.getId()));
}
//保存日志(异常捕获处理,防止数据太大存储失败,导致业务失败)JT-238
try {
logDTO.setCreateTime(new Date());
baseCommonMapper.saveLog(logDTO);
} catch (Exception e) {
log.warn(" LogContent length : "+logDTO.getLogContent().length());
log.warn(e.getMessage());
}
}
4.调用
在需要记录日志的接口上使用自定义的注解@AdminLog 即可
@AdminLog(value = "任务活动-新增活动", type = CommonConstant.OPERATE_TYPE_2)
@ApiOperation("活动添加")
@RequiresPermissions("game:activity:save")
@RequestMapping(value = "save", method = RequestMethod.POST)
public Result<String> save(@RequestBody GameActivitySaveDTO dto) {
if (dto.getId() != null && dto.getId() != 0) {
throw new JeecgBootException("参数错误");
}
gameActivityService.activitySaveOrUpdate(dto);
return Result.ok("添加成功");
}