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