基于AOP实现访客日志记录

1.准备所需的日志表

1
2
3
4
5
6
7
8
9
10
11
create table t_visit_log
(
id int auto_increment comment 'id'
primary key,
page varchar(50) null comment '访问页面',
ip_address varchar(50) null comment '访问ip',
ip_source varchar(50) null comment '访问地址',
os varchar(50) null comment '操作系统',
browser varchar(50) null comment '浏览器',
create_time datetime not null comment '访问时间'
);

2.pom依赖

3.实体

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
/**
* 访问日志
*/
@Data
public class VisitLog {
/**
* id
*/
@TableId(type = IdType.AUTO)
private Integer id;

/**
* 访问页面
*/
private String page;

/**
* 访问ip
*/
private String ipAddress;

/**
* 访问地址
*/
private String ipSource;

/**
* 操作系统
*/
private String os;

/**
* 浏览器
*/
private String browser;

/**
* 访问时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

}

4.自定义日志注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author ehyzil
* @Description 访问日志注解
* @create 2023-10-2023/10/1-20:37
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitLogger {
/**
*
* @return 访问页面
*/
String value() default "";
}

5.Spring实例获取工具类

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
// 这是一个实用工具类,用于与Spring容器交互,获取Bean实例等功能。
public final class SpringUtils implements BeanFactoryPostProcessor {

private static ConfigurableListableBeanFactory beanFactory;

@Override
public void postProcessBeanFactory(@NotNull ConfigurableListableBeanFactory beanFactory) throws BeansException {
SpringUtils.beanFactory = beanFactory;
}

/**
* 获取指定名称的对象
* @param name 对象的名称
* @return 获取到的对象实例
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) beanFactory.getBean(name);
}

/**
* 获取指定类型的对象
* @param clz 对象的类型
* @return 获取到的对象实例
*/
public static <T> T getBean(Class<T> clz) throws BeansException {
return beanFactory.getBean(clz);
}

/**
* 获取指定名称的Bean的类型
* @param name Bean的名称
* @return Bean的类型
* @throws NoSuchBeanDefinitionException 如果找不到对应的Bean定义
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return beanFactory.getType(name);
}
}

6.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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.ehzyil.utils;

import com.ehzyil.exception.ServiceException;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;

/**
* IP地址工具类
*
* @author ehzyil
*/
@SuppressWarnings("all")
public class IpUtils {

private static Searcher searcher;

static {
// 解决项目打包找不到ip2region.xdb
try {
InputStream inputStream = new ClassPathResource("/ipdb/ip2region.xdb").getInputStream();
//将 ip2region.db 转为 ByteArray
byte[] cBuff = FileCopyUtils.copyToByteArray(inputStream);
searcher = Searcher.newWithBuffer(cBuff);
} catch (IOException e) {
throw new ServiceException("ip2region.xdb加载失败");
}

}

/**
* 在Nginx等代理之后获取用户真实IP地址
*/
public static String getIpAddress(HttpServletRequest request) {
String ip;
try {
ip = request.getHeader("X-Real-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
//根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
throw new UnknownHostException("无法确定主机的IP地址");
}
ip = inet.getHostAddress();
}
}
// 使用代理,则获取第一个IP地址
if (!StringUtils.hasText(ip) && Objects.requireNonNull(ip).length() > 15) {
int idx = ip.indexOf(",");
if (idx > 0) {
ip = ip.substring(0, idx);
}
}
} catch (Exception e) {
ip = "";
}
return ip;
}

/**
* 根据ip从 ip2region.db 中获取地理位置
*
* @param ip
* @return
*/
public static String getIpSource(String ip) {
try {
String address = searcher.searchByStr(ip);
if (StringUtils.hasText(address)) {
address = address.replace("|0", "");
address = address.replace("0|", "");
return address;
}
return address;
} catch (Exception e) {
return "";
}
}

}

7.线程池配置

ThreadPoolProperties类是一个自定义的属性类,用于配置线程池的相关属性。

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
package com.ican.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
* 线程池参数
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "thread.pool")
public class ThreadPoolProperties {

/**
* 核心线程池大小
*/
private int corePoolSize;

/**
* 最大可创建的线程数
*/
private int maxPoolSize;

/**
* 队列最大长度
*/
private int queueCapacity;

/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds;
}

线程池配置

threadPoolTaskExecutor()方法,使用@Bean注解将其声明为一个Bean。该方法返回了一个ThreadPoolTaskExecutor对象,用于创建线程池。

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
/**
* 线程池配置
**/
@Configuration
public class ThreadPoolConfig {

@Autowired
private ThreadPoolProperties threadPoolProperties;

/**
* 创建线程池
*
* @return 线程池
*/
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程池大小
executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
// 最大可创建的线程数
executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
// 等待队列最大长度
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
// 线程池维护线程所允许的空闲时间
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}

/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
return new ScheduledThreadPoolExecutor(threadPoolProperties.getCorePoolSize(),
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy()) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
ThreadUtils.printException(r, t);
}
};
}

}

使用了ThreadPoolExecutor.CallerRunsPolicy(),表示当线程池无法接受新任务时,将任务回退到调用者线程中执行。

8.异步任务配置

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
package com.ehzyil.manager;


import com.ehzyil.utils.SpringUtils;
import com.ehzyil.utils.ThreadUtils;

import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* 异步任务管理器
*
* @author ehzyil
*/
public class AsyncManager {

/**
* 单例模式,确保类只有一个实例
*/
private AsyncManager() {
}

/**
* 饿汉式,在类加载的时候立刻进行实例化
*/
private static final AsyncManager INSTANCE = new AsyncManager();

public static AsyncManager getInstance() {
return INSTANCE;
}

/**
* 异步操作任务调度线程池
*/
private final ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

/**
* 执行任务
*
* @param task 任务
*/
public void execute(TimerTask task) {
executor.schedule(task, 10, TimeUnit.MILLISECONDS);
}

/**
* 停止任务线程池
*/
public void shutdown() {
ThreadUtils.shutdownAndAwaitTermination(executor);
}

}

线程工具类ThreadUtils

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
package com.ehzyil.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.*;

/**
* 线程工具类
*
* @author ehzyil
*/
public class ThreadUtils {

private static final Logger logger = LoggerFactory.getLogger(ThreadUtils.class);

private static final long OVERTIME = 120;

/**
* 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* 如果仍然超時,則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/
public static void shutdownAndAwaitTermination(ExecutorService pool) {
if (pool != null && !pool.isShutdown()) {
pool.shutdown();
try {
if (!pool.awaitTermination(OVERTIME, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(OVERTIME, TimeUnit.SECONDS)) {
logger.info("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

/**
* 打印线程异常信息
*/
public static void printException(Runnable r, Throwable t) {
if (t == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (t != null) {
logger.error(t.getMessage(), t);
}
}
}

9.任务工厂配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 异步工厂(产生任务用)
*/
public class AsyncFactory {
/**
* 记录访问日志
*
* @param visitLog 访问日志信息
* @return 任务task
*/
public static TimerTask recordVisit(VisitLog visitLog) {
return new TimerTask() {
@Override
public void run() {
SpringUtils.getBean(VisitLogService.class).saveVisitLog(visitLog);
}
};
}
}

核心切面配置

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
/**
* AOP记录访问日志
**/
@Aspect
@Component
public class VisitLogAspect {

@Pointcut("@annotation(com.ehzyil.annotation.VisitLogger)")
public void visitLogPointCut() {
}

/**
* 连接点正常返回通知,拦截用户操作日志,正常执行完成后执行, 如果连接点抛出异常,则不会执行
*
* @param joinPoint 切面方法的信息
* @param result 返回结果
*/
@AfterReturning(value = "visitLogPointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
VisitLogger visitLogger = method.getAnnotation(VisitLogger.class);
// 获取request
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
VisitLog visitLog = new VisitLog();
String ipAddress = IpUtils.getIpAddress(request);
String ipSource = IpUtils.getIpSource(ipAddress);
// 解析browser和os
Map<String, String> userAgentMap = UserAgentUtils.parseOsAndBrowser(request.getHeader("User-Agent"));
visitLog.setIpAddress(ipAddress);
visitLog.setIpSource(ipSource);
visitLog.setOs(userAgentMap.get("os"));
visitLog.setBrowser(userAgentMap.get("browser"));
visitLog.setPage(visitLogger.value());
// 保存到数据库
AsyncManager.getInstance().execute(AsyncFactory.recordVisit(visitLog));
}

}

日志Service层

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 访问业务接口
*/
public interface VisitLogService extends IService<VisitLog> {

/**
* 保存访问日志
*
* @param visitLog 访问日志信息
*/
void saveVisitLog(VisitLog visitLog);
}

1
2
3
4
5
6
7
8
9
10
11
@Service
public class VisitLogServiceImpl extends ServiceImpl<VisitLogMapper, VisitLog> implements VisitLogService {

@Autowired
private VisitLogMapper visitLogMapper;

@Override
public void saveVisitLog(VisitLog visitLog) {
// 保存访问日志
visitLogMapper.insert(visitLog);
}

控制器测试

1
2
3
4
5
6
7
8
9
10
11
/**
* 查看首页文章列表
*
* @return {@link Result<ArticleHomeVO>}
*/
@VisitLogger(value = "首页")
@ApiOperation(value = "查看首页文章列表")
@GetMapping("/article/list")
public Result<PageResult<ArticleHomeVO>> listArticleHomeVO() {
return Result.success(articleService.listArticleHomeVO());
}