spring项目aop实现接口防止连续点击锁

使用场景:

1,防止数据保存连续点击,保存多条信息

2,防止多次点击,非幂等接口业务代码重复执行

3,短信验证码功能

4,接口流量锁

5,登录频率控制

aop实现

        1,注解

        注解用于接口方法、接口参数、和请求实体的属性上。

package com.navigation.sys.config.aop;

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

/**
 * @author dll
 */
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLock {
    /** lock的key前缀,不填时用会用方法路径作为key前缀*/
    String prefix() default "";

    /** 服务名,非必填,多服务时用*/
    String serverName() default "";

    /**加锁失败,提示信息*/
    String msg() default "服务器忙,请等待片刻再操作。";

    /**是否需要解锁,不解锁可实现类似校验发送短信验证码的功能*/
    boolean unLock() default true;

    /**默认锁的过期时间60秒 */
    int timeOut() default 60000;

    /**在实体类参数上加属性 */
    String attrs() default "";

    /** 锁的value值 默认等于1为锁模式,大于1为流量控制模式 */
    int lockValue()  default 1;
}

        2,切面

import com.navigation.berth.common.util.LocalCache;
import com.navigation.common.core.exception.BizException;
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.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static java.util.stream.Collectors.toList;

@Aspect
@Component
public class ApiLockAspect {

    /**
     * 接口数据锁
     * 1,方法上存在ApiLock注解,没有key参数,用 package+类名+方法名 组合成一个lock的key
     * 2,方法上存在ApiLock注解,有key参数 ,用key做lock的key
     * 3,若参数有注解,且注解是基本类型或String,Number,则用1,2,的key+参数做lock的key
     * 4,若参数是实体类有注解且注解有attrs参数,则用1,2,的key+实体类attrs指定的属性 做lock的key
     * 5,若参数是实体类有注解,则用1,2,的key+实体类带注解的属性 做lock的key
     * 有 attrs 参数就不会在遍历属性,
     */

    final static String SEPARATOR = ":";

    @Around("@annotation(com.navigation.berth.aop.ApiLock)")
    public Object dataLockAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String msg = method.getAnnotation(ApiLock.class).msg();
        int lockTime = method.getAnnotation(ApiLock.class).lockTime();
        boolean unlock = method.getAnnotation(ApiLock.class).unlock();

        String lockKey = getLockKey(joinPoint);
        System.out.println("lockKey = " + lockKey);

        if (LocalCache.LOCK.containsKey(lockKey)) {
            throw new BizException(msg);
        }
        // 加锁  多服务时改成redis锁,
        try {
            LocalCache.lock(lockKey, "1", lockTime * 1000L);
            return joinPoint.proceed();
        } catch (Throwable e) {
            throw e;
        } finally {
            if (unlock) {
                LocalCache.unLock(lockKey);
            }
        }
    }

/**
     * 关键点:如何生成lock的key
     */
    private String getLockKey(ProceedingJoinPoint joinPoint) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        StringBuilder lockKey = new StringBuilder();
        // 获取方法上的ApiLock注解的key属性值
        String serverName = method.getAnnotation(ApiLock.class).serverName();
        String prefix = method.getAnnotation(ApiLock.class).prefix();
        if (!serverName.trim().isEmpty()) {
            lockKey.append(serverName).append(SEPARATOR);
        }
        if (!prefix.trim().isEmpty()) {
            lockKey.append(prefix);
        } else {
            // 获取方法所在的类名和方法名
            String className = signature.getDeclaringTypeName().replace(".", SEPARATOR);
            String methodName = method.getName();
            lockKey.append(className).append(SEPARATOR).append(methodName);
        }

        Object[] args = joinPoint.getArgs();
        Annotation[][] pa = method.getParameterAnnotations();
        for (int i = 0; i < args.length; i++) {
            for (Annotation annotation : pa[i]) {
                if (annotation instanceof ApiLock) {
                    // 参数是基本类型/包装类型/String,则只到这一层
                    if (args[i].getClass().isPrimitive() || args[i] instanceof String || args[i] instanceof Number
                            || args[i] instanceof Boolean || args[i] instanceof Character || args[i] instanceof Byte) {
                        lockKey.append(SEPARATOR).append(args[i]);
                        break;
                    }

                    // 实体类参数有attrs属性值,则用指定的attrs属性值做key
                    String attrs = ((ApiLock) annotation).attrs();
                    boolean flag = !attrs.isEmpty();
                    if (flag) {
                        List<String> list = Arrays.stream(attrs.split(",")).map(String::trim).map(this::capitalizeFirstLetter).collect(toList());
                        for (String field : list) {
                            Method getMethod  = args[i].getClass().getDeclaredMethod("get"+field);
                            getMethod.setAccessible(true);
                            Object result = getMethod.invoke(args[i]);
                            lockKey.append( SEPARATOR + result);
                        }
                        break;
                    }

                    // 实体类再遍历属性   fields 并不包含继承的属性
                    Field[] fields = args[i].getClass().getDeclaredFields();
                    for (Field field : fields) {
                        if (field.isAnnotationPresent(ApiLock.class)) {
                            lockKey.append(getFieldValue(field, args[i]));
                        }
                    }
                }
            }
        }
        return lockKey.toString();
    }

    String getFieldValue(Field field, Object obj) throws IllegalAccessException {
        field.setAccessible(true);
        Object value = field.get(obj);
        if (!Objects.isNull(value)) {
            return SEPARATOR + value.toString();
        }
        return "";
    }

    // 将字符串的首字母转换为大写的方法
    String capitalizeFirstLetter(String original) {
        if (original == null || original.isEmpty()) {
            return original;
        }
        return Character.toUpperCase(original.charAt(0)) + original.substring(1);
    }
}

       另一种方法getLockKey()

        这种方法可以找到继承的属性,但多个一个依赖项,上面的方法没有其他依赖但不会查找父类的属性(不过也可以用反射遍历父类的方法查找属性解决clazz.getSuperclass())。

        依赖了

     <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.4</version>
        </dependency>

    /**
     * 关键点:如何生成lock的key
     */
    private String getLockKey(ProceedingJoinPoint joinPoint) throws IllegalAccessException, InvocationTargetException, NoSuchFieldException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        StringBuilder lockKey = new StringBuilder();
        // 获取方法上的ApiLock注解的key属性值
        String serverName = method.getAnnotation(ApiLock.class).serverName();
        String prefix = method.getAnnotation(ApiLock.class).prefix();
        if (!serverName.trim().isEmpty()) {
            lockKey.append(serverName).append(SEPARATOR);
        }
        if (!prefix.trim().isEmpty()) {
            lockKey.append(prefix);
        } else {
            // 获取方法所在的类名和方法名
            String className = signature.getDeclaringTypeName().replace(".", SEPARATOR);
            String methodName = method.getName();
            lockKey.append(className).append(SEPARATOR).append(methodName);
        }

        Object[] args = joinPoint.getArgs();
        Annotation[][] pa = method.getParameterAnnotations();
        // 遍历方法参数
        for (int i = 0; i < args.length; i++) {
            //遍历方法参数注解
            for (Annotation annotation : pa[i]) {
                if (annotation instanceof ApiLock) {
                    // 参数是基本类型/包装类型/String,则只到这一层
                    if (args[i].getClass().isPrimitive() || args[i] instanceof String || args[i] instanceof Number
                            || args[i] instanceof Boolean || args[i] instanceof Character || args[i] instanceof Byte) {
                        lockKey.append(SEPARATOR).append(args[i]);
                        break;
                    }

                    PropertyDescriptor[] targetPds = getPropertyDescriptors(args[i].getClass());
                    // 实体类参数有attrs属性值,则用指定的attrs属性值做key
                    String attrs = ((ApiLock) annotation).attrs();
                    if (!attrs.isEmpty()) {
                        List<String> list = Arrays.stream(attrs.split(",")).collect(toList());
                        Map<String, PropertyDescriptor> propertyMap = Arrays.stream(targetPds).collect(Collectors.toMap(PropertyDescriptor::getName, pd -> pd));
                        for (String field : list) {
                            Method readMethod = propertyMap.get(field).getReadMethod();
                            readMethod.setAccessible(true);
                            Object obj = readMethod.invoke(args[i]);
                            lockKey.append(SEPARATOR).append(null == obj ? "" : obj);
                        }
                        break;
                    }

                    for (PropertyDescriptor pd : targetPds) {
                        // 如果属性仅由 getter 和 setter 方法定义,没有对应的字段,或者字段不在 getter 方法的声明类中,代码会抛出 NoSuchFieldException。
                        Field field = pd.getReadMethod().getDeclaringClass().getDeclaredField(pd.getName());
                        if (field.isAnnotationPresent(ApiLock.class)) {
                            field.setAccessible(true);
                            Object obj = field.get(args[i]);
                            lockKey.append(SEPARATOR).append(null == obj ? "" : obj);
                        }
                    }
                }
            }
        }
        return lockKey.toString();
    }
}

        3,hutool本地锁,可用redis锁替换

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


/**
 * @author dll
 */
public class LocalCache {
    // 超过一分钟缓存自动删除
    public static final TimedCache<String, String> LOCK = CacheUtil.newTimedCache(1000*60);

    static {
        /** 每100ms检查一次过期 */
        LOCK.schedulePrune(100);
    }

    public static void put(TimedCache<String, String>  cache,String key, String value, Long timeout) {
        /** 设置消逝时间 */
        cache.put(key, value, timeout);
    }

    static Lock rtLock = new ReentrantLock();
    public static boolean lock(String key, String value, Long timeout) {
        if (rtLock.tryLock()) {
            try {
                if (LOCK.containsKey(key)) {
                    return false;
                } else {
                    LocalCache.put(LOCK, key, "1", timeout);
                    return true;
                }
            } finally {
                rtLock.unlock();
            }
        } else {
            return false;
        }
    }

    public static void unLock(String key) {
        LOCK.remove(key);
    }

}

自定义异常处理

        1,自定义异常类

public class BizException extends RuntimeException {

    public BizException(String message) {
        super(message);
    }
}

        2,全局异常处理


import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 业务异常处理
     */
    @ExceptionHandler(BizException.class)
    public Result handleBizException(HttpServletRequest request, BizException ex) {
        log.error("请求地址URL: " + request.getRequestURL());
        log.error(SeExceptionUtils.getStackTrace(ex), ex);
        return Result.error(ex.getMessage());
    }
}

测试

        1,测试接口请求实体

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;


/**
 * @author dll
 */
@Data
@SuperBuilder
@NoArgsConstructor
@Accessors(chain = true)
public class MyQueryVO {
    @ApiLock
    private Integer abc;
    private String recordNo;
    @ApiLock
    private String deviceSn;
    @ApiLock
    private String spaceNo;
    @ApiLock
    private int num ;
    @ApiLock
    private boolean bool ;
}

        2,测试接口

import cn.hutool.core.thread.ThreadUtil;
import com.navigation.berth.aop.ApiLock;
import com.navigation.berth.aop.MyQueryVO;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("lock")
public class ApiLockTestController {

    final static String prefix = "test:api:list";

    @PostMapping("list/{id}/{b}")
    @ApiLock(prefix = prefix,msg="正在处理...")
    public String list(@PathVariable int id, @PathVariable @ApiLock boolean b, @RequestBody @ApiLock MyQueryVO vo) {
        ThreadUtil.sleep(10000);
        return "ok";
    }

    @GetMapping("{id}")
    @ApiLock(serverName = "testSystem")       // prefix没赋值会用方法路径赋值,prefix只在方法上有效 在参数和属性上没用
    public String get(@PathVariable @ApiLock Long id) {
        return "ok";
    }


    @ApiOperation(value = "发送验证码", notes = "60秒只发送一次")
    @GetMapping("sendCode/{phone}")
    @ApiLock(msg = "一分钟只能发送一次验证码",lockTime = 60,unlock = false)
    public String sendCode(@PathVariable @ApiLock String phone) {

        return "发送成功,注意查收。";
    }

}

        3,模拟连续操作

                第一次操作

                10秒内第二次操作

        4,测试

流量控制

        这里用的redis锁

        1,redis配置

RedisConfig
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis缓存配置
 */
@Slf4j
@Configuration
public class RedisConfig {
	public static final String redisTemplateBeanName = "redisTemplate";

	@Bean(redisTemplateBeanName)
//	@Bean
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
		redisTemplate.setConnectionFactory(connectionFactory);
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
		redisTemplate.setHashKeySerializer(new StringRedisSerializer());
		redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
		redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer());
		redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer());

		return redisTemplate;
	}

	// java8 时间
	private GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() {
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.registerModule(new JavaTimeModule());
		objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
		objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY);

		GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
		return new GenericJackson2JsonRedisSerializer(objectMapper);
	}

	private static ApplicationContext applicationContext;
	protected RedisConfig(ApplicationContext context) {
		RedisConfig.applicationContext = context;
	}
	public static <T> T getBean(Class<T> clazzType) {
		return applicationContext.getBean(clazzType);
	}
	public static <T> T getBean(String name, Class<T> requiredType) {
		return applicationContext.getBean(name, requiredType);
	}
}
SeRedisUtils

import com.xxx.RedisConfig;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;

import java.util.List;
import java.util.concurrent.TimeUnit;

public class SeRedisUtils {
    public static RedisTemplate RT = RedisConfig.getBean(RedisConfig.redisTemplateBeanName,
        RedisTemplate.class);

    /**
     * 设置指定 key 的值
     * @param key
     * @param value
     */
    public static void set(String key, Object value) {
        RT.opsForValue().set(key, value);
    }
    public static void set(String key, Object value, long timeout, TimeUnit unit) {
        RT.opsForValue().set(key, value, timeout, unit);
    }

    public static Boolean delete(String key) {
        return RT.delete(key);
    }

    /**
     * 获取指定 key 的值
     * @param key
     * @return
     */
    public static Object get(String key) {
        return RT.opsForValue().get(key);
    }
    public static String getStr(String key) {
        return (String) RT.opsForValue().get(key);
    }
    public static List<Object> executePipelined(SessionCallback<?> session) {
        return RT.executePipelined(session);
    }
    public static List<Object> executePipelined(RedisCallback<?> action) {
        return RT.executePipelined(action);
    }


    public static boolean lock (String key,Object value) {
        return RT.opsForValue().setIfAbsent(key, value);
    }

    public static boolean lock(String key, Object value, long timeOut) {
        return RT.opsForValue().setIfAbsent(key, value, timeOut, TimeUnit.SECONDS);
    }

    public static boolean lock (String key,Object value,long timeOut, TimeUnit unit) {
        return RT.opsForValue().setIfAbsent(key, value,  timeOut,  unit);
    }

    public static void unLock (String key) {
        delete(key);
    }

}

        2,ApiLockAspect.dataLockAdvice()

    @Around("@annotation(com.navigation.sys.config.aop.ApiLock)")
    public Object dataLockAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        String msg = method.getAnnotation(ApiLock.class).msg();
        int timeOut = method.getAnnotation(ApiLock.class).timeOut();
        boolean unLock = method.getAnnotation(ApiLock.class).unLock();
        int lockValue = method.getAnnotation(ApiLock.class).lockValue();

        String lockKey = getLockKey(joinPoint);
        log.info("lockKey = {}", lockKey);

        if (lockValue == 1) {
            if (!SeRedisUtils.lock(lockKey, lockValue, timeOut, TimeUnit.MILLISECONDS)) {
                // 加锁失败
                throw new BizException(msg);
            }
            // 加锁成功
            try {
                return joinPoint.proceed();
            } finally {
                if (unLock) {
                    SeRedisUtils.unLock(lockKey);
                }
            }
        } else if (lockValue > 1) {
            Object count = SeRedisUtils.get(lockKey);
            if (count == null) {
                // 第一次加锁  ;
                if (SeRedisUtils.lock(lockKey, lockValue, timeOut, TimeUnit.MILLISECONDS)) {
                    return joinPoint.proceed();
                }
            } else if ((int) count > 0) {
                // 锁已存在,value > 0, 减一操作;
                if (SeRedisUtils.RT.opsForValue().decrement(lockKey, 1L) > 0L) {
                    return joinPoint.proceed();
                }
                throw new BizException(msg);
            } else {
                throw new BizException(msg);
            }
        }
        return null;
    }

        3,测试

             接口

@Slf4j
@RestController
@RequestMapping("test")
public class ApiLockTestController {

    @Autowired
    ApplicationContext context;

    @GetMapping("testLockValue/{phone}/{password}")
    public String testLockValue(@PathVariable String phone, String password) {
        LocalDate now = LocalDateTimeUtil.now().toLocalDate();
        log.info("当前日期:{}", now);
        // 为了让spring注解生效 serviceLogin方法可以放到其他类里边,就不要从容器里边获调用方法了
        context.getBean(ApiLockTestController.class).serviceLogin(phone, password, now.toString());
        return "登录成功";
    }

    @ApiLock(prefix = "testLockValue", timeOut = 1000 * 3600 * 24, msg = "一天只能登录2次!", lockValue = 2, attrs = "phone,date")
    public void serviceLogin( String phone, String password, String date) {

    }
}

        第一,二次请求

        redis中的key,value

        当天大于第二次的请求

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring AOP可以使用AspectJ注解来实现接口重复调用的拦截。假设我们有一个接口`UserService`,其中有一个`saveUser`方法,我们希望在该方法被调用时进行拦截,防止重复调用。我们可以定义一个切面类`DuplicateInvokeAspect`,并为其添加一个`@Around`注解的方法,该方法将拦截`UserService`的`saveUser`方法,并在该方法中添加重复调用判断逻辑。 ``` @Aspect public class DuplicateInvokeAspect { private Map<String, Long> lastInvokeMap = new ConcurrentHashMap<>(); @Around("execution(* com.example.UserService.saveUser(..))") public Object checkDuplicateInvoke(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); if (lastInvokeMap.containsKey(methodName)) { long lastInvokeTime = lastInvokeMap.get(methodName); long currentTime = System.currentTimeMillis(); if (currentTime - lastInvokeTime < 5000) { // 限制5秒内不能重复调用 throw new RuntimeException("不能重复调用" + methodName); } } lastInvokeMap.put(methodName, System.currentTimeMillis()); return joinPoint.proceed(); } } ``` 在上述代码中,我们使用`ConcurrentHashMap`来存储方法名和上一次调用时间的映射关系。在`checkDuplicateInvoke`方法中,我们首先判断该方法是否已经在`lastInvokeMap`中存在,如果存在则判断距离上一次调用的时间是否已经超过了5秒,如果没有超过则抛出异常,否则将当前时间更新为上一次调用时间,并继续执行原有的方法逻辑。 最后,我们需要在Spring配置文件中启用该切面类: ``` <aop:aspectj-autoproxy/> <bean id="duplicateInvokeAspect" class="com.example.DuplicateInvokeAspect"/> ``` 这样就完成了接口重复调用的拦截功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值