记载接口调用状况的诉求

通常状况下,开发完一个接口,无论是在测验阶段仍是生产上线,咱们都需求对接口的履行状况做一个监控,比方记载接口的调用次数、失败的次数、调用时刻、包含对接口进行限流,这些都需求咱们开发人员进行把控的,以便提高整体服务的运行质量,也能便利咱们分析接口的履行瓶颈,能够更好的对接口进行优化。

常见监测服务的工具

通过一些常见第三方的工具,比方: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缓存存储状况 恳求次数

Spring Aop+Redis优雅的记录接口调用情况
异常key

Spring Aop+Redis优雅的记录接口调用情况

  • 查询缓存成果

Spring Aop+Redis优雅的记录接口调用情况
能够看到,统计到了接口恳求的时刻以及异常信息,还有接口的恳求次数。

Spring Aop+Redis优雅的记录接口调用情况

总结

某些场景下仍是需求用到接口恳求统计的,包含也能够做限流操作,大部分中间件的底层做监控,底层完结方法也差不了多少, 记住很多年前有道面试题,还被问到如何做接口的恳求次数统计,以及限流战略。