Java拦截器统计PV UV实战

在Java Web应用中,通常使用Servlet Filter或Spring MVC Interceptor来拦截请求,并结合缓存(如Redis)或数据库来统计网页的PV(页面浏览量)和UV(独立访客数)。以下是一个基于Spring Boot的详细实现方案。

1. 核心实现:自定义拦截器

首先,创建一个自定义的拦截器 PvUvInterceptor,用于在请求处理前进行统计。

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class PvUvInterceptor implements HandlerInterceptor {

    // 假设使用RedisTemplate操作Redis,实际项目中需注入 // private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI(); // 获取请求路径,代表特定网页 String clientIp = getClientIp(request); // 获取客户端IP,用于UV去重
        String userAgent = request.getHeader("User-Agent"); // 可结合User-Agent进一步识别用户

        // 1. 统计PV:对每个有效页面请求进行计数
        incrementPv(requestURI);

        // 2. 统计UV:基于IP(或IP+UserAgent哈希)进行去重计数
        countUv(requestURI, clientIp);

        return true; // 继续执行后续拦截器和处理器
    }

    /**
     * 获取客户端真实IP(处理代理情况)
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 对于多级代理,取第一个IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }

    /**
     * PV计数:使用Redis的INCR命令,按日期和页面维度存储 */
    private void incrementPv(String requestURI) {
        String date = java.time.LocalDate.now().toString(); // 例如:2023-10-27 String pvKey = "pv:" + date + ":" + requestURI;
        // redisTemplate.opsForValue().increment(pvKey, 1);
        System.out.println("PV Key incremented: " + pvKey); // 模拟操作 }

    /**
     * UV计数:使用Redis的HyperLogLog进行去重统计,节省内存
     */
    private void countUv(String requestURI, String clientIp) {
        String date = java.time.LocalDate.now().toString();
        String uvKey = "uv:" + date + ":" + requestURI;
        // redisTemplate.opsForHyperLogLog().add(uvKey, clientIp);
        System.out.println("UV Key added member: " + uvKey + ", IP: " + clientIp); // 模拟操作
    }
}

2. 注册拦截器到Spring MVC

在Spring Boot配置类中,将自定义拦截器注册到拦截器链中。

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,并指定拦截的路径模式(例如,拦截所有API请求,排除静态资源)
        registry.addInterceptor(new PvUvInterceptor())
                .addPathPatterns("/**") // 拦截所有路径
                .excludePathPatterns("/css/**", "/js/**", "/images/**", "/error"); // 排除静态资源等 }
}

3. 数据查询与展示

统计完成后,可以通过Redis命令或编写Service层方法来查询PV和UV数据。

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class StatisticsService {

    // 假设已注入RedisTemplate
    // @Autowired
    // private RedisTemplate<String, String> redisTemplate;

    /**
     * 查询某日某页面的PV
     */
    public Long getPv(String date, String requestURI) {
        String pvKey = "pv:" + date + ":" + requestURI;
        // String value = redisTemplate.opsForValue().get(pvKey);
        // return value != null ? Long.parseLong(value) : 0L;
        return 0L; // 模拟返回值 }

    /**
     * 查询某日某页面的UV(基于HyperLogLog)
     */
    public Long getUv(String date, String requestURI) {
        String uvKey = "uv:" + date + ":" + requestURI;
        // return redisTemplate.opsForHyperLogLog().size(uvKey);
        return 0L; // 模拟返回值
    }
}

4. 方案对比与选型

统计维度 实现方式 优点 缺点 适用场景
PV Redis INCR 命令 简单高效,原子操作,性能极高。 数据需定期持久化到DB做长期分析。 实时计数,高并发页面访问统计。
UV Redis HyperLogLog 内存占用极小(~12KB/Key),去重准确度高(标准误差0.81%)。 是概率算法,有微小误差,无法获取具体用户列表。 海量数据下的独立访客去重统计。
UV Redis Set 存储IP 精确去重,可获取具体用户列表。 内存占用随用户数线性增长,成本高。 用户量不大且需要精确明细的场景。

5. 扩展:使用AOP进行更灵活的统计

对于需要更细粒度控制(如仅统计特定注解的方法)的场景,可以使用Spring AOP。

java 复制代码
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PvUvAspect {

    @Pointcut("@annotation(com.example.annotation.PageView)") // 自定义注解
    public void pageViewPointcut() {}

    @Before("pageViewPointcut()")
    public void countPvAndUv() {
        // 通过RequestContextHolder获取当前HttpServletRequest
        // 执行与拦截器中类似的统计逻辑
        System.out.println("AOP: Counting PV and UV for annotated method.");
    }
}

总结 :在Java Web应用中,通过实现HandlerInterceptor或使用Servlet Filter拦截HTTP请求,结合Redis的INCRHyperLogLog数据结构,可以高效、准确地实现网页PV和UV的实时统计。此方案性能高、扩展性强,是生产环境的常见实践。


参考来源