Hyperf方案 自定义注解开发
?php/** * 案例标题自定义注解开发 * 说明开发一个RateLimit自定义注解配合AOP拦截器实现接口限流 * 需要安装的包 * composer require hyperf/di * composer require hyperf/aop * composer require hyperf/redis */declare(strict_types1);// app/Annotation/RateLimit.php // 定义注解类打上这个注解的方法会被自动限流namespaceApp\Annotation;useAttribute;useHyperf\Di\Annotation\AbstractAnnotation;/** * 限流注解贴在Controller方法上就能限流 * 用法#[RateLimit(limit: 100, period: 60)] 表示60秒内最多100次 */#[Attribute(Attribute::TARGET_METHOD)]// 只能打在方法上classRateLimitextendsAbstractAnnotation{publicfunction__construct(publicreadonlyint$limit100,// 最大请求次数publicreadonlyint$period60,// 时间窗口秒publicreadonlystring$key,// 限流key空则用 类名方法名publicreadonlystring$message请求太频繁请稍后再试// 触发限流时的提示){}}// app/Aspect/RateLimitAspect.php namespaceApp\Aspect;useApp\Annotation\RateLimit;useHyperf\Di\Annotation\Aspect;useHyperf\Di\Aop\AbstractAspect;useHyperf\Di\Aop\ProceedingJoinPoint;useHyperf\HttpServer\Contract\RequestInterface;useHyperf\Redis\Redis;usePsr\Log\LoggerInterface;/** * 限流切面拦截所有带RateLimit注解的方法 */#[Aspect]classRateLimitAspectextendsAbstractAspect{// 告诉框架只拦截带RateLimit注解的方法publicarray$annotations[RateLimit::class,];publicfunction__construct(privateRedis$redis,privateRequestInterface$request,privateLoggerInterface$logger){}/** * 每次调用被注解的方法时都会先跑这里 */publicfunctionprocess(ProceedingJoinPoint$proceedingJoinPoint){// 从切入点拿到RateLimit注解的参数/** var RateLimit $annotation */$annotation$proceedingJoinPoint-getAnnotationMetadata()-method[RateLimit::class];// 构建限流Redis key注解指定的key 用户IP$clientIp$this-request-getServerParams()[remote_addr]??0.0.0.0;$baseKey$annotation-key?:($proceedingJoinPoint-className..$proceedingJoinPoint-methodName);$redisKeyrate_limit:{$baseKey}:{$clientIp};// 区分不同用户的请求次数$limit$annotation-limit;$period$annotation-period;// 用Redis INCR EXPIRE 实现滑动计数$current$this-redis-incr($redisKey);// 计数1if($current1){// 第一次请求设置过期时间$this-redis-expire($redisKey,$period);}if($current$limit){// 超过限制拒绝请求$this-logger-warning(触发限流,[key$redisKey,current$current,limit$limit,]);// 抛出限流异常由异常处理器统一返回错误响应thrownew\RuntimeException($annotation-message,429);}// 没超限制放行执行原始方法return$proceedingJoinPoint-process();}}// app/Controller/ApiController.php 使用注解 namespaceApp\Controller;useApp\Annotation\RateLimit;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\GetMapping;useHyperf\HttpServer\Annotation\PostMapping;#[Controller(prefix:/api)]classApiController{/** * GET /api/products - 查询商品列表 * 打上注解每分钟最多200次超了返回429 */#[GetMapping(path:/products)]#[RateLimit(limit:200,period:60)]publicfunctionproducts():array{return[code0,data[[id1,name商品A]]];}/** * POST /api/order - 下单接口限制更严每分钟最多10次 */#[PostMapping(path:/order)]#[RateLimit(limit:10,period:60,message:下单太频繁了请1分钟后再试)]publicfunctioncreateOrder():array{return[code0,msg下单成功];}/** * POST /api/sms - 发短信每天最多5次用自定义key防止换IP绕过 */#[PostMapping(path:/sms)]#[RateLimit(limit:5,period:86400,key:sms_send)]publicfunctionsendSms():array{return[code0,msg短信已发送];}}// app/Exception/Handler/RateLimitExceptionHandler.php namespaceApp\Exception\Handler;useHyperf\ExceptionHandler\ExceptionHandler;useHyperf\HttpMessage\Stream\SwooleStream;usePsr\Http\Message\ResponseInterface;useThrowable;/** * 捕获限流异常统一返回标准JSON格式 */classRateLimitExceptionHandlerextendsExceptionHandler{publicfunctionhandle(Throwable$throwable,ResponseInterface$response):ResponseInterface{$bodyjson_encode([code429,msg$throwable-getMessage(),],JSON_UNESCAPED_UNICODE);return$response-withStatus(429)-withHeader(Content-Type,application/json; charsetutf-8)-withBody(newSwooleStream($body));}publicfunctionisValid(Throwable$throwable):bool{return$throwableinstanceof\RuntimeException$throwable-getCode()429;}}