简介
无论在什么系统中,日志管理模块都属于十分重要的部分,接下来会通过注解+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绑定:
-
@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名。另外,搜索公众号Linux中文社区后台回复“私房菜”,获取一份惊喜礼包。 -
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消息 --->日志插入到数据库