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

系列文章

1.SpringBoot整合RabbitMQ并实现消息发送与接收

  1. 解析JSON格式参数 & 修改对象的key

  2. VUE整合Echarts实现简单的数据可视化

  3. Java中运用BigDecimal对字符串的数值进行加减乘除等操作

  4. List<HashMap<String,String>>实现自定义字符串排序(key排序、Value排序)

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


目录


前言

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

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

文章说明

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

一、准备工作

首先我们导入Aop的坐标

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

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

xml 复制代码
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.3</version>
        </dependency>

二、准备实操

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

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

模块名称、业务类型等可以根据自己的实际情况去添加和删除

之后我们将注解写在需要记录的方法上面,这里是一个简单的分页查询 ,入参 为每页条数和页码,出参就是分页的结果

java 复制代码
    @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、定义切面
java 复制代码
    /**
     * 定义切面
     */
   @Pointcut("@annotation(com.example.masks.annotation.Log)")
   public void pt() {
  }
2.2.2、代码编写

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

java 复制代码
    /**
     * 环绕切点
     * @param
     * @return result
     */
    @Around("pt()")
    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 等信息:

java 复制代码
    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 封装的实体

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注解

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

LogAspect.java 切面类

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 这是一个枚举

java 复制代码
	/**
 * @Description 业务操作类型
 */
public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,

    /**
     * 授权
     */
    GRANT,
}

IPutils.java 这个主要是获取ip

java 复制代码
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;
    }
}

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

相关推荐
The Future is mine20 分钟前
Python计算经纬度两点之间距离
开发语言·python
Enti7c21 分钟前
HTML5和CSS3的一些特性
开发语言·css3
腥臭腐朽的日子熠熠生辉26 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
爱吃巧克力的程序媛28 分钟前
在 Qt 创建项目时,Qt Quick Application (Compat) 和 Qt Quick Application
开发语言·qt
ejinxian28 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之33 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
俏布斯1 小时前
算法日常记录
java·算法·leetcode
独好紫罗兰1 小时前
洛谷题单3-P5719 【深基4.例3】分类平均-python-流程图重构
开发语言·python·算法
27669582921 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿