一、前言

我们在企业级的开发中,必不可少的是对日志的记录,实现有很多种方式,常见的就是基于AOP+注解进行保存,但是考虑到程序的流畅和效率,我们可以使用异步进行保存,小编最近在spring和springboot源码中看到有很多的监听处理贯穿前后:这就是著名的观察者模式!!

二、基础环境

项目这里小编就不带大家创建了,直接开始!!

1. 导入依赖

小编这里的springboot版本是:2.7.4

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>

2. 编写yml配置

server:
port: 8088 spring:
datasource:
#使用阿里的Druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.239.131:3306/test?serverTimezone=UTC
username: root
password: root

三、数据库设计

数据库保存日志表的设计,小编一切从简,一般日志多的后期会进行分库分表,或者搭配ELK进行分析,分库分表一般采用根据方法类型,这需要开发人员遵循rest风格,不然肯定都是post,纯属个人见解哈!!大家可以根据自己的公司的要求进行补充哈!!

DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
`title` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模块标题',
`business_type` int(2) NULL DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)',
`method` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '方法名称',
`request_method` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求方式',
`oper_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人员',
`oper_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求URL',
`oper_ip` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主机地址',
`oper_time` datetime(0) NULL DEFAULT NULL COMMENT '操作时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1585197503834284034 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;

实体类:

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; }

四、主要功能

大体思路:

先手写一个注解--->切面来进行获取要保存的数据--->一个发布者来发布要保存的数据--->一个监听者监听后保存(异步)

完整项目架构图如下:

1. 编写注解

import com.example.demo.constant.BusinessTypeEnum;

import java.lang.annotation.*;

/**
* 自定义操作日志记录注解
* @author wangzhenjun
* @date 2022/10/26 15:37
*/
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface Log { String value() default "";
/**
* 模块
*/
String title() default "测试模块"; /**
* 功能
*/
BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}

2. 业务类型枚举

/**
* @author wangzhenjun
* @date 2022/10/26 11:22
*/
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;
}
}

3. 编写切片

这里小编是以切片后进行发起的,当然规范流程是要加异常后的切片,这里以最简单的进行测试哈,大家按需进行添加!!

import com.example.demo.annotation.Log;
import com.example.demo.entity.SysLog;
import com.example.demo.listener.EventPubListener;
import com.example.demo.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 wangzhenjun
* @date 2022/10/26 15:39
*/
@Aspect
@Component
public class SysLogAspect { private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class); @Autowired
private EventPubListener eventPubListener; /**
* 以注解所标注的方法作为切入点
*/
@Pointcut("@annotation(com.example.demo.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);
}
}

4. ip工具类

import com.baomidou.mybatisplus.core.toolkit.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
* @author wangzhenjun
* @date 2022/10/26 16:27
* 获取IP方法
*
* @author jw
*/
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);
}
}

5. 事件发布

事件发布是由ApplicationContext对象进行发布的,直接注入使用即可!

使用观察者模式的目的:为了业务逻辑之间的解耦,提高可扩展性

这种模式在spring和springboot底层是经常出现的,大家可以去看看。

发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可。

import com.example.demo.entity.SysLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; /**
* @author wangzhenjun
* @date 2022/10/26 16:38
*/
@Component
public class EventPubListener {
@Autowired
private ApplicationContext applicationContext; // 事件发布方法
public void pushListener(SysLog sysLogEvent) {
applicationContext.publishEvent(sysLogEvent);
}
}

6. 监听者

@Async:单独开启一个新线程去保存,提高效率!

@EventListener:监听

/**
* @author wangzhenjun
* @date 2022/10/25 15:22
*/
@Slf4j
@Component
public class MyEventListener { @Autowired
private TestService testService; // 开启异步
@Async
// 开启监听
@EventListener(SysLog.class)
public void saveSysLog(SysLog event) {
log.info("=====即将异步保存到数据库======");
testService.saveLog(event);
} }

五、测试

1. controller

/**
* @author wangzhenjun
* @date 2022/10/26 16:51
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController { @Log(title = "测试呢",businessType = BusinessTypeEnum.INSERT)
@GetMapping("/saveLog")
public void saveLog(){
log.info("我就是来测试一下是否成功!");
}
}

2. service

/**
* @author wangzhenjun
* @date 2022/10/26 16:55
*/
public interface TestService { int saveLog(SysLog sysLog);
}
/**
* @author wangzhenjun
* @date 2022/10/26 16:56
*/
@Service
public class TestServiceImpl implements TestService { @Autowired
private TestMapper testMapper; @Override
public int saveLog(SysLog sysLog) { return testMapper.insert(sysLog);
}
}

3. mapper

这里使用mybatis-plus进行保存

/**
* @author wangzhenjun
* @date 2022/10/26 17:07
*/
public interface TestMapper extends BaseMapper<SysLog> {
}

4. 测试

5. 数据库

六、总结

铛铛铛,终于完成了!这个实战在企业级必不可少的,每个项目搭建人不同,但是结果都是一样的,保存日志到数据,这样可以进行按钮的点击进行统计,分析那个功能是否经常使用,那些东西需要优化。只要是有数据的东西,分析一下总会有收获的!后面日志多了就行分库分表,ELK搭建。知道的越多不知道的就越多,这一次下来,知道下面要学什么了嘛!!


可以看下一小编的微信公众号,和网站文章首发看,欢迎关注,一起交流哈!!
![](https://img2022.cnblogs.com/blog/2471401/202210/2471401-20221028085553420-1506161649.jpg)

点击访问!小编自己的网站,里面也是有很多好的文章哦!

SpringBoot自定义注解+异步+观察者模式实现业务日志保存的更多相关文章

  1. 自定义注解,利用AOP实现日志保存(数据库),代码全贴,复制就能用

    前言 1,在一些特定的场景我们往往需要看一下接口的入参,特别是跨系统的接口调用(下发,推送),这个时候的接口入参就很重要,我们保存入参入库,如果出问题就可以马上定位是上游还是下游的问题(方便扯皮) 2 ...

  2. SpringBoot 自定义注解 实现多数据源

    SpringBoot自定义注解实现多数据源 前置学习 需要了解 注解.Aop.SpringBoot整合Mybatis的使用. 数据准备 基础项目代码:https://gitee.com/J_look/ ...

  3. [技术博客] SPRINGBOOT自定义注解

    SPRINGBOOT自定义注解 在springboot中,有各种各样的注解,这些注解能够简化我们的配置,提高开发效率.一般来说,springboot提供的注解已经佷丰富了,但如果我们想针对某个特定情景 ...

  4. SpringBoot自定义注解、AOP打印日志

    前言 在SpringBoot中使用自定义注解.aop切面打印web请求日志.主要是想把controller的每个request请求日志收集起来,调用接口.执行时间.返回值这几个重要的信息存储到数据库里 ...

  5. springboot+自定义注解实现灵活的切面配置

    利用aop我们可以实现业务代码与系统级服务例如日志记录.事务及安全相关业务的解耦,使我们的业务代码更加干净整洁. 最近在做数据权限方面的东西,考虑使用切面对用户访问进行拦截,进而确认用户是否对当前数据 ...

  6. 使用IDEA创建SpringBoot自定义注解

    创建SpringBoot项目 添加组织名 选择web 输入项目名称 创建后目录结构为 使用Spring的AOP先加入Maven依赖 <dependency> <groupId> ...

  7. SpringBoot自定义注解

    1.注解的概念 注解是一种能被添加到java代码中的元数据,类.方法.变量.参数和包都可以用注解来修饰.注解对于它所修饰的代码并没有直接的影响. 2.注解的使用范围 1)为编译器提供信息:注解能被编译 ...

  8. java/springboot自定义注解实现AOP

    java注解 即是注释了,百度解释:也叫元数据.一种代码级别的说明. 个人理解:就是内容可以被代码理解的注释,一般是一个类. 元数据 也叫元注解,是放在被定义的一个注解类的前面 ,是对注解一种限制. ...

  9. Spring aop+自定义注解统一记录用户行为日志

    写在前面 本文不涉及过多的Spring aop基本概念以及基本用法介绍,以实际场景使用为主. 场景 我们通常有这样一个需求:打印后台接口请求的具体参数,打印接口请求的最终响应结果,以及记录哪个用户在什 ...

随机推荐

  1. 痞子衡嵌入式:聊聊i.MXRT1170双核下不同GPIO组的访问以及中断设计

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT1170双核下不同GPIO组的访问以及中断设计. 在双核 i.MXRT1170 下设计应用程序,有一个比较重要的考虑点就是外 ...

  2. LuoguAT2827 LIS (LIS)

    裸题 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm ...

  3. Express 项目,res.cookie() 设置 Cookie 无法被保存在浏览器的 Application 中

    res.cookie() 给客户端响应头封装的 Cookie 无法被保存在客户端浏览器的 Application 中,只能在 Set-Cookie 中看到有这个值: 在前后端分离项目中,存在跨域问题, ...

  4. q 短引用标签

    <q/>标签可以使一段文本作为引用. <p>他说:<q>明天要下雨</q>.</p> 注意,源代码中并没有为这段文字添加引用符号,而是添加了 ...

  5. 牛客IOI周赛26-提高组 A. 逆序对

    题面 逆序对 有一个长度为 N \tt N N 的排列 a a a,进行 M \tt M M 次操作,操作有 4 \tt 4 4 种: 1 l r :交换 a l \tt a_l al​ 和 a r ...

  6. Spring5中JdbcTemplate

    JdbcTemplate是什么 JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理. 在spri ...

  7. 创建swarm集群并自动编排

    1.基础环境配置 主机名 master node1 node2 IP地址 192.168.***.1 192.168.***.2 192.168.***.3 角色     管理节点 工作节点 工作节点 ...

  8. 学习ASP.NET Core Blazor编程系列二——第一个Blazor应用程序(下)

    学习ASP.NET Core Blazor编程系列一--综述 学习ASP.NET Core Blazor编程系列二--第一个Blazor应用程序(上) 学习ASP.NET Core Blazor编程系 ...

  9. 璞华PLM为全场景产品生命周期管理赋能,助力产品主线的企业数字化转型

    英文版的<产品生命周期管理(PLM)软件市场--增长.趋势.COVID-19影响和预测(2022 - 2027)>中对未来PLM市场概述的描述为:"产品生命周期管理(PLM)软件 ...

  10. GitHub desktop常见问题及解决办法

    1.There are unresolved conflicts in the working directory. 问题出现:A台电脑push代码后,可能新建了分支,然后B电脑打开GitHub de ...