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

news2025/3/1 6:56:22

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;

    /**
     * 日志主键
     */
    @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> {

}

验证

控制台输出:
在这里插入图片描述
数据库:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/426485.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VSCode将markdown文件导出为带书签的PDF文件

背景 之前找工作的总结性文章&#xff0c;全是markdown形式的想直接变成PDF好携带。方便查找 初步探索遇到的问题 markdown直接在chrome钟打开&#xff0c;右键有打印选项&#xff0c;有一说一&#xff0c;效果不错&#xff0c;唯一缺点&#xff0c;没书签。 怎么办 查资料…

收音机知识,调谐(选频/滤波),调制(升频)

参考&#xff1a;https://www.bilibili.com/video/BV1d14y1N7nm/?spm_id_from333.999.0.0&vd_source00bd76f9d6dc090461cddd9f0deb2d51 有关知识提纲整个信号的传输变化调谐人耳听到声音的频率范围&#xff08;20~20000Hz&#xff09;天线和传送信号的波长关系波长和天线长…

LeetCode 剑指 Offer II 106. 二分图【二分图匹配】中等

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

京东pop店铺订单导出

下载安装与运行 下载、安装与运行 语雀 特别提醒 只能导出已登录店铺的订单导出的收件人手机号是虚拟号 功能 主要是方便线下工厂发货的店主 所见即所得的导出自由选择导出项自由排序Excel导出列顺序导出过程中有进度提示&#xff0c;用户可以随时提前中止 什么是所见即所…

Linux网络编程概述

文章目录 前言一、客户端与服务端二、客户端程序编写三、程序的编译和运行总结前言 本篇文章将带大家来正式学习Linux网络编程。 一、客户端与服务端 一般认为服务器是一个长时间运行的程序(既守护程序)他只在相应来自网络请求时才发送网络消息 协议的另一端是客户端程序,…

Swagger文档注释

本文以DRF框架为例使用 为什么要接口文档注释 一. 方便后端调试与后续接口更新&#xff1b; 二. 对于大型前后端分离项目&#xff0c;前后端人员是分开开发的&#xff0c;甚至前端的人你都不知道远在何处&#xff0c;这时候接口文档的重要性就太重要了。 三. 接口注释文档常用…

巧用千寻位置GNSS软件| 点放样操作指南

在工程测量中&#xff0c;点放样就是将设计或图纸上的点位在实地上测量出来&#xff0c;将目标坐标输入软件在实地放样出来的过程。本文将围绕“如何在千寻位置GNSS软件上完成点放样操作”进行分步骤讲解。点击【测量】->【点放样】->【坐标点库】&#xff0c;选择一个点…

气传导和骨传导耳机的区别?这两种耳机哪个更好更实用?

气传导和骨传导耳机的最大区别&#xff1a;就在于发声原理的不同。 这两种耳机&#xff0c;一种是空气传播声音&#xff0c;一种是骨骼固体传声。 气传导耳机&#xff0c;就是声音从外部传过来&#xff0c;并由外耳、中耳传导到内耳&#xff0c;这是我们日常中接触最多的传导…

第三方支付接口测试面试要点

第三方支付接口测试面试要点 第三方支付接口测试&#xff0c;主要是看你的接口功能实现的是否满足需求&#xff0c;以及你的测试思路是否正确。因此&#xff0c;接口测试工程师要从以下几个方面来准备。 首先&#xff0c;我们需要了解第三方支付的流程; 最后&#xff0c;我们需…

Spring 之 AOP 原理详解

Spring 是一个流行的 Java 企业应用程序开发框架。其中的 AOP&#xff08;面向切面编程&#xff09;是 Spring 框架中的一个核心概念。本文将介绍 Spring AOP 的底层实现原理&#xff0c;并通过源代码解析来详细阐述其实现过程。 什么是AOP&#xff1f; AOP是一种编程范式&…

three.js之自定义一个正方体(网格)

本节主要通过自定义顶点和平面的方式&#xff0c;创造一个立方体。真正的开始走近three.js。 效果图 坐标系 坐标系支持右手定则。图中红色是x轴&#xff0c;绿色是y轴&#xff0c;蓝色是z轴 源码 引入的插件js【本人的csdn也有下载资源&#xff0c;如果打不开git可以在csd…

AI网站汇总(免费chatgpt)(60个持续增加中)

本文总结了6大类AI工具,包括:聊天AI、绘画AI、AI提示词、图像处理、UI设计和3D设计,汇总60个AI网站,一键收藏。 目录 一、聊天AI 二、绘画AI 三、AI提示词 四、图像处理

SQL笔记(2)——MySQL的表操作与索引(收藏吃灰版)

本文详细记录如何通过命令的方式修改MySQL的表结构&#xff0c;例如新增列、删除列等&#xff1b;不止学会了&#xff0c;你还学懂了&#xff0c;收藏吃灰~ 开始之前 上一篇文章创建了一些表&#xff0c;ER图如下。本文针对score表进行操作&#xff0c;场景就是新增一个备注rem…

自动化测试面试一周拿到3个offer,只因为我记下了这个文档

目录 一、接口测试基础 二、 接口测试工具 三、自动化测试 四、自动化测试工具 五、总结 一、接口测试基础 1、公司接口测试流程是什么&#xff1f; 从开发那边获取接口设计文档、分析接口并进行用例设计、并提前录入到接口测试工具 jmeter&#xff0c;等开发那边进行…

客户关系管理小程序实战教程01-需求分析

日常企业经常需要在网上拓展业务&#xff0c;通过互联网工具来宣传自己的产品。用户在看到企业宣传的内容后&#xff0c;如果有需要就会通过各种方式联系到企业。 为了方便的跟踪这些销售的机会&#xff0c;我们开发一款企业内部销售团队使用的小程序&#xff0c;便于管理潜在…

Win11的两个实用技巧系列之磁盘分区后再恢复的方法、调高进程的优先级方法

Win11磁盘分区后怎么恢复到分区前?Win11磁盘分区后在恢复的方法 很多人不知道win11磁盘分区怎么恢复回去?今日为你们带来的文章是win11磁盘分区的恢复方法&#xff0c;还有不清楚小伙伴和小编一起去学习一下吧 有不少小伙伴在使用电脑的时候经常会根据自身需求对其进行磁盘的…

【CSS】轮播图案例开发 ( 基本设置 | 子绝父相 | 浏览器水平居中 | 圆角设置 | 绝对定位居中设置 )

文章目录一、开发要点1、基本设置 - 取消默认内外边距 / 取消基本样式 / 图片自适应2、外层父容器设置 - 子绝父相 / 盒子浏览器水平居中 / 设置圆角 / 设置溢出隐藏3、左右按钮设置 - 绝对定位垂直居中设置 / 使用圆角矩形设置半圆 / 文字垂直居中4、底部小圆点设置 - 绝对定位…

Web_python_template_injection(Python模块注入)

打开链接&#xff0c;提示是Python的模块注入 我们先了解一些基本概念&#xff1a; 模板引擎可以让&#xff08;网站&#xff09;程序实现界面与数据分离&#xff0c;业务代码与逻辑代码的分离&#xff0c;这大大提升了开发效率&#xff0c;良好的设计也使得代码重用变得更加容…

智慧停车场解决方案,停车场导航技术怎么实现

停车场导航技术怎么实现&#xff1f;随着城市化的不断发展&#xff0c;停车场建的越来越大&#xff0c;同时也越来越复杂&#xff0c;停车、找车成为很多人感到十分头疼的问题。在这种情况下&#xff0c;一个高效的停车场电子地图应用已经成为城市交通管理中不可缺少的组成部分…

架构设计三原则

作为程序员&#xff0c;很多人都希望成为一名架构师&#xff0c;但并非简单地通过编程技能就能够达成这一目标。事实上&#xff0c;优秀的程序员和架构师之间存在一个明显的鸿沟——不确定性。 编程的本质是确定性的&#xff0c;也就是说&#xff0c;对于同一段代码&#xff0c…