Spring Boot 2系列(四十七):Spring AOP 实现API接口处理请求耗时监控
本篇文章算是 Spring Boot 2实践系列(四十六):API接口请求限制之AOP与拦截器实现 的下篇,也是基于拦截器和 AOP 两种方式实现对 API 接口请求响应的耗时进行统计,也是对涉及的知识点学习和巩固。
开发任何一个系统,都应当对 API 接口响应时长进行监控以了解系统性能,帮助判断性能瓶颈(当然很多小公司或小系统并没有这个意识,或有这个意识但没有落地实行)。
收集响应时长数据,可以与监控系统联动,或对接短信系统、IM 即时通信系统 或 邮件系统实现告警通知。
大型系统可能会有多层监控(数据库层,应用层,网络层等),来缩小判断耗时的具体范围(节点),查找性能短板,在分布式微服务环境下可以在网关层实现统一监控,单个微服务应用也需要对自己的 API 响应时长进行监控。例如,Spring Cloud 的分布式链路跟踪组件 Sleuth
就有收集请求链路上各个节点的消耗时长。
Spring Boot 项目集成 spring-boot-starter-actuator
也能收集接口的请求次数,响应时长,接口状态等指标。
拦截器方式
先了解下拦截器的特性和方法,可参考 SpringMVC之HandlerInterceptor拦截器。注:过滤器也能实现同样的功能。
拦截器
拦截器接口 HandlerInterceptor
提供了三个方法来实现对请求前、请求后,响应后进行自定义处理,并且拦截器的前置处理和后置处理是具体关联性的。
- **preHandle()**:在 Controller 方法执行之前执行。即在 HandlerMapping 确定适当的处理程序对象之后调用,但在HandlerAdapter 调用处理程序之前调用。关于 Spring MVC 的执行流程,可参考 SpringMVC执行流程。
- **postHandle()**:在 Controller 方法执行之后执行。即在 HandlerAdapter 实际调用处理程序之后,但在DispatcherServlet 呈现视图之前调用。
- **afterCompletion()**:完成请求处理后(即渲染视图之后)的回调。 将在处理程序执行的任何结果上被调用,从而允许适当的资源清理。
思路
要统计接口处理请求的时长,可以在拦截器的 preHandle() 方法记录请求开始时间(startTime),在 afterCompletion() 方法中记录请求处理完后的结束时间(endTime),请求处理时间(响应时间) = 结束时间 - 开始时间。
这里有个问题是 afterCompletion() 方法要拿到 preHandle() 方法记录的开始时间才能计算,这个记录时间不能定义成类变量,而是与当前请求的线程绑定的,可以使用 ThreadLocal
将开始时间绑定到当前线程,在 afterCompletion() 方法里从 ThreadLocal 取出开始时间(startTime)。
实现
API 耗时统计
理解上面描述的拦截器方法和原理后,实现起来就简单了(重点在于理解原理,综合撑握的知识点灵活应用,理清思路,编码就 so easy)。
定义拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class APIHandleTimeInterceptor implements HandlerInterceptor {
private static final Logger logger = LogManager.getLogger(APIHandleTimeInterceptor.class);
private NamedThreadLocal<Long> threadLocal = new NamedThreadLocal("StopWatch_StartTime");
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
logger.info("请求进入API处理时长拦截器.................");
//1.开始时间
long startTime = System.currentTimeMillis();
//2.绑定变量(只有当前请求线程可见)
threadLocal.set(startTime);
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex)throws Exception {
//3.结束时间(请求处理完成)
long endTime = System.currentTimeMillis();
//4.取出开始时间
long startTime = threadLocal.get();
//5.计算耗时
long consumeTime = endTime - startTime;
//此处认为处理时间超过500毫秒的请求为慢请求
//此处可以做一些复杂的告警通知,如与 IM即时通信应用、邮件系统、短信系统对接
if (consumeTime > 500) {
// logger.info(String.format("【WARN】%s consume %d millis", request.getRequestURI(), consumeTime));
logger.info("【WARN】API:{}, consumeTime:{} millis", request.getRequestURI(), consumeTime);
} else {
// logger.info(String.format("【NORMAL】%s consume %d millis", request.getRequestURI(), consumeTime));
logger.info("【NORMAL】API:{}, consumeTime:{} millis", request.getRequestURI(), consumeTime);
}
logger.info("请示处理完成从API处理时长拦截器结束.................");
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView)throws Exception {
}
}配置拦截器使其生效
1
2
3
4
5
6
7
8
9
10
11
12
13
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login");
registry.addInterceptor(new APIHandleTimeInterceptor())
.addPathPatterns("/**");
// registry.addInterceptor(new RequestLimitInterceptor());
}
}
API 请求频率
拦截器这里可以对 API 接口访问频率进行统计。在 preHandle() 方法中通过 Request 拿到 URI 进行存储计算。
例如,要计算每个接口每天调用的频率,可以将数据写入到 Redis,使用 Sorted Set
数据类型,每天的时间作为 Key,URI 做为元素,访问次数做为 权重分,接口再次被访问时则增加该元素的权重分,使用时可直接使用命令获取已排好序的元素集合。
备注:
拦截器实现方式有个弊端,即如果采用的是路径传参, request.getRequestURI() 方法会取得包含路径参数的 URI,这样每个不同的参数都会被误认为是不同的 URI,无法识别出真正的 URI。
或许可以通过请求映射处理器(RequestMappingHandlerMapping) 获取所有 URI ,再使用正则匹配请求进来的 URI 来找到正确的 URI,待验证。。。。可参考 SpringMvc请求如何获取相关HandlerMapping。
AOP切面方式
AOP 的相关知识可以参考 Spring 之 AOP理解 和 使用。
思路
AOP 的通知类型中有一种是 **环绕通知(around)**,即可以在方法执行的前和执行后对代理的目标方法进行增强,这个特性就可以满足对方法处理请求的时长进行计算。
使用环绕通知,在方法执行前获取当前时间做为开始时间(startTime),在方法执行后获取当前时间做为结束时间(endTime),方法处理请求的时长 = 结束时间 - 开始时间。
实现
1 |
|
注意:网上有些例子是在切面类里定义类变量(startTime),然后在 @Before
注解方法中给 startTime 赋值;在 @After
注解方法中取当前时间为 endTime ,请求处理时间 = endTime - startTime。这种处理是不可取的,很明显会存在线程安全问题。
若要使用这种处理且解决线程安问题,可以定义一个 ThreadLocal 对象,在 @Before 注解的方法中将 startTime 值设置到 ThreadLocal 对象中绑定为当前线程可见,在 @After 注解的方法中从 ThreadLocal 取出 startTime 计算请求处理时间。可参考上面 拦截器方式 中的实现处理。
备注
在 AOP 中要获取 HttpServletRequest,可以使用注入方式,但不能注入 HttpServletResponse,可以通过下面方式获取:
1 | HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); |
接口优化
接口优化是个系统工程,涉及到数据库、缓存、网络、CDN、业务逻辑等等,一时难以概述。下面引用几篇文章:
其它参考
- Spring项目中监控方法执行时间:提供了另一 种基于 AOP 实现思路,有参考价值。
- 使用SpringAOP获取一次请求流经方法的调用次数和调用耗时
Spring Boot 2系列(四十七):Spring AOP 实现API接口处理请求耗时监控
http://blog.gxitsky.com/2019/11/30/SpringBoot-47-aop-api-response-time-watch/