簡介:
無論在什麼系統中,日誌管理模塊都屬於十分重要的部分,接下來會通過註解+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消息 --->日誌插入到數據庫