言成言成啊 | Kit Chen's Blog

“不蒜子”统计总访问人数脚本

发布于2021-07-19 23:47:24,更新于2022-11-21 01:42:53,标签:java open sqlite  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

2022年11月21更新

由于之前的代码太过弱智,已经重写,在count-page标签

meethigher/count-for-page: 类似于“不蒜子”的统计功能,根据ip来统计页面访问人数

之所以要实现这个脚本,还是受不蒜子启发。

我从2019年,就开始使用不蒜子了,但是2020年末,我发现不蒜子有一个问题。就是在IOS端跟PC端,数据总是不变,查看接口返回内容,就是一个一成不变的数据。

在旧版的安卓Chrome浏览器中,数据是正确的,换成新版之后,又出问题了。

我怀疑是不蒜子后台的逻辑可能出了问题,因为网上也查不到相关资料,所以就打算自己实现一个。

正好今天公司停电,不上班,所以就花时间完成了这个脚本。

环境

  1. Java
  2. SQLite
    • 一开始我是想用记事本,主要是直接持久化到硬盘,不会浪费太多内存。想了很久,想实现类似于外键这种功能,还真不好整
    • SQLite,一款自给自足、无服务器、无配置的数据库,不就是一个记事本嘛。解决了占用内存过大的问题。

使用

  1. 创建SQLite数据库,路径在application-dev.properties下面修改

  2. 启动java项目之后,浏览器访问http://localhost:9090/,出现跳转页面,说明启动成功

  3. 用Postman发送post请求到http://localhost:9090/count,请求体内容是要统计的url,后台会根据ip进行计数统计。后台记录该ip第一次请求的设备、时间、来源

页面访问时,前端页面在所有资源加载完毕之后,携带当前网页url,开始执行ajax请求,获取访问总人数。

后台的数据如下,两张表通过vId来进行关联。

遇到的难题

  1. ajax访问同站不同端口跨域,添加配置类允许跨域即可
  2. https发送ajax时,目标必须为https
    • 这个地方,我一开始是通过nginx启动443端口配置https,反向代理apache9090端口和tomcat80端口,但是有点麻烦。
    • 目前使用的方法是,apache配置443端口配置https,tomcat配置9090端口配置https,占用内存会稍微小点。
  3. 记录访问的来源,也就是referer时,比较麻烦。比如客户端A访问网页B,B通过ajax获取C上的数据,这就导致C直接获取referer是获取到B->C的。为了解决这个问题,B通过document.referrer获取来源,添加请求头,发送个C获取
  4. 接口访问过慢。慢的原因就是在于多次进行io,同时又调用了第三方api,就如下面的这三步。通过分析,发现耗时主要在第二步,调第三方api时,会比较耗时,为了解决这个问题,采用了jdk8提供的CompletableFuture。这样就解决了不少问题。
    • 查询:根据访问url查询是否有访问量,有则自增,无则记录
    • 获取ip信息:通过request获取到ip,再调用第三方api获取ip的地理位置
    • 入库:将以上所有获取到的数据入库。入库分为插入IP和更新Visit
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
private void asyncAsync(String url, HttpServletRequest request) {
SimpleDateFormat sdf = Utils.sdfThreadLocal.get();
SaveInfo saveInfo = new SaveInfo(url, Utils.getUserAgent(request), Utils.getOriginReferer(request), Utils.getIpAddr(request));
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new Supplier<Integer>() {
@Override
public Integer get() {
Visit visit = verifyVisit(saveInfo.getUrl());
Integer count = visit.getCount();
//之所有不用visit.getIp(),是因为在异步线程里会有懒加载问题,具体为啥不知道。
List<String> ipList = ipRepository.findIpByVid(visit.getvId());
if (ObjectUtils.isEmpty(ipList)) {
IP ip = getFullIP(url, saveInfo);
return update(ip, visit);
}
if (!ipList.contains(saveInfo.getIp())) {
IP ip = getFullIP(url, saveInfo);
return update(ip, visit);
}
return count;
}
});
//future成功后的回调
future.thenAccept(integer -> System.out.println(sdf.format(new Date()) + " success 最新访问数" + integer));
//future异常后的回调。这个必须要有,不然即使有异常也没有日志。
future.exceptionally(throwable -> {
throwable.printStackTrace();
System.out.println(sdf.format(new Date()) + " failure");
return null;
});
}

/**
* 之前的做法,导致接口访问太慢了。
* 现在的做法是直接返回上次的数据,本次的更新操作、ip信息的查询交给异步线程后台执行。
*
* @param request
* @param url
* @return
*/
@Override
public Integer getStatistic(HttpServletRequest request, String url) {
System.out.println(url);
SimpleDateFormat sdf = Utils.sdfThreadLocal.get();

System.out.println(sdf.format(new Date()) + " start");

Visit visit = verifyVisit(url);
System.out.println(sdf.format(new Date()) + " verifyVisit");


Integer count = visit.getCount();
System.out.println(sdf.format(new Date()) + " getCount");

asyncAsync(url, request);

System.out.println(sdf.format(new Date()) + " returnCount");
//防止太难看
return count == 0 ? 1 : count;
}

至于java模板引擎,我也是第一次接触,spring推荐Thymeleaf,但我个人感觉freeMarker语法更简单一点。直接放上上手的例子。

pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

application.properties

1
2
3
4
5
6
7
8
9
10
spring.freemarker.template-loader-path=classpath:/static
# 关闭缓存,及时刷新,上线生产环境需要修改为true
spring.freemarker.cache=true
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.ftl

控制器中,ModelMap其实是存储的SessionScope,这个参照SpringMVC博客中的@ModelAttribute注解

控制器如果使用@RestController,就会将返回内容原封不动的打印到接口里。

如果使用@Controller,就会模板引擎返回到指定的返回字符串的名称的模板、JSP、HTML里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class HtmlController {
@Autowired
CountService countService;
@GetMapping(value="/today")
public String today(ModelMap map){
List<TopResponse> top = countService.getTop();
if(!ObjectUtils.isEmpty(top)){
String time = new SimpleDateFormat("MM月dd日").format(new Date());
map.put("title",time+"统计"+top.size()+"条");
map.put("today",top);
}
return "/index";
}
}

index.ftl模板。接口返回的内容可能会有null值,使用freemarker提供的判空语法。

title不存在时,默认值为null

1
${title!"null"}

if判断

1
2
3
4
5
<#if today??>
//TODO: today存在时
<#else >
//TODO: today不存在时
</#if>

完整的index.ftl模板

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>${title!"null"}</title>
<link rel="stylesheet" href="layui/css/layui.css">
</head>
<body>
<#--https://www.cnblogs.com/panchanggui/p/9342246.html-->
<#if today??>
<table class="layui-table">
<colgroup>
<col width="150">
<col width="150">
<col width="150">
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<th>ip</th>
<th>位置</th>
<th>时间</th>
<th>设备</th>
<th>访问</th>
<th>来源</th>
</tr>
</thead>
<tbody>
<#list today as item>
<tr>
<td>${item.ip!"null"}</td>
<td>${item.location!"null"}</td>
<td>${item.firstVisitTime!"null"}</td>
<td>${item.userAgent!"null"}</td>
<td>${item.url!"null"}</td>
<td>${item.originReferer!"null"}</td>
</tr>
</#list>
</tbody>
</table>
<#else >
<h1 align="center">nobody</h1>
</#if>
<script src="layui/layui.js"></script>
</body>
</html>

参考文章

发布:2021-07-19 23:47:24
修改:2022-11-21 01:42:53
链接:https://meethigher.top/blog/2021/count-for-page/
标签:java open sqlite 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏