记录一个自定义注解通过AOP拦截接口

需求场景

现有一个需求场景:

在需要的接口上防止用户在规定时间内重复提交导致业务短时间内多次进行。

思路

这里可能会用到多个接口上。

所以采用注解去拦截对应的控制器。

然后将接受的参数的中取唯一标识当做key保存标识到redis中,设置过期时间。

在执行业务前先通过key获取当前redis是否有值。

如果有,说明在规定时间内多次请求;如果没有,则继续执行方法。

执行

注解

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

    String type(); //redis的key类型
    String value(); //作为标识的参数值
    int expireSecond() default 5; //过期时间
}

这里加了枚举类型

package com.example.demo.dto;

import java.util.HashMap;
import java.util.Map;

public enum IdEnum {

    ORDER_ID("orderId","mall:cashOut:order:"),
    PROXY_ID("proxyId","mall:cashOut:proxy:");

    private String type;
    private String redisKey;

    IdEnum(String type, String redisKey) {
        this.type = type;
        this.redisKey = redisKey;
    }

    private static final Map<String, IdEnum> map = new HashMap<>();
    /**
     * 初始化把值存放到map对象
     */
    static {
        for (IdEnum idEnum : values()) {
            map.put(idEnum.getType(), idEnum);
        }
    }

    public static String getRedisK(String type) {
        return map.get(type).getRedisKey();
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getRedisKey() {
        return redisKey;
    }

    public void setRedisKey(String redisKey) {
        this.redisKey = redisKey;
    }
}

Aspect

切面类主要就是拦截的逻辑处理等等

package com.example.demo.annotation.aspect;


import com.example.demo.annotation.CommitLimit;
import com.example.demo.dto.IdEnum;
import com.example.demo.dto.Result;
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.annotation.Pointcut;

import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class CommitLimitAspect {

    @Resource
    private RedisTemplate redisTemplate;

    private static final String proxyIdKey = "PROXY:ID:" ;

    //这里需要注意了,这个是将自己自定义注解作为切点的根据,路径一定要写正确了
    @Pointcut(value = "@annotation(com.example.demo.annotation.CommitLimit)")
    public void access() {}
    
    @Around("@annotation(commitLimit)")
    public Object around(ProceedingJoinPoint point,CommitLimit commitLimit) throws Throwable {
        //注解参数标识,属于哪种id
        String type = commitLimit.type();
        //参数值id的值
        String value = commitLimit.value();
        //过期时间 默认5s
        int expireSecond = commitLimit.expireSecond();
        Object[] args = point.getArgs();
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = localVariableTable.getParameterNames(method);
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法参数放入SPEL上下文中
        for(int i=0;i<paraNameArr.length;i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        String id = null;
        // 使用变量方式传入业务动态数据
        if(value.matches("^#.*.$")) {
            id = parser.parseExpression(value).getValue(context, String.class);
        }

        //限制提交以及过期操作
        //通过type获取对应redis的key
        String redisKey = IdEnum.getRedisK(type) + id;
        String flag = (String) redisTemplate.opsForValue().get(redisKey);
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        if (!StringUtils.isBlank(flag)) {
            //返回前端 
            returnJson(response, Result.me().setMessage(expireSecond + "s内重复请求").setState(20003).toString());
            //打印信息 可以改成日志log打印
            System.out.println(expireSecond + "s内重复请求");
            //不继续执行
            return null;
        } else {
            //没有重复请求 保存并设置过期时间
            redisTemplate.opsForValue().set(redisKey,"falg",expireSecond, TimeUnit.SECONDS);
        }
        //继续执行方法
        return point.proceed();
    }

    public static void returnJson(HttpServletResponse response, String json) throws Exception {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(json);
        } catch (IOException e) {
        //  log.info(e.getMessage());
            System.out.println(e.getMessage());
        } finally {
            if (writer != null)
                writer.close();
        }
    }
}

控制器调用

编写两个测试接口

package com.example.demo.web;

import com.example.demo.annotation.CommitLimit;
import com.example.demo.dto.CashOut;
import com.example.demo.dto.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class TestController {

    @PostMapping("/orderId")
    @CommitLimit(type = "orderId",value = "#cashOut.orderId")
    public Result orderId(@RequestBody CashOut cashOut) {
        return Result.me().setData(cashOut).setMessage("成功");
    }

    @PostMapping("/proxyId")
    @CommitLimit(type = "proxyId",value = "#cashOut.proxyId",expireSecond = 3)
    public Result proxyId(@RequestBody CashOut cashOut) {
        return Result.me().setData(cashOut).setMessage("成功");
    }
}

测试

在idea里面测试接口

单次请求如下:

规定时间重复请求:

文章作者: 已删除用户
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yida
Back-end AOP Spring Spring Boot
喜欢就支持一下吧