在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的INCR和HyperLogLog数据结构,可以高效、准确地实现网页PV和UV的实时统计。此方案性能高、扩展性强,是生产环境的常见实践。