说明: 本篇文章采用过滤器的方式,通过HttpServletRequestWrapper重新封装request,以用来实现打印请求相关日志的操作。其中,该日志过滤器主要针对
POST中传值在BODY中的方式,来实现相关日志打印的操作。
具体的说明以及代码如下:
1。 RequestLoggingFilter过滤器,以及相关的工具类等。
package com.ffcs.icity.log;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import com.ffcs.icity.config.ApplicationConfigHolder;
import com.ffcs.icity.config.BaseApplicationConfig;
import com.ffcs.icity.config.RequestContextHolder;
import com.ffcs.icity.util.JSONHelper;
import com.ffcs.icity.util.RequestUtils;
public class RequestLoggingFilter implements Filter {
private final static Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
private RequestDumper requestDumper = new RequestDumper();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
putMDC(request);
try {
// if (isDumpRequest()){
if(true) {
long startTime = System.currentTimeMillis();
//采用装饰器模式,重新封装一次HttpServletRequest,供后部的程序使用
ResettableStreamRequestWrapper requestWrapper = new ResettableStreamRequestWrapper((HttpServletRequest) request);
this.requestDumper.dumpRequest(requestWrapper);
requestWrapper.resetInputStream();
chain.doFilter(requestWrapper, response);
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("request processing time: {} ms", elapsedTime);
} else {
chain.doFilter(request, response);
}
} finally {
MDC.clear();
}
}
private void putMDC(ServletRequest request) {
MDC.put("IP", RequestUtils.getIp((HttpServletRequest) request));
String requestId = RequestUtils.generateRequestId();
//在日志配置文件中设置好相关REQUEST_ID显示的设置
//<property name="ENCODER_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{REQUEST_ID}] [%thread] %-5level %logger{80} - %msg%n" />
MDC.put("REQUEST_ID", requestId);
RequestContextHolder.setRequestId(requestId);
}
private boolean isDumpRequest() {
return getApplicationConfig().isDumpRequest();
}
private BaseApplicationConfig getApplicationConfig() {
return ApplicationConfigHolder.getApplicationConfig();
}
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
private static class RequestDumper {
private final static Logger logger = LoggerFactory.getLogger(RequestDumper.class);
public void dumpRequest(HttpServletRequest request) {
try {
if (logger.isInfoEnabled()) {
logger.info("--------------------request dump begin-------------------------");
logger.info("request uri: {}", request.getRequestURI());
logger.info("request type: {}", request.getClass().getName());
logger.info("request method: {}", request.getMethod());
logger.info("request characterEncoding: {}", request.getCharacterEncoding());
dumpRequestHeaders(request);
dumpSession(request);
dumpRequestParams(request);
dumpRequestBody(request);
logger.info("--------------------request dump end---------------------------");
}
} catch (Throwable e) {
logger.info("failure to dump request", e);
}
}
@SuppressWarnings("unchecked")
private void dumpRequestHeaders(HttpServletRequest request) {
for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) {
String headerName = headerNames.nextElement();
String value = request.getHeader(headerName);
logger.info("request header[{}]:{}", headerName, value);
}
}
@SuppressWarnings("unchecked")
public void dumpSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (null != session) {
for (Enumeration<String> attributeNames = session.getAttributeNames(); attributeNames.hasMoreElements();) {
String attributeName = attributeNames.nextElement();
Object value = session.getAttribute(attributeName);
logger.info("session[{}]:{}", attributeName, JSONHelper.toJSONStringQuietly(value, false));
}
}
}
@SuppressWarnings("unchecked")
private void dumpRequestParams(HttpServletRequest request) {
for (Enumeration<String> paramNames = request.getParameterNames(); paramNames.hasMoreElements();) {
String paramName = paramNames.nextElement();
String[] values = request.getParameterValues(paramName);
logger.info("request param[{}]:{}", paramName, JSONHelper.toJSONStringQuietly(values, false));
}
}
private void dumpRequestBody(HttpServletRequest request) {
if (isPayloadInBody(request)) {
String body = RequestUtils.resolveBody(request);
logger.info("request body:{}", body);
} else {
logger.info("request body:[unsupported payload:[{},{}]]", request.getMethod(), request.getContentType());
}
}
private boolean isPayloadInBody(HttpServletRequest request) {
String method = request.getMethod();
if (!("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method))) {
return false;
}
String contentType = request.getContentType();
return StringUtils.containsIgnoreCase(contentType, "application/json")
|| StringUtils.containsIgnoreCase(contentType, "text/plain")
|| StringUtils.containsIgnoreCase(contentType, "text/xml")
|| StringUtils.containsIgnoreCase(contentType, "text/html");
}
}
private static class ResettableStreamRequestWrapper extends HttpServletRequestWrapper {
private static final String DEFAULT_CHARACTER_ENCODING = "UTF-8";
private byte[] bodyData;
private HttpServletRequest request;
private ResettableServletInputStream resettableServletInputStream;
public ResettableStreamRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
this.resettableServletInputStream = new ResettableServletInputStream();
}
public void resetInputStream() {
if (this.bodyData != null) {
this.resettableServletInputStream.is = new ByteArrayInputStream(this.bodyData);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.bodyData == null) {
resolveBodyData();
}
return this.resettableServletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
if (this.bodyData == null) {
resolveBodyData();
}
return new BufferedReader(new InputStreamReader(this.resettableServletInputStream));
}
private void resolveBodyData() throws IOException {
this.bodyData = IOUtils.toByteArray(this.request.getReader(), getCharacterEncoding());
this.resettableServletInputStream.is = new ByteArrayInputStream(this.bodyData);
}
public String getCharacterEncoding() {
return super.getCharacterEncoding() != null ? super.getCharacterEncoding() : DEFAULT_CHARACTER_ENCODING;
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream is;
@Override
public int read() throws IOException {
return this.is.read();
}
}
}
}
说明:ResettableStreamRequestWrapper继承HttpServletRequestWrapper,通过覆盖相关方法实现request的重新设置工作。
2。 RequestUtils以及JSONHelper两个工具类
package com.ffcs.icity.util;
import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 请求工具类.
*
*
*/
public abstract class RequestUtils {
private static final Logger logger = LoggerFactory.getLogger(RequestUtils.class);
/**
* 生成请求ID
*
* @return
*/
public static String generateRequestId() {
return Long.toString(new Date().getTime());
}
/**
* 获取请求参数.如果请求参数为空时,返回一个默认值.
*
* @param request
* @param name 参数名
* @param defaultValue 默认值
* @return
*/
public static String getRequestParam(HttpServletRequest request, String name, String defaultValue) {
String value = request.getParameter(name);
return value != null ? value : defaultValue;
}
/**
* 获取请求参数.如果请求参数为空时,返回一个默认值.
*
* @param request
* @param name 参数名
* @param defaultValue 默认值
* @return
*/
public static int getRequestParam(HttpServletRequest request, String name, int defaultValue) {
String value = request.getParameter(name);
return StringUtils.isNotBlank(value) ? Integer.valueOf(value) : defaultValue;
}
/**
* 获取请求头列表
*
* @param request
* @return
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> resolveHeaders(HttpServletRequest request) {
Map<String, Object> result = new HashMap<String, Object>();
for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) {
String headerName = headerNames.nextElement();
result.put(headerName, request.getHeader(headerName));
}
return result;
}
/**
* 获取请求参数列表
*
* @param request
* @return
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> resolveParams(HttpServletRequest request) {
Map<String, Object> result = new HashMap<String, Object>();
for (Enumeration<String> paramNames = request.getParameterNames(); paramNames.hasMoreElements();) {
String paramName = paramNames.nextElement();
result.put(paramName, request.getParameter(paramName));
}
return result;
}
/**
* 获取请求体
*
* @param request
* @return
*/
public static String resolveBody(HttpServletRequest request) {
String result;
try {
result = IOUtils.toString(request.getInputStream());
} catch (IOException e) {
logger.error("failure to resolve body", e);
result = "[failure to resolve body]";
}
return result;
}
/**
* 获取绝对路径,以"/"开头.
*
* @param request
* @param path 相对路径
* @return
*/
public static String getFullPath(HttpServletRequest request, String path) {
String tmp = StringUtils.defaultIfBlank(path, "");
if (StringUtils.startsWithAny(tmp.toLowerCase(), "http", "https")) {
return path;
}
if (!StringUtils.startsWith(path, "/")) {
path = "/" + path;
}
return request.getContextPath()+ request.getServletPath() + path;
}
/**
* 获取完整的URL.
*
* @param request
* @param path 相对URL
* @return
*/
public static String getFullUrl(HttpServletRequest request, String path) {
return request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + getFullPath(request, path);
}
/**
* 格式化URL
*
* @param url
* @return
*/
public static String normalizeUrl(String url) {
return url.replaceAll("/{2,}", "/");
}
/**
* 获取客户端的请求IP.
*
* @param request
* @return
*/
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Real-IP");
if (null == ip) {
ip = request.getRemoteAddr();
}
return ip;
}
}
package com.ffcs.icity.util;
import java.util.Map;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.core.JsonParser;
/**
* json工具类.
*
*
*/
public class JSONHelper {
private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final static ObjectMapper LCWU_OBJECT_MAPPER = new ObjectMapper();
static{
OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES,true);
OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES,true);
LCWU_OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
LCWU_OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES,true);
LCWU_OBJECT_MAPPER.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES,true);
LCWU_OBJECT_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
}
/**
* 将json字符串转换成Map对象
*
* json类型与java类型的对应关系查看:http://wiki.fasterxml.com/JacksonInFiveMinutess
*
* @param json
* @return Map
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static Map<String,Object> toMap(String json) throws Exception {
return OBJECT_MAPPER.readValue(json, Map.class);
}
/**
* 将对象转换成json字符串
*
* @param object
* @param camelCaseToLowerCaseWithUnderscores 将小写下划线分割的json属性名到驼峰格式的java属性名
* @return String
* @throws Exception
*/
public static String toJSONString(Object object, boolean camelCaseToLowerCaseWithUnderscores) throws Exception {
ObjectMapper mapper = camelCaseToLowerCaseWithUnderscores ? LCWU_OBJECT_MAPPER : OBJECT_MAPPER;
return mapper.writeValueAsString(object);
}
/**
* 将对象转换成json字符串
*
* @param object
* @return String
* @throws Exception
*/
public static String toJSONString(Object object) throws Exception {
return toJSONString(object, false);
}
/**
* 类似toJSONString()方法,除了在转换过程出现异常时,返回""(空串).
*
* @param object
* @param camelCaseToLowerCaseWithUnderscores 将小写下划线分割的json属性名到驼峰格式的java属性名
* @return String
*/
public static String toJSONStringQuietly(Object object, boolean camelCaseToLowerCaseWithUnderscores) {
ObjectMapper mapper = camelCaseToLowerCaseWithUnderscores ? LCWU_OBJECT_MAPPER : OBJECT_MAPPER;
try {
return mapper.writeValueAsString(object);
} catch (Exception e) {
return "";
}
}
/**
* 类似toJSONString()方法,除了在转换过程出现异常时,返回""(空串).
*
* @param object
* @return String
*/
public static String toJSONStringQuietly(Object object) {
return toJSONStringQuietly(object, false);
}
@SuppressWarnings("unchecked")
public static <T> T getValue(String json, String... paths) throws Exception {
Object value = null;
Map<String, Object> node = OBJECT_MAPPER.readValue(json, Map.class);
for (int i = 0; i < paths.length; i++) {
value = node.get(paths[i]);
if (i == paths.length - 1) {
break;
} else if (value instanceof Map) {
node = (Map<String, Object>) value;
} else {
return null;
}
}
return (T)value;
}
}
3。 slf4j中的logback.xml的配置内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- 应用名称 -->
<property name="APP_NAME" value="Spring" />
<!--日志文件的保存路径,首先查找系统属性-Dlog.dir,如果存在就使用其;否则,在当前目录下创建名为logs目录做日志存放的目录 -->
<property name="LOG_HOME" value="${log.dir:-logs}/${APP_NAME}" />
<!-- 日志输出格式 -->
<property name="ENCODER_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] IP[%X{IP}] REQUEST_ID[%X{REQUEST_ID}] %-5level %logger{80} - %msg%n"/>
<contextName>${APP_NAME}</contextName>
<!-- 控制台日志:输出全部日志到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>${ENCODER_PATTERN}</Pattern>
</encoder>
</appender>
<!-- 文件日志:输出全部日志到文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/output.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${ENCODER_PATTERN}</pattern>
</encoder>
</appender>
<!-- 错误日志:用于将错误日志输出到独立文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${ENCODER_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<!-- 设置日志级别,从配置文件中获取日志级别 -->
<property name="LOGGER_ROOT_LEVEL" value="${log.logger.root.level:-DEBUG}" />
<root>
<level value="${LOGGER_ROOT_LEVEL}" />
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>
说明:
<!-- 设置日志级别,从配置文件中获取日志级别 -->
<property name="LOGGER_ROOT_LEVEL" value="${log.logger.root.level:-DEBUG}" />
表示properties被spring配置文件加载后,该logback.xml中能够获取到的数值.
4. config.properties配置文件配置内容,如下:
#log level
log.logger.root.level=DEBUG
#log open flag
log.isDumpRequest=true
5。 web.xml中添加该过滤器
<filter>
<filter-name>requestLoggingFilter</filter-name>
<filter-class>com.ffcs.icity.log.RequestLoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestLoggingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
6。 使用POST在BODY中传值的方式,实现如下的日志打印功能:
2014-12-12 10:45:57.449 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - --------------------request dump begin-------------------------
2014-12-12 10:45:57.450 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request uri: /Spring/prototype/test
2014-12-12 10:45:57.450 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request type: com.ffcs.icity.log.RequestLoggingFilter$ResettableStreamRequestWrapper
2014-12-12 10:45:57.450 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request method: POST
2014-12-12 10:45:57.450 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request characterEncoding: UTF-8
2014-12-12 10:45:57.451 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[host]:localhost:8080
2014-12-12 10:45:57.451 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[user-agent]:Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
2014-12-12 10:45:57.452 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[accept]:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
2014-12-12 10:45:57.452 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[accept-language]:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
2014-12-12 10:45:57.452 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[accept-encoding]:gzip, deflate
2014-12-12 10:45:57.452 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[content-type]:application/json; charset=UTF-8
2014-12-12 10:45:57.452 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[content-length]:454
2014-12-12 10:45:57.452 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[connection]:keep-alive
2014-12-12 10:45:57.453 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[pragma]:no-cache
2014-12-12 10:45:57.453 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request header[cache-control]:no-cache
2014-12-12 10:45:57.544 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - request body:{
"org_code":"unknow",
"city_code":"unknow",
"client_type":"unknow",
"imsi":"imsi123",
"imei":"imei123",
"longitude":"longitude123",
"latitude":"latitude123",
"product_id":"123",
"client_version":"123",
"client_channel_type":"123",
"os_type":"123",
"timestamp":"2014-11-21 00:00:00",
"mobile":"13338290745",
"type":"1",
"relaKey":"relaKey",
"sign":"MMl81q28tiyHA0KENSQTQNxYpmv18wna2AAMt7eBzjctqg1%2F5jHVBI2qEyYGKvp4Y6ywgnk9GMU%3D"
}
2014-12-12 10:45:57.544 [http-8080-1] IP[127.0.0.1] REQUEST_ID[1418352357443] INFO com.ffcs.icity.log.RequestLoggingFilter$RequestDumper - --------------------request dump end---------------------------
其中REQUEST_ID为单次某一线程请求的唯一标识,可供后期的查询搜索使用。