日志记录功能技术点

日志功能技术点


运用技术点:

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;
    }
}

posted @ 2024-12-12 10:14  BingBing爱化学-04044  阅读(20)  评论(0)    收藏  举报