一、spring-mvc添加拦截器配置: 对所有/下的访问都做拦截
二、 定义VisitCounterStatInterceptor
1. 这里我们使用了guava的中的AtomicLongMap, 它的底层是ConcurrentHashMap,可以用来记录每个key的counter, 可以作为一种很高效的计数器。
2. 这里我们用定义了两个AtomicLongMap, 分别记录每个controller-uri对应的访问次数以及慢查询次数(例如超过100秒)。
3. 耗时是通过ThreadLocal来记录,在preHandle记录开始时间,在afterCompletion计算整个controller的耗时,但是这里强调一下finally中一定要调用costThreadLocal.remove();
package com.sohu.tv.mobil.web.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.google.common.util.concurrent.AtomicLongMap;
/**
* 访问相关统计拦截器
*
* @author leifu
* @Date 2015年10月30日
* @Time 上午9:36:16
*/
public class VisitCounterStatInterceptor extends HandlerInterceptorAdapter {
private Logger logger = LoggerFactory.getLogger(VisitCounterStatInterceptor.class);
/**
* 记录接口访问
*/
public static final AtomicLongMap VISIT_COUNT_MAP = AtomicLongMap.create();
/**
* 记录接口慢查询
*/
public static final AtomicLongMap VISIT_SLOW_COST_MAP = AtomicLongMap.create();
/**
* 耗时
*/
private ThreadLocal costThreadLocal = new ThreadLocal();
/**
* 最大接受的耗时
*/
private final static long MAX_ACCEPT_TIME = 100;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if (StringUtils.isNotBlank(uri)) {
VISIT_COUNT_MAP.incrementAndGet(uri);
// 记录startTime
costThreadLocal.set(System.currentTimeMillis());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
try {
String uri = request.getRequestURI();
if (StringUtils.isNotBlank(uri)) {
long startTime = costThreadLocal.get();
long costTime = System.currentTimeMillis() - startTime;
if (costTime > MAX_ACCEPT_TIME) {
VISIT_SLOW_COST_MAP.incrementAndGet(uri);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
costThreadLocal.remove();
}
}
}
三、利用jmx记录controller-uri的调用统计
1. 定义MBean:
package com.sohu.tv.mobil.common.jmx;
import java.util.Map;
public interface CounterMapMBean {
void clear();
Map getCounterMap();
}
2.定义MBean的实现:
package com.sohu.tv.mobil.common.jmx.impl;
import com.google.common.util.concurrent.AtomicLongMap;
import com.sohu.tv.mobil.common.jmx.CounterMapMBean;
import java.util.*;
public class CounterMapImpl implements CounterMapMBean {
public final AtomicLongMap counterMap;
public CounterMapImpl(AtomicLongMap counterMap) {
this.counterMap = counterMap;
}
@Override
public void clear() {
counterMap.clear();
}
@Override
public Map getCounterMap() {
List> entryList = new ArrayList<>(counterMap.asMap().entrySet());
Collections.sort(entryList, new Comparator>() {
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
Long v1 = o1.getValue();
Long v2 = o2.getValue();
if (v1 > v2) {
return -1;
} else if (v1 < v2) {
return 1;
} else {
return o1.getKey().compareTo(o2.getKey());
}
}
});
Map resultMap = new LinkedHashMap();
for (Map.Entry entry : entryList) {
resultMap.put(entry.getKey(), entry.getValue());
}
return resultMap;
}
}
3. 在spring中定义jmx:
com.sohu.tv.mobil.common.jmx.CounterMapMBean
4. 上线后,就可以在jvisualvm或者jconsole中看到MBean的调用统计:

5. 我们在后台,调用jmx,并做成界面:

四、 存在的几个问题
1. request.getRequestURI()可能会存在问题,比如如果使用了如下配置,可能会撑爆AtomicLongMap, 造成内存溢出,解决方法还要进一步观察(但是暂时我们的系统没有使用这种调用方式)
@RequestMapping(value = "/drama/{pid}", produces = "text/javascript; charset=UTF-8")
2. 使用了后,出现了很多奇怪的uri, 比如下图中的情况,具体原因还要查询。

3. jmx是非持久化的,只能查询实时数据,如果需要的话可以定期统计jmx到mysql或者其他存储,方便查询历史数据,帮助有效定位问题
4. jmx的数据可以结合nagios或者ganglia来使用,不需要单独开发后台界面。
附图一张: