2022年11月21更新
由于之前的代码太过弱智,已经重写,在count-page标签
meethigher/count-for-page: 类似于“不蒜子”的统计功能,根据ip来统计页面访问人数
之所以要实现这个脚本,还是受不蒜子启发。
我从2019年,就开始使用不蒜子了,但是2020年末,我发现不蒜子有一个问题。就是在IOS端跟PC端,数据总是不变,查看接口返回内容,就是一个一成不变的数据。
在旧版的安卓Chrome浏览器中,数据是正确的,换成新版之后,又出问题了。
我怀疑是不蒜子后台的逻辑可能出了问题,因为网上也查不到相关资料,所以就打算自己实现一个。
正好今天公司停电,不上班,所以就花时间完成了这个脚本。
环境
- Java
- SQLite
- 一开始我是想用记事本,主要是直接持久化到硬盘,不会浪费太多内存。想了很久,想实现类似于外键这种功能,还真不好整
- SQLite,一款自给自足、无服务器、无配置的数据库,不就是一个记事本嘛。解决了占用内存过大的问题。
使用
创建SQLite数据库,路径在application-dev.properties下面修改
启动java项目之后,浏览器访问http://localhost:9090/,出现跳转页面,说明启动成功
用Postman发送post请求到http://localhost:9090/count,请求体内容是要统计的url,后台会根据ip进行计数统计。后台记录该ip第一次请求的设备、时间、来源
页面访问时,前端页面在所有资源加载完毕之后,携带当前网页url,开始执行ajax请求,获取访问总人数。
后台的数据如下,两张表通过vId来进行关联。
遇到的难题
- ajax访问同站不同端口跨域,添加配置类允许跨域即可
- https发送ajax时,目标必须为https
- 这个地方,我一开始是通过nginx启动443端口配置https,反向代理apache9090端口和tomcat80端口,但是有点麻烦。
- 目前使用的方法是,apache配置443端口配置https,tomcat配置9090端口配置https,占用内存会稍微小点。
- 记录访问的来源,也就是referer时,比较麻烦。比如客户端A访问网页B,B通过ajax获取C上的数据,这就导致C直接获取referer是获取到
B->C
的。为了解决这个问题,B通过document.referrer
获取来源,添加请求头,发送个C获取 - 接口访问过慢。慢的原因就是在于多次进行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(); 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.thenAccept(integer -> System.out.println(sdf.format(new Date()) + " success 最新访问数" + integer)); future.exceptionally(throwable -> { throwable.printStackTrace(); System.out.println(sdf.format(new Date()) + " failure"); return null; }); }
@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
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
if判断
1 2 3 4 5
| <#if today??>
<#else >
</#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>
|
参考文章