日志记录功能技术点
日志功能技术点
运用技术点:
AOP切面编程(前置增强和后置增强 注:可以尝试使用环绕增强但是遇到问题并未解决)
自定义注解
RabbitMQ(进行异步处理,提高主线程性能)
Redis(用处少,只是获取用户名)
MyBatisPlus(进行数据库的写入)
AOP切面编程:
package com.cbd.log;
import cn.dev33.satoken.stp.StpUtil;
import com.cbd.entity.OpLogEntity;
import com.cbd.rabbitmq.MyMQProducer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author Li
* @desc 日志切面类
* @date 2024/11/25 10:14
*/
@Component
@Aspect
@Slf4j
public class LogAspect {
@Resource
MyMQProducer myMQProducer;
// 定义切点
@After("@annotation(com.cbd.log.OpLog)") // 注解切点
public void log(JoinPoint joinPoint) {
String username = StpUtil.getLoginIdAsString();//从缓存中获取登录用户
log.info("登录用户:{}", username);
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取方法对象
OpLog opLog = method.getAnnotation(OpLog.class); // 获取注解对象
String type = opLog.type(); // 获取注解属性值(类型)
String info = opLog.info(); // 获取注解属性值(信息)
// 记录日志时间戳
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String time = now.format(formatter);
OpLogEntity opLogEntity = new OpLogEntity(null, username, info, time);
// 记录日志
myMQProducer.send(opLogEntity);
}
@Before("@annotation(com.cbd.log.LogOut)")
public void before(JoinPoint joinPoint) {
String username = StpUtil.getLoginIdAsString();
log.info("注销用户:{}", username);
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取方法对象
LogOut opLog = method.getAnnotation(LogOut.class); // 获取注解对象
String type = opLog.type(); // 获取注解属性值(类型)
String info = opLog.info(); // 获取注解属性值(信息)
// 记录日志时间戳
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String time = now.format(formatter);
OpLogEntity opLogEntity = new OpLogEntity(null, username, info, time);
// 记录日志
myMQProducer.send(opLogEntity);
}
}
后置增强:
// 定义切点
@After("@annotation(com.cbd.log.OpLog)") // 注解切点
public void log(JoinPoint joinPoint) {
String username = StpUtil.getLoginIdAsString();//从缓存中获取登录用户
log.info("登录用户:{}", username);
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取方法对象
OpLog opLog = method.getAnnotation(OpLog.class); // 获取注解对象
String type = opLog.type(); // 获取注解属性值(类型)
String info = opLog.info(); // 获取注解属性值(信息)
// 记录日志时间戳
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String time = now.format(formatter);
OpLogEntity opLogEntity = new OpLogEntity(null, username, info, time);
// 记录日志
myMQProducer.send(opLogEntity);
}
注意:最后一步是调用了消息中间件的代码,通过调用消息生产者向消息队列发送消息
前置增强:
@Before("@annotation(com.cbd.log.LogOut)")
public void before(JoinPoint joinPoint) {
String username = StpUtil.getLoginIdAsString();
log.info("注销用户:{}", username);
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取方法对象
LogOut opLog = method.getAnnotation(LogOut.class); // 获取注解对象
String type = opLog.type(); // 获取注解属性值(类型)
String info = opLog.info(); // 获取注解属性值(信息)
// 记录日志时间戳
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String time = now.format(formatter);
OpLogEntity opLogEntity = new OpLogEntity(null, username, info, time);
// 记录日志
myMQProducer.send(opLogEntity);
}
消息中间件:
消息生产者:
package com.cbd.rabbitmq;
import com.alibaba.fastjson.JSON;
import com.cbd.entity.OpLogEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author Li
* @desc 消息生成者类
* @date 2024/11/14 11:38
*/
@Component
@Slf4j
public class MyMQProducer {
@Resource
private MyMQConfirmCallback myMQConfirmCallback;
@Resource
private MyMQReturnCallBack myMQReturnCallback;
@Resource // RabbitTemplate 封装了RabbitMQ的API,包括发送消息、接收消息等等
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
* @param opLogEntity 消息内容
*/
public void send(OpLogEntity opLogEntity) {
CorrelationData correlationData = new CorrelationData("id" + System.currentTimeMillis());
rabbitTemplate.setConfirmCallback(myMQConfirmCallback);
rabbitTemplate.setReturnsCallback(myMQReturnCallback);
// 将opLogEntity对象转换为json字符串
String message = JSON.toJSONString(opLogEntity);
log.info("消息内容:{}", message);
// 发送消息
rabbitTemplate.convertAndSend("opExchange", "opRoutingKey", message, correlationData);
}
}
注意:其中需要将传入的消息进行转换,因为rabbitMQ不能直接发送对象类型的消息,只能转换为json格式
消息消费者:
package com.cbd.rabbitmq;
import com.cbd.entity.OpLogEntity;
import com.cbd.mapper.OpLogMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author Li
* @desc RabbitMQ消费者类
* @date 2024/11/14 14:33
*/
@Component
@Slf4j
public class MyMQConsumer {
@Resource
OpLogMapper opLogMapper;
@RabbitListener(queues = "opQueue", ackMode = "MANUAL") // 监听队列名为opQueue的队列
public void receiveMessage(Channel channel, Message message) {
// 手动确认消息
try {
log.info("Received message1:{} ", message);
// 将消息转换为OpLogEntity对象
OpLogEntity opLogEntity = new OpLogEntity();
// 反序列化
ObjectMapper objectMapper = new ObjectMapper();
opLogEntity = objectMapper.readValue(message.getBody(), OpLogEntity.class);
opLogMapper.insert(opLogEntity); // MyBatisPlus操作数据库
// 参数1:deliveryTag,消息的序号,消息的唯一标识,在消费者中被标记为已确认
// 参数2:multiple,是否批量确认,false表示单条确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("消息确认失败:{}", e.getMessage());
}
}
}
注意:需要将json格式的消息进行反序列化,转换为对象,然后使用MyBatisPlus(opLogMapper.insert(opLogEntity); // MyBatisPlus操作数据库)进行存入数据库
注解:
注销注解:
package com.cbd.log;
import java.lang.annotation.*;
/**
* @Author: Li
* @Date: 2024/11/29 20:37
* @Description:
*/
@Target(ElementType.METHOD) // 注解可以用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解可以保留到运行时
@Documented // 注解可以生成javadoc
public @interface LogOut {
// 日志类型
String type();
// 日志信息
String info();
}
登录注解:
package com.cbd.log;
import java.lang.annotation.*;
/**
* @author Li
* @desc 自定义日志注解
* @date 2024/11/25 10:20
*/
@Target(ElementType.METHOD) // 注解可以用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解可以保留到运行时
@Documented // 注解可以生成javadoc
public @interface OpLog {
// 日志类型
String type();
// 日志信息
String info();
}
图解:

使用消息中间件后,主线程将继续执行业务流程,不再负责日志记录
常见问题:
Redis:
Redis的五个数据结构:1.strings(字符串)2.lists(字符串列表)3.sets(字符串集合)4.sorted sets(有序字符串集合)4.hashes(哈希)
Redis为什么速度快:
1.纯内存访问:redis 将所有的数据存放在内存中,内存的相应时间大约为100ns,这是 redis 达到每秒万级别访问的基础 2.非阻塞 I/Oredis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 redis 自身的时间处理模型将 epoll 中的连接、读写、关闭都转换为时间,不在网络 I/O 上浪费过多的时间 3.单线程模式:单线程避免了线程切换和资源竞争,这样服务端也就没有了线程同步和竞争锁问题
热key问题:当大量请求访问同一个数据会导致数据过热。解决方案:为解决这一问题我们可以进行预热,将多次访问的数据提前加载到内存里,这样就不用访问数据库,提高效率。
缓存穿透问题:有时客户端会发送一些无效或者数据库中不存在的数据请求,这样请求会穿过redis和数据库,会增大数据库压力。解决方案:为解决这一问题我们将有两种解决办法:1.将请求中的数据保存下来,这样每次请求服务端都会返回一个空,但是这并不是有效的解决方式,如果遇到恶意攻击,会占用大量内存。2.采用布隆过滤器。布隆过滤器是建立一个数组,将数据转换为二进制保存在数组中,每次客户端发出请求会先先来到布隆过滤器,通过查询数组可以判断redis缓存和数据库中是否存在该数据。但是并不是百分百准确,因为数组可能会存在重复问题,所以布隆过滤器中不存在,那么缓存和数据库中一定不存在,如果布隆过滤器中显示存在那么可能存在。数组越长,误差越小。虽然这种方法存在误差,但是此误差系统可以接受,所以推荐这种方法解决缓存传统问题。
缓存击穿问题:数据库中存在该数据,但是缓存中该数据key值过期导致访问时要重新访问数据库。解决方案:为解决该问题我们有三种方法:1.永不过期,这是简单且暴力的解决方法,但是需要牺牲内存。2.设置合理的过期时间。3.设置分布式锁。
缓存雪崩问题:当大量key值过期或者某些平时不常用key值突然受到大量请求,这就造成了缓存雪崩问题。解决方案:为解决该问题我们有两种方法:1.提高redis的高可用性 2.采用限流的方式。
Redis应用环境:数据缓存、排行榜、计数器
rabbitMQ:
rabbitMQ优缺点:优点:1.解耦:生产者和消费者不需要同时在线,它们只需要通过消息队列进行通信,从而降低了系统间的耦合度。2.异步通信:消息
消息积压问题:当大量的消息产生,但是消息消费者无法处理这么多的消息,大量的消息就积压在了消息队列中,这就造成了消息积压。解决方案:1.告诉消息生产者暂时不要产生消息。2.多设置几个消息消费者。
消息丢失:当消息队列发出消息但是消费者没有收到消息,这就造成了消息丢失。解决方案:1.生产者在发送消息时,可以开启消息确认模式。这样,RabbitMQ在成功接收到消息并存储到队列时,会向生产者发送确认(ack)消息。如果RabbitMQ因某些原因无法存储消息,则发送一个未确认(nack)消息。这样,生产者可以通过收到的ack/nack来判断消息是否成功发送并存储,进而重试发送或采取其他补救措施。2.消费者在处理消息后需要发送确认回执给RabbitMQ,以表明消息已被成功处理。如果消费者在处理消息过程中失败,RabbitMQ会将消息重新入队,以便其他消费者可以处理。这需要将消费者的确认模式设置为手动确认。
消息重发:当消息生产者向消息队列发送消息,但是出现了抖动。造成消息生产者无法得知是否发送消息,生产者会再次发送消息,这就导致了消息重发。解决方案:在生产者发送的消息中加入版本号等特殊字段。
补充(环绕增强):
解决了环绕增强遇到的问题,使用环绕增强不需要再使用两个注解,只需要OpLog注解即可
package com.cbd.log;
import cn.dev33.satoken.stp.StpUtil;
import com.cbd.entity.OpLogEntity;
import com.cbd.rabbitmq.MyMQProducer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author Li
* @desc 日志切面类
* @date 2024/11/25 10:14
*/
@Component
@Aspect
@Slf4j
public class LogAspect {
@Resource
MyMQProducer myMQProducer;
// 定义切点
@Around("@annotation(com.cbd.log.OpLog)")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
String username = StpUtil.getLoginIdAsString(); // 从缓存中获取登录用户
log.info("用户:{}", username);
Object target = joinPoint.getTarget();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取方法对象
OpLog opLog = method.getAnnotation(OpLog.class); // 获取注解对象
String methodName = method.getName();
// 记录日志时间戳
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime now = LocalDateTime.now();
String time = now.format(formatter);
OpLogEntity opLogEntity = new OpLogEntity(null, username, opLog.info(), time);
Object result = null;
try {
if ("logout".equals(methodName)) {
// 如果是logout方法,在方法执行前记录日志
log.info("用户:{},操作:{}", username, opLog.info());
myMQProducer.send(opLogEntity);
}
// 执行原方法
result = joinPoint.proceed();
if (!"logout".equals(methodName)) {
// 如果不是logout方法,在方法执行后记录日志
log.info("用户:{},操作:{}", username, methodName);
log.info("操作结果:{}", result);
myMQProducer.send(opLogEntity);
}
} catch (Throwable e) {
log.error("操作失败,用户:{},操作:{}", username, methodName, e);
// 处理异常情况,可能需要重新抛出异常或者返回错误信息
throw e;
}
return result;
}
}

浙公网安备 33010602011771号