摘要

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

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

正文

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请求,获取访问总人数。

1.png

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

2.png

遇到的难题

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

3.png

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

pom.xml

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

application.properties

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里

java
 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

text
1
${title!"null"}

if判断

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

完整的index.ftl模板

html
 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>

参考文章