Skip to content

Commit 66b8e06

Browse files
committed
feat(customer): article visit async record
1 parent df280a0 commit 66b8e06

4 files changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.hackyle.blog.customer.infrastructure.threadpool;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.slf4j.MDC;
5+
import org.springframework.stereotype.Component;
6+
7+
import javax.annotation.PreDestroy;
8+
import java.util.Map;
9+
import java.util.concurrent.Callable;
10+
import java.util.concurrent.Future;
11+
import java.util.concurrent.LinkedBlockingQueue;
12+
import java.util.concurrent.ThreadFactory;
13+
import java.util.concurrent.ThreadPoolExecutor;
14+
import java.util.concurrent.TimeUnit;
15+
import java.util.concurrent.atomic.AtomicInteger;
16+
17+
/**
18+
* IO型任务线程池:异步日志记录 —— 登录日志、操作日志的记录
19+
*/
20+
@Component
21+
@Slf4j
22+
public class LogTaskThreadPool {
23+
/**
24+
* CPU核心数
25+
*/
26+
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
27+
28+
/**
29+
* IO型任务线程池
30+
*/
31+
private ThreadPoolExecutor ioTaskThreadPool;
32+
33+
public LogTaskThreadPool() {
34+
ioTaskThreadPool = new ThreadPoolExecutor(
35+
CPU_COUNT *2,
36+
CPU_COUNT *4,
37+
60L,
38+
TimeUnit.SECONDS,
39+
new LinkedBlockingQueue<>(1000), //todo 设计合理的阻塞队列
40+
new NamedThreadFactory("blog-admin-log-task"), //线程名称前缀,例如专门用于异步保存日志的命名为log-pool
41+
new ThreadPoolExecutor.CallerRunsPolicy() //todo 设计合理的拒绝策略
42+
);
43+
}
44+
45+
/**
46+
* 自定义线程工厂:命名每个线程
47+
*/
48+
private static class NamedThreadFactory implements ThreadFactory {
49+
private final String namePrefix;
50+
private final AtomicInteger counter = new AtomicInteger(1);
51+
52+
public NamedThreadFactory(String namePrefix) {
53+
this.namePrefix = namePrefix;
54+
}
55+
56+
@Override
57+
public Thread newThread(Runnable r) {
58+
Thread thread = new Thread(r, namePrefix + "-" + counter.getAndIncrement());
59+
thread.setDaemon(false);
60+
thread.setUncaughtExceptionHandler((th, ex) -> {
61+
log.error("LogTask线程 {} 发生异常:", th.getName(), ex);
62+
});
63+
64+
return thread;
65+
}
66+
}
67+
68+
/**
69+
* 执行一个没有返回值的任务
70+
*/
71+
public void execute(Runnable task) {
72+
//获取当前线程的 MDC 上下文,便于线程打日志时也能获取到主线程的tranceId
73+
Map<String, String> contextMap = MDC.getCopyOfContextMap();
74+
75+
ioTaskThreadPool.execute(() -> {
76+
try {
77+
if (contextMap != null) {
78+
MDC.setContextMap(contextMap);
79+
}
80+
task.run();
81+
} finally {
82+
MDC.clear(); // 避免线程复用时污染
83+
}
84+
});
85+
}
86+
87+
/**
88+
* 执行一个有返回值的任务
89+
*/
90+
public <T> Future<T> submit(Callable<T> task) {
91+
//获取当前线程的 MDC 上下文,便于线程打日志时也能获取到主线程的tranceId
92+
Map<String, String> contextMap = MDC.getCopyOfContextMap();
93+
94+
return ioTaskThreadPool.submit(() -> {
95+
try {
96+
if (contextMap != null) {
97+
MDC.setContextMap(contextMap);
98+
}
99+
return task.call();
100+
} finally {
101+
MDC.clear(); // 避免线程复用时污染
102+
}
103+
});
104+
}
105+
106+
/**
107+
* 实例销毁时关闭线程池
108+
*/
109+
@PreDestroy
110+
public void destroy() {
111+
ioTaskThreadPool.shutdown();
112+
}
113+
}

blog-customer/src/main/java/com/hackyle/blog/customer/module/article/controller/ArticleController.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.hackyle.blog.customer.module.article.controller;
22

33
import com.github.pagehelper.PageInfo;
4+
import com.hackyle.blog.customer.infrastructure.threadpool.LogTaskThreadPool;
45
import com.hackyle.blog.customer.module.article.model.dto.ArticleQueryDto;
6+
import com.hackyle.blog.customer.module.article.model.entity.VisitLogEntity;
57
import com.hackyle.blog.customer.module.article.model.vo.ArticleVo;
68
import com.hackyle.blog.customer.module.article.model.vo.CommentVo;
79
import com.hackyle.blog.customer.module.article.model.vo.MetaVo;
810
import com.hackyle.blog.customer.module.article.service.ArticleService;
911
import com.hackyle.blog.customer.module.article.service.CommentService;
12+
import com.hackyle.blog.customer.module.article.service.VisitLogService;
1013
import lombok.extern.slf4j.Slf4j;
1114
import org.apache.commons.lang3.StringUtils;
1215
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,6 +31,11 @@ public class ArticleController {
2831
private ArticleService articleService;
2932
@Autowired
3033
private CommentService commentService;
34+
@Autowired
35+
private VisitLogService visitLogService;
36+
@Autowired
37+
private LogTaskThreadPool logTaskThreadPool;
38+
3139

3240
/**
3341
* 分页获取所有文章
@@ -71,6 +79,7 @@ public ModelAndView articleDetail(ModelAndView modelAndView, HttpServletRequest
7179
modelAndView.setViewName("common/404");
7280
return modelAndView;
7381
}
82+
long startTime = System.currentTimeMillis();
7483

7584
//查文章主体内容
7685
ArticleVo articleVo = articleService.get(articleCode);
@@ -89,6 +98,14 @@ public ModelAndView articleDetail(ModelAndView modelAndView, HttpServletRequest
8998
List<CommentVo> commentVos = commentService.getTopByArticle(articleVo.getId());
9099
modelAndView.addObject("commentVos", commentVos);
91100

101+
//保存访问日志。为什么不在service做?统计耗时时更精确
102+
VisitLogEntity visitLogEntity = new VisitLogEntity();
103+
visitLogEntity.setArticleId(articleVo.getId());
104+
visitLogEntity.setTimeUse((int) (System.currentTimeMillis() - startTime));
105+
//提交任务到线程池,异步保存日志,注意:将耗时的操作放在异步线程中完成,例如解析IP地址
106+
logTaskThreadPool.execute(() -> visitLogService.add(visitLogEntity));
107+
108+
92109
modelAndView.setViewName("article");
93110
return modelAndView;
94111
}
@@ -105,6 +122,7 @@ public ModelAndView articleDetail(ModelAndView modelAndView, HttpServletRequest
105122
modelAndView.setViewName("index");
106123
return modelAndView;
107124
}
125+
long startTime = System.currentTimeMillis();
108126

109127
//查文章主体内容,文章完整path为/{categoryCode}/{articleCode}
110128
ArticleVo articleVo = articleService.get(categoryCode, articleCode);
@@ -120,6 +138,13 @@ public ModelAndView articleDetail(ModelAndView modelAndView, HttpServletRequest
120138
List<CommentVo> commentVos = commentService.getTopByArticle(articleVo.getId());
121139
modelAndView.addObject("commentVos", commentVos);
122140

141+
//保存访问日志。为什么不在service做?统计耗时时更精确
142+
VisitLogEntity visitLogEntity = new VisitLogEntity();
143+
visitLogEntity.setArticleId(articleVo.getId());
144+
visitLogEntity.setTimeUse((int) (System.currentTimeMillis() - startTime));
145+
//提交任务到线程池,异步保存日志,注意:将耗时的操作放在异步线程中完成,例如解析IP地址
146+
logTaskThreadPool.execute(() -> visitLogService.add(visitLogEntity));
147+
123148
modelAndView.setViewName("article");
124149
return modelAndView;
125150
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.hackyle.blog.customer.module.article.service;
2+
3+
import com.baomidou.mybatisplus.extension.service.IService;
4+
import com.hackyle.blog.customer.module.article.model.entity.VisitLogEntity;
5+
6+
public interface VisitLogService extends IService<VisitLogEntity> {
7+
8+
void add(VisitLogEntity visitLogEntity);
9+
10+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.hackyle.blog.customer.module.article.service.impl;
2+
3+
import com.alibaba.fastjson2.JSON;
4+
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
5+
import com.hackyle.blog.common.ip.IpUtils;
6+
import com.hackyle.blog.common.ip.PconlineIpRegionDto;
7+
import com.hackyle.blog.common.ip.PconlineIpRegionUtils;
8+
import com.hackyle.blog.customer.module.article.mapper.VisitLogMapper;
9+
import com.hackyle.blog.customer.module.article.model.entity.VisitLogEntity;
10+
import com.hackyle.blog.customer.module.article.service.VisitLogService;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.stereotype.Service;
13+
import org.springframework.web.context.request.RequestContextHolder;
14+
import org.springframework.web.context.request.ServletRequestAttributes;
15+
16+
import javax.servlet.http.HttpServletRequest;
17+
18+
@Slf4j
19+
@Service
20+
public class VisitLogServiceImpl extends ServiceImpl<VisitLogMapper, VisitLogEntity>
21+
implements VisitLogService {
22+
23+
@Override
24+
public void add(VisitLogEntity visitLogEntity) {
25+
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
26+
if(attributes != null) {
27+
HttpServletRequest request = attributes.getRequest();
28+
String userAgent = request.getHeader("User-Agent");
29+
visitLogEntity.setUserAgent(userAgent);
30+
31+
}
32+
String publicIp = IpUtils.getPublicIp();
33+
visitLogEntity.setIp(publicIp);
34+
PconlineIpRegionDto ipRegion = PconlineIpRegionUtils.getIpRegion(publicIp);
35+
if(ipRegion != null) {
36+
visitLogEntity.setIpLocation(JSON.toJSONString(ipRegion)); //记录IP的位置信息
37+
}
38+
39+
boolean saved = this.save(visitLogEntity);
40+
log.info("保存访问日志-visitLogEntity:{}, saved:{}", JSON.toJSONString(visitLogEntity), saved);
41+
}
42+
}
43+
44+
45+
46+

0 commit comments

Comments
 (0)