日志动态打印访问接口时的入参、返回结果、调用耗时,经常会利用过滤器Filter,但是一定会遇到报错getReader() has already been called for this request,因为只支持一次读取,流转到controller层的时候已经读不到数据流了。解决方案如下:
重写HttpServletRequestWrapper
public class BaseRequestWrapper extends HttpServletRequestWrapper {
/* private final String body;
public BaseRequestWrapper(HttpServletRequest request) throws IOException{
super(request);
StringBuilder sb = new StringBuilder();
InputStream ins = request.getInputStream();
BufferedReader isr = null;
try{
if(ins != null){
isr = new BufferedReader(new InputStreamReader(ins));
char[] charBuffer = new char[1024 * 8];
int readCount;
while((readCount = isr.read(charBuffer)) != -1){
sb.append(charBuffer,0,readCount);
}
}
}catch (IOException e){
throw e;
}finally {
if(isr != null) {
isr.close();
}
}
body = sb.toString();
}
public String getBody() {
return body;
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayIns = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletIns = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return byteArrayIns.read();
}
};
return servletIns;
}*/
/**
* 用于将流保存下来
*/
private byte[] requestBody;
public BaseRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
IOUtils.toString(requestBody, "utf-8");
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
public String getBody() {
return new String(requestBody, StandardCharsets.UTF_8);
}
}
自定义过滤器
@Slf4j
@Component
@WebFilter(urlPatterns = "/*", filterName = "baseFilter")
public class BaseFilter implements Filter {
private final static String BIZ_MONITOR = "biz-monitor";
private static final Logger monitorLogger = LoggerFactory.getLogger(BIZ_MONITOR);
private static final Logger logger = LoggerFactory.getLogger(BaseFilter.class);
private final String GET = "get";
private final String POST = "post";
public void preHandle(HttpServletRequest request,BaseRequestWrapper bodyReaderWrapper){
String servPath = request.getServletPath();
String method = request.getMethod();
String contentType = request.getContentType();
String methodType = "multipart/form-data";
if (this.GET.equalsIgnoreCase(method)) {
logger.info("url:{},request parameters={}",servPath, request.getQueryString());
monitorLogger.info("url:{},request parameters={}",servPath, request.getQueryString());
} else if (this.POST.equalsIgnoreCase(method)) {
if (contentType != null && contentType.contains(methodType)) {
logger.info("multipart/form-data,url=" + servPath);
monitorLogger.info("multipart/form-data,url=" + servPath);
}else {
if(bodyReaderWrapper != null){
logger.info("url:{},request parameters={}",servPath, bodyReaderWrapper.getBody());
monitorLogger.info("url:{},request parameters={}",servPath, bodyReaderWrapper.getBody());
}
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String contentType = request.getContentType();
String methodType = "multipart/form-data";
if (contentType != null && contentType.contains(methodType)) {
this.preHandle(req,null);
chain.doFilter(request, response);
}else{
BaseRequestWrapper bodyReaderWrapper = new BaseRequestWrapper(req);
this.preHandle(req,bodyReaderWrapper);
chain.doFilter(bodyReaderWrapper, response);
}
}
}
这样基本就可以解决问题了,前提是项目开发要有严格的规范,不能随便定义数据结构。一般都是要求post提交,不论是查询还是保存,为了防止别人爬虫,增加难度。如果遇到multipart/form-data格式的,我们项目中极少出现,一般都是B端上传文件会有,此种场景直接过滤掉,不做处理。
日志拦截器,记录日志。
@Order(-1)
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@Value("${merchant.name: default}")
private String merchant;
private final static String BIZ_MONITOR = "biz-monitor";
private static final Logger monitorLogger = LoggerFactory.getLogger(BIZ_MONITOR);
private static final Logger logger = LoggerFactory.getLogger(TraceIdFilter.class);
public static final String MDC_KEY_IDENTITY = "identity";
public static final String MDC_KEY_SYSNAME = "sysName";
public static final String MDC_KEY_SYSNAME_VALUE = "base-api";
public static final String MDC_KEY_IDENTITY_VALUE = "server";
public static final String MDC_KEY_TRACEID = "traceId";
public static final String MDC_KEY_SERVICE = "service";
public static final String MDC_KEY_ELAPSEDTIME = "elapsedTime";
public static final String MDC_KEY_INTERFACE = "interface";
public static final String MDC_KEY_CODE = "code";
public static final String MDC_KEY_MSG = "msg";
public static final String MDC_KEY_RESULT = "result";
public static final String MDC_KEY_MERCHANT = "merchant";
public static final String URL_SUFFIX_CSS = ".css";
public static final String MDC_KEY_BIZCONTENT = "bizContent";
public static final String URL_SUFFIX_HTML = ".html";
public static final String[] URL_SUFFIX_IMG = {".png", ".jpg", ".gif", ".jpeg", ".webp", ".bmp", ".apng"};
public static final String URL_SUFFIX_JSP = ".jsp";
public static final String URL_SUFFIX_JS = ".js";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String traceId = IdUtil.fastUUID().toLowerCase().replace("-","");
MDC.put(MDC_KEY_TRACEID, traceId);
MDC.put(MDC_KEY_SERVICE, request.getRequestURI());
MDC.put(MDC_KEY_INTERFACE, request.getRequestURI());
MDC.put(MDC_KEY_MERCHANT, merchant);
long start = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} catch (Throwable e) {
logger.error(e.getMessage(), e);
throw e;
} finally {
handlerMonitor(request, start);
clearMDC();
}
}
private void handlerMonitor(HttpServletRequest request, long start) {
String time = "-1";
try {
String requestURI = request.getRequestURI();
if (requestURI != null && !(requestURI.contains(URL_SUFFIX_CSS)
|| requestURI.contains(URL_SUFFIX_HTML)
|| Arrays.stream(URL_SUFFIX_IMG).anyMatch(suffix -> requestURI.toLowerCase().contains(suffix.toLowerCase()))
|| requestURI.contains(URL_SUFFIX_JSP)
|| requestURI.contains(URL_SUFFIX_JS))) {
long end = System.currentTimeMillis();
time = String.valueOf(end - start);
MDC.put(MDC_KEY_SYSNAME, MDC_KEY_SYSNAME_VALUE);
MDC.put(MDC_KEY_IDENTITY, MDC_KEY_IDENTITY_VALUE);
logger.info("request ended:{}", requestURI);
}
}catch (Exception e){
logger.error(e.getMessage(), e);
} finally {
MDC.put(MDC_KEY_ELAPSEDTIME, time);
monitorLogger.info("");
}
}
private void clearMDC() {
try{
MDC.clear();
}catch (Exception e){
logger.error(e.getMessage(), e);
}
}
}
@Aspect
@Component
public class MonitorExecTimeAspect {
private final static Logger logger = LoggerFactory.getLogger(MonitorExecTimeAspect.class);
private final static String BIZ_MONITOR = "biz-monitor";
private static final Logger monitorLogger = LoggerFactory.getLogger(BIZ_MONITOR);
public final static String MDC_KEY_RESULT_Y = "Y";
public final static String MDC_KEY_RESULT_N = "N";
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = null;
Integer code = 200;
String msg = "success";
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
try {
obj = joinPoint.proceed();
return obj;
} catch (Throwable e) {
logger.error(e.getMessage(), e);
code = -1;
msg = e.getMessage();
throw e;
} finally {
setMDC(obj, code, msg);
}
}
private void setMDC(Object obj, Integer code, String msg) {
try{
logger.info("return result="+JSONObject.toJSONString(obj));
if(null == obj){
return;
}
if(obj instanceof CommonResult){
code = ((CommonResult<?>) obj).result().getCode();
msg = ((CommonResult<?>) obj).result().getMsg();
if (!code.equals(1)) {
MDC.put(TraceIdFilter.MDC_KEY_CODE, code + "");
MDC.put(TraceIdFilter.MDC_KEY_MSG, msg);
MDC.put(TraceIdFilter.MDC_KEY_RESULT, MDC_KEY_RESULT_N);
monitorLogger.info("");
return;
}
}
if(obj instanceof GenericResult){
code = ((GenericResult<?>) obj).result().getCode();
if (!code.equals(0)) {
MDC.put(TraceIdFilter.MDC_KEY_CODE, code + "");
//MDC.put(TraceIdFilter.MDC_KEY_MSG, msg);
MDC.put(TraceIdFilter.MDC_KEY_RESULT, MDC_KEY_RESULT_N);
monitorLogger.info("");
return;
}
}
MDC.put(TraceIdFilter.MDC_KEY_RESULT, MDC_KEY_RESULT_Y);
MDC.put(TraceIdFilter.MDC_KEY_CODE, code + "");
MDC.put(TraceIdFilter.MDC_KEY_MSG, msg);
monitorLogger.info("");
}catch (Exception e){
logger.error(e.getMessage(), e);
}
}
}
以上切面,打印请求的结果。
此配置为集成了阿里sls日志组件
<configuration>
<springProperty scope="context" name="endpoint" source="sls.endpoint" defaultValue=""/>
<springProperty scope="context" name="accessKeyId" source="sls.accessKeyId" defaultValue=""/>
<springProperty scope="context" name="accessKeySecret" source="sls.accessKeySecret" defaultValue=""/>
<springProperty scope="context" name="project" source="sls.project" defaultValue=""/>
<springProperty scope="context" name="logStore" source="sls.logStore" defaultValue=""/>
<!-- 日志存放路径-->
<property name="log.path" value="var/www/java/loginfo"/>
<!-- 日志输出格式 -->
<property name="log.pattern" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%X{traceId}] [%thread] %logger{50}:%L -- %msg%n"/>
<!-- 编码格式设置 -->
<property name="ENCODING" value="UTF-8"/>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="appName" source="merchant.name"/>
<springProperty scope="context" name="profileName" source="spring.profiles.active"/>
<!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
<appender name="aliyun-info" class="com.aliyun.openservices.log.logback.LoghubAppender">
<!--必选项-->
<!-- 账号及网络配置 -->
<endpoint>${endpoint}</endpoint>
<accessKeyId>${accessKeyId}</accessKeyId>
<accessKeySecret>${accessKeySecret}</accessKeySecret>
<project>${project}</project>
<logStore>${logStore}</logStore>
<!--必选项 (end)-->
<!-- 可选项 -->
<!--指定日志主题,默认为 "",可选参数-->
<topic>java</topic>
<!--指的日志来源,默认为应用程序所在宿主机的 IP,可选参数-->
<!--<source>your source</source>-->
<!-- 可选项 详见 '参数说明'-->
<!--单个 producer 实例能缓存的日志大小上限,默认为 100MB-->
<totalSizeInBytes>104857600</totalSizeInBytes>
<!--如果 producer 可用空间不足,调用者在 send 方法上的最大阻塞时间,默认为 60 秒。为了不阻塞打印日志的线程,强烈建议将该值设置成 0-->
<maxBlockMs>0</maxBlockMs>
<!--执行日志发送任务的线程池大小,默认为可用处理器个数-->
<ioThreadCount>8</ioThreadCount>
<!--当一个 ProducerBatch 中缓存的日志大小大于等于 batchSizeThresholdInBytes 时,该 batch 将被发送,默认为 512 KB,最大可设置成 5MB-->
<batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
<!--当一个 ProducerBatch 中缓存的日志条数大于等于 batchCountThreshold 时,该 batch 将被发送,默认为 4096,最大可设置成 40960-->
<batchCountThreshold>4096</batchCountThreshold>
<!--一个 ProducerBatch 从创建到可发送的逗留时间,默认为 2 秒,最小可设置成 100 毫秒-->
<lingerMs>2000</lingerMs>
<!--如果某个 ProducerBatch 首次发送失败,能够对其重试的次数,默认为 10 次,如果 retries 小于等于 0,该 ProducerBatch 首次发送失败后将直接进入失败队列-->
<retries>10</retries>
<!--首次重试的退避时间,默认为 100 毫秒,Producer 采样指数退避算法,第 N 次重试的计划等待时间为 baseRetryBackoffMs * 2^(N-1)-->
<baseRetryBackoffMs>100</baseRetryBackoffMs>
<!--重试的最大退避时间,默认为 50 秒-->
<maxRetryBackoffMs>50000</maxRetryBackoffMs>
<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式,如果配了这个,就有log字段 -->
<encoder>
<pattern>[%d][${profileName}][%X{traceId}][%-5level][%logger{50}]%n</pattern>
</encoder>
<!-- 可选项 设置 time 字段呈现的格式 -->
<timeFormat>yyyy-MM-dd HH:mm:ss.SSS</timeFormat>
<!-- 可选项 设置 time 字段呈现的时区 -->
<timeZone>Asia/Shanghai</timeZone>
<!-- 可选项 设置是否要添加 Location 字段(日志打印位置),默认为 true -->
<includeLocation>false</includeLocation>
<mdcFields>
traceId,merchant,service,elapsedTime,sysName,result
</mdcFields>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
</filter>
</appender>
<appender name="aliyun-monitor" class="com.aliyun.openservices.log.logback.LoghubAppender">
<!--必选项-->
<!-- 账号及网络配置 -->
<endpoint>${endpoint}</endpoint>
<accessKeyId>${accessKeyId}</accessKeyId>
<accessKeySecret>${accessKeySecret}</accessKeySecret>
<project>${project}</project>
<logStore>${logStore}-monitor</logStore>
<!--必选项 (end)-->
<!-- 可选项 -->
<!--指定日志主题,默认为 "",可选参数-->
<topic>java</topic>
<!--指的日志来源,默认为应用程序所在宿主机的 IP,可选参数-->
<!--<source>your source</source>-->
<!-- 可选项 详见 '参数说明'-->
<!--单个 producer 实例能缓存的日志大小上限,默认为 100MB-->
<totalSizeInBytes>104857600</totalSizeInBytes>
<!--如果 producer 可用空间不足,调用者在 send 方法上的最大阻塞时间,默认为 60 秒。为了不阻塞打印日志的线程,强烈建议将该值设置成 0-->
<maxBlockMs>0</maxBlockMs>
<!--执行日志发送任务的线程池大小,默认为可用处理器个数-->
<ioThreadCount>8</ioThreadCount>
<!--当一个 ProducerBatch 中缓存的日志大小大于等于 batchSizeThresholdInBytes 时,该 batch 将被发送,默认为 512 KB,最大可设置成 5MB-->
<batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
<!--当一个 ProducerBatch 中缓存的日志条数大于等于 batchCountThreshold 时,该 batch 将被发送,默认为 4096,最大可设置成 40960-->
<batchCountThreshold>4096</batchCountThreshold>
<!--一个 ProducerBatch 从创建到可发送的逗留时间,默认为 2 秒,最小可设置成 100 毫秒-->
<lingerMs>2000</lingerMs>
<!--如果某个 ProducerBatch 首次发送失败,能够对其重试的次数,默认为 10 次,如果 retries 小于等于 0,该 ProducerBatch 首次发送失败后将直接进入失败队列-->
<retries>10</retries>
<!--首次重试的退避时间,默认为 100 毫秒,Producer 采样指数退避算法,第 N 次重试的计划等待时间为 baseRetryBackoffMs * 2^(N-1)-->
<baseRetryBackoffMs>100</baseRetryBackoffMs>
<!--重试的最大退避时间,默认为 50 秒-->
<maxRetryBackoffMs>50000</maxRetryBackoffMs>
<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式,如果配了这个,就有log字段 -->
<encoder>
<pattern>[%d][${profileName}][%X{traceId}][%-5level][%logger{50}]%n</pattern>
</encoder>
<!-- 可选项 设置 time 字段呈现的格式 -->
<timeFormat>yyyy-MM-dd HH:mm:ss.SSS</timeFormat>
<!-- 可选项 设置 time 字段呈现的时区 -->
<timeZone>Asia/Shanghai</timeZone>
<!-- 可选项 设置是否要添加 Location 字段(日志打印位置),默认为 true -->
<includeLocation>false</includeLocation>
<mdcFields>
traceId,merchant,service,elapsedTime,sysName,result
</mdcFields>
</appender>
<!-- ==========================控制台输出设置========================== -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>${ENCODING}</charset>
</encoder>
</appender>
<!-- 系统模块日志级别控制-->
<logger name="com.agtech" level="INFO" />
<!-- Spring日志级别控制 warn-->
<logger name="org.springframework" level="INFO"/>
<logger name="biz-monitor" level="INFO" additivity="false">
<appender-ref ref="aliyun-monitor"/>
</logger>
<!--rootLogger是默认的logger-->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="aliyun-info"/>
</root>
</configuration>
为什么日志能通过log.info()或者log.error()就能把信息打印出来?
org.slf4j里有一个MDC类,类似于map,请看定义的接口,所以打印log的时候将写入此类中的数据打印出来,没有写入或者在log后写入的数据不会被打印。
public void put(String key, String val);
/**
* Get the context identified by the <code>key</code> parameter.
* The <code>key</code> parameter cannot be null.
*
* @return the string value identified by the <code>key</code> parameter.
*/
public String get(String key);
/**
* Remove the the context identified by the <code>key</code> parameter.
* The <code>key</code> parameter cannot be null.
*
* <p>
* This method does nothing if there is no previous value
* associated with <code>key</code>.
*/
public void remove(String key);
/**
* Clear all entries in the MDC.
*/
public void clear();
/**
* Return a copy of the current thread's context map, with keys and
* values of type String. Returned value may be null.
*
* @return A copy of the current thread's context map. May be null.
* @since 1.5.1
*/
public Map<String, String> getCopyOfContextMap();
/**
* Set the current thread's context map by first clearing any existing
* map and then copying the map passed as parameter. The context map
* parameter must only contain keys and values of type String.
*
* @param contextMap must contain only keys and values of type String
*
* @since 1.5.1
*/
public void setContextMap(Map<String, String> contextMap);
log日志打印的xml配置中可以通过过滤标签设置想要打印的日志级别,如果只想打印某一类level时,则设置如下
<filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>INFO</level> <!-- 设置拦截的对象为INFO级别日志 --> <onMatch>ACCEPT</onMatch> <!-- 当遇到了INFO级别时,启用该段配置 --> <onMismatch>DENY</onMismatch> <!-- 没有遇到INFO级别日志时,屏蔽该段配置 --> </filter>
如果想根据类似阈值方式打印时,设置如下
<!-- ===日志输出级别,OFF level > FATAL > ERROR > WARN > INFO > DEBUG > ALL level=== --> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <!-- 过滤的级别 --> <level>INFO</level> </filter>
刚接触日志的小伙伴包括我自己,开发的时候集成日志,突然发现控制台不打印日志信息了,不知所措。原来日志配置里面还需要添加固定的配置如下
<!-- ==========================控制台输出设置========================== --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>${ENCODING}</charset> </encoder> </appender>