AOP与日志(下)

发布时间:2023年12月20日
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

日志分类

有时候我们所谓的“记录一下日志”,可能有两种含义:

  • 通过@Slf4j打印日志,方便开发人员排查问题(包括统一的接口日志和自己打的log)
  • 对重要接口进行用户行为记录,出现问题时方便管理员追责

一般来说,前者的数据被保存在log文件中,只是面向开发者,普通用户不易阅读,而后者被称为“用户行为日志”,通常还会额外提供查询接口和专门的页面,方便管理员查看操作记录。今天,我们就来聊聊所谓的“用户行为日志”。

代码

之前已经写过很多AOP和枚举的代码了,这里就不再重复介绍,直接展示代码(MyBatis-Plus):

用户日志SQL

CREATE TABLE `sys_user_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `module_code` varchar(255) NOT NULL DEFAULT '',
  `type` tinyint(2) NOT NULL,
  `title` varchar(255) NOT NULL DEFAULT '',
  `operator_id` bigint(20) NOT NULL,
  `operate_time` datetime NOT NULL,
  `content` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4;

UserLogDO

@Data
@TableName("sys_user_log")
public class UserLogDO {

    private Long id;

    /**
     * 本次操作的系统模块
     *
     * @see com.bravo.demo.aspect.ModuleEnum
     */
    private String moduleCode;
    /**
     * 操作类型
     *
     * @see com.bravo.demo.aspect.OperationEnum
     */
    private Integer type;
    /**
     * 标题
     */
    private String title;
    /**
     * 操作人
     */
    private Long operatorId;
    /**
     * 操作时间
     */
    private Date operateTime;
    /**
     * 操作内容
     */
    private String content;

}

UserLogDTO

@Data
public class UserLogDTO {

    private Long id;

    private String moduleCode;

    private Integer type;

    private String title;

    private Long operatorId;

    private Date operateTime;

    private String content;

}

UserLogService

public interface UserLogService {

    /**
     * 插入用户操作日志
     *
     * @param userLogDTO
     * @return
     */
    Boolean addSysLog(UserLogDTO userLogDTO);

}

@Service("UserLogService")
public class UserLogServiceImpl implements UserLogService {

    @Autowired
    private UserLogMapper userLogMapper;

    @Override
    public boolean addSysLog(UserLogDTO userLogDTO) {
        UserLogDO userLogDO = new UserLogDO();
        userLogDO.setModuleCode(userLogDTO.getModuleCode());
        userLogDO.setType(userLogDTO.getType());
        userLogDO.setTitle(userLogDTO.getTitle());
        userLogDO.setOperatorId(userLogDTO.getOperatorId());
        userLogDO.setOperateTime(userLogDTO.getOperateTime());
        userLogDO.setContent(userLogDTO.getContent());

        return userLogMapper.insert(userLogDO) > 0;
    }

}

UserLogMapper

public interface UserLogMapper extends BaseMapper<UserLogDO> {
}

核心代码:@UserLog

/**
 * 用户操作日志注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserLog {
	/**
	 * 所属模块名
	 */
	ModuleEnum module();

	/**
	 * 操作标题
	 */
	String title();

	/**
	 * 操作类型
	 */
	OperationEnum type();
}

核心代码:@EnableUserLog

/**
 * 开启用户日志记录(还需要在相关方法上添加{@link UserLog})
 *
 * @author sunting
 * @date 2021-02-11 14:11
 */
@Import(UserLogAspect.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableUserLog {
}

核心代码:OperationEnum

/**
 * 操作类型枚举
 */
@Getter
public enum OperationEnum {
    /**
     * 新建
     */
    ADD(1, "新建"),
    /**
     * 修改
     */
    MODIFY(2, "修改"),
    /**
     * 删除
     */
    DELETE(3, "删除"),
    /**
     * 导入
     */
    IMPORT(4, "导入"),
    /**
     * 导出
     */
    EXPORT(5, "导出");

    private final Integer value;
    private final String operationType;

    OperationEnum(Integer value, String operationType) {
        this.value = value;
        this.operationType = operationType;
    }
    
}

核心代码:ModuleEnum

/**
 * 系统模块枚举类
 * 可以根据自己系统的实际情况增添模块
 */
@Getter
public enum ModuleEnum {
	/**
	 * 课程
	 */
	COURSE("课程"),
	/**
	 * 用户
	 */
	USER("用户"),
	/**
	 * 消息
	 */
	MESSAGE("消息");

	private final String moduleCode;

	ModuleEnum(String moduleCode) {
		this.moduleCode = moduleCode;
	}

}

核心代码:UserLogAspect

/**
 * 操作日志注解切面实现
 */
@Slf4j
@Aspect
public class UserLogAspect {
	// 这次不用RequestContextHolder了,改成直接注入
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    UserLogService userLogService;


    @Pointcut("@annotation(com.bravo.demo.aspect.UserLog)")
    public void pointcut() {
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point) {
        saveSysUserLog(point);
    }

    private void saveSysUserLog(JoinPoint point) {
        // 获取当前登录用户
        UserInfoDTO userInfoDTO = getUserInfoDTO();

        // 目标方法、以及方法上的@UserLog注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        UserLog userLogAnnotation = method.getAnnotation(UserLog.class);
        if (userLogAnnotation == null) {
            return;
        }

        // 收集相关信息并保存
        UserLogDTO userLogDTO = new UserLogDTO();
        userLogDTO.setModuleCode(userLogAnnotation.module().getModuleCode());
        userLogDTO.setContent(getContentJson(point));
        userLogDTO.setTitle(userLogAnnotation.title());
        userLogDTO.setOperatorId(userInfoDTO.getId());
        userLogDTO.setOperateTime(new Date());
        userLogDTO.setType(userLogAnnotation.type().getValue());

        userLogService.addSysLog(userLogDTO);
    }

    private UserInfoDTO getUserInfoDTO() {
        // UserInfoDTO userInfoDTO = (UserInfoDTO) ThreadLocalMap.get(WebConst.USER_INFO_DTO);
		// 模拟从ThreadLocal获取用户信息,关于ThreadLocal请参考小册相关章节
        UserInfoDTO userInfoDTO = new UserInfoDTO();
        userInfoDTO.setId(10086L);
        return userInfoDTO;
    }

    private String getContentJson(JoinPoint point) {
        String requestType = request.getMethod();
        if ("GET".equals(requestType)) {
            // 如果是GET请求,直接返回QueryString(目前没有针对查询操作进行日志记录,先留着吧)
            return request.getQueryString();
        }

        Object[] args = point.getArgs();
        Object[] arguments = new Object[args.length];

        for (int i = 0; i < args.length; i++) {
            // 只打印客户端传递的参数,排除Spring注入的参数,比如HttpServletRequest
            if (args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }

        try {
            return objectMapper.writeValueAsString(arguments);
        } catch (JsonProcessingException e) {
            log.error("UserLogAspect#getContentJson JsonProcessingException", e);
        }
        return "";
    }
}

测试

@Slf4j
@RestController
public class UserController {

    @UserLog(module = ModuleEnum.USER, title = "批量更新用户", type = OperationEnum.MODIFY)
    @PostMapping("updateBatchUser")
    public Result<Boolean> updateBatchUser(@Validated @RequestBody ValidationList<User> userList) {
        return Result.success(null);
    }

    @UserLog(module = ModuleEnum.USER, title = "新增用户", type = OperationEnum.ADD)
    @PostMapping("insertUser")
    public Result<Boolean> insertUser(@RequestBody User user) {
        return Result.success(null);
    }

}

上面的代码主要提供一个思路,大家可以根据实际需求扩展或改编。

注意:

本文的处理是把所有参数都转为JSON存储,遇到保存文章等大文本数据时会报错。处理办法是:代码中进行长度处理或者调大content的长度(也可以改为text)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

?

文章来源:https://blog.csdn.net/smart_an/article/details/135098788
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。