转载请注明出处哈:http://carlosfu.iteye.com/blog/2240426
一、spring-mvc添加拦截器配置: 对所有/下的访问都做拦截
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.sohu.tv.mobil.web.interceptor.VisitCounterStatInterceptor"/> </mvc:interceptor> </mvc:interceptors>
二、 定义VisitCounterStatInterceptor
1. 这里我们使用了guava的中的AtomicLongMap, 它的底层是ConcurrentHashMap<String,Long>,可以用来记录每个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<String> VISIT_COUNT_MAP = AtomicLongMap.create(); /** * 记录接口慢查询 */ public static final AtomicLongMap<String> VISIT_SLOW_COST_MAP = AtomicLongMap.create(); /** * 耗时 */ private ThreadLocal<Long> costThreadLocal = new ThreadLocal<Long>(); /** * 最大接受的耗时 */ 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<String,Long> 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<String> counterMap; public CounterMapImpl(AtomicLongMap<String> counterMap) { this.counterMap = counterMap; } @Override public void clear() { counterMap.clear(); } @Override public Map<String, Long> getCounterMap() { List<Map.Entry<String, Long>> entryList = new ArrayList<>(counterMap.asMap().entrySet()); Collections.sort(entryList, new Comparator<Map.Entry<String, Long>>() { @Override public int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> 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<String, Long> resultMap = new LinkedHashMap<String, Long>(); for (Map.Entry<String, Long> entry : entryList) { resultMap.put(entry.getKey(), entry.getValue()); } return resultMap; } }
3. 在spring中定义jmx:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd" default-autowire="byName"> <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"> <property name="locateExistingServerIfPossible" value="true"/> </bean> <bean id="visitCounterMap" class="com.sohu.tv.mobil.common.jmx.impl.CounterMapImpl"> <constructor-arg index="0"> <util:constant static-field="com.sohu.tv.mobil.web.interceptor.VisitCounterStatInterceptor.VISIT_COUNT_MAP"/> </constructor-arg> </bean> <bean id="visitSlowCostMap" class="com.sohu.tv.mobil.common.jmx.impl.CounterMapImpl"> <constructor-arg index="0"> <util:constant static-field="com.sohu.tv.mobil.web.interceptor.VisitCounterStatInterceptor.VISIT_SLOW_COST_MAP"/> </constructor-arg> </bean> <bean id="mBeanServer" class="org.springframework.jmx.export.MBeanExporter"> <property name="beans"> <map> <entry key="mobilJMX:myjavaobj=visitCounterMap" value-ref="visitCounterMap"/> <entry key="mobilJMX:myjavaobj=visitSlowCostMap" value-ref="visitSlowCostMap"/> </map> </property> <property name="server" ref="mbeanServer"/> <property name="assembler" ref="assembler"/> </bean> <bean id="assembler" class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler"> <property name="managedInterfaces"> <list> <value>com.sohu.tv.mobil.common.jmx.CounterMapMBean</value> </list> </property> </bean> </beans>
4. 上线后,就可以在jvisualvm或者jconsole中看到MBean的调用统计:
5. 我们在后台,调用jmx,并做成界面:
四、 存在的几个问题
1. request.getRequestURI()可能会存在问题,比如如果使用了如下配置,可能会撑爆AtomicLongMap, 造成内存溢出,解决方法还要进一步观察(但是暂时我们的系统没有使用这种调用方式)
@RequestMapping(value = "/drama/{pid}", produces = "text/javascript; charset=UTF-8")
2. 使用了<mvc:mapping path="/**"/>后,出现了很多奇怪的uri, 比如下图中的情况,具体原因还要查询。
3. jmx是非持久化的,只能查询实时数据,如果需要的话可以定期统计jmx到mysql或者其他存储,方便查询历史数据,帮助有效定位问题
4. jmx的数据可以结合nagios或者ganglia来使用,不需要单独开发后台界面。
附图一张: