使用AOP切面实现日志记录功能

news2025/4/9 0:22:27

系列文章

1.SpringBoot整合RabbitMQ并实现消息发送与接收
2. 解析JSON格式参数 & 修改对象的key
3. VUE整合Echarts实现简单的数据可视化
4. Java中运用BigDecimal对字符串的数值进行加减乘除等操作
5. List<HashMap<String,String>>实现自定义字符串排序(key排序、Value排序)

更多该系列文章可以看我主页哦


目录

  • 系列文章
  • 前言
    • 一、准备工作
    • 二、准备实操
      • 2.1、编写一个自己定义的Log注解
      • 2.2、编写切面类LogAspect.java
          • 2.2.1、定义切面
          • 2.2.2、代码编写
      • 总结一下
      • 源码展示


前言

说到AOP大家都可以想到他是面向切面的编程,它通过将横切关注点(例如日志记录、事务管理、权限控制等)从主要业务逻辑中分离出来,以模块化的方式进行管理。在AOP中,通过定义切面(Aspect)来捕获和处理横切关注点,然后将其应用于特定的目标对象或方法。

官方的解释有点抽象,我们举个例子说明:假设我们需要在多个方法中添加日志记录功能。传统的方式是在每个方法中都添加日志代码,但这样会导致代码重复,并且当我们需要修改日志记录逻辑时,需要逐个修改所有方法。而使用AOP,我们只需定义一个切面,将日志记录的逻辑写在切面中。然后,通过在需要添加日志的地方进行配置,就能自动将切面应用到目标方法中,实现日志记录的功能。

文章说明

本篇文章主要是使用Aop的环绕通知去实现将每次请求的接口信息(操作的模块,请求方法,请求的url,请求的ip,入参,出参,以及耗时)进行记录并存到数据库。

一、准备工作

首先我们导入Aop的坐标

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

因为我们有一些结果要json输出 ,所以用了fastjson依赖,下面给出xml坐标,当然你也可以喜欢着其他的转json工具

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.3</version>
        </dependency>

二、准备实操

2.1、编写一个自己定义的Log注解

在这里插入图片描述

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    // 业务类型
    BusinessType businessType() default BusinessType.OTHER;
    // 模块名称
    String title() default "";
}

模块名称、业务类型等可以根据自己的实际情况去添加和删除
之后我们将注解写在需要记录的方法上面,这里是一个简单的分页查询 ,入参为每页条数和页码,出参就是分页的结果

    @Log(title = "分页查询商品",businessType = BusinessType.GETAll)
    @GetMapping("/goods/list")
    public Result pageList(int pageNum,int pageSize,String name , String useage){
        return Result.success(goodsService.selectPage(pageNum,pageSize,name,useage));
    }

2.2、编写切面类LogAspect.java

2.2.1、定义切面
    /**
     * 定义切面
     */
   @Pointcut("@annotation(com.example.masks.annotation.Log)")
   public void pt() {
  }
2.2.2、代码编写

我们定义一个环绕切点,首先记录当前时间,作为切点方法执行前的时间戳,使用 pjp.proceed() 执行切点方法,之后接着计算切点方法执行的时长,并记录日志。这里调用了 handleLog() 方法来处理日志记录,它需要传入 pjp、runTime 和 result 三个参数。

    /**
     * 环绕切点
     * @param
     * @return result
     */
    @Around("Log()")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        long beginTime = System.currentTimeMillis();
        // 执行切点方法
        Object result = pjp.proceed();
        // 执行时长
        Long runTime = System.currentTimeMillis() - beginTime;
        handleLog(pjp,runTime,result);
        return result;
    }

handleLog() 方法中,首先获取切点方法的签名和注解信息,在从注解中获取模块和业务类型信息, 之后依次获取、请求参数HTTP方法IP地址请求URL 等信息:

    private void handleLog(ProceedingJoinPoint pjp,Long runTime, Object result) {
        MethodSignature signature = (MethodSignature) 		    
        pjp.getSignature();
        Method method = signature.getMethod();
        // 获取注解内容
        Log logAnnotation = method.getAnnotation(Log.class);
        // 获取模块
        String title = logAnnotation.title();
        // 获取业务类型
        BusinessType businessType = logAnnotation.businessType();
        Object[] args = pjp.getArgs();
        // 入参数
        String params = JSON.toJSONString(args);
        //出参
        String res = JSON.toJSONString(result);
        // 请求方法
        String httpMenthod = httpServletRequest.getMethod();
        // ip
        String ip = IPUtils.getIpAddr(httpServletRequest);
        // 请求url
        String requestURL = httpServletRequest.getRequestURL().toString();
        // 封装日志对象
        SysLog sysLog = new SysLog(title, businessType, httpMenthod, requestURL, ip, params, res, runTime);
        // 这里可以根据自己的需求去处理sysLog,可以存储到数据库等,储存到数据库的操作就不展示了,比较简单,我这里就控制台输出一下这一条信息
        System.out.println(sysLog);
        

展示一下:因为我把日志存储到了数据库、就给大家展示一下数据库的结果
在这里插入图片描述

总结一下

总的来说,AOP 日志记录是一种实现代码模块化和复用的好方法,可以提高代码的可维护性和可读性。在实际开发中,我们应该灵活运用 AOP 技术,根据实际需求选择合适的切点表达式和日志记录方式,并注意日志级别和格式的设置,以便更好地记录和分析日志信息。

希望通过本篇文章,让大家对Aop有一个更深入的了解,尤其是AOP去处理日志的功能,是Aop最常见的一个功能,我这里只是进行简单的AOP日志功能的运用,如果大家有什么更好的方法和对我代码改进的地方,请大家积极私信,一起努力

源码展示

sysLog.java 封装的实体

public class SysLog {
    private Long id;
    /**
     * 操作模块
     */
    private String title;
    /**
     * 业务类型
     */
    private BusinessType businessType;
    /**
     * 请求类型
     */
    private String requestMethod;
    /**
     * 请求URl
     */
    private String operUrl;
    /**
     * 请求IP
     */
    private String operIp;
    /**
     * 请求参数
     */
    private String operParam;
    /**
    * 出参
    */
    private String resultParam;
    /**
     * 消耗时间-ms
     */
    private Long costTime;
    public SysLog(String title, BusinessType businessType, String requestMethod, String operUrl, String operIp, String operParam,String resultParam,  Long costTime) {
        this.title = title;
        this.businessType = businessType;
        this.requestMethod = requestMethod;
        this.operUrl = operUrl;
        this.operIp = operIp;
        this.operParam = operParam;
        this.resultParam = resultParam;
        this.costTime = costTime;
    }

Log注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    // 业务类型
    BusinessType businessType() default BusinessType.OTHER;
    // 模块名称
    String title() default "";
}

LogAspect.java 切面类

@Aspect
@Component
public class LogAspect {

    @Autowired
    HttpServletRequest httpServletRequest;

    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    /**
     * 定义切面
     */
    @Pointcut("@annotation(com.xiaoke.annotation.Log)")
    public void pt() {
    }

    /**
     * 环绕切点
     */
    @Around("pt()")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        long beginTime = System.currentTimeMillis();
        // 执行切点方法M
        Object result = pjp.proceed();
        // 执行时长
        Long runTime = System.currentTimeMillis() - beginTime;
        // 记录日志
        handleLog(pjp,runTime,result);
        return result;
    }
   private void handleLog(ProceedingJoinPoint pjp,Long runTime, Object result) {
        MethodSignature signature = (MethodSignature) 		    
        pjp.getSignature();
        Method method = signature.getMethod();
        // 获取注解内容
        Log logAnnotation = method.getAnnotation(Log.class);
        // 获取模块
        String title = logAnnotation.title();
        // 获取业务类型
        BusinessType businessType = logAnnotation.businessType();
        Object[] args = pjp.getArgs();
        // 入参数
        String params = JSON.toJSONString(args);
        //出参
        String res = JSON.toJSONString(result);
        // 请求方法
        String httpMenthod = httpServletRequest.getMethod();
        // ip
        String ip = IPUtils.getIpAddr(httpServletRequest);
        // 请求url
        String requestURL = httpServletRequest.getRequestURL().toString();
        // 封装日志对象
        SysLog sysLog = new SysLog(title, businessType, httpMenthod, requestURL, ip, params, res, runTime);
        // 这里可以根据自己的需求去处理sysLog,可以存储到数据库等,储存到数据库的操作就不展示了,比较简单,我这里就控制台输出一下这一条信息
        System.out.println(sysLog);
  }
}

下面是俩个工具类,百度可以搜索到 这里也给出源码

BusinessType.java 这是一个枚举

	/**
 * @Description 业务操作类型
 */
public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 授权
     */
    GRANT,
}

IPutils.java 这个主要是获取ip

import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class IPUtils {
    private static final String IP_UTILS_FLAG = ",";
    private static final String UNKNOWN = "unknown";
    private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
    private static final String LOCALHOST_IP1 = "127.0.0.1";
    /**
     * 获取IP地址
     * <p>
     * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
     * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = null;
        try {
            //以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
            ip = request.getHeader("X-Original-Forwarded-For");
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("X-Forwarded-For");
            }
            //获取nginx等代理的ip
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("x-forwarded-for");
            }
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            //兼容k8s集群获取ip
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
                if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
                    //根据网卡取本机配置的IP
                    InetAddress iNet = null;
                    try {
                        iNet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        System.out.println();
                        System.out.println("getClientIp error"+e.getMessage());
                    }
                    assert iNet != null;
                    ip = iNet.getHostAddress();
                }
            }
        } catch (Exception e) {
            System.out.println("IPUtils ERROR"+e.getMessage());
        }
        //使用代理,则获取第一个IP地址
        if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
            ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
        }

        return ip;
    }
}

以上就是全部源码了 有兴趣的朋友可以观看我其他的文章和私信我哦

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

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

相关文章

【Javascript】函数(变量作用域)

变量&#xff1a;全局变量&#xff0c;局部变量 全局变量 挂载到window对象上的 var a全局变量;console.log(a);var a全局变量;console.log(window.a);var a全局变量;在控制台里输入a也能打印a的值 局部变量 函数体内部声明的变量 var a全局变量;function test(){var b局部…

软考高级之系统架构师系列之UP、RUP、4+1视图、JAD、JRP、RAD

概述 软件工程是一个很庞杂的系统工程&#xff0c;而我们面对的软件需求也很复杂&#xff1a; 面对不同规模&#xff08;复杂度&#xff0c;模块量&#xff0c;用户量&#xff0c;开发周期等等&#xff09;的软件项目&#xff0c;人员储备不尽不同的开发团队也会采用不同的软…

数据可视化在行业解决方案中的实践应用 ——华为云Astro Canvas大屏开发研究及指南

本文主要探讨华为云Astro Canvas在数据可视化大屏开发中的应用及效果。首先阐述Astro Canvas的基本概念、功能和特性说明&#xff0c;接着集中分析展示其在教育、金融、交通行业等不同领域实际应用案例&#xff1b;之后&#xff0c;详细介绍使用该工具进行大屏图表创建的开发指…

22年下半年上午题

计算机指令集 cpu的构成 存储器 决策表 原型模型 白盒测试 活动图 构件图 半圆是需接口&#xff0c;满圆是供接口&#xff0c;上图有小错误。 故障类型 b-树 排序算法复杂度 二分查找平均比较次数 成功查找比较平均次数 失败查找平均比较次数 如有 OSI 模型层次对应典型机器…

Vue+ElementUI项目打包部署到Ubuntu服务器中

1、修改config/index.js中的assetsPublicPath: /,修改为assetsPublicPath: ./ assetsPublicPath: ./2、在build/utils.js中增加publicPath: ../../ publicPath: ../../3、打开终端&#xff0c;在根目录下执行npm run build进行打包&#xff0c;打包成功后会生成dist npm run…

前端使用 printJS 插件打印多页:第一页空白问题解决

printJS({printable: [data:image/jpg;base64,${this.printData.url}],type: image,style: media print { page {size: auto; margin: 0; } body{margin:0 5px}} // 解决出现多页打印时第一页空白问题 })

java基础 集合2

9.List遍历方式&#xff1a; 10.Arraylist底层原理&#xff1a; 11.Linklist底层原理&#xff1a; 1.LinkedList做队列和栈&#xff1a; package day01;import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List;publ…

Vue3 + Tsx 集成 ace-editor编辑器

Ace Editor介绍 Ace Editor&#xff08;全名&#xff1a;Ajax.org Cloud9 Editor&#xff09;是一个开源的代码编辑器&#xff0c;旨在提供强大的代码编辑功能&#xff0c;通常用于构建基于Web的代码编辑应用程序。它最初由Cloud9 IDE开发&#xff0c;现在由开源社区维护。 主…

计算机网络 第四章网络层

文章目录 1 网络层的功能2 数据交换方式&#xff1a;电路交换3 数据交换方式&#xff1a;报文交换4 数据交换方式&#xff1a;分组交换5 数据交换方式&#xff1a;数据报方式6 数据交换方式&#xff1a;虚电路方式及各种方式对比7 路由算法及路由协议8 IP数据报的概念和格式9 I…

数据存储成本降低50%!图匠数据搭载OceanBase全新出发

近日&#xff0c;AI 技术公司 ImageDT 图匠数据&#xff08;以下简称“图匠”&#xff09;上线 OceanBase。目前&#xff0c;公司两大核心业务“数货宝”、“数智柜”已全面接入 OB Cloud 云数据库&#xff0c;保障图匠一站式全渠道销售数字化闭环作战平台的每一笔「数据」都算…

浮动面试题

浮动元素特点&#xff1a;

找不到mfc100u.dll怎么解决,总结了多种修复方法帮你解决

首先&#xff0c;让我们来了解一下mfc100u.dll文件是什么&#xff1f;其实&#xff0c;mfc100u.dll是Microsoft Foundation Class(MFC)库中的一个动态链接库文件&#xff0c;它包含了一些常用的类、函数和变量等资源&#xff0c;用于支持Windows应用程序的开发。 那么&#xf…

顺序表的查找(按位查找、按值查找)(数据结构与算法)

顺序表的基本操作&#xff1a;按位查找、按值查找 顺序表的按位查找 GetElem(L, i) :按位查找&#xff0c;获取表L中第 i 个位置元素的值 #define MaxSize 10 //定义最大长度 typedef struct{ElemType data[MaxSize]; //用静态的“数组”存放数据元…

大模型如何商业变现?小i机器人发布华藏大模型生态

华藏通用大模型生态体系由“113”三部分组分&#xff0c;即&#xff1a;一个能力基座一项产品支撑三项服务保障。 今年以来&#xff0c;市场上各类人工智能大模型如雨后春笋&#xff0c;但如何将大模型进行科学的商业变现&#xff0c;成为摆在行业面前的一道难题。在刚刚召开的…

Nginx+cpolar实现内网穿透多个Windows Web站点端口

文章目录 1. 下载windows版Nginx2. 配置Nginx3. 测试局域网访问4. cpolar内网穿透5. 测试公网访问6. 配置固定二级子域名7. 测试访问公网固定二级子域名【总结】&#xff1a; 1. 下载windows版Nginx 进入官方网站(http://nginx.org/en/download.html)下载windows版的nginx 下载…

Postman如何导出接口的几种方法?

本文主要介绍了Postman如何导出接口的几种方法&#xff0c;文中通过示例代码介绍的非常详细&#xff0c;具有一定的参考价值&#xff0c;感兴趣的小伙伴们可以参考一下 前言&#xff1a; 我的文章还是一贯的作风&#xff0c;简确用风格&#xff08;简单确实有用&#xff09;&a…

Apifox创建团队 项目 接口 邀请成员步骤演示

我们打开Apifox 找到 个人空间 然后 点击新建团队 然后这里 我们输入名字 点击确定 我们的团队就出来了 然后 我们点击新建项目 然后肯定是 http 项目名称输入一下 然后 语言 我们中国肯定是中文的 然后点击确定 建好之后 我们就会进入自己的项目啦 然后 我们可以新建个接…

VScode中配置python环境

声明&#xff1a;本文出自B站UP主---火星动力猿 下载教程点击下面链接 【文档包】VScode配置Python【发布】.zip - 蓝奏云文件大小&#xff1a;2.4 M|https://wwn.lanzouh.com/iG5tn03iqwwh

单例模式及其使用场景

单例模式&#xff08;Singleton&#xff09;&#xff1a;指在一个系统中某个类只存在一个实例&#xff0c;类中自行实例化&#xff0c;实例向该系统提供统一的访问接口。 单例模式有两种表现形式&#xff0c;饿汉式&#xff1a;类加载时&#xff0c;就进行实例化&#xff1b;懒…

微信小程序获取用户信息

个人博客 微信小程序获取用户信息 个人微信公众号&#xff0c;求关注&#xff0c;求收藏&#xff0c;求指错。 文章概叙 本文主要讲的是小程序获取用户信息的&#xff0c;更新测试时间是2023-10-25 更改原因 首先&#xff0c;官网上的解释是这样的&#xff0c;为了安全合…