1. 场景
从request进入Controller到出去的时间, 可以统计接口访问的一些数据,如:平均处理时间、最大处理时间
2. 代码
2.1 mvc-servlet 定义切面和拦截器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="com.rocky.api.spring.MappingFastjsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean> </list>
</property>
</bean> <!-- 扫描controller -->
<context:component-scan base-package="com.rocky.api.controller"/>
<!-- 对模型视图名称的解析,即在模型视图名称添加前后缀 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/view/" p:suffix=".jsp" />
<mvc:interceptors>
<!-- 性能拦截器 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/static/**"/>
<bean class="com.rocky.common.aop.PerformanceInterceptor"/>
</mvc:interceptor>
</mvc:interceptors> <mvc:resources location="/resources/" mapping="/resources/**"/> <aop:aspectj-autoproxy proxy-target-class="true"/> <bean class="com.rocky.api.advice.ControllerExecutionTime"/>
</beans>
2.2 引入jar包
<!-- perf4j -->
<dependency>
<groupId>org.perf4j</groupId>
<artifactId>perf4j</artifactId>
<version>${perf4j.version}</version>
</dependency>
2.3 切面
import com.rocky.common.utils.NetworkUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Aspect
public class ControllerExecutionTime { private static final Logger logger = LoggerFactory.getLogger(ControllerExecutionTime.class); @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestMappingMethod(){ } @Around("requestMappingMethod()")
public Object aroundControllerAdvice(ProceedingJoinPoint joinPoint)throws Throwable{
return executeTimeLog(joinPoint);
}
private Object executeTimeLog(ProceedingJoinPoint joinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Signature sig = joinPoint.getSignature();
String className = sig.getDeclaringType().getName();
String method = sig.getName(); StringBuffer stringBuffer = new StringBuffer();
Object[] argsArray = joinPoint.getArgs();
String[] parameterNamesArray = ((CodeSignature)joinPoint.getStaticPart().getSignature()).getParameterNames();
for (int i = 0; i < argsArray.length; i++) {
stringBuffer.append(parameterNamesArray[i] + ":" + argsArray[i]);
if(i < argsArray.length - 1)
stringBuffer.append(",");
}
if(argsArray.length > 0){
logger.info("{} - begin, " + stringBuffer.toString(), method);
} try {
Object result = joinPoint.proceed(argsArray);
if(result != null) {
logger.info("{} - end, result:" + result.toString(), method);
}
return result;
} finally {
long end = System.currentTimeMillis(); float execTime = (end - begin);
//logger.info(String.format("[%s#%s] Executed %f ms.", className, method, execTime));
//class_name,method,exectime,server_host,client_host
HttpServletRequest request = getRequest();
logger.info("{} - Executed {} ms, server host is {}, remote ip is {}", method, execTime, NetworkUtil.getServerHost(request), NetworkUtil.getRemoteIP(request));
}
} private HttpServletRequest getRequest() {
return (RequestContextHolder.getRequestAttributes() == null) ? null : ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
2.4 拦截器
package com.rocky.common.aop; import com.rocky.common.utils.IpUtil;
import org.perf4j.StopWatch;
import org.perf4j.slf4j.Slf4JStopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID; /**
* @author sxf
*
*/
public class PerformanceInterceptor extends HandlerInterceptorAdapter { private static ThreadLocal<StopWatch> local = new ThreadLocal<>(); /**
* 追踪ID
*/
private final static String TRACE_KEY = "traceId"; private final Logger switchLogger = LoggerFactory.getLogger("org.perf4j.TimingLogger"); @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StopWatch stopWatch = new Slf4JStopWatch("shell");
local.set(stopWatch);
MDC.put(TRACE_KEY, UUID.randomUUID().toString().replace("-", ""));
return true;
} @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
StopWatch watch = local.get();
if (watch != null) {
watch.stop(generateOperatonIdendifier(request, watch.getElapsedTime()));
local.remove();
}
MDC.remove(TRACE_KEY);
} private String generateOperatonIdendifier(HttpServletRequest request, long exeTime) {
StringBuilder sb = new StringBuilder(64); sb.append(request.getMethod()).append("|"); // URI
if (switchLogger.isTraceEnabled()) { // 如果是trace级别,统计到具体的URI
sb.append(request.getRequestURL());
sb.append('|');
String clientIp = IpUtil.getIp(request);
sb.append(clientIp);
sb.append('|');
sb.append(request.getHeader("User-Agent"));
} else { // 按URI pattern匹配,方便汇总
sb.append(request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
} // 记录慢得url,
if (exeTime > 100) {
sb.append("|SLOW");
} return sb.toString();
} }
2.5 logback 增加 日志输出 及 traceID输出(以区分不同请求)
<?xml version="1.0" encoding="UTF-8"?> <configuration scan="false" scanPeriod="1 minutes" debug="true"> <property name="LOG_HOME" value="/rocky/logs"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender" additivity="false">
<encoder>
<pattern>%d{yyyy-MM-dd.HH:mm:ss.SSS} %t %p %c{0} %X{traceId} : %m%n</pattern>
</encoder>
</appender> <appender name="DETAIL" class="ch.qos.logback.core.rolling.RollingFileAppender" additivity="false">
<file>${LOG_HOME}/integral-detail-${port}.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/integral-detail-${port}.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>90</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd.HH:mm:ss.SSS} %t %p %c{0} %X{traceId} : %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender> <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender" additivity="false">
<file>${LOG_HOME}/integral-error-${port}.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/integral-error-${port}.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>90</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd.HH:mm:ss.SSS} %t %p %c{0} %X{traceId} : %m%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender> <appender name="perf4jFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/integral-api-perf4j-${port}.log</file><!--${catalina.home}-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/integral-api-perf4j-${port}.log.%d{yyyyMMdd}</FileNamePattern>
<MaxHistory>20</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%date [%thread] %-5level %F{16} [%M:%L] - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
</appender>
<appender name="CoalescingStatistics" class="org.perf4j.logback.AsyncCoalescingStatisticsAppender">
<param name="TimeSlice" value="60000"/>
<appender-ref ref="perf4jFile"/>
</appender>
<logger name="org.perf4j.TimingLogger" level="info" additivity="false">
<appender-ref ref="CoalescingStatistics"/>
</logger> <root level="INFO" additivity="true">
<appender-ref ref="DETAIL"/>
<appender-ref ref="ERROR"/>
</root> <!-- #模式符号 - 用途(附加说明);{可选附加选项}(附加选项说明)
# %c - 日志名称(通常是构造函数的参数);{数字}("a.b.c" 的名称使用 %c{2} 会输出 "b.c")
# %C - 调用者的类名(速度慢,不推荐使用);{数字}(同上)
# %d - 日志时间;{SimpleDateFormat所能使用的格式}
# %F - 调用者的文件名(速度极慢,不推荐使用)
# %l - 调用者的函数名、文件名、行号(速度极其极其慢,不推荐使用)
# %L - 调用者的行号(速度极慢,不推荐使用)
# %m - 日志
# %M - 调用者的函数名(速度极慢,不推荐使用)
# %n - 换行符号
# %p - 日志优先级别(DEBUG, INFO, WARN, ERROR)
# %r - 输出日志所用毫秒数
# %t - 调用者的进程名
# %x - Used to output the NDC (nested diagnostic context) associated with the thread that generated the logging event.
# %X - Used to output the MDC (mapped diagnostic context) associated with the thread that generated the logging event.
#
#模式修饰符 - 对齐 - 最小长度 - 最大长度 - 说明
# %20c 右 20 ~
# %-20c 左 20 ~
# %.30c ~ ~ 30
# %20.30c 右 20 30
# %-20.30c 左 20 30 -->
</configuration>
2.6 工具类
package com.rocky.common.utils; import javax.servlet.http.HttpServletRequest;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Pattern; public class IpUtil {
/**
* 取客户端的真实ip,考虑了反向代理等因素的干扰
*/
public static String getIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
String ip = resolveClientIPFromXFF(xff);
if (isValidIP(ip)) {
return ip;
}
ip = request.getHeader("Proxy-Client-IP");
if (isValidIP(ip)) {
return ip;
}
ip = request.getHeader("WL-Proxy-Client-IP");
if (isValidIP(ip)) {
return ip;
}
return request.getRemoteAddr();
} /**
* 从X-Forwarded-For头部中获取客户端的真实IP。
* X-Forwarded-For并不是RFC定义的标准HTTP请求Header,可以参考http://en.wikipedia.org/wiki/X-Forwarded-For
*
* @param xff X-Forwarded-For头部的值
* @return 如果能够解析到client IP,则返回表示该IP的字符串,否则返回null
*/
private static String resolveClientIPFromXFF(String xff) {
if (xff == null || xff.length() == 0) {
return null;
}
String[] ss = xff.split(",");
for (String ip : ss) {
ip = ip.trim();
if (isValidIP(ip))
return ip;
}
return null;
} // long ip to string
public static String iplongToIp(long ipaddress) {
StringBuffer sb = new StringBuffer("");
sb.append(String.valueOf((ipaddress >>> 24)));
sb.append(".");
sb.append(String.valueOf((ipaddress & 0x00FFFFFF) >>> 16));
sb.append(".");
sb.append(String.valueOf((ipaddress & 0x0000FFFF) >>> 8));
sb.append(".");
sb.append(String.valueOf((ipaddress & 0x000000FF)));
return sb.toString();
} //string ip to long
public static long ipStrToLong(String ipaddress) {
long[] ip = new long[4];
int position1 = ipaddress.indexOf(".");
int position2 = ipaddress.indexOf(".", position1 + 1);
int position3 = ipaddress.indexOf(".", position2 + 1);
ip[0] = Long.parseLong(ipaddress.substring(0, position1));
ip[1] = Long.parseLong(ipaddress.substring(position1 + 1, position2));
ip[2] = Long.parseLong(ipaddress.substring(position2 + 1, position3));
ip[3] = Long.parseLong(ipaddress.substring(position3 + 1));
return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
} /**
* 检查是否是一个合格的ipv4 ip
*
* @param ip
* @return
*/
public static boolean isValidIP(String ip) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
return false;
return ipPattern.matcher(ip).matches();
} /**
* 取得一个合格的ipv4 ip,外网ip
*
* @return
*/
public static String getLocalIP() {
for (String a : getAllNoLoopbackAddresses()) {
if (isValidIP(a)) {
return a;
}
}
return "";
} /**
* 获取本地地址
* 内网ip
*
* @return 本机ipv4地址
*/
public static final String getLocalAddress() {
try {
for (Enumeration<NetworkInterface> ni = NetworkInterface.getNetworkInterfaces(); ni.hasMoreElements(); ) {
NetworkInterface eth = ni.nextElement();
for (Enumeration<InetAddress> add = eth.getInetAddresses(); add.hasMoreElements(); ) {
InetAddress i = add.nextElement();
if (i instanceof Inet4Address) {
if (i.isSiteLocalAddress()) {
return i.getHostAddress();
}
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return "";
} /**
* 取得本地所有的ipv4列表
*
* @return
*/
public static List<String> getLocalIPs() {
List<String> localIps = new ArrayList<String>();
for (String a : getAllNoLoopbackAddresses()) {
if (isValidIP(a)) {
localIps.add(a);
}
}
return localIps;
} private static final Pattern ipPattern = Pattern.compile("([0-9]{1,3}\\.){3}[0-9]{1,3}"); private static Collection<InetAddress> getAllHostAddress() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
Collection<InetAddress> addresses = new ArrayList<InetAddress>(); while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
addresses.add(inetAddress);
}
} return addresses; } catch (Exception e) {
e.printStackTrace();
}
return null;
} private static Collection<String> getAllNoLoopbackAddresses() {
Collection<String> noLoopbackAddresses = new ArrayList<String>();
Collection<InetAddress> allInetAddresses = getAllHostAddress(); for (InetAddress address : allInetAddresses) {
if (!address.isLoopbackAddress()) {
noLoopbackAddresses.add(address.getHostAddress());
}
} return noLoopbackAddresses;
}
}
2.7 效果
详细日志 有traceid 方便过滤
统计日志