1.场景
在项目中,需要对某些功能或者接口进行PV统计,那么这些需求一般在项目初期可能不会出现。如果项目基本开发完成,或者在迭代的过程中需要增加PV等统计功能,那么,最原始的做法是在原有代码的基础上增加统计代码,但这样一来,对原有的代码有入侵性,而且与主业务逻辑耦合,代码可读性以及维护性和扩展性会非常差。
2.思路
如果项目中使用了Spring,可以通过Aop的方式,对原有需要进行统计的功能进行增强,不对原有程序进行修改,从而完成PV统计的实现,可以使用AspectJ来实现该需求。
3.实现
1.在pom.xml文件中增加aspectJ的支持,当然还要有spring基础组件以及spring-aop的支持。
2.拿我们的项目来说,需要统计的功能有,首页,商品列表,商品详情,机构列表,机构详情,新闻资讯等等。
3.为了方便扩展,可将需要统计的功能做成一个Enum,当然比如存到库中之类也是可以的。
/**
* 统计PV类型枚举
*/
public enum PvTypeEnum {
DEFAULT("未知"),
HOME("首页"),
GOODS_LIST("商品列表"),
GOODS_DETAIL("商品详情"),
CAMPUS_LIST("机构列表"),
CAMPUS_DETAIL("机构详情"),
INFORMATION_LIST("资讯列表"),
INFORMATION_DETAIL("资讯详情"),
USERINFO("我的");
private String value;
public String getValue() {
return value;
}
PvTypeEnum(String value) {
this.value = value;
}
}
4.Aop可以设置切点(pointcut),实现功能的增加,比如在某个方法执行前,方法执行后,抛出异常时,或者环绕通知,相关内容不在做过多解释了,详细的大家可以了解AspectJ的相关用法,比较简单。AspectJ可以通过设置,拦截某一个类中的某些方法,或者某个包下的某些方法,表达式与Spring声明式事务的表达式基本一致。但是上述两种方式灵活度可能不太高,还有一种方法,可以拦截带有特定注解的方法,完成功能的增强。本次采用的就是通过拦截注解的方式,首先定义一个自定义注解。
/**
* 统计PV用注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PvCount {
PvTypeEnum description() default PvTypeEnum.DEFAULT;
}
5.在需要统计的功能上增加注解,本次将注解写在了Controller上,当然,写在Service方法上也是可以的,根据自己的需求来决定注解的位置。
6.编写用于处理逻辑的Aop类。就是当拦截到带有注解的方法时,该执行哪些特殊逻辑。
/**
* PV日志统计
*/
@Component
@Aspect
public class PvAopLog {
@Autowired
private TokenUtil tokenUtil;
@Autowired
private LogPvMapper logPvMapper;
@Autowired
private RedisTemplate<String,LogPv> redisTemplate;
//定义切点
@Pointcut(value = "@annotation(com.meilong.annotation.PvCount)")
public void pointcut() {
}
/**
* 拦截带有PvCount注解的方法
* 在执行方法后 执行该切面,并将注解传递到切面中
* @param joinPoint
* @param pvCount
*/
@After(value = "pointcut() && @annotation(pvCount)")
public void after(JoinPoint joinPoint,PvCount pvCount){
LogPv logPv = new LogPv();
//方法参数
Object[] args = joinPoint.getArgs();
//当前请求
HttpServletRequest request = HttpUtil.getCurrentRequest();
//类型
logPv.setPvType(pvCount.description().getValue());
//请求ip地址
logPv.setIpAddr(request.getRemoteAddr());
//时间
logPv.setPvTime(new Date());
//用户id
if(!StringUtils.isEmpty(request.getHeader("token"))){
logPv.setUserId(tokenUtil.getToken(request.getHeader("token")).getId().intValue());
}
//向redis中保存数据
redisTemplate.opsForList().leftPush(RedisKey.LOG_REDIS_KEY,logPv);
}
}
7.代码比较简单,这里面可能存在一些争议。在这个方法中我使用了Redis,因为如果每次直接将记录存库的话,比较耗费性能,所以可以选择先放入Redis,在使用定时任务或者其他方式一次性将数据落地,因为统计日志并不需要那么高的时效性或者一致性。当然这里也可以选择将记录写入文件,或者使用消息队列,让记录日志的操作与主业务逻辑解耦。方法比较多,大家可根据需求自行选择。
8.接下来,需要增加spring对aspectj的配置支持。*******这里千万注意,如果需要拦截Controller中的某个方法,那么一定要将配置写到springmvc的配置文件中,而不是spring的配置文件,千万注意********。本次我拦截的就是Controller中的方法。
9.到这里基本就结束了,后续日志记录该怎么保存的逻辑在这就不列出来了,上面也提到了一些方法,大家可以参考一下。