后台管理系统的通用权限解决方案(七)SpringBoot整合SpringEvent实现操作日志记录(基于注解和切面实现)

news2024/11/23 1:31:23

1 Spring Event框架

除了记录程序运行日志,在实际项目中一般还会记录操作日志,包括操作类型、操作时间、操作员、管理员IP、操作原因等等(一般叫审计)。

操作日志一般保存在数据库,方便管理员查询。通常的做法在每个请求方法中构建审计对象,并写入数据库,但这比较繁琐和冗余。更简便的做法是使用Spring Event框架进行统一处理。

Spring Event是Spring的事件通知机制,可以将相互耦合的代码解耦。Spring Event是监听者模式的一个具体实现。

监听者模式包含了监听者Listener、事件Event、事件发布者EventPublish,过程就是事件发布者EventPublish发布一个事件,被监听者Listener捕获到,然后执行事件Event相应的方法。

2 Spring Event案例

  • 1)创建maven工程spring-event-demo,并配置其pom.xml文件如下。由于Spring Event的相关API在spring-context包中,所以只需引入Spring相关依赖,而无需额外配置。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.hsgx</groupId>
    <artifactId>spring-event-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>
  • 2)创建审计信息类Audit、审计事件类AuditEvent、审计监听器类LogListener
package com.hsgx.event.pojo;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 审计信息
 */
@Data
public class Audit {
    private String type; //操作类型
    private LocalDateTime time; //操作时间
    private String userName; //操作员
    private String requestIp; //操作员IP
    private String description; //操作原因
}
package com.hsgx.event.pojo;

import org.springframework.context.ApplicationEvent;

/**
 * 定义审计事件
 */
public class AuditEvent extends ApplicationEvent {
    public AuditEvent(Audit audit) {
        super(audit);
    }
}
package com.hsgx.event.listener;

import com.hsgx.event.pojo.Audit;
import com.hsgx.event.pojo.AuditEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * 审计监听器
 */
@Component
public class AuditListener {

    // 异步监听AuditEvent事件
    @Async
    @EventListener(AuditEvent.class)
    public void saveAudit(AuditEvent auditEvent) {
        Audit audit = (Audit) auditEvent.getSource();
        long id = Thread.currentThread().getId();
        System.out.println("监听到审计事件:" + audit + " 线程id:" + id);
        // 将日志信息保存到数据库...
    }
}
  • 3)创建UserController用于发布事件
package com.hsgx.event.controller;

import com.hsgx.event.pojo.Audit;
import com.hsgx.event.pojo.AuditEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

/**
 * 发布事件
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private ApplicationContext applicationContext;

    @GetMapping("/get")
    public String getUser(){
        // 构造操作日志信息
        Audit audit = new Audit();
        audit.setType("获取用户信息");
        audit.setTime(LocalDateTime.now());
        audit.setUserName("admin");
        audit.setRequestIp("127.0.0.1");
        audit.setDescription("获取用户信息");
        // 构造事件对象
        ApplicationEvent event = new AuditEvent(audit);
        // 发布事件
        applicationContext.publishEvent(event);
        long id = Thread.currentThread().getId();
        return "发布事件成功,线程id:" + id;
    }
}
  • 5)创建启动类SpringEventApp,使用@EnableAsync注解启用异步处理
package com.hsgx.event;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync //启用异步处理
public class SpringEventApp {
    public static void main(String[] args) {
        SpringApplication.run(SpringEventApp.class,args);
    }
}
  • 6)启动项目后访问/user/get请求,触发发布事件,在监听器类AuditListener中监听到事件并进行相关操作

  • 7)在UserController中,需要注入ApplicationContext对象并调用publishEvent()方法手动发布事件,有点繁琐。我们可以通过创建一个审计注解@Audit,并通过切面拦截该注解的方式来完成。先引入AOP的依赖、hutool工具依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.1.0</version>
</dependency>
  • 8)创建审计注解@Audit
package com.hsgx.event.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Audit {
    /**
     * 描述
     */
    String value();
    /**
     * 类型
     */
    String type() default "";
}
  • 9)创建切面类AuditAspect,做以下事情:
  • 在切面类AuditAspect中定义切点,拦截Controller中添加@Audit注解的方法
  • 在切面类AuditAspect中定义前置通知,在前置通知方法doBefore()中收集操作相关信息封装为Audit对象并保存到ThreadLocal中
  • 在切面类AuditAspect中定义成功返回通知,在成功返回通知方法doAfterReturning中通过ThreadLocal获取Audit对象并继续设置其他的成功操作信息,随后发布事件
  • 在切面类AuditAspect中定义异常返回通知,在异常返回通知方法doAfterThrowable中通过ThreadLocal获取Audit对象并继续设置其他的异常操作信息,随后发布事件
package com.hsgx.event.aspect;

import cn.hutool.core.convert.Convert;
import cn.hutool.extra.servlet.ServletUtil;
import com.hsgx.event.pojo.Audit;
import com.hsgx.event.pojo.AuditEvent;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Objects;

@Slf4j
@Aspect
public class AuditAspect {

    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 用于保存线程中的审计对象
     */
    private static final ThreadLocal<Audit> THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 定义Controller切入点拦截规则,拦截 @Audit 注解的方法
     */
    @Pointcut("@annotation(com.hsgx.event.annotation.Audit)")
    public void auditAspect() {
    }

    /**
     * 从ThreadLocal中获取审计对象,没有则创建一个
     */
    private Audit getAudit() {
        Audit audit = THREAD_LOCAL.get();
        if (audit == null) {
            return new Audit();
        }
        return audit;
    }

    /**
     * 前置通知,收集操作相关信息封装为Audit对象并保存到ThreadLocal中
     */
    @Before(value = "auditAspect()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        Audit audit = getAudit();
        audit.setTime(LocalDateTime.now());
        audit.setRequestIp(ServletUtil.getClientIP(request));
        // 操作员一般通过读取当前登录的管理员信息获取
        audit.setUserName("zhangsan");
        // 获取 @Audit 注解的信息
        com.hsgx.event.annotation.Audit ann = joinPoint.getTarget().getClass().getAnnotation(com.hsgx.event.annotation.Audit.class);
        if (ann != null) {
            audit.setDescription(ann.value());
            audit.setType(ann.type());
        }
        // 保存到线程容器
        THREAD_LOCAL.set(audit);
    }

    /**
     * 成功返回通知
     */
    @AfterReturning(returning = "ret", pointcut = "auditAspect()")
    public void doAfterReturning(Object ret) {
        // 根据返回对象 ret 再做一些操作
        Audit audit = getAudit();
        audit.setDescription(audit.getDescription() + " 成功 ");
        // 发布事件
        applicationContext.publishEvent(new AuditEvent(audit));
        THREAD_LOCAL.remove();
    }

    /**
     * 异常返回通知
     */
    @AfterThrowing(throwing = "e", pointcut = "auditAspect()")
    public void doAfterThrowable(Throwable e) {
        // 根据异常返回对象 e 再做一些操作
        Audit audit = getAudit();
        audit.setDescription(audit.getDescription() + " 失败 " + e.getMessage());
        // 发布事件
        applicationContext.publishEvent(new AuditEvent(audit));
        THREAD_LOCAL.remove();
    }
}
  • 10)在UserController中使用@Audit注解
// com.hsgx.event.controller.UserController

@com.hsgx.event.annotation.Audit(type = "saveUser", value = "新增用户")
@PostMapping("/save")
public String saveUser(){
    return "新增用户成功";
}
  • 11)重启服务并调用/user/save请求

本节完,更多内容查阅:后台管理系统的通用权限解决方案

延伸阅读:后台管理系统的通用权限解决方案(六)SpringBoot整合Logback实现日志记录

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

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

相关文章

曹操出行借助 ApsaraMQ for Kafka Serverless 提升效率,成本节省超 20%

本文整理于 2024 年云栖大会主题演讲《云消息队列 ApsaraMQ Serverless 演进》&#xff0c;杭州优行科技有限公司消息中间件负责人王智洋分享 ApsaraMQ for Kafka Serverless 助力曹操出行实现成本优化和效率提升的实践经验。 曹操出行&#xff1a;科技驱动共享出行未来 曹操…

Mysql常用语法一篇文章速成

文章目录 前言前置环境数据库的增删改查查询数据查询所有条件查询多条件查询模糊查询分页查询排序查询分组查询⭐️⭐️关联查询关联分页查询 添加数据insert插入多条记录不指定列名(适用于所有列都有值的情况) 更新数据更新多条记录更新多个列更新不满足条件的记录 删除统计数…

信息安全数学基础(43)理想和商环

理想&#xff08;Ideal&#xff09; 定义&#xff1a; 设R是一个环&#xff0c;I是R的一个非空子集。如果I满足以下条件&#xff0c;则称I为R的一个理想&#xff1a; 对于任意的r1, r2 ∈ I&#xff0c;有r1 - r2 ∈ I&#xff08;加法封闭性&#xff09;。对于任意的r ∈ I&am…

node.js下载、安装、设置国内镜像源(永久)(Windows11)

目录 node-v20.18.0-x64工具下载安装设置国内镜像源&#xff08;永久&#xff09; node-v20.18.0-x64 工具 系统&#xff1a;Windows 11 下载 官网https://nodejs.org/zh-cn/download/package-manager 版本我是跟着老师选的node-v20.18.0-x64如图选择 Windows、x64、v20.18…

《JVM第3课》运行时数据区

无痛快速学习入门JVM&#xff0c;欢迎订阅本免费专栏 运行时数据区结构图如下&#xff1a; 可分为 5 个区域&#xff0c;分别是方法区、堆区、虚拟机栈、本地方法栈、程序计数器。这里大概介绍一下各个模块的作用&#xff0c;会在后面的文章展开讲。 类加载子系统会把类信息…

苏州金龙新V系客车创新引领旅游出行未来

10月25日&#xff0c;为期三天的“2024第六届旅游出行大会”在风景秀丽的云南省丽江市落下帷幕。本次大会由中国旅游车船协会主办&#xff0c;全面展示了中国旅游出行行业最新发展动态和发展成就&#xff0c;为旅游行业带来全新发展动力。 在大会期间&#xff0c;备受瞩目的展车…

QML旋转选择器组件Tumbler

1. 介绍 Tumbler是一个用于创建旋转选择器的组件。它提供了一种直观的方式来让用户从一组选项中进行选择&#xff0c;类似于转盘式数字密码锁。网上找的类似网图如下&#xff1a; 在QML里&#xff0c;这种组件一共有两个版本&#xff0c;分别在QtQuick.Extras 1.4(旧)和QtQuic…

思科路由器静态路由配置

转载请注明出处 该实验为静态路由配置实验&#xff0c;仅供参考 选择三台2811路由器 关闭电源-安装模块-开启电源&#xff08;以R1为例&#xff0c;其他两台也是一样操作&#xff01;&#xff09; 连线。注意R1与R3之间、R3与R2之间用DCE串口线&#xff08;如下图&#xff09;…

闪存学习_2:Flash-Aware Computing from Jihong Kim

闪存学习_2&#xff1a;Flash-Aware Computing from Jihong Kim【1】 一、三个闪存可靠性问题二、内存的分类三、NAND 闪存和 NOR 闪存四、HDD和SSD比较Reference 一、三个闪存可靠性问题 耐性&#xff08;即寿命&#xff09;&#xff1a;最多能经受编程和擦除的次数。数据保留…

双指针问题解法集(一)

1.指针对撞问题&#xff08;利用有序数组的单调性衍生&#xff09; 1.1LCR 179. 查找总价格为目标值的两个商品 - 力扣&#xff08;LeetCode&#xff09;​​​​​​ 1.1.1题目解析 有序数组&#xff0c;找到和为price的两个元素&#xff0c;只需要一个解即可。 1.1.2算法…

深度学习(十):伦理与社会影响的深度剖析(10/10)

深度学习&#xff1a;伦理与社会影响的深度剖析 一、深度学习的伦理挑战 &#xff08;一&#xff09;数据隐私之忧 深度学习模型的训练往往需要大量数据&#xff0c;而数据的收集过程可能会侵犯个人隐私。例如&#xff0c;据统计&#xff0c;面部识别技术在全球范围内每天会收…

网络模型——二层转发原理

网课地址&#xff1a;网络模型_二层转发原理&#xff08;三&#xff09;_哔哩哔哩_bilibili 一、路由交换 网络&#xff1a;用来信息通信&#xff0c;信息共享的平台。 网络节点&#xff08;交换机&#xff0c;路由器&#xff0c;防火墙&#xff0c;AP&#xff09;介质&#…

探索NetCat:网络流量监测与数据传输的利器

从简单的数据传输到复杂的网络调试&#xff0c;NetCat的灵活性和多功能性让人赞叹不已&#xff0c;在这篇文章中我将深入探讨NetCat的魅力&#xff0c;揭示它的基本功能、实用技巧以及在日常工作中的应用场景&#xff0c;发现如何用这一小工具提升的网络技能与效率。 目录 Net…

提高交换网络可靠性之链路聚合

转载请注明出处 该实验为链路聚合的配置实验。 1.改名&#xff0c;分别将交换机1和交换机2改名为S1&#xff0c;S2&#xff0c;然后查看S1&#xff0c;S2的STP信息。以交换机1为例&#x1f447;。 2.交换机S1&#xff0c;S2上创建聚合端口&#xff0c;将端口加入聚合端口。以S…

Kubernetes:(三)Kubeadm搭建K8s 1.20集群

文章目录 一、Kubeadm安装流程二、实验1.环境准备2.所有节点安装kubeadm&#xff0c;kubelet和kubectl&#xff08;除了Harbor节点&#xff09;3.部署 Dashboard4.安装Harbor私有仓库 一、Kubeadm安装流程 集群名称IP地址安装软件master&#xff08;2C/4G&#xff0c;cpu核心数…

使用MongoDB Atlas构建无服务器数据库

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用MongoDB Atlas构建无服务器数据库 MongoDB Atlas 简介 注册账户 创建集群 配置网络 设置数据库用户 连接数据库 设计文档模式…

Windows 10 安装使用Docker踩过的坑和解决-31/10/2024

目录 环境版本 一、Docker Desktop双击启动没反应&#xff0c;open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified. 二、Docker Desktop运行run命令时显示错误HTTP code 500 并且错误大意是服务器拒绝访问 三、Docker Engine stopped/启动…

parted 磁盘分区

目录 磁盘格式磁盘分区文件系统挂载使用扩展 - parted、fdisk、gdisk 区别 磁盘格式 parted /dev/vdcmklabel gpt # 设置磁盘格式为GPT p # 打印磁盘信息此时磁盘格式设置完成&#xff01; 磁盘分区 开始分区&#xff1a; mkpart data_mysql # 分区名&…

论 ONLYOFFICE:开源办公套件的深度探索

公主请阅 引言第一部分&#xff1a;ONLYOFFICE 的历史背景1.1 开源软件的崛起1.2 ONLYOFFICE 的发展历程 第二部分&#xff1a;ONLYOFFICE 的核心功能2.1 文档处理2.2 电子表格2.3 演示文稿 第三部分&#xff1a;技术架构与兼容性3.1 技术架构3.2 兼容性 第四部分&#xff1a;部…

NVIDIA GeForce RTX 4090 Mobile 16G 性能分析

最近在看台式电脑&#xff0c;看到1W内居然能买到 i9-14900HX、RTX4090 16G&#xff0c;感觉不对劲&#xff0c;就离谱~ 4090的显卡什么时候出了16G的&#xff0c;而且还这这么便宜&#xff1f;&#xff1f;&#xff1f;&#xff1f; 目录 1、NVIDIA官网查查4090 2、对比RT…