思路:通過(guò)AOP攔截注解標(biāo)記的方法,在Redis中維護(hù)一個(gè)計(jì)數(shù)器來(lái)記錄接口訪問(wèn)的頻率,
并根據(jù)限流策略來(lái)判斷是否允許繼續(xù)處理請(qǐng)求。
另一篇:springboot 自定義注解 ,aop切面@Around; 為接口實(shí)現(xiàn)日志插入【強(qiáng)行喂飯版】
不多說(shuō),直接上代碼:
一:創(chuàng)建限流類型
/**
* 限流類型
*
*/
public enum LimitType
{
/**
* 默認(rèn)策略全局限流
*/
DEFAULT,
/**
* 根據(jù)請(qǐng)求者IP進(jìn)行限流
*/
IP
}
二:創(chuàng)建注解
import 你上面限流類型的路徑.LimitType;
import java.lang.annotation.*;
/**
* 限流注解
*
*/
// 注解的作用目標(biāo)為方法
@Target(ElementType.METHOD)
// 注解在運(yùn)行時(shí)保留,編譯后的class文件中存在,在jvm運(yùn)行時(shí)保留,可以被反射調(diào)用
@Retention(RetentionPolicy.RUNTIME)
// 指明修飾的注解,可以被例如javadoc此類的工具文檔化,只負(fù)責(zé)標(biāo)記,沒(méi)有成員取值
@Documented
public @interface LimiterToShareApi{
/**
* 限流key
*/
public String key() default "";
/**
* 限流時(shí)間,單位秒
*/
public int time() default 60;
/**
* 限流次數(shù)
*/
public int count() default 100;
/**
* 限流類型,默認(rèn)全局限流
*/
public LimitType limitType() default LimitType.DEFAULT;
}
**三:編寫業(yè)務(wù)異常類 **
/**
* 業(yè)務(wù)異常
*
*/
public final class ServiceException extends RuntimeException
{
// 序列化的版本號(hào)的屬性
private static final long serialVersionUID = 1L;
/**
* 錯(cuò)誤碼
*/
private Integer code;
/**
* 錯(cuò)誤提示
*/
private String message;
/**
* 空構(gòu)造方法,避免反序列化問(wèn)題
*/
public ServiceException(){
}
/**
* 異常信息
*/
public ServiceException(String message){
this.message = message;
}
}
四:實(shí)現(xiàn)aop切面攔截,限流邏輯處理
import 你上面限流類型的路徑.LimitType;
import 你上面業(yè)務(wù)異常的路徑.ServiceException;
import 你上面限流注解的路徑.LimiterToShareApi;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
// 聲明這是一個(gè)切面類
@Aspect
// 表明該類是一個(gè)組件,將該類交給spring管理。
@Component
// 指定執(zhí)行順序,值越小,越先執(zhí)行。限流策略一般最先執(zhí)行。
@Order(1)
public class LimiterToShareApiAspect {
// 記錄日志的Logger對(duì)象
private static final Logger log = LoggerFactory.getLogger(LimiterToShareApiAspect.class);
// 操作Redis的RedisTemplate對(duì)象
private RedisTemplate<Object, Object> redisTemplate;
//在Redis中執(zhí)行Lua腳本的對(duì)象
private RedisScript<Long> limitScript;
@Autowired
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Autowired
public void setLimitScript(RedisScript<Long> limitScript) {
this.limitScript = limitScript;
}
// 這個(gè)注解作用及普及 見(jiàn)文章后面解析
@Before("@annotation(limiter)")
public void doBefore(JoinPoint point, LimiterToShareApi limiter) throws Throwable {
// 根據(jù)業(yè)務(wù)需求,看看是否去數(shù)據(jù)庫(kù)查詢對(duì)應(yīng)的限流策略,還是直接使用注解傳遞的值
// 這里演示為 獲取注解的值
int time = limiter.time();
int count = limiter.count();
String combineKey = getCombineKey(limiter, point);
List<Object> keys = Collections.singletonList(combineKey);
try {
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (number == null || number.intValue() > count) {
throw new ServiceException("限流策略:訪問(wèn)過(guò)于頻繁,請(qǐng)稍候再試");
}
log.info("限制請(qǐng)求'{}',當(dāng)前請(qǐng)求'{}',緩存key'{}'", count, number.intValue(), combineKey);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("服務(wù)器限流異常,請(qǐng)稍候再試");
}
}
/**
* 獲取用于限流的組合鍵,根據(jù)LimterToShareApi注解和JoinPoint對(duì)象來(lái)生成。
*
* @param rateLimiter LimiterToShareApi注解,用于獲取限流配置信息。
* @param point JoinPoint對(duì)象,用于獲取目標(biāo)方法的信息。
* @return 生成的用于限流的組合鍵字符串。
*/
public String getCombineKey(LimiterToShareApi rateLimiter, JoinPoint point) {
// 創(chuàng)建一個(gè)StringBuffer用于拼接組合鍵
StringBuffer stringBuffer = new StringBuffer(rateLimiter.key() + "-");
// 根據(jù)LimterToShareApi注解的limitType判斷是否需要添加IP地址信息到組合鍵中【判斷限流類型 是否根據(jù)ip進(jìn)行限流】
if (rateLimiter.limitType() == LimitType.IP) {
// 如果需要添加IP地址信息,調(diào)用IpUtils.getIpAddr()方法獲取當(dāng)前請(qǐng)求的IP地址,并添加到組合鍵中
stringBuffer.append(getClientIp()).append("-");
}
// 使用JoinPoint對(duì)象獲取目標(biāo)方法的信息
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
// 將目標(biāo)方法所屬類的名稱和方法名稱添加到組合鍵中
stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
// 返回生成的用于限流的組合鍵字符串
return stringBuffer.toString();
}
/**
* 獲取調(diào)用方真實(shí)ip [本機(jī)調(diào)用則得到127.0.0.1]
* 首先嘗試從X-Forwarded-For請(qǐng)求頭獲取IP地址,如果沒(méi)有找到或者為unknown,則嘗試從X-Real-IP請(qǐng)求頭獲取IP地址,
* 最后再使用request.getRemoteAddr()方法作為備用方案。注意,在多個(gè)代理服務(wù)器的情況下,
* X-Forwarded-For請(qǐng)求頭可能包含多個(gè)IP地址,我們?nèi)〉谝粋€(gè)IP地址作為真實(shí)客戶端的IP地址。
*/
public String getClientIp() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("X-Real-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
// 多個(gè)代理服務(wù)器時(shí),取第一個(gè)IP地址
int index = ipAddress.indexOf(",");
if (index != -1) {
ipAddress = ipAddress.substring(0, index);
}
return ipAddress;
}
}
五:哪里需要點(diǎn)哪里
@PostMapping("/接口api")
// 根據(jù)自己業(yè)務(wù)選擇是否需要這些參數(shù),如果是想從數(shù)據(jù)庫(kù)讀取,不填參數(shù)即可
// 這里意思為對(duì)key的限制為 全局 每60秒內(nèi)2次請(qǐng)求,超過(guò)2次則限流
@LimiterToShareApi(key = "key",time = 60,count = 2,limitType = LimitType.DEFAULT)
public AjaxResult selectToUserId(參數(shù)){}
限流(代碼)結(jié)果:
解析:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-609972.html
@Before("@annotation(limiter)"):
- 使用了@Before 來(lái)表示這是一個(gè)切面注解,用于定義在目標(biāo)方法執(zhí)行前執(zhí)行的邏輯
- @annotation(limiter) 中的limiter是指參數(shù)名稱,而不是注解名稱。
- @annotation(limiter) 中的limiter參數(shù)類型為LimiterToShareApi,
表示你將攔截被@LimiterToShareApi注解標(biāo)記的方法,并且可以通過(guò)這個(gè)參數(shù)來(lái)獲取@LimiterToShareApi注解的信息。
如果你想攔截其他注解,只需將第二個(gè)參數(shù)的類型修改為對(duì)應(yīng)的注解類型即可。
普及:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-609972.html
JoinPoint是Spring AOP中的一個(gè)接口,它代表了在程序執(zhí)行過(guò)程中能夠被攔截的連接點(diǎn)(Join Point)。
連接點(diǎn)指的是在應(yīng)用程序中,特定的代碼塊,比如方法的調(diào)用、方法的執(zhí)行、構(gòu)造器的調(diào)用等。
JoinPoint在AOP中的作用是用于傳遞方法調(diào)用的信息,比如方法名、參數(shù)、所屬的類等等。
當(dāng)AOP攔截到一個(gè)連接點(diǎn)時(shí),就可以通過(guò)JoinPoint對(duì)象來(lái)獲取這些信息,并根據(jù)需要進(jìn)行相應(yīng)的處理。
在AOP中,常見(jiàn)的通知類型(advice)如下:
@Before:在目標(biāo)方法執(zhí)行之前執(zhí)行。
@After:在目標(biāo)方法執(zhí)行之后(無(wú)論是否拋出異常)執(zhí)行。
@AfterReturning:在目標(biāo)方法成功執(zhí)行之后執(zhí)行。
@AfterThrowing:在目標(biāo)方法拋出異常后執(zhí)行。
@Around:在目標(biāo)方法執(zhí)行前后都執(zhí)行,可以控制目標(biāo)方法的執(zhí)行。
在以上各種通知中,可以使用JoinPoint參數(shù)來(lái)獲取連接點(diǎn)的相關(guān)信息。
例如,在@Around通知中,可以使用JoinPoint對(duì)象來(lái)獲取目標(biāo)方法的信息,
比如方法名、參數(shù)等。這樣,我們就可以根據(jù)這些信息來(lái)實(shí)現(xiàn)我們需要的切面邏輯。
eg:
// 獲取方法名
String methodName = joinPoint.getSignature().getName();
//獲取方法參數(shù)
Object[] args = joinPoint.getArgs();
// 獲取所屬類名
String className = joinPoint.getSignature().getDeclaringTypeName();
// 獲取源代碼位置信息
SourceLocation sourceLocation = joinPoint.getSourceLocation();
到了這里,關(guān)于springboot 自定義注解 ,實(shí)現(xiàn)接口限流(計(jì)數(shù)器限流)【強(qiáng)行喂飯版】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!