基于注解的接口检测实现

背景

最近在写小组项目时,想到自己写的项目以后还要不断进行维护,于是乎,对接口进行优化是必须的,但是如果对接口一个个统计响应时间或者取查询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 仓库地址点这里

相关推荐
monkey_meng12 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss21 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万1 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
转转技术团队1 小时前
空间换时间-将查询数据性能提升100倍的计数系统实践
java·后端·架构
酸奶代码2 小时前
Spring AOP技术
java·后端·spring
代码小鑫2 小时前
A034-基于Spring Boot的供应商管理系统的设计与实现
java·开发语言·spring boot·后端·spring·毕业设计
paopaokaka_luck2 小时前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法