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;
}
}