SpringBoot 封装接口性能统计组件

大纲

前言

本文将介绍在 SpringBoot 项目中,如何通过 AOP + 反射 + 自定义注解来统计接口的性能,且支持拔插式使用。在不引入分布式日志链路追踪技术的情况下,统计接口的性能一般有以下几种实现方式:

  • (1) 基于 Filter(过滤器)实现
  • (2) 基于 AOP + 反射 + 自定义注解实现
  • (3) 在 Spring Cloud GateWay 中,基于 GlobalFilter(全局过滤器)实现

代码下载

完整的案例代码可以从 这里 下载得到。值得一提的是,本文的案例代码不仅适用于 SpringBoot 项目,理论上适用于任何 Spring 项目。

案例代码

  • 引入依赖,如果仅仅是 Spring 项目,需要改为引入 spring-aopaspectjweaver 依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
</dependencies>
  • 自定义注解
1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodExporter {

}
  • AOP 切面类
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import com.clay.log.tracing.annotations.MethodExporter;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Slf4j
@Aspect
@Component
public class MethodExporterAspect {

/**
* 环绕通知,使用了MethodExporter注解将会触发Around业务逻辑
*/
@Around("@annotation(com.clay.log.tracing.annotations.MethodExporter)")
public Object methodExporter(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 方法的返回结果
Object resultValue = null;

log.info("----- @Around before");
long startTime = System.currentTimeMillis();

// 执行目标方法
resultValue = proceedingJoinPoint.proceed();

long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;

// 通过反射获取目标方法
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();

// 通过反射获取目标方法上的注解标签,如果存在,则说明需要统计性能
MethodExporter methodExporterAnnotation = method.getAnnotation(MethodExporter.class);

if (methodExporterAnnotation != null) {
// 获得方法里面的形参信息
StringBuilder jsonParam = new StringBuilder();
Object[] parameterValues = proceedingJoinPoint.getArgs();
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
for (int i = 0; i < parameterNames.length; i++) {
jsonParam.append(parameterNames[i] + " = " + parameterValues[i].toString() + "; ");
}
// 将返回结果序列化
String jsonResult = null;
if (resultValue != null) {
jsonResult = new ObjectMapper().writeValueAsString(resultValue);
} else {
jsonResult = "null";
}

log.info("\n方法分析上报中 " +
"\n类名方法名: " + proceedingJoinPoint.getTarget().getClass().getName() + "." +
proceedingJoinPoint.getSignature().getName() + "()" +
"\n执行耗时: " + costTime + "毫秒" +
"\n输入参数: " + jsonParam + "" +
"\n返回结果: " + jsonResult + "" +
"\n方法分析上报结束"
);
log.info("----- @Around after");
}

return resultValue;
}

}
  • 主启动类,添加了 @EnableAspectJAutoProxy 注解
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableAspectJAutoProxy
public class MainApplication {

public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}

}

测试代码

  • 控制器类,在需要统计性能的接口上添加 @MethodExporter 自定义注解即可
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
46
47
48
49
50
51
52
53
54
55
56
57
58
import com.clay.log.tracing.annotations.MethodExporter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/pay")
public class PayController {

@MethodExporter
@GetMapping(value = "/list")
public Map list(@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "rows", defaultValue = "5") int rows) {

Map<String, String> result = new LinkedHashMap<>();
result.put("code", "200");
result.put("message", "success");

//暂停毫秒
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}

return result;
}

@MethodExporter
@GetMapping(value = "/get")
public Map get() {
Map<String, String> result = new LinkedHashMap<>();
result.put("code", "404");
result.put("message", "not-found");

//暂停毫秒
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}

return result;
}

@GetMapping(value = "/update")
public String update() {
System.out.println("invoke method without @MethodExporter");
return "success";
}

}

访问 http://127.0.0.1:8080/pay/list 接口后,控制台输出的日志信息如下:

1
2
3
4
5
6
方法分析上报中 
类名方法名: com.clay.log.tracing.controller.PayController.list()
执行耗时: 865毫秒
输入参数: page = 1; rows = 5;
返回结果: {"code":"200","message":"success"}
方法分析上报结束

访问 http://127.0.0.1:8080/pay/update 接口后,控制台输出的日志信息如下:

1
invoke method without @MethodExporter

参考资料