最近工作上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和%这些来处理数据之间的比对,保证能正常复制和删除上个月数据