Spring?Aop+Redis實現(xiàn)優(yōu)雅記錄接口調(diào)用情況
記錄接口調(diào)用情況的訴求
通常情況下,開發(fā)完一個接口,無論是在測試階段還是生產(chǎn)上線,我們都需要對接口的執(zhí)行情況做一個監(jiān)控,比如記錄接口的調(diào)用次數(shù)、失敗的次數(shù)、調(diào)用時間、包括對接口進行限流,這些都需要我們開發(fā)人員進行把控的,以便提高整體服務(wù)的運行質(zhì)量,也能方便我們分析接口的執(zhí)行瓶頸,可以更好的對接口進行優(yōu)化。
常見監(jiān)測服務(wù)的工具
通過一些常見第三方的工具,比如:Sentinel、Arthas、Prometheus等都可以進行服務(wù)的監(jiān)控、報警、服務(wù)治理、qps并發(fā)情況,基本大多數(shù)都支持Dodcker、Kubernetes,也相對比較好部署,相對來說比較適應(yīng)于大型業(yè)務(wù)系統(tǒng),服務(wù)比較多、并發(fā)量比較大、需要更好的服務(wù)治理,從而更加方便對服務(wù)進行管理,但是一般小型的業(yè)務(wù)系統(tǒng)其實也沒太必要引入這些服務(wù),畢竟需要花時間和人力去搭建和運維。
Spring實現(xiàn)接口調(diào)用統(tǒng)計
引入依賴 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方法執(zhí)行前進行切面處理,記錄接口名、方法、接口調(diào)用次數(shù)、調(diào)用情況、調(diào)用ip、并且寫入redis緩存,提供查詢接口,可以查看調(diào)用情況。
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;
/**
* 前置處理,記錄接口在調(diào)用剛開始的時候,每次調(diào)用+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());
//訪問次數(shù)大于20次就進行接口熔斷
if (count > 20) {
throw new RuntimeException("已超過允許失敗訪問次數(shù),不允許再次訪問");
}
redisTemplate.opsForValue().increment(apiKey, 1);
} else {
redisTemplate.opsForValue().set(apiKey, "1", 1L, TimeUnit.DAYS);
}
}
/**
* 后置處理,接口在調(diào)用結(jié)束后,有返回結(jié)果,對接口調(diào)用成功后進行記錄。
*/
@After("execution(* com.example.system.controller.*.*(..))")
public void after() {
// 接收到請求,記錄請求內(nèi)容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//獲取請求的request
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURI();
log.info("調(diào)用完成手的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的時候,會出現(xiàn)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緩存存儲情況 請求次數(shù)

異常key

查詢緩存結(jié)果

可以看到,統(tǒng)計到了接口請求的時間以及異常信息,還有接口的請求次數(shù)。

總結(jié)
某些場景下還是需要用到接口請求統(tǒng)計的,包括也可以做限流操作,大部分中間件的底層做監(jiān)控,底層實現(xiàn)方式也差不了多少, 記得很多年前有道面試題,還被問到如何做接口的請求次數(shù)統(tǒng)計,以及限流策略。
以上就是Spring Aop+Redis實現(xiàn)優(yōu)雅記錄接口調(diào)用情況的詳細內(nèi)容,更多關(guān)于Spring Redis接口調(diào)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Spring Cache時設(shè)置緩存鍵的注意事項詳解
在現(xiàn)代的Web應(yīng)用中,緩存是提高系統(tǒng)性能和響應(yīng)速度的重要手段之一,Spring框架提供了強大的緩存支持,通過??@Cacheable??、??@CachePut??、??@CacheEvict??等注解可以方便地實現(xiàn)緩存功能,本文給大家介紹了使用Spring Cache時設(shè)置緩存鍵的注意事項2025-01-01
Win10 Java jdk14.0.2安裝及環(huán)境變量配置詳細教程
這篇文章主要介紹了Win10 Java jdk14.0.2安裝及環(huán)境變量配置,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
使用synchronized實現(xiàn)一個Lock代碼詳解
這篇文章主要介紹了使用synchronized實現(xiàn)一個Lock代碼詳解,具有一定借鑒價值,需要的朋友可以參考下。2017-12-12
SpringBoot使用spring.config.import多種方式導(dǎo)入配置文件
本文主要介紹了SpringBoot使用spring.config.import多種方式導(dǎo)入配置文件,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

