Spring?Aop+Redis實現優(yōu)雅記錄接口調用情況
記錄接口調用情況的訴求
通常情況下,開發(fā)完一個接口,無論是在測試階段還是生產上線,我們都需要對接口的執(zhí)行情況做一個監(jiān)控,比如記錄接口的調用次數、失敗的次數、調用時間、包括對接口進行限流,這些都需要我們開發(fā)人員進行把控的,以便提高整體服務的運行質量,也能方便我們分析接口的執(zhí)行瓶頸,可以更好的對接口進行優(yōu)化。
常見監(jiān)測服務的工具
通過一些常見第三方的工具,比如:Sentinel、Arthas、Prometheus等都可以進行服務的監(jiān)控、報警、服務治理、qps并發(fā)情況,基本大多數都支持Dodcker、Kubernetes,也相對比較好部署,相對來說比較適應于大型業(yè)務系統,服務比較多、并發(fā)量比較大、需要更好的服務治理,從而更加方便對服務進行管理,但是一般小型的業(yè)務系統其實也沒太必要引入這些服務,畢竟需要花時間和人力去搭建和運維。
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方法執(zhí)行前進行切面處理,記錄接口名、方法、接口調用次數、調用情況、調用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
查詢緩存結果
可以看到,統計到了接口請求的時間以及異常信息,還有接口的請求次數。
總結
某些場景下還是需要用到接口請求統計的,包括也可以做限流操作,大部分中間件的底層做監(jiān)控,底層實現方式也差不了多少, 記得很多年前有道面試題,還被問到如何做接口的請求次數統計,以及限流策略。
以上就是Spring Aop+Redis實現優(yōu)雅記錄接口調用情況的詳細內容,更多關于Spring Redis接口調用的資料請關注腳本之家其它相關文章!
相關文章
Win10 Java jdk14.0.2安裝及環(huán)境變量配置詳細教程
這篇文章主要介紹了Win10 Java jdk14.0.2安裝及環(huán)境變量配置,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08SpringBoot使用spring.config.import多種方式導入配置文件
本文主要介紹了SpringBoot使用spring.config.import多種方式導入配置文件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-05-05