基於Spring Cloud實現日誌管理模塊

簡介:

        無論在什麼系統中,日誌管理模塊都屬於十分重要的部分,接下來會通過註解+AOP+MQ的方式實現一個簡易的日誌管理系統

 

思路:

       註解:標記需要記錄日誌的方法

       AOP:通過AOP增強代碼,利用後置/異常通知的方式獲取相關日誌信息,最後使用MQ將日誌信息發送到專門處理日誌的系統

       RabbitMQ:利用解耦、異步的特性,協調完成各個微服務系統之間的通信

 

1、日誌表結構

 

表結構(sys_log):

CREATE TABLE `sys_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
  `opt_id` int(11) DEFAULT NULL COMMENT '操作用戶id',
  `opt_name` varchar(50) DEFAULT NULL COMMENT '操作用戶名',
  `log_type` varchar(20) DEFAULT NULL COMMENT '日誌類型',
  `log_message` varchar(255) DEFAULT NULL COMMENT '日誌信息(具體方法名)',
  `create_time` datetime DEFAULT NULL COMMENT '創建時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 COMMENT='系統日誌表';

實體類(SysLog):

@Data
public class SysLog  {

    private static final long serialVersionUID = 1L;

    /**
     * 唯一ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 操作用戶id
     */
    private Integer optId;
    /**
     * 操作用戶名
     */
    private String optName;
    /**
     * 日誌類型
     */
    private String logType;
    /**
     * 日誌信息(具體方法名)
     */
    private String logMessage;
    /**
     * 創建時間
     */
    private Date createTime;

}

 

2、註解

 

註解(SystemLog):

        僅作爲標記的作用,目的讓JVM可以識別,然後可以從中獲取相關信息

        @Target:定義註解作用的範圍,這裏是方法

        @Retention:定義註解生命週期,這裏是運行時

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemLog {

    SystemLogEnum type();

}

 

枚舉(SystemLogEnum):

          限定日誌類型範圍

public enum SystemLogEnum {

    SAVE_LOG("保存"),
    DELETE_LOG("刪除"),
    REGISTER_LOG("註冊"),
    LOGIN_LOG("登錄"),
    LAUD_LOG("點贊"),
    COLLECT_LOG("收藏"),
    THROW_LOG("異常"),
    ;
    private String type;

    SystemLogEnum(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

 

3、AOP切面

 

AOP(SysLogAspect):

        實現代碼的增強,主要通過動態代理方式實現的代碼增強。攔截註解,並獲取攔截到的相關信息,封裝成日誌對象發送到MQ隊列(生產端

Component
@Aspect
@Slf4j
public class SysLogAspect {

    @Autowired
    MqStream stream;

    //切點
    @Pointcut("@annotation(cn.zdxh.commons.utils.SystemLog)")
    public void logPointcut(){}

    //後置通知
    @After("logPointcut()")
    public void afterLog(JoinPoint joinPoint) {
        //一般日誌
        SysLog sysLog = wrapSysLog(joinPoint);

        log.info("Log值:"+sysLog);

        //發送mq消息
        stream.logOutput().send(MessageBuilder.withPayload(sysLog).build());

    }

    //異常通知
    @AfterThrowing(value = "logPointcut()", throwing = "e")
    public void throwingLog(JoinPoint joinPoint, Exception e) {
        //異常日誌
        SysLog sysLog = wrapSysLog(joinPoint);
        sysLog.setLogType(SystemLogEnum.THROW_LOG.getType());
        sysLog.setLogMessage(sysLog.getLogMessage()+"==="+e);

        log.info("異常Log值:"+sysLog);

        //發送mq消息
        stream.logOutput().send(MessageBuilder.withPayload(sysLog).build());
    }

    /**
     * 封裝SysLog對象
     * @param joinPoint
     * @return
     */
    public SysLog wrapSysLog(JoinPoint joinPoint){
        //獲取請求響應對象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        SysLog sysLog = new SysLog();

        //獲取方法全路徑
        String methodName = signature.getDeclaringTypeName()+"."+signature.getName();
        //獲取註解參數值
        SystemLog systemLog = signature.getMethod().getAnnotation(SystemLog.class);
        //從header取出token
        String token = request.getHeader("token");
        if (!StringUtils.isEmpty(token)) {
            //操作人信息
            Integer userId = JwtUtils.getUserId(token);
            String username = JwtUtils.getUsername(token);
            sysLog.setOptId(userId);
            sysLog.setOptName(username);
        }
        if (!StringUtils.isEmpty(systemLog.type())){
            sysLog.setLogType(systemLog.type().getType());
        }
        sysLog.setLogMessage(methodName);
        sysLog.setCreateTime(new Date());
        return sysLog;
    }

}

 

3、RabbitMQ消息隊列

 

MQ:

        這裏主要是通過Spring Cloud Stream集成的RabbitMQ

 

Spring Cloud Stream:

        作爲MQ的抽象層,已屏蔽各種MQ的各自名詞,統稱爲input、output兩大塊。可以更方便靈活地切換各種MQ,如 kafka、RocketMQ等

 

(1)定義Input/Ouput接口(MqStream)

 

@Component
public interface MqStream {

    String LOG_INPUT = "log_input";

    String LOG_OUTPUT = "log_output";
  
    @Input(LOG_INPUT)
    SubscribableChannel logInput();

    @Output(LOG_OUTPUT)
    MessageChannel logOutput();

}

 

(2)MQ生產者

 

注:這裏使用到AOP切面的微服務,都屬於MQ生產者服務

 

引入依賴:

          這裏沒有版本號的原因是spring cloud已經幫我們管理好各個版本號,已無需手動定義版本號

<!--Spring Cloud Stream-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>

 

在程序入口開啓MQ的Input/Output綁定:

          @EnableBinding(MqStream.class)

@SpringBootApplication(scanBasePackages = {"cn.zdxh.user","cn.zdxh.commons"})
@EnableEurekaClient
@MapperScan("cn.zdxh.user.mapper")
@EnableBinding(MqStream.class) //開啓綁定
@EnableFeignClients 
public class YouquServiceProviderUserApplication {

    public static void main(String[] args) {
        SpringApplication.run(YouquServiceProviderUserApplication.class, args);
    }

}

 

yml配置:

       在生產者端設置output

       destination:相當於rabbitmq的exchange

       group:相當於rabbitmq的queue,不過是和destination一起組合成的queue名

       binder:需要綁定的MQ

#Spring Cloud Stream相關配置
spring:
  cloud:
    stream:
      bindings: # exchange與queue綁定
        log_output: # 日誌生產者設置output
          destination: log.exchange
          content-type: application/json
          group: log.queue
          binder: youqu_rabbit #自定義名稱
      binders:
        youqu_rabbit:  #自定義名稱
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: 25802580

 

注:完成以上操作,即完成MQ生產端的所有工作

 

(3)MQ消費者

 

引入依賴、開啓Input/Output綁定:均和生產者的設置一致

 

yml配置:

       在生產者端設置input

spring:
  cloud:  # Spring Cloud Stream 相關配置
    stream:
      bindings: # exchange與queue綁定
        log_input: # 日誌消費者設置input
          destination: log.exchange
          content-type: application/json
          group: log.queue
          binder: youqu_rabbit
      binders:
        youqu_rabbit:
          type: rabbit
          environment:
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: 25802580

 

消費者監聽(LogMqListener):

      監聽生產者發過來的日誌信息,將信息添加到數據庫即可

 

@Service
@Slf4j
public class LogMqListener {

    @Autowired
    SysLogService sysLogService;

    @StreamListener(MqStream.LOG_INPUT)
    public void input(SysLog sysLog)  {
        log.info("開始記錄日誌========================");

        sysLogService.save(sysLog);

        log.info("結束記錄日誌========================");

    }
}

 

注:完成以上操作,即完成MQ消費端的所有工作

 

4、應用

 

簡述:

       只需將@SystemLog(type = SystemLogEnum.REGISTER_LOG),標記在需要記錄的方法上,當有客戶端訪問該方法時,就可以自動完成日誌的記錄

 

 

5、總結

 

流程:

       註解標記--->AOP攔截--->日誌發送到MQ--->專門處理日誌的系統監聽MQ消息 --->日誌插入到數據庫

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章