基于切面的访问次数限制

博客介绍了对某些接口限制用户获取验证码次数的需求,如限制用户10分钟内最多获取5次验证码,并提及了实现相关内容,但未给出具体实现细节。

对一个某些接口,比如获取验证码接口想限制用户10分钟内最多获取5次验证码。实现如下:
AccessLimit.java

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 接口防刷注解(访问限制)
 *
 * @author redreamer
 */
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {

    /**
     * 限制的时间长度
     */
    int timeLength();

    /**
     * 限制的时间长度单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 最大访问次数
     */
    int maxCount();

    /**
     * 唯一标识的参数名:作为唯一的条件
     */
    String keyArgName() default "";

    /**
     * 超限提示语
     */
    String message() default "";
}

AccessLimitAop.java

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 防刷切面实现类
 *
 * @author redreamer
 */
@Slf4j
@Aspect
@Component
public class AccessLimitAop {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 切入点
     */
    @Pointcut("@annotation(mypackage.accesslimit.AccessLimit)")
    public void pointcut() {
    }


    /**
     * 处理前
     */
    @Before("pointcut()")
    public void joinPoint(JoinPoint joinPoint) throws Exception {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = joinPoint.getTarget().getClass().getMethod(methodSignature.getName(),
                methodSignature.getParameterTypes());

        AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
        String methodFullName = method.getDeclaringClass().getName() + "." + method.getName();
        String argName = accessLimit.keyArgName();

        String paramValue = "";
        String[] parameterNames = ((CodeSignature) joinPoint.getStaticPart().getSignature()).getParameterNames();
        for (int i = 0; i < parameterNames.length; i++) {
            String parameterName = parameterNames[i];
            if (argName.equals(parameterName)) {
                paramValue = (String) joinPoint.getArgs()[i];
                break;
            }
        }

		// 可能参数为中文
		String base64Str = toBase64String(paramValue);
        int timeLength = accessLimit.timeLength();
        int maxCount = accessLimit.maxCount();
        TimeUnit timeUnit = accessLimit.timeUnit();
        String message = accessLimit.message();

        String key = methodFullName + base64Str;

        int count = Optional.ofNullable(redisTemplate.boundValueOps(key).get()).map(Integer::valueOf).orElse(0);
        if (count <= maxCount) {
            redisTemplate.boundValueOps(key).set(String.valueOf(count + 1), timeLength, timeUnit);
        } else {
            log.warn("访问过于频繁:{} - {}", methodFullName, paramValue);
            message = StringUtils.isBlank(message) ? "您的访问过于频繁,请稍后再试!" : message;
            throw new ServiceException(message);
        }
        
    }

    /**
     * 对象转换为base64字符串
     *
     * @param paramValue 参数值
     * @return base64字符串
     */
    private String toBase64String(String paramValue) throws Exception {
        if (StringUtils.isEmpty(paramValue)) {
            return null;
        }
        Base64.Encoder encoder = Base64.getEncoder();
        byte[] bytes = paramValue.getBytes(StandardCharsets.UTF_8);
        return encoder.encodeToString(bytes);
    }

}

使用示例:

@ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "p1", value = "参数1"),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "p2", value = "参数2"),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "phoneNumber", value = "手机号码")
    })
@ApiOperation(value = "获取手机验证码")
@AccessLimit(timeLength = 10, timeUnit = TimeUnit.MINUTES, maxCount = 5, keyArgName = "phoneNumber")
@GetMapping("/code")
public ResponseEntity getCheckCode(@RequestParam(value = "p1") String p1,
                                   @RequestParam(value = "p2") String p2,
                                   @Mobile @RequestParam("phoneNumber") String phoneNumber) {
    String code = checkCodeService.sendSms(CountryCodeEnum.CN, phoneNumber, 6);
    return ResponseEntity.ok(ImmutableMap.of("code", code));
}
<think>首先,用户的问题是关于面向切面编程(AOP)中反射的使用方式。具体问题是:“我想了解面向切面编程中反射的使用方式 请问面向切面编程 AOP 如何使用反射实现”。 我需要参考提供的引用内容。引用包括: - [^1]: 定义了AOP,提到它是通过预编译方式和运行期动态代理实现的。 - [^2]: 讨论了一篇文章关于Spring AOP,包括动态代理实现。 - [^3]: 详细介绍了Spring AOP,包括动态代理的实现原理。 - [^4]: 解释了AOP的实现,提到横向抽取关注点。 用户的问题焦点是反射在AOP中的应用。反射是Java中的一个特性,允许在运行时检查和修改类、方法、字段等。 在AOP中,反射通常用于动态代理的实现。动态代理是AOP的核心技术之一,它允许在运行时创建代理对象来拦截方法调用。 从引用中: - [^2] 提到“动态代理方式实现AOP”。 - [^3] 详细描述了“动态代理”,并区分了JDK Proxy和CGLIB。 - [^4] 提到AOP通过动态代理实现。 反射在动态代理中的作用: - 在JDK动态代理中,使用`java.lang.reflect.Proxy`类,它依赖于反射来创建代理实例。 - 在CGLIB中,它使用字节码生成,但也可能间接使用反射。 具体到反射的使用: - 反射用于动态调用方法、获取方法信息等。 - 在AOP中,代理对象拦截方法调用时,使用反射来调用原始方法或应用通知。 我的回答需要: - 使用中文。 - 结构清晰,逐步解决问题。 - 保证回答真实可靠。 - 在行内数学表达式中使用`$...$`,但用户的问题中没有数学内容,所以可能不需要。 - 独立公式使用`$$...$$`并单独成段,但可能不适用。 - 生成相关问题。 - 在引用段落末尾添加引用标识。 回答结构: 1. **解释AOP和反射的基本概念**:简要回顾AOP是什么,以及反射是什么。 2. **描述AOP如何实现**:重点在动态代理中使用反射。 3. **具体反射的使用方式**:详细说明在动态代理中反射如何被应用。 4. **举例说明**:可能用伪代码或简单例子。 5. **生成相关问题**:在最后添加相关问题。 参考引用: - 从[^1]:AOP通过预编译方式和运行期动态代理实现。 - 从[^2]和[^3]:动态代理是AOP的实现方式。 - 从[^3]:Spring AOP使用JDK动态代理或CGLIB。 反射在JDK动态代理中的使用: - `Proxy.newProxyInstance()`方法使用反射来创建代理对象。 - `InvocationHandler`接口的`invoke`方法使用反射调用原始方法。 例如: ```java public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 在方法调用前执行通知(如日志) Object result = method.invoke(target, args); // 使用反射调用原始方法 // 在方法调用后执行通知 return result; } ``` 这里,`method.invoke(target, args)` 就是使用反射调用目标对象的方法。 在CGLIB中,它使用ASM库生成字节码,可能不直接使用反射,但原理类似。 确保回答真实:基于标准Java和Spring知识。 最后,生成相关问题。 添加引用:在回答中引用相关段落时,末尾添加[^n]。 例如,如果提到动态代理实现,可以引用[^2]或[^3]。 完整回答大纲: - 引言:重述问题,并简要回答。 - 主体:分步解释。 - 步骤1: AOP和反射概述。 - 步骤2: AOP实现机制:动态代理。 - 步骤3: 反射在动态代理中的具体使用。 - 步骤4: 示例(可选)。 - 结尾:总结。 - 相关问题。 语言:中文。</think>### 面向切面编程(AOP)中反射的使用方式 面向切面编程(AOP)是一种编程范式,旨在将横切关注点(如日志、事务、安全等)从业务逻辑中分离出来,以提高代码的可维护性和复用性。在AOP的实现中,反射(Reflection)扮演着关键角色,尤其是在动态代理机制中。反射允许程序在运行时动态地获取和操作类、方法、字段等信息,从而实现方法的拦截和增强。下面我将逐步解释AOP如何使用反射实现,确保内容真实可靠,并基于Spring AOP框架(Java生态中最常见的AOP实现)进行说明。 #### 步骤1: AOP和反射的基本概念回顾 - **AOP核心**:AOP通过“切面”(Aspect)来封装横切关注点。切面由“切点”(Pointcut,定义在何处拦截)和“通知”(Advice,定义拦截后执行的动作)组成。目标是减少代码侵入性,例如,在不修改业务代码的情况下添加日志[^1]。 - **反射的作用**:反射是Java语言的一个特性,它允许程序在运行时动态加载类、调用方法、访问字段等。在AOP中,反射用于动态创建代理对象、拦截方法调用,并执行通知逻辑。这使得AOP能在运行时“织入”(Weave)额外行为,而无需修改源代码[^3]。 #### 步骤2: AOP的实现机制:动态代理依赖反射 AOP的实现主要依赖动态代理(Dynamic Proxy),而动态代理的核心就是反射。动态代理分为两种方式: - **JDK动态代理**:基于Java的`java.lang.reflect.Proxy`类,适用于接口代理。它使用反射动态生成代理类实例。 - **CGLIB动态代理**:基于字节码生成库(如CGLIB),适用于类代理(即使目标类未实现接口)。它通过生成子类来代理,内部也间接使用反射来调用方法。 在Spring AOP中,默认优先使用JDK动态代理(如果目标对象实现了接口),否则回退到CGLIB。反射在这里用于: - 动态识别目标类的方法。 - 在运行时创建代理对象,拦截方法调用。 - 执行通知(如前置通知、后置通知)[^2][^3]。 #### 步骤3: 反射在动态代理中的具体使用方式 反射在AOP的动态代理中主要通过`InvocationHandler`(JDK代理)或`MethodInterceptor`(CGLIB代理)来实现。以下是详细步骤和代码示例: 1. **创建代理对象时使用反射**: - 在JDK动态代理中,`Proxy.newProxyInstance()`方法使用反射加载目标类的接口信息,并动态生成代理类。 - 示例代码(简化版,基于JDK代理): ```java import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JdkProxyExample { public static void main(String[] args) { // 目标对象(业务逻辑) UserService target = new UserServiceImpl(); // 创建代理对象:使用反射动态生成 UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 反射获取类加载器 target.getClass().getInterfaces(), // 反射获取接口数组 new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 使用反射调用方法前执行通知(如日志) System.out.println("前置通知: 方法 " + method.getName() + " 被调用"); // 反射调用原始方法:method.invoke() 使用反射执行目标方法 Object result = method.invoke(target, args); // 方法调用后执行通知 System.out.println("后置通知: 方法执行完成"); return result; } } ); // 调用代理方法,实际会触发反射逻辑 proxy.addUser(); } } ``` - 关键点:`method.invoke(target, args)` 通过反射调用目标对象的原始方法。`method` 对象是通过反射从目标类获取的,它封装了方法信息(如名称、参数类型)。这使得AOP能在运行时动态“插入”通知逻辑[^3][^4]。 2. **在通知中应用反射**: - 通知(Advice)如`@Before`或`@Around`注解的方法,在Spring AOP中会被代理拦截。反射用于: - 获取方法元数据:例如,通过`Method`对象获取方法名、参数等,用于日志或权限检查。 - 动态调用:在环绕通知中,反射允许灵活地控制方法执行(如跳过、修改参数或返回值)。 - 示例(Spring AOP 环绕通知): ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Around("execution(* com.example.service.*.*(..))") // 切点定义 public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable { // 使用反射获取方法信息 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); System.out.println("拦截方法: " + method.getName()); // 反射调用目标方法:joinPoint.proceed() 内部使用反射执行 Object result = joinPoint.proceed(); System.out.println("方法执行结果: " + result); return result; } } ``` - 这里,`ProceedingJoinPoint.proceed()` 底层通过反射调用目标方法,体现了AOP与反射的紧密集成[^2][^4]。 3. **反射在性能与安全方面的考虑**: - **性能开销**:反射调用比直接调用慢,因为涉及动态解析。在高性能场景中,Spring AOP通过缓存代理类或使用CGLIB(字节码增强)来优化,减少反射调用次数。 - **安全性**:反射可以绕过访问控制(如调用私有方法),因此AOP框架通常结合安全管理器(如Spring Security)来限制反射操作。 - 总体而言,反射使AOP实现“运行时织入”,而无需静态编译,但开发者需权衡灵活性与性能[^3][^4]。 #### 步骤4: 总结 在面向切面编程中,反射是实现动态代理的核心技术。它允许AOP框架在运行时动态创建代理对象、拦截方法调用,并执行通知逻辑。关键是通过反射的`Method.invoke()`来调用原始方法,实现业务逻辑与横切关注点的解耦。Spring AOP等框架优化了反射的使用,使其在大多数应用场景中高效可靠。如果您使用Java,可以结合Spring Boot轻松实践AOP的反射机制[^1][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值