基于SpringMVC拦截器实现接口耗时监控功能

 更新时间:2024年02月03日 08:48:02   作者:CIT  
本文呢主要介绍了基于SpringMVC拦截器实现的接口耗时监控功能,统计接口的耗时情况属于一个可以复用的功能点,因此这里直接使用 SpringMVC的HandlerInterceptor拦截器来实现,需要的朋友可以参考下

前言

在日常的项目开发过程中,后端开发人员应该主动去关心自己的接口性能。这种关心需要量化,而量化的直接方式就是对接口的响应时间进行监控,以了解系统性能,帮助判断性能瓶颈。本文基于已有的全链路日志系统进一步补充了接口耗时的方案。已有的全链路日志系统是围绕ELK+Jaeger构建起来的,在Spring Cloud微服务架构中,可以实现跨服务的请求日志追踪 ,帮助我们进行线上问题排查。

服务告警部分则是通过Frostmourne平台来实现了,该平台可以接入Elasticsearch,配置相关的项目监控与告警。当监控到接口超时以后,可以通过接口超时日志中的traceId,在Jaeger平台上查看整个请求链路的耗时分布,快速明确问题发生的位置,提升问题发现与响应的速度。

实现

基本介绍

统计接口的耗时情况属于一个可以复用的功能点,因此这里直接使用 SpringMVC的HandlerInterceptor拦截器来实现,后续抽取成一个公共组件,方便复用。

拦截器接口 HandlerInterceptor 提供了三个方法来实现对请求前、请求后,响应后进行自定义处理,并且拦截器的前置处理和后置处理是具体关联性的。

  • preHandle() :在 Controller 方法执行之前执行。即在 HandlerMapping 确定适当的处理程序对象之后调用,但在HandlerAdapter 调用处理程序之前调用。
  • postHandle() :在 Controller 方法执行之后执行。即在 HandlerAdapter 实际调用处理程序之后,但在DispatcherServlet 呈现视图之前调用。
  • afterCompletion() :完成请求处理后(即渲染视图之后)的回调。 将在处理程序执行的任何结果上被调用,从而允许适当的资源清理。

实现思路

要统计接口处理请求的时长,可以在拦截器的 preHandle() 方法记录请求开始时间(startTime),在 afterCompletion() 方法中记录请求处理完后的结束时间(endTime),请求处理时间(响应时间) = 结束时间 - 开始时间。

实现过程

  • 定义一个拦截器
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
​
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
​
/**
 * 拦截器,统计接口耗时
 */
@Slf4j
public class TimeConsumingInterceptor implements HandlerInterceptor {
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 记录请求开始时间
        request.setAttribute("_startTime", System.currentTimeMillis());
        return true;
    }
​
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        // no need to override
    }
​
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        // 请求结束时间
        Long endTime = System.currentTimeMillis();
        try {
            // 从HttpServletRequest获取开始时间
            Long startTime = (long) request.getAttribute("_startTime");
            String clientIP = ServletUtil.getClientIP(request, "");
            String fullUrl = getFullUrl(request);
            Long cost = endTime - startTime;
            MDC.put("cost_time", cost.toString());
            MDC.put("request_url", fullUrl);
            MDC.put("client_ip", clientIP);
            // 打印接口信息及耗时
            log.info("client IP {}, url {}, cost {}ms", clientIP, fullUrl, cost);
        } catch (Exception e) {
            log.error("fail to calculate time cost", e);
        } finally {
            MDC.remove("cost_time");
            MDC.remove("request_url");
            MDC.remove("client_ip");
        }
    }
​
    /**
     * 获取完整的URL路径
     *
     * @param request 请求对象{@link HttpServletRequest}
     * @return 完整的URL路径
     */
    private String getFullUrl(HttpServletRequest request) {
        //记录请求参数
        StringBuilder sb = new StringBuilder();
        String method = request.getMethod();
        sb.append(method).append(" ");
        sb.append(request.getRequestURL().toString());
        if (RequestMethod.POST.name().equals(method)) {
            //获取参数
            Map<String, String[]> pm = request.getParameterMap();
            Set<Map.Entry<String, String[]>> es = pm.entrySet();
            Iterator<Map.Entry<String, String[]>> iterator = es.iterator();
            appendPathVariable(iterator, sb);
        }
        return sb.toString();
    }
​
    private void appendPathVariable(Iterator<Map.Entry<String, String[]>> iterator, StringBuilder sb) {
        int pointer = 0;
        while (iterator.hasNext()) {
            if (pointer == 0) {
                sb.append("?");
            } else {
                sb.append("&");
            }
            Map.Entry<String, String[]> next = iterator.next();
            String key = next.getKey();
            String[] value = next.getValue();
            for (int i = 0; i < value.length; i++) {
                if (i != 0) {
                    sb.append("&");
                }
                if (value[i].length() <= 20) {
                    sb.append(key).append("=").append(value[i]);
                } else {
                    sb.append(key).append("=").append(CharSequenceUtil.subPre(value[i], 20)).append("…");
                }
            }
            pointer++;
        }
    }
}
  • 配置拦截器使其生效
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
​
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
​
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TimeConsumingInterceptor())
                // 需拦截的URI配置
                .addPathPatterns("/**")
                // 不需拦截的URI配置
                .excludePathPatterns("/swagger/**", "/static/**", "/resource/**");
        log.info("***************** ADD TIME CONSUMING INTERCEPTOR  ******************");
    }
}
  • 添加logback配置,在开发和测试环境由于流量小,可以通过TCP监听的方式直接将接口的耗时日志传输至logstash,生产环境最好还是通过filebeat监听日志文件的方式去实现。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <contextName>log</contextName>
​
    <property name="logback.logDir" value="${LOG_PATH}"/>
    <springProperty name="logback.appName" scope="context" source="spring.application.name"/>
    <springProperty name="logback.elastic" scope="context" source="logback.elastic"/>
    <springProperty name="env" scope="context" source="spring.profiles.active"/>
    <springProperty name="serverIP" scope="context" source="spring.cloud.client.ip-address" defaultValue="0.0.0.0"/>
    <property name="commonLayoutPattern"
              value="[${serverIP}] %d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} [%mdc{trace_id:-N/A}] ${LOG_LEVEL_PATTERN:-%p} ${PID:- } --- [%t] %logger{39}.%method[%line] : %m%n"/>
​
    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <!--展示格式 layout -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${commonLayoutPattern}</pattern>
        </layout>
    </appender>
​
    <appender name="logStash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <!--可以访问的logstash日志收集端口-->
        <destination>192.168.xxx.xxx:4560</destination>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>Asia/Shanghai</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "appName": "${logback.appName}-${env}",
                        "serverIP": "${serverIP}",
                        "traceId":"%mdc{trace_id:-N/A}",
                        "requestUrl":"%mdc{request_url:-N/A}",
                        "clientIP":"%mdc{client_ip:-N/A}",
                        "costTime": "%mdc{cost_time:-N/A}"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
​
    <springProfile name="dev,pre">
        <logger name="com.xxx.xxx.log.autoconfigure.TimeConsumingInterceptor" additivity="false">
            <appender-ref ref="logStash"/>
            <appender-ref ref="consoleLog"/>
            <appender-ref ref="fileRequestLog"/>
        </logger>
    </springProfile>
​
    <springProfile name="prod">
        <logger name="com.xxx.xxx.log.autoconfigure.TimeConsumingInterceptor" additivity="false">
            <appender-ref ref="fileRequestLog"/>
        </logger>
    </springProfile>
​
    <appender name="asyncRequestLog" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <appender-ref ref="fileRequestLog"/>
    </appender>
</configuration>
  • 配置开发、测试环境的logstash传输耗时日志
input {
  tcp {
    host => "192.168.xxx.xxx"
    port => 4560
    codec => json_lines
  }
}
​
​
filter {
  mutate {
    convert => {
      "costTime" => "integer"
    }
  }
}
​
output {
  elasticsearch {
    hosts => ["http://192.168.xxx.xxx:9200"]
    index => "request-%{[appName]}-%{+YYYY.MM.dd}"
    }
}
  • 查看耗时结果

  • 配置耗时监控与结果验证

以上就是基于SpringMVC拦截器实现的接口耗时监控功能的详细内容,更多关于SpringMVC接口耗时监控的资料请关注脚本之家其它相关文章!

相关文章

  • Java中Struts2的值栈ValueStack详解

    Java中Struts2的值栈ValueStack详解

    这篇文章主要介绍了Java中Struts2的值栈ValueStack详解,值栈(ValueStack)就是 OGNL 表达式存取数据的地方,在一个值栈中,封装了一次请求所需要的所有数据,需要的朋友可以参考下
    2023-08-08
  • Spring中的监听器SpringApplicationRunListener详解

    Spring中的监听器SpringApplicationRunListener详解

    这篇文章主要介绍了Spring中的监听器SpringApplicationRunListener详解,命名我们就可以知道它是一个监听者,分析springboot启动流程我们会发现,它其实是用来在整个启动流程中接收不同执行点事件通知的监听者,需要的朋友可以参考下
    2023-11-11
  • Java排列组合字符串的方法

    Java排列组合字符串的方法

    这篇文章主要介绍了Java排列组合字符串的方法
    2018-02-02
  • java.util.Collection源码分析与深度理解

    java.util.Collection源码分析与深度理解

    这篇文章主要给大家介绍了关于java.util.Collection的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03
  • Go Java算法重复的DNA序列详解

    Go Java算法重复的DNA序列详解

    这篇文章主要为大家介绍了Go Java算法之重复的DNA序列的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Java日常练习题,每天进步一点点(58)

    Java日常练习题,每天进步一点点(58)

    下面小编就为大家带来一篇Java基础的几道练习题(分享)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望可以帮到你
    2021-08-08
  • Spring boot 处理大文件上传完整代码

    Spring boot 处理大文件上传完整代码

    这篇文章主要介绍了Spring boot 处理大文件上传,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • 详解如何在SpringBoot中使用WebMvc

    详解如何在SpringBoot中使用WebMvc

    Spring Boot 是一个快速、简单的开发框架,在 Spring Boot 中,我们可以使用 WebMvc 来构建 Web 应用程序,所以本文就来讲讲如何在SpringBoot中使用WebMvc吧
    2023-06-06
  • java中循环删除list中元素的方法总结

    java中循环删除list中元素的方法总结

    下面小编就为大家带来一篇java中循环删除list中元素的方法总结。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-12-12
  • Java中Stream API的使用示例详解

    Java中Stream API的使用示例详解

    Java 在 Java 8 中提供了一个新的附加包,称为 java.util.stream,该包由类、接口和枚举组成,允许对元素进行函数式操作, 本文主要介绍了Java中Stream API的具体使用,感兴趣的小伙伴可以了解下
    2023-11-11

最新评论