8、springboot3 vue3开发平台-后端-使用aop 添加系统访问日志

1. 添加依赖, 创建数据库

		 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- IP地址解析 -->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
            <version>2.6.5</version>
        </dependency>

数据表创建:

CREATE TABLE `sys_log` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `log_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '日志类型',
  `create_date` datetime NOT NULL COMMENT '创建时间',
  `oper_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '操作人员',
  `request_uri` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求URI',
  `request_type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求方式',
  `request_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '请求参数',
  `request_ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求IP',
  `oper_location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '请求地点',
  `response_result` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT '' COMMENT '返回参数',
  `oper_status` int DEFAULT '0' COMMENT '操作状态(0正常1异常)',
  `exception_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '异常信息',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `execute_time` bigint DEFAULT NULL COMMENT '执行时间',
  `user_agent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户代理',
  `device_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作系统',
  `browser_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '浏览器名称',
  `module` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '模块名称',
  `oper_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作说明',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_sys_log_lt` (`log_type`) USING BTREE,
  KEY `idx_sys_log_cd` (`create_date`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=716 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='系统日志表';

2. ip2region.xdb 下载使用

使用原因: 内网环境提供离线解析, ip2region.xdb文件,需要不定期的更新
地址: https://gitee.com/lionsoul/ip2region/tree/master/data
使用:下载后将其放到resources 下, 在工具类中加载配置文件
在这里插入图片描述

2.1 工具类封装

package com.ylp.sys.utils;

import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;

import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;

@Component
public class IPUtils {
    private static Searcher searcher;

    /**
     * 在 Nginx 等代理之后获取用户真实 IP 地址
     * @return 用户的真实 IP 地址
     */
    public static String getIpAddress(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        String ip = request.getHeader("x-forwarded-for");
        if (isIpaddress(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (isIpaddress(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (isIpaddress(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (isIpaddress(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (isIpaddress(ip)) {
            ip = request.getRemoteAddr();
            if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
                //根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ip = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
            }
        }
        return ip;
    }

    /**
     * 判断是否为 IP 地址
     * @param ip  IP 地址
     */
    public static boolean isIpaddress(String ip) {
        return ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip);
    }

    /**
     * 获取本地 IP 地址
     * @return 本地 IP 地址
     */
    public static String getHostIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return "127.0.0.1";
    }

    /**
     * 获取主机名
     * @return 本地主机名
     */
    public static String getHostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        return "未知";
    }

    /**
     * 根据 IP 地址从 ip2region.db 中获取地理位置
     * @param ip IP 地址
     * @return IP归属地
     */
    public static String getCityInfo(String ip) {
        try {
            return searcher.search(ip);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 在服务启动时加载 ip2region.db 到内存中
     * 解决打包 jar 后找不到 ip2region.db 的问题
     * @throws Exception 出现异常应该直接抛出终止程序启动,避免后续 invoke 时出现更多错误
     */
    @PostConstruct
    private static void initIp2regionResource() {
        try {
            InputStream inputStream = new ClassPathResource("/ipdb/ip2region.xdb").getInputStream();
            byte[] dbBinStr = FileCopyUtils.copyToByteArray(inputStream);
            // 创建一个完全基于内存的查询对象
            searcher = Searcher.newWithBuffer(dbBinStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据 IP 地址返回归属地,国内返回但省份,国外返回到国家
     * @param ip IP 地址
     * @return IP 归属地
     */
    public static String getIpRegion(String ip) {
        initIp2regionResource();
        HashMap<String, String> cityInfo = new HashMap<>();
        String searchIpInfo = getCityInfo(ip);
        //-------------------------------------------------------
        //searchIpInfo 的数据格式: 国家|区域|省份|城市|ISP
        //192.168.31.160 0|0|0|内网IP|内网IP
        //47.52.236.180 中国|0|香港|0|阿里云
        //220.248.12.158 中国|0|上海|上海市|联通
        //164.114.53.60 美国|0|华盛顿|0|0
        //-------------------------------------------------------
        String[] splitIpInfo = searchIpInfo.split("\\|");
        cityInfo.put("ip",ip);
        cityInfo.put("searchInfo", searchIpInfo);
        cityInfo.put("country",splitIpInfo[0]);
        cityInfo.put("region",splitIpInfo[1]);
        cityInfo.put("province",splitIpInfo[2]);
        cityInfo.put("city",splitIpInfo[3]);
        cityInfo.put("ISP",splitIpInfo[3]);

        //--------------国内属地返回省份--------------
        if ("中国".equals(cityInfo.get("country"))){
            return cityInfo.get("province");
        }
        //------------------内网 IP----------------
        if ("0".equals(cityInfo.get("country"))){
//            if ("内网IP".equals(cityInfo.get("ISP"))){
//                return "";
//            }
//            else return "";
            return cityInfo.get("ISP");
        }
        //--------------国外属地返回国家--------------
        else {
            return cityInfo.get("country");
        }
    }

}

3. 使用AOP 注册访问日志

3.1 创建注解,用于标注接口

package com.ylp.sys.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLogAnnotation {
    String module()  default "";//模块
    String operDesc() default "";   // 操作说明
}

3.2 创建AOP 配置

package com.ylp.sys.aop;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ylp.common.response.Result;
import com.ylp.sys.annotation.SysLogAnnotation;
import com.ylp.sys.auth.entity.UserInfo;
import com.ylp.sys.common.SysLogConstant;
import com.ylp.sys.domain.entity.SysLog;
import com.ylp.sys.service.SysLogService;
import com.ylp.sys.utils.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
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 java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * 系统日志切面
 *
 * @author ylp
 *
 */
@Aspect
@Component
public class SysLogAspect {
    private static final Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
    private ThreadLocal<SysLog> sysLogThreadLocal = new ThreadLocal<>();

    @Autowired
    private Executor customThreadPoolTaskExecutor;

    @Autowired
    private SysLogService sysLogService;

    /**
     * 日志切点
     */
    @Pointcut("execution(public * com.ylp..*controller.*.*(..))")
    public void sysLogOperAspect() {
    }

    @Pointcut("execution(public * com.ylp.sys.auth.controller.*.*(..))")
    public void sysLogAuthAspect() {
    }

    // 定义一个组合切点
    @Pointcut("sysLogOperAspect() || sysLogAuthAspect()")
    public void combinedExecution() {}

    /**
     * 前置通知
     *
     * @param joinPoint
     */
    @Before(value = "combinedExecution()")
    public void doBefore(JoinPoint joinPoint) {
        //System.out.println("doBefore aop===============joinPoint===="+ joinPoint);
      try {
          HttpServletRequest request = ((ServletRequestAttributes) Objects
                  .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
          SysLog sysLog = new SysLog();
          // 创建人信息请根据实际项目获取方式获取
          //登录后拿用户信息
          if(StpUtil.isLogin()) {
              UserInfo userLoginInfo = (UserInfo) StpUtil.getSession().get("userInfo");
              sysLog.setOperName(userLoginInfo.getUsername());
          }
          sysLog.setStartTime(LocalDateTime.now());
          sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
          sysLog.setRequestParams(formatParams(request.getParameterMap()));
          sysLog.setRequestType(request.getMethod());
          sysLog.setRequestIp(IPUtils.getIpAddress(request));
          String userAgentStr = request.getHeader("User-Agent");
          sysLog.setUserAgent(userAgentStr);
          UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
          sysLog.setDeviceName(userAgent.getOs().getName());
          sysLog.setBrowserName(userAgent.getBrowser().getName());

          // 获取请求体参数
          Object[] args = joinPoint.getArgs();
          for(Object arg : args){
              try {
                  String jsonString = JSON.toJSONString(arg);
                  JSONObject jsonObject = JSON.parseObject(jsonString);
                  if (jsonObject.containsKey("password")) {
                      jsonObject.put("password", "*****");
                  }
                  System.out.println("参数=" + JSON.toJSONString(jsonObject));
                  sysLog.setRequestParams(sysLog.getRequestParams() + JSON.toJSONString(jsonObject));
              } catch (Exception e) {
                  //e.printStackTrace();
              }
          }

          // 获取日志注解
          MethodSignature signature = (MethodSignature)joinPoint.getSignature();
          SysLogAnnotation annotation = signature.getMethod().getAnnotation(SysLogAnnotation.class);
          if (annotation != null) {
              sysLog.setModule(annotation.module());
              sysLog.setOperDesc(annotation.operDesc());
          }
          sysLogThreadLocal.set(sysLog);
         // System.out.println("doBefore aop111111===============");

//          logger.info("开始计时: {}  URI: {}  IP: {}", sysLog.getStartTime(), sysLog.getRequestUri(), sysLog.getRequestIp());
      } catch (Exception e) {
          logger.error(e.getMessage());
      }
    }

    /**
     * 返回通知
     *
     * @param ret
     */
    @AfterReturning(pointcut = "combinedExecution()", returning = "ret")
    public void doAfterReturning(Object ret) {
       // System.out.println("doAfterReturning aop===============");
        try {
            SysLog sysLog = sysLogThreadLocal.get();
            sysLog.setLogType(SysLogConstant.LOG_INGO);
            sysLog.setEndTime(LocalDateTime.now());
            sysLog.setExecuteTime(Long.valueOf(ChronoUnit.MILLIS.between(sysLog.getStartTime(), sysLog.getEndTime())));
            Result<?> r = Convert.convert(Result.class, ret);
//            if (SysLogConstant.TRUE.equals(String.valueOf(r.getCode()))) {
            if (r.getCode() == 0) {
                sysLog.setOperStatus(SysLogConstant.OPER_SUCCESS);
            } else {
                sysLog.setOperStatus(SysLogConstant.OPER_EXECPTION);
                sysLog.setExceptionInfo(r.getMessage());
            }
            sysLog.setResponseResult(JSON.toJSONString(r));
            customThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, sysLogService));
            sysLogThreadLocal.remove();

           // Runtime runtime = Runtime.getRuntime();
//            logger.info("计时结束: {}  用时: {}ms  URI: {}  总内存: {}  已用内存: {}", sysLog.getEndTime(), sysLog.getExecuteTime(),
//                    sysLog.getRequestUri(), ByteUtils.formatByteSize(runtime.totalMemory()),
//                    ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
    }

    /**
     * 异常通知
     *
     * @param e
     */
    @AfterThrowing(pointcut = "combinedExecution()", throwing = "e")
    public void doAfterThrowable(Throwable e) {
        try {
            SysLog sysLog = sysLogThreadLocal.get();
            sysLog.setLogType(SysLogConstant.LOG_ERROR);
            sysLog.setEndTime(LocalDateTime.now());
            sysLog.setExecuteTime(Long.valueOf(ChronoUnit.MINUTES.between(sysLog.getStartTime(), sysLog.getEndTime())));
            sysLog.setOperStatus(SysLogConstant.OPER_EXECPTION);
            sysLog.setExceptionInfo(e.getMessage());
            customThreadPoolTaskExecutor.execute(new SaveLogThread(sysLog, sysLogService));
            sysLogThreadLocal.remove();

//            Runtime runtime = Runtime.getRuntime();
//            logger.info("计时结束: {}  用时: {}ms  URI: {}  总内存: {}  已用内存: {}", sysLog.getEndTime(), sysLog.getExecuteTime(),
//                    sysLog.getRequestUri(), ByteUtils.formatByteSize(runtime.totalMemory()),
//                    ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
        } catch (Exception e1) {
            logger.error(e1.getMessage());
        }
    }

    /**
     * 格式化参数
     *
     * @param parameterMap
     * @return
     */
    private String formatParams(Map<String, String[]> parameterMap) {
        if (parameterMap == null) {
            return null;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry<String, String[]> param : (parameterMap).entrySet()) {
            if (params.length() != 0) {
                params.append("&");
            }
            params.append(param.getKey() + "=");
            if (StrUtil.endWithIgnoreCase(param.getKey(), "password")) {
                params.append("*");
            } else if (param.getValue() != null) {
                params.append(ArrayUtil.join(param.getValue(), ","));
            }
        }
        return params.toString();
    }

    /**
     * 保存日志线程
     *
     * @author ylp
     */
    private static class SaveLogThread extends Thread {
        private SysLog sysLog;
        private SysLogService sysLogService;

        public SaveLogThread(SysLog sysLog, SysLogService sysLogService) {
            this.sysLog = sysLog;
            this.sysLogService = sysLogService;
        }

        @Override
        public void run() {
            try {
                sysLog.setCreateDate(LocalDateTime.now());
                String ipLocation = IPUtils.getIpRegion(sysLog.getRequestIp());
                logger.info("ip地址{}", ipLocation);
                sysLog.setOperLocation(ipLocation);
                sysLogService.save(sysLog);
            } catch (Exception e) {
                logger.error(e.getMessage());
            }
        }
    }
}

3.3 配置线程池

将配置放到support 模块下, 后续其他模块也可以直接使用, 开发环境不要将线程数配置到极限,会影响其他应用。

package com.ylp.support.config.thread;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池配置类
 *
 * @author ylp
 *
 */
@Configuration
public class ThreadPoolTaskExecutorConfig {

    @Bean
    public Executor customThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // Java虚拟机可用的处理器数
        int corePoolSize = Runtime.getRuntime().availableProcessors();
        // 配置核心线程数
        executor.setCorePoolSize(corePoolSize);
        // 配置最大线程数
//        executor.setMaxPoolSize(corePoolSize * 2 + 1);
        executor.setMaxPoolSize(corePoolSize + 1);
        // 配置队列大小
        executor.setQueueCapacity(100);
        // 空闲的多余线程最大存活时间
        executor.setKeepAliveSeconds(3);
        // 配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("thread-execute-");
        // 当线程池达到最大大小时,在调用者的线程中执行任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 执行初始化
        executor.initialize();
        return executor;
    }
}

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不知所云,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值