AOP结合消息队列实现异常记录

261 阅读4分钟

最近工作上mt提出了需求,我目前封装了几个公共服务注册在dubbo中,后续会提供给项目的其他模块调用,作为公共模块,调用次数是比较频繁的,那么我们就有必要记录接口的调用情况,成功失败都进行记录存入数据库中,以确保我们能及时发现接口异常并进行处理。

一开始我是使用了try catch进行异常捕获并组装log实体存入数据库。其实这个操作是有问题的,

  • 有些内容不能放入try catch中,如果放入会有作用域问题,无法组装好log实体。
  • 没有扩展性,每多一个接口都需要进行try catch去包裹service层代码实现异常捕获。

因此我询问了大模型,他给出了几种方案,结合具体业务需求和性能等考虑,我选了AOP的环绕通知来实现这个功能,并用RabbitMQ实现异步存入数据库操作,因为在异常情况下,我的log实体有几个属性需要用到传入参数,如果使用异常通知是没法获取到传入参数的,只能得到异常实体,除非自己去自定义封装异常类,但是这个方案不如环绕通知好

具体实现如下,这里只是给出例子代码

1. 日志实体类和mapper层

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
 
@Entity
public class Log {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String message;
    private String level;
    private String timestamp;
 
    // Getters and Setters
}
import org.springframework.data.jpa.repository.JpaRepository;
 
public interface LogRepository extends JpaRepository<Log, Long> {
}

2. 建一个rabbitMQ发送消息的日志消息实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LogMessage {
    private String message;
    private String level;
    private String timestamp;
}

3. 配置RabbitMQ的消息发送者

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LogSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendLog(LogMessage logMessage) {
        rabbitTemplate.convertAndSend("logs.exchange", "logs.routingKey", logMessage);
    }
}

4. 配置RabbitMQ的消息消费者

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LogConsumer {

    @Autowired
    private LogRepository logRepository;

    @RabbitListener(queues = "logs.queue")
    public void receiveMessage(LogMessage logMessage) {
        Log log = new Log();
        log.setMessage(logMessage.getMessage());
        log.setLevel(logMessage.getLevel());
        log.setTimestamp(logMessage.getTimestamp());
        logRepository.save(log);
    }
}

5. 配置AOP切面类

@Aspect
@Component
public class LoggingAspect {

    @Autowired
    private LogSender logSender;

    @AfterThrowing(pointcut = "execution(* com.example.demo..*(..))", throwing = "e")
    public void logAfterThrowing(Throwable e) {
        LogMessage logMessage = new LogMessage(
                e.getMessage(),
                "ERROR",
                LocalDateTime.now().toString()
        );
        logSender.sendLog(logMessage);
    }
}

6. 配置RabbitMQ队列、交换机、绑定

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    public static final String EXCHANGE_NAME = "logs.exchange";
    public static final String ROUTING_KEY = "logs.routingKey";
    public static final String QUEUE_NAME = "logs.queue";

    @Bean
    public Queue queue() {
        return new Queue(QUEUE_NAME, true);
    }

    @Bean
    public DirectExchange exchange() {
        return new DirectExchange(EXCHANGE_NAME);
    }

    @Bean
    public Binding binding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY);
    }

    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        return new RabbitAdmin(connectionFactory);
    }

    @Bean
    public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapter) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(QUEUE_NAME);
        container.setMessageListener(listenerAdapter);
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(LogConsumer receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
}

我的环绕通知捕获异常以及记录正常情况日志参考的是下图代码

@Aspect
@Component
public class ChatAspect {
 //这里记录要使用AOP增强的方法
    @Around("execution(* your.package.name.YourController.chatOne(..))")
    public Object logChatExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            // 执行目标方法
            result = joinPoint.proceed();//joinPoint中包括了传入参数
            // 记录成功日志
            logSuccess(joinPoint);
        } catch (Throwable throwable) {
            // 记录异常日志
           //跟成功的情况相同,组装你的log实体,然后调用RabbitMQ就好了
            // 之后重新抛出异常,让上层调用处理它
            throw throwable;
        }
        return result;
    }
 
    private void logSuccess(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();//拿到全部传入参数
        
        if (args.length > 0 && args[0] instanceof E3wRequestParam) {
            E3wRequestParam requestParam = (E3wRequestParam) args[0];//拿到需要的传入参数
          //然后就可以组装实体了,组装好后调用上面的RabbitMQ就可以了
        }
     
    }

之后还有一个问题,那就是调用的比较频繁会导致单表数据快速增长,于是采用了按月分表的操作,实际逻辑就是使用cron表达式实现定时任务,每个月就从总表复制上个月的信息到新表,之后从总表中删除上个月的相关数据

public class ShardingCronUtil {

    @Resource
    private LogService logService;
    //记录表名后缀
public static final SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy_MM");
//记录时间格式
    public   static final SimpleDateFormat dateFormat2 = new SimpleDateFormat("yyyy-MM");

    @Scheduled(cron = "0 0 1 1 * ?")
        public void insertNewLogoinSurface() throws IOException {

            Calendar c = Calendar.getInstance();
            c.setTime(new Date());
            c.add(Calendar.MONTH, -1);
            Date m = c.getTime();
            String mon = dateFormat1.format(m);

            String createLogTableSQL = "CREATE TABLE t_tsaa_1000_" +mon+ " AS SELECT * FROM t_tsaa_1000  WHERE 1=1";

            Map<String, Object> param = new HashMap<String, Object>();
            param.put("createDate", dateFormat2.format(m));
            param.put("createLogTableSQL", createLogTableSQL);
            logService.createServiceInvocationJournal(param);
            Boolean deleteCount = logService.deleteByCreateTime(dateFormat2.format(m));

mapper.xml代码如下,数据库中字段存的时间是按照公司框架格式的yy-MM-dd xx-zz-yy。而我传入的数据格式是yy-MM,因为我只要统计月的数据,所以用了concat和%这些来处理数据之间的比对,保证能正常复制和删除上个月数据