SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库
前言
我们在企业级的开发中,必不可少的是对日志的记录,实现有很多种方式,常见的就是基于AOP+注解进行保存,但是考虑到程序的流畅和效率,我们可以使用异步进行保存,最近在spring和springboot源码中看到有很多的监听处理贯穿前后:这就是著名的观察者模式!!
基础环境
导入依赖
springboot版本是:2.1.5.RELEASE
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
编写yml配置
server.port= 8888
#使用阿里的Druid
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tools?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
数据库设计
数据库保存日志表的设计,一般日志多的后期会进行分库分表,或者搭配ELK进行分析,分库分表一般采用根据方法类型。
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) DEFAULT '' COMMENT '模块标题',
`business_type` int(2) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
`method` varchar(100) DEFAULT '' COMMENT '方法名称',
`request_method` varchar(10) DEFAULT '' COMMENT '请求方式',
`oper_name` varchar(50) DEFAULT '' COMMENT '操作人员',
`oper_url` varchar(255) DEFAULT '' COMMENT '请求URL',
`oper_ip` varchar(128) DEFAULT '' COMMENT '主机地址',
`oper_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1646387544953581571 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='操作日志记录';
代码实现
整体思路:先手写一个注解—>切面来进行获取要保存的数据—>一个发布者来发布要保存的数据—>一个监听者监听后保存(异步)。
实体类
package com.mry.springboottools.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作日志记录表 sys_log
*
*/
@Data
@TableName("sys_log")
public class SysLog {
private static final long serialVersionUID = 1L;
/**
* 日志主键
*/
@TableId
private Long id;
/**
* 操作模块
*/
private String title;
/**
* 业务类型(0其它 1新增 2修改 3删除)
*/
private Integer businessType;
/**
* 请求方式
*/
private String requestMethod;
/**
* 操作人员
*/
private String operName;
/**
* 请求url
*/
private String operUrl;
/**
* 操作地址
*/
private String operIp;
/**
* 操作时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operTime;
}
编写注解
package com.mry.springboottools.annotation;
import com.mry.springboottools.enums.BusinessTypeEnum;
import java.lang.annotation.*;
/**
* 自定义操作日志记录注解
* @author
* @date
*/
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface Log {
String value() default "";
/**
* 模块
*/
String title() default "测试模块";
/**
* 功能
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}
业务类型枚举
package com.mry.springboottools.enums;
/**
* 业务类型枚举
*/
public enum BusinessTypeEnum {
/**
* 其它
*/
OTHER(0,"其它"),
/**
* 新增
*/
INSERT(1,"新增"),
/**
* 修改
*/
UPDATE(2,"修改"),
/**
* 删除
*/
DELETE(3,"删除");
private Integer code;
private String message;
BusinessTypeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
编写切片
package com.mry.springboottools.aspect;
import com.mry.springboottools.annotation.Log;
import com.mry.springboottools.entity.SysLog;
import com.mry.springboottools.event.EventPubListener;
import com.mry.springboottools.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
/**
* 编写切片: 这里是以切片后进行发起的,当然规范流程是要加异常后的切片,
* 这里以最简单的进行测试哈,大家按需进行添加!!
* @author
* @date
*/
@Aspect
@Component
public class SysLogAspect {
private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private EventPubListener eventPubListener;
/**
* 以注解所标注的方法作为切入点
*/
@Pointcut("@annotation(com.mry.springboottools.annotation.Log)")
public void sysLog() {}
/**
* 在切点之后织入
* @throws Throwable
*/
@After("sysLog()")
public void doAfter(JoinPoint joinPoint) {
Log log = ((MethodSignature) joinPoint.getSignature()).getMethod()
.getAnnotation(Log.class);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String method = request.getMethod();
String url = request.getRequestURL().toString();
String ip = IpUtils.getIpAddr(request);
SysLog sysLog = new SysLog();
sysLog.setBusinessType(log.businessType().getCode());
sysLog.setTitle(log.title());
sysLog.setRequestMethod(method);
sysLog.setOperIp(ip);
sysLog.setOperUrl(url);
// 从登录中token获取登录人员信息即可
sysLog.setOperName("我是测试人员");
sysLog.setOperTime(LocalDateTime.now());
// 发布消息
eventPubListener.pushListener(sysLog);
logger.info("=======日志发送成功,内容:{}",sysLog);
}
}
ip工具类
package com.mry.springboottools.utils;
import javax.servlet.http.HttpServletRequest;
/**
* @author
* @date
* 获取IP方法
*
*/
public class IpUtils {
/**
* 获取客户端IP
*
* @param request 请求对象
* @return IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
}
/**
* 从多级反向代理中获得第一个非unknown IP地址
*
* @param ip 获得的IP地址
* @return 第一个非unknown IP地址
*/
public static String getMultistageReverseProxyIp(String ip) {
// 多级反向代理检测
if (ip != null && ip.indexOf(",") > 0) {
final String[] ips = ip.trim().split(",");
for (String subIp : ips) {
if (false == isUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return ip;
}
/**
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
*
* @param checkString 被检测的字符串
* @return 是否未知
*/
public static boolean isUnknown(String checkString) {
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
}
}
事件发布
事件发布是由ApplicationContext对象进行发布的,直接注入使用即可!
使用观察者模式的目的:为了业务逻辑之间的解耦,提高可扩展性。
这种模式在spring和springboot底层是经常出现的,大家可以去看看。
发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可。
package com.mry.springboottools.event;
import com.mry.springboottools.entity.SysLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* 事件发布:
* 事件发布是由ApplicationContext对象进行发布的,直接注入使用即可!
* 使用观察者模式的==目的==:为了业务逻辑之间的解耦,提高可扩展性。
* 这种模式在spring和springboot底层是经常出现的,大家可以去看看。
* 发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可。
* @author
* @date
*/
@Component
public class EventPubListener {
@Autowired
private ApplicationContext applicationContext;
// 事件发布方法
public void pushListener(SysLog sysLogEvent) {
applicationContext.publishEvent(sysLogEvent);
}
}
监听者
@Async:单独开启一个新线程去保存,提高效率!
@EventListener:监听
package com.mry.springboottools.event;
import com.mry.springboottools.entity.SysLog;
import com.mry.springboottools.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* 监听者:
* @Async:单独开启一个新线程去保存,提高效率!
* @EventListener:监听
* @author
* @date
*/
@Slf4j
@Component
public class MyEventListener {
@Autowired
private TestService testService;
// 开启异步
@Async
// 开启监听
@EventListener(SysLog.class)
public void saveSysLog(SysLog event) {
log.info("=====即将异步保存到数据库======");
testService.saveLog(event);
}
}
Controller控制层
package com.mry.springboottools.controller;
import com.mry.springboottools.annotation.Log;
import com.mry.springboottools.enums.BusinessTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* SpringBoot自定义注解+异步+观察者模式实现业务日志保存
* @author
* @date
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class SysLogController {
@Log(title = "测试呢",businessType = BusinessTypeEnum.INSERT)
@GetMapping("/saveLog")
public String saveLog(){
log.info("我就是来测试一下是否成功!");
return "我就是来测试一下是否成功!";
}
}
service
package com.mry.springboottools.service;
import com.mry.springboottools.entity.SysLog;
public interface TestService {
int saveLog(SysLog sysLog);
}
package com.mry.springboottools.service.impl;
import com.mry.springboottools.entity.SysLog;
import com.mry.springboottools.mapper.TestMapper;
import com.mry.springboottools.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public int saveLog(SysLog sysLog) {
return testMapper.insert(sysLog);
}
}
mapper
这里使用mybatis-plus进行保存
package com.mry.springboottools.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mry.springboottools.entity.SysLog;
/**
* 这里使用mybatis-plus进行保存
*/
public interface TestMapper extends BaseMapper<SysLog> {
}
验证
控制台输出:
数据库: