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)) {//根据网卡取本机配置的IPtry {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 时出现更多错误*/@PostConstructprivate 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<>();@Autowiredprivate Executor customThreadPoolTaskExecutor;@Autowiredprivate 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;}@Overridepublic 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 {@Beanpublic 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;}
}