基于注解的接口检测实现

背景

最近在写小组项目时,想到自己写的项目以后还要不断进行维护,于是乎,对接口进行优化是必须的,但是如果对接口一个个统计响应时间或者取查询msyql的慢查询日志还是挺麻烦的,所以想着能不能简化下操作,这个注解就出现了。当然这只是笔者的一个不成熟想法,笔者只是一个大学生,如果代码或者思路有什么不好的地方,还请大家指正!

具体实现 想必大家对于注解都已经十分了解,这边就不再详细介绍,只是简单说一下实现思路,我们在接口上添加注解之后,利用aop的环绕通知结合**StopWatch**分别在接口执行前后调用相应方法即可获取接口响应时间,而且这个spring自带的StopWatch对于我们接口影响可以忽略不记,之后即使对于接口耗时的统计,本来打算结合maxwell直接获取日志,这样做不仅可以获取我们的慢sql而且同时对于哪个接口的耗时也能够直接统计,但是由于时间原因,先画个饼,后续进行实现。 这边先将数据都存到了Redis中的list中去,然后对于相同的接口我们会计算平均的响应时间。下面直接看代码:

定义注解Monitor

java 复制代码
package org.zrq.annotation;

import java.lang.annotation.*;

/**
 * @author zrq
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Monitor {

}

定义切面

java 复制代码
package org.zrq.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.zrq.utils.RedisCache;

import javax.annotation.Resource;


/**
 * @author zrq
 * @ClassName RequestManyValidationAspect
 * @date 2023/11/22 9:14
 * @Description TODO
 */
@Aspect
@Slf4j
@Component
public class MonitorAspect {
    @Resource
    private RedisCache redisCache;
    @Around("@annotation(org.zrq.annotation.Monitor)")
    public Object validateIdempotent(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        try {
            return joinPoint.proceed();
        } finally {
            stopWatch.stop();
            log.info(String.valueOf(stopWatch.getTotalTimeMillis()) + "=====================");
            redisCache.setCacheList("api_request_times:" + methodName,  stopWatch.getTotalTimeMillis());
        }
    }

}

创建任务

ini 复制代码
package org.zrq.task;

import com.sun.xml.internal.messaging.saaj.packaging.mime.MessagingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.zrq.utils.RedisCache;

import javax.annotation.Resource;
import javax.mail.internet.MimeMessage;
import java.util.*;

/**
 * @author zrq
 * @ClassName RequestTimeStatisticsTask
 * @date 2023/12/9 15:36
 * @Description TODO
 */
@Component
public class RequestTimeStatisticsTask {
    @Resource
    private JavaMailSender javaMailSender;
    @Resource
    private RedisCache redisCache;

    private final String fromEmail = "************@**.com";
    private final String toEmail = "**********@**.com";
    public void sendRequestTimeStatistics() {
        Map<String, Double> averageRequestTimes = calculateAverageRequestTimes();

        // 对平均耗时进行排序,获取前十名
        List<Map.Entry<String, Double>> sortedRequestTimes = new ArrayList<>(averageRequestTimes.entrySet());
        sortedRequestTimes.sort(Map.Entry.comparingByValue());
        sortedRequestTimes = sortedRequestTimes.subList(0, Math.min(sortedRequestTimes.size(), 10));
        // 发送前十名的接口请求耗时数据到邮箱
        sendEmail(sortedRequestTimes);
    }

    private Map<String, Double> calculateAverageRequestTimes() {
        Collection<String> apiNames = redisCache.keys("api_request_times:*");
        Map<String, Double> averageRequestTimes = new HashMap<>();

        for (String apiName : apiNames) {
            List<String> requestTimes = redisCache.getCacheList(apiName);
            if (!requestTimes.isEmpty()) {
                double averageTime = requestTimes.stream()
                        .mapToLong(Long::parseLong)
                        .average()
                        .orElse(0);
                averageRequestTimes.put(apiName, averageTime);
            }
        }
        return averageRequestTimes;
    }

    private void sendEmail(List<Map.Entry<String, Double>> requestTimes) {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(fromEmail);
            helper.setTo(toEmail);
            helper.setSubject("接口请求耗时排行榜");

            StringBuilder content = new StringBuilder();
            content.append("<h1>接口请求耗时排行榜</h1>");
            content.append("<table><tr><th>接口</th><th>平均耗时</th></tr>");
            for (Map.Entry<String, Double> entry : requestTimes) {
                content.append("<tr><td>").append(entry.getKey()).append("</td><td>").append(entry.getValue()).append("ms</td></tr>");
            }
            content.append("</table>");

            helper.setText(content.toString(), true);
            javaMailSender.send(message);
        } catch (javax.mail.MessagingException e) {
            e.printStackTrace();
        }
    }
}

执行任务

kotlin 复制代码
package org.zrq.task;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author zrq
 * @ClassName ScheduleTask
 * @date 2023/12/9 15:42
 * @Description TODO
 */
@Component
public class ScheduleTask {
    @Autowired
    private RequestTimeStatisticsTask requestTimeStatisticsTask;
    @Scheduled(cron = "0 0 0/1 * * ?") // 每小时执行一次
    public void sendRequestTimeStatistics() {
        requestTimeStatisticsTask.sendRequestTimeStatistics();
    }
}

接口演示

kotlin 复制代码
package org.zrq.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zrq.annotation.Monitor;
import org.zrq.service.UserService;

import javax.annotation.Resource;
import javax.naming.ldap.PagedResultsControl;

/**
 * @author zrq
 * @ClassName UserController
 * @date 2023/12/9 15:13
 * @Description TODO
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;
    @GetMapping("/demo")
    @Monitor
    public String test(Integer id){
        return userService.queryById(id);
    }
}

Redis工具类

typescript 复制代码
package org.zrq.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
@Slf4j
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }
    public boolean hasKey(final String key)
    {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)

    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }
    public <T> long setCacheList(final String methodName, final Object value){
        Long aLong = redisTemplate.opsForList().leftPush("api_request_times:" + methodName, String.valueOf(value));
        return aLong == null ? 0 : aLong;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<Integer, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }

    public Boolean sign(String key, int day, boolean sign) {
      return  redisTemplate.opsForValue().setBit(key, day, sign);
    }

    public List<Long> result(String key, int day, int num){
        List<Long> result = stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(day)).valueAt(0)
        );
        return result;
    }

    public Long bigCount(String key) {
        Long execute = (Long)redisTemplate.execute((RedisCallback) cbk -> cbk.bitCount(key.getBytes()));
        return execute;
    }
    public <T> void setCacheZSet(String name,T key, double num) {
        if (key != null) {
            redisTemplate.opsForZSet().add(name,key,num);
        }
    }
    public <T> Long getCacheZSetRanking(String name,String key) {
        Long aLong = null;
        if (key != null) {
            aLong = redisTemplate.opsForZSet().reverseRank(name, key);
        }
        return aLong;
    }
    public <T> Double getCacheZSetScore(String name,T key) {
        Double score = null;
        if (key != null) {
            score = redisTemplate.opsForZSet().score(name, key);
        }
        return score;
    }
    public  Set getCacheZSetLookTop(String name,int nums) {
        Set set = null;
        if (name != null) {
            set = redisTemplate.opsForZSet().reverseRange(name, 0, nums);
        }
        return set;
    }
    public Long getCacheZSetSize(String key) {
        Long aLong = null;
        if (key != null) {
            aLong = redisTemplate.opsForZSet().zCard(key);
        }
        return aLong;
    }

    public <T> Long deleteCacheZSet(String key, T value) {
        Long remove = null;
        if (key != null) {
            remove = redisTemplate.opsForZSet().remove(key, value);
        }
        return remove;
    }
    public List<Long> getBitMap(String key,Integer day) {
        List<Long> bitFieldList = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) cbk
                -> cbk.bitField(key.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(day)).valueAt(0)));
        return bitFieldList;
    }
    public Long incrExpire(String key, long time) {
        Long count = redisTemplate.opsForValue().increment(key, 1);
        if (count != null && count == 1) {
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
        return count;
    }
    public boolean removeList(String listName, Integer count, String value) {
        redisTemplate.opsForList().remove(listName,count,value);
        return true;
    }
}

完整实现

这样我们可以统计一定时间内接口耗时,根据排行榜去优化接口,后续再添加上慢sql我们就可以知道到底是我们业务逻辑有问题,还是sql效率太低,我觉着挺有用的,不管是我们维护老项目,还是在写项目,添加上这个方法后我们可以轻易的知道项目中存在的问题,针对性的进行优化。目前的想法还不太成熟,代码写的也很烂,希望各位大佬能够指导下,如何更好的实现上面的方案。 具体代码我会上传到github 仓库地址点这里

相关推荐
武子康19 分钟前
Java-152 深入浅出 MongoDB 索引详解 从 MongoDB B-树 到 MySQL B+树 索引机制、数据结构与应用场景的全面对比分析
java·开发语言·数据库·sql·mongodb·性能优化·nosql
间彧1 小时前
Windows Server,如何使用WSFC+nginx实现集群故障转移
后端
间彧1 小时前
Nginx + Keepalived 实现高可用集群(Linux下)
后端
间彧1 小时前
在Kubernetes中如何部署高可用的Nginx Ingress Controller?
后端
间彧1 小时前
Ribbon负载均衡器和Nginx负载均衡器有什么区别
后端
间彧1 小时前
Nacos详解与项目实战
后端
间彧1 小时前
nginx、网关Gateway、Nacos、多个服务实例之间的数据链路详解
后端
间彧1 小时前
Nacos与Eureka在性能上有哪些具体差异?
后端
间彧1 小时前
详解Nacos健康状态监测机制
后端
间彧2 小时前
如何利用Nacos实现配置的灰度发布?
后端