【实践】SpringAOP + 自定义注解实现接口限流

3 篇文章 0 订阅

【实践】SpringAOP + 自定义注解实现接口限流 

目录

【实践】SpringAOP + 自定义注解实现接口限流 

前置情景:

实践背景:

实践过程:

总结:


前置情景:

        1. 对 SpringAOP 相关知识有一定理解              

        2. 对 自定义注解 相关知识有一定理解     

        3. 对 Redis + lua脚本 相关知识有一定了解

实践背景:

        现某 ToC 接口存在 QPS 远高于设计预期,但内部逻辑较为复杂无法快速进行迭代优化情况

        现需对此接口改造,同时为了方便其他接口后续接入同样限流功能,采用AOP+自定义注解+Redis实现

实践过程

        1. 准备redis

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;

import java.util.List;


@Slf4j
@Component
public class RedisComponent {

    protected JedisPool jedisPool;

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public RedisComponent(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    public Long eval(final String script, final List<String> keys, final List<String> args) {
        Object result = RedisCaller.call(this.jedisPool, (jedis) -> jedis.eval(script, keys, args));
        return Long.parseLong(String.valueOf(result));
    }
}

注:RedisCaller为公司内部工具类,本质还是为了执行 jedis.eval(),使用其他工具类平替即可

        2. 准备自定义注解 + 枚举

import java.lang.annotation.*;


@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RateLimit {

    /**
     * 场景prefix
     */
    RateLimiterPrefixEnum prefix() default RateLimiterPrefixEnum.DEFAULT;

    /**
     * 资源key
     */
    String key() default "";

    /**
     * 给定的时间段
     * 单位秒
     */
    int period() default 15;

    /**
     * 最多的访问限制次数
     */
    int count() default 5;

    /**
     * 类型
     */
    LimitType limitType() default LimitType.CUSTOMER;
}
import com.google.common.collect.Maps;

import java.util.Map;


public enum RateLimiterPrefixEnum {

    /**
     * 默认
     */
    DEFAULT(0, "default"),

    /**
     * TEST
     */
    TEST(1, "test");

    private final Integer code;

    private final String desc;

    private static Map<Integer, RateLimiterPrefixEnum> map;

    RateLimiterPrefixEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    static {
        map = Maps.newHashMap();
        for (RateLimiterPrefixEnum prefixEnum : RateLimiterPrefixEnum.values()) {
            map.put(prefixEnum.getCode(), prefixEnum);
        }
    }

    public Integer getCode() {
        return this.code;
    }

    public String getDesc() {
        return this.desc;
    }

    public static RateLimiterPrefixEnum parse(Integer code){
        return map.get(code);
    }
}
public enum LimitType {

    /**
     * 自定义key
     */
    CUSTOMER,

    /**
     * 根据请求者IP
     */
    IP;
}

        3. 准备切面

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Objects;


@Slf4j
@Aspect
@Component
@Order(value = 1)
public class RateLimiterAspect {

    private static final String UNKNOWN = "unknown";

    private static final String PERIOD = "period";

    private static final String COUNT = "count";

    private static final String REDIS_SCRIPT;

    private final RedisComponent redisComponent;

    @Value("${app.limit.config.map}")
    private String limitConfigMapJson;

    static {
        // 构建lua脚本
        REDIS_SCRIPT = "local c" +
                "\nc = redis.call('get', KEYS[1])" +
                // 调用不超过最大值,则直接返回
                "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
                "\nreturn c;" +
                "\nend" +
                // 执行计算器自加
                "\nc = redis.call('incr', KEYS[1])" +
                "\nif tonumber(c) == 1 then" +
                // 从第一次调用开始限流,设置对应键值的过期
                "\nredis.call('expire', KEYS[1], ARGV[2])" +
                "\nend" +
                "\nreturn c;";
    }

    public RateLimiterAspect(RedisComponent redisComponent) {
        this.redisComponent = redisComponent;
    }

    /**
     * 限流拦截器
     *
     * @param pjp       连接点
     * @return java.lang.Object
     */
    @Around("execution(public * a.b.c..*Controller.*(..)) && @annotation(自定义注解的全限定类名)")
    public Object limitInterceptor(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        RateLimit limit = method.getAnnotation(RateLimit.class);

        // 组装参数,优先取配置中对应场景的限流配置,没有就取接口上的兜底配置
        Map<String, Map<String, Integer>> configMap =
                new ObjectMapper().readValue(limitConfigMapJson, new TypeReference<Map<String, Map<String, Integer>>>(){});
        Map<String, Integer> periodCountMap = configMap.get(limit.prefix().getDesc() + "_" + limit.key());
        int limitPeriod = Objects.nonNull(periodCountMap) && Objects.nonNull(periodCountMap.get(PERIOD)) ?
                periodCountMap.get(PERIOD) : limit.period();
        int limitCount = Objects.nonNull(periodCountMap) && Objects.nonNull(periodCountMap.get(COUNT)) ?
                periodCountMap.get(COUNT) : limit.count();
        List<String> args = Lists.newArrayList(Integer.toString(limitCount), Integer.toString(limitPeriod));
        ImmutableList<String> keys = this.getLimitKey(limit);
        Long count = redisComponent.eval(REDIS_SCRIPT, keys, args);
        // 如果达到限流数则抛出异常
        if(count != null && count.intValue() <= limitCount) {
            return pjp.proceed();
        } else {
            throw new AppGatewayException(AppGatewayErrorCode.REQ_RATE_LIMIT_ERROR, "请求次数已达上限");
        }
    }

    /**
     * 获取各场景限流key
     *
     * @param limit     方法注解配置
     * @return com.google.common.collect.ImmutableList<java.lang.String>
     */
    private ImmutableList<String> getLimitKey(RateLimit limit) {
        String key = null;
        LimitType limitType = limit.limitType();
        switch (limitType) {
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                // 可以自定义限流
                key = limit.key();
                break;
            default:
                break;
        }
        return ImmutableList.of(StringUtils.join(limit.prefix(), key));
    }

    /**
     * 获取请求的ip
     *
     * @return 获取请求的ip
     */
    public String getIpAddress() {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String ip = null;
        //X-Forwarded-For:Squid 服务代理
        String ipAddresses = request.getHeader("X-Forwarded-For");

        if (ipAddresses == null || ipAddresses.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
            //Proxy-Client-IP:apache 服务代理
            ipAddresses = request.getHeader("Proxy-Client-IP");
        }

        if (ipAddresses == null || ipAddresses.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
            //WL-Proxy-Client-IP:weblogic 服务代理
            ipAddresses = request.getHeader("WL-Proxy-Client-IP");
        }

        if (ipAddresses == null || ipAddresses.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
            //HTTP_CLIENT_IP:有些代理服务器
            ipAddresses = request.getHeader("HTTP_CLIENT_IP");
        }

        if (ipAddresses == null || ipAddresses.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
            //X-Real-IP:nginx服务代理
            ipAddresses = request.getHeader("X-Real-IP");
        }

        //有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
        if (ipAddresses != null && ipAddresses.length() != 0) {
            ip = ipAddresses.split(",")[0];
        }

        //还是不能获取到,最后再通过request.getRemoteAddr();获取
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ipAddresses)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

注:

    1. 配置文件中需添加 app.limit.config.map = {"test_generate":{"period":管控时间,"count":上限次数}}

    2. 若出现 "error Type referred to is not an annotation type: xxx" 报错,请检查环绕通知中路径是否正确

        4. 在controller接口上增加自定义注解


import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;


@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @Resource
    private TestBizService testBizService;

    /**
     * test(增加流量管控)
     *
     * @param req       请求json
     * @return TestDTO
     */
    @NoSign
    @PostMapping("/generate")
    @ApiOperation(value = "generate")
    @RateLimit(prefix = RateLimiterPrefixEnum.TEST, key = "generate", limitType = LimitType.CUSTOMER)
    public TestDTO generate(@RequestPart TestRequest req) {
        try {
            return testBizService.generate(req);
        } catch (Exception e) {
            log.error("generate test error", e);
            return new TestDTO();
        }
    }
}

注:若出现 "getInputStream() has already been called for this request" 说明请求中数据已被获取过了,请确认是否有其他拦截器等配置,并自行百度解决

        5. 使用postman验证效果

总结

        需求其实很简单,但为了方便后续其他接口快速接入限流能力,还是使用了比较复杂但泛用的实现方式,正好修补一下自己的知识漏洞,也挺好的。

        愿这篇实践能对后来者有所帮助

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,我们需要定义一个自定义注解 `@RequiresPermissions`,用于标识需要授权访问的方法,例如: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiresPermissions { String[] value(); // 权限值 } ``` 然后,我们需要实现一个切面,用于拦截被 `@RequiresPermissions` 标识的方法,并进行权限校验,例如: ```java @Component @Aspect public class PermissionCheckAspect { @Autowired private AuthService authService; @Around("@annotation(requiresPermissions)") public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermissions requiresPermissions) throws Throwable { // 获取当前用户 User user = authService.getCurrentUser(); if (user == null) { throw new UnauthorizedException("用户未登录"); } // 获取当前用户的权限列表 List<String> permissions = authService.getUserPermissions(user); // 校验权限 for (String permission : requiresPermissions.value()) { if (!permissions.contains(permission)) { throw new ForbiddenException("没有访问权限:" + permission); } } // 执行目标方法 return joinPoint.proceed(); } } ``` 在切面中,我们首先通过 `AuthService` 获取当前用户及其权限列表,然后校验当前用户是否拥有被 `@RequiresPermissions` 标识的方法所需的所有权限,如果没有则抛出 `ForbiddenException` 异常,如果有则继续执行目标方法。 最后,我们需要在 Spring 配置文件中启用 AOP 自动代理,并扫描切面所在的包,例如: ```xml <aop:aspectj-autoproxy /> <context:component-scan base-package="com.example.aspect" /> ``` 这样,我们就通过 Spring AOP自定义注解模拟实现了类似 Shiro 权限校验的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值