背景
在实际的开发中,可能一个用户会调用不同的三方接口进行获取数据,比如针对银行接口有发起授信申请、授信查询、借款申请借、借款查询。 那么如何统计每个API的三方耗时。
@Component
public class UserServiceImpl implements UserService{
@Override
public void creditApply() {
// 模拟调用三方
System.out.println("creditApply");
}
@Override
public void creditQuery() {
// 模拟调用三方
System.out.println("creditQuery");
}
@Override
public void loanApply() {
// 模拟调用三方
System.out.println("loanApply");
}
@Override
public void loanQuery() {
// 模拟调用三方
System.out.println("loanQuery");
}
}
显然这个耗时统计是一个非功能性需求,那么我们考虑几种方案
方案1
比较简单粗暴,那就是在每个接口调用时 直接编码统计,优点是简单 好实现,缺点是和业务代码强耦合在一起,显然不是一个好的方案
方案2
既然想剥离开,那么简单的方式就是SpringAOP机制,进行切面编程,将公共的统计耗时逻辑进行抽取出来,这里其实又遇到一个问题,那就是,如果只是单纯的计算一个接口耗时,没有问题,如果想计算一个用户请求,所有的三方API耗时,就需要串连起来,那么既然同一个用户请求 如果不是异步处理,那么必定在一个线程内,所以就可以结合ThreadLocal使用。
方案2 code
定义耗时统计map,这里有个注意点就是 因为要保证添加到Map中的有序性,所以使用 LinkedHashMap
public class ApiTimeMap {
private Map<String, BigDecimal> map;
public ApiTimeMap() {
map = new LinkedHashMap<>();
}
public void add(String key, BigDecimal value) {
map.put(key, value);
}
public Map<String, BigDecimal> getMap() {
return map;
}
}
定义ThreadLocal 保存的是一个Map
public class TimeHolder {
private static final ThreadLocal<ApiTimeMap> timeThreadLocal = new ThreadLocal<>();
public static void setMethodTime(String method, BigDecimal time) {
ApiTimeMap apiTimeMap = timeThreadLocal.get();
if (Objects.isNull(apiTimeMap)) {
apiTimeMap = new ApiTimeMap();
}
apiTimeMap.add(method, time);
timeThreadLocal.set(apiTimeMap);
}
public static ApiTimeMap getThreadApiTimeMap() {
return timeThreadLocal.get();
}
public static void clear() {
timeThreadLocal.remove();
}
}
核心切面类
**
* @author qxlx
* @date 2024/11/22 21:52
*/
@Aspect
@Component
public class TimeAspect {
private static final Logger log = LoggerFactory.getLogger(TimeAspect.class);
@Pointcut("execution(* com.qxlx.clactime.UserServiceImpl.*(..))")
public void pointCuts() {
}
@Around("pointCuts()")
public Object arounds(ProceedingJoinPoint joinPoint) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Signature signature = joinPoint.getSignature();
log.info(Thread.currentThread().getName()+"\t【注解】日志记录执行开始 ===== ");
// 方法执行
try {
return joinPoint.proceed();
} catch (Throwable e) {
return e;
} finally {
stopWatch.stop();
TimeHolder.setMethodTime(((MethodSignature) signature).getMethod().getName(), BigDecimal.valueOf(stopWatch.getTotalTimeSeconds()));
// 方法执行后
System.out.println(Thread.currentThread().getName()+"\t【注解】日志记录执行完毕 ===== ");
}
}
}
测试类
public static void main(String[] args) {
AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = ioc.getBean(UserService.class);
new Thread(()->{
try {
userService.creditApply();
userService.creditQuery();
userService.loanApply();
ApiTimeMap threadApiTimeMap = TimeHolder.getThreadApiTimeMap();
System.out.println(Thread.currentThread().getName()+"\t"+JSONUtils.toJSONString(threadApiTimeMap.getMap()));
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
TimeHolder.clear();
}
},"T1").start();
new Thread(()->{
try {
userService.creditApply();
userService.creditQuery();
userService.loanApply();
userService.loanQuery();
ApiTimeMap threadApiTimeMap = TimeHolder.getThreadApiTimeMap();
System.out.println(Thread.currentThread().getName()+"\t"+JSONUtils.toJSONString(threadApiTimeMap.getMap()));
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
TimeHolder.clear();
}
},"T2").start();
}
最终实现的效果就是这样。
通过这样的方式,即便是其他程序员编写别的三方接口,也可以直接复用这部分功能,拓展性也是很好的。
总结
其实在平时的开发中,要多思考,那些是核心功能,或者通用功能,就需要在设计上多花点功夫进行设计,把不变的抽取出来,变化的进行隔离。 AOP 在很多场景可以使用。
- 基于AOP 统计API接口耗时
- 基于AOP 打印统一的日志
- 基于AOP 事务管理
- 基于AOP + 注解 实现接口幂等功能
- 基于AOP + 注解 实现接口限流功能
- 数据校验、缓存处理、异常处理等。