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


    系列文章

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

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



    前言

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

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

    文章说明

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

    一、准备工作

    首先我们导入Aop的坐标

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4

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

            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>fastjsonartifactId>
                <version>2.0.3version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    二、准备实操

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

    在这里插入图片描述

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Log {
        // 业务类型
        BusinessType businessType() default BusinessType.OTHER;
        // 模块名称
        String title() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

        @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));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2、编写切面类LogAspect.java

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

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

        /**
         * 环绕切点
         * @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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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);
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

    总结一下

    总的来说,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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    Log注解

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Log {
        // 业务类型
        BusinessType businessType() default BusinessType.OTHER;
        // 模块名称
        String title() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

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

    BusinessType.java 这是一个枚举

    	/**
     * @Description 业务操作类型
     */
    public enum BusinessType {
        /**
         * 其它
         */
        OTHER,
    
        /**
         * 新增
         */
        INSERT,
    
        /**
         * 修改
         */
        UPDATE,
    
        /**
         * 删除
         */
        DELETE,
    
        /**
         * 授权
         */
        GRANT,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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地址
         * 

    * 使用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; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

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

  • 相关阅读:
    docker部署neo4j
    数据库的级联删除
    在react+typescript中使用echarts
    PCB - 封装焊盘阻焊层的检查
    有趣的前端知识(三)
    DOM系列之DOM事件流
    内网渗透之Linux反弹shell(三)
    〖Python网络爬虫实战㊲〗- JavaScript 逆向实战(一)
    【编程题】【Scratch四级】2020.09 创意画图
    小试单一职责原则
  • 原文地址:https://blog.csdn.net/weixin_52258054/article/details/134033030