Spring Aop+Redis优雅的记录接口调用情况
记录接口调用情况的诉求
通常情况下,开发完一个接口,无论是在测试阶段还是生产上线,我们都需要对接口的执行情况做一个监控,比如记录接口的调用次数、失败的次数、调用时间、包括对接口进行限流,这些都需要我们开发人员进行把控的,以便提高整体服务的运行质量,也能方便我们分析接口的执行瓶颈,可以更好的对接口进行优化。
常见监测服务的工具
通过一些常见第三方的工具,比如:Sentinel、Arthas、Prometheus等都可以进行服务的监控、报警、服务治理、qps并发情况,基本大多数都支持Dodcker、Kubernetes,也相对比较好部署,相对来说比较适应于大型业务系统,服务比较多、并发量比较大、需要更好的服务治理,从而更加方便对服务进行管理,但是一般小型的业务系统其实也没太必要引入这些服务,毕竟需要花时间和人力去搭建和运维。
Spring实现接口调用统计
引入依赖 Spring boot、redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
思路就是通过AOP切面,在controller方法执行前进行切面处理,记录接口名、方法、接口调用次数、调用情况、调用ip、并且写入redis缓存,提供查询接口,可以查看调用情况。
RequestApiAdvice切面处理
package com.example.system.aspect;
import cn.hutool.core.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Slf4j
public class RequestApiAdvice {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 前置处理,记录接口在调用刚开始的时候,每次调用+1
*
* @param joinPoint
*/
@Before("execution(* com.example.system.controller.*.*(..))")
public void before(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取请求的request
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURI();
String ip = getRequestIp(request);
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = joinPoint.getSignature().getName();
log.info("请求接口的类名:{}", className);
log.info("请求的方法名:{}", methodName);
//redis key由 url+类名+方法名+日期
String apiKey = ip + "_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
//判断是否存在key
if (!redisTemplate.hasKey(apiKey)) {
int count = Integer.parseInt(redisTemplate.boundValueOps(ip).get().toString());
//访问次数大于20次就进行接口熔断
if (count > 20) {
throw new RuntimeException("已超过允许失败访问次数,不允许再次访问");
}
redisTemplate.opsForValue().increment(apiKey, 1);
} else {
redisTemplate.opsForValue().set(apiKey, "1", 1L, TimeUnit.DAYS);
}
}
/**
* 后置处理,接口在调用结束后,有返回结果,对接口调用成功后进行记录。
*/
@After("execution(* com.example.system.controller.*.*(..))")
public void after() {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//获取请求的request
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURI();
log.info("调用完成手的url:{}", url);
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
if (redisTemplate.hasKey(url)) {
redisTemplate.boundHashOps(url).increment(date, 1);
} else {
redisTemplate.boundHashOps(url).put(date, "1");
}
}
@AfterThrowing(value = "execution(* com.example.system.controller.*.*(..))", throwing = "e")
public void throwing(Exception e) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURI() + "_exception";
//精确到时分秒
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
String date = format.format(new Date());
//异常报错
String exception = e.getMessage();
redisTemplate.boundHashOps(url).put(date, exception);
}
private String getRequestIp(HttpServletRequest request) {
//获取ip
String ip = request.getHeader("x-forwarded-for");
Assert.notBlank(ip, "请求接口ip不能为空!");
return ip;
}
}
RedisSerialize序列化处理
⚠️这边需要对redis的序列化方式进行简单配置,要不然在进行set key的操作的时候,由于key和value是字符串类型,如果不进行反序化配置,redis通过key获取value的时候,会出现null值。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes", "deprecation" })
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(connectionFactory);
// 定义value的序列化方式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer(Charset.forName("UTF-8")));
//save hash use StringRedisSerializer as serial method
template.setHashKeySerializer(new StringRedisSerializer(Charset.forName("UTF-8")));
template.setHashValueSerializer(new StringRedisSerializer(Charset.forName("UTF-8")));
return template;
}
}
RedisTestController查询redis缓存接口
@RestController
@Slf4j
@RequestMapping("/api/redis")
public class RedisTestController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/getApiRequestCount")
public List<String> getApiRequestCount() {
List list =new ArrayList();
Set<String> keys = stringRedisTemplate.keys("/api/*");
for (int i = 0; i < keys.size(); i++) {
Map<Object, Object> m = null;
try {
m = stringRedisTemplate.opsForHash().entries((String) keys.toArray()[i]);
} catch (Exception e) {
e.printStackTrace();
}
List result = new ArrayList();
for (Object key : m.keySet()) {
//将字符串反序列化为list
String value = (String) m.get(key);
result.add(String.format("%s: %s", key, value));
}
list.addAll(result);
}
return list;
}
@GetMapping("/{methodName}")
public String getCount(@PathVariable String methodName) {
List<Object> values = stringRedisTemplate.boundHashOps("/api/" + methodName).values();
return String.format("%s: %s", methodName, values);
}
}
- redis缓存存储情况 请求次数
异常key
- 查询缓存结果
可以看到,统计到了接口请求的时间以及异常信息,还有接口的请求次数。
总结
某些场景下还是需要用到接口请求统计的,包括也可以做限流操作,大部分中间件的底层做监控,底层实现方式也差不了多少, 记得很多年前有道面试题,还被问到如何做接口的请求次数统计,以及限流策略。
转载自:https://juejin.cn/post/7244047701305950245