集成sls日志组件并解决:getReader() has already been called for this request

日志动态打印访问接口时的入参、返回结果、调用耗时,经常会利用过滤器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>
  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值