> 文章列表 > SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库

SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库

SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库

SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库

    • 前言
    • 基础环境
      • 导入依赖
      • 编写yml配置
    • 数据库设计
    • 代码实现
      • 实体类
      • 编写注解
      • 业务类型枚举
      • 编写切片
      • ip工具类
      • 事件发布
      • 监听者
      • Controller控制层
      • service
      • mapper
      • 验证

前言

我们在企业级的开发中,必不可少的是对日志的记录,实现有很多种方式,常见的就是基于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;/*** 日志主键*/@TableIdprivate 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);@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate TestMapper testMapper;@Overridepublic 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> {}

验证

控制台输出:
SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库
数据库:
SpringBoot自定义注解+异步+观察者模式实现业务日志异步入库