Boot++:後端驗證防止重複提交

防止重複提交,主要是使用鎖的形式來處理,如果是單機部署,可以使用本地緩存鎖(Guava)即可,如果是分佈式部署,則需要使用分佈式鎖(可以使用zk分佈式鎖或者redis分佈式鎖),本文的分佈式鎖以redis分佈式鎖爲例。

一、本地鎖(Guava)

1、導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>21.0</version>
</dependency>   

2、自定義本地鎖註解

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
    String key() default "";
    //過期時間,使用本地緩存可以忽略,如果使用redis做緩存就需要
    int expire() default 5;
}

3、本地鎖註解實現

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Configuration
public class LockMethodInterceptor {
    //定義緩存,設置最大緩存數及過期日期
    private static final Cache<String,Object> CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build();

    @Around("execution(public * *(..))  && @annotation(com.example.demo.utils.LocalLock)")
    public Object interceptor(ProceedingJoinPoint joinPoint){
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LocalLock localLock = method.getAnnotation(LocalLock.class);
        String key = getKey(localLock.key(),joinPoint.getArgs());
        if(!StringUtils.isEmpty(key)){
            if(CACHE.getIfPresent(key) != null){
                throw new RuntimeException("請勿重複請求!");
            }
            CACHE.put(key,key);
        }
        try{
            return joinPoint.proceed();
        }catch (Throwable throwable){
            throw new RuntimeException("服務器異常");
        }finally {

        }
    }

    private String getKey(String keyExpress, Object[] args){
        for (int i = 0; i < args.length; i++) {
            keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
        }
        return keyExpress;
    }

}

4、控制層

@ResponseBody
@PostMapping(value ="/localLock")
@ApiOperation(value="重複提交驗證測試--使用本地緩存鎖")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
@LocalLock(key = "localLock:test:arg[0]")
public String localLock(String token){

    return "sucess====="+token;
}

二、Redis分佈式鎖

1、導入依賴

  導入aop依賴和redis依賴即可

2、配置

  配置redis連接信息即可

3、自定義分佈式鎖註解

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLock {
    //redis鎖前綴
    String prefix() default "";
    //redis鎖過期時間
    int expire() default 5;
    //redis鎖過期時間單位
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    //redis  key分隔符
    String delimiter() default ":";
}

4、自定義key規則註解

由於redis的key可能是多層級結構,例如 redistest:demo1:token:kkk這種形式,因此需要自定義key的規則。

import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
    String name() default "";
}

5、定義key生成策略接口

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Service;

public interface CacheKeyGenerator {
    //獲取AOP參數,生成指定緩存Key
    String getLockKey(ProceedingJoinPoint joinPoint);
}

6、定義key生成策略實現類

import com.example.demo.service.CacheKeyGenerator;
import com.example.demo.utils.CacheLock;
import com.example.demo.utils.CacheParam;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class CacheKeyGeneratorImp implements CacheKeyGenerator {
    @Override
    public String getLockKey(ProceedingJoinPoint joinPoint) {
        //獲取連接點的方法簽名對象
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //Method對象
        Method method = methodSignature.getMethod();
        //獲取Method對象上的註解對象
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);
        //獲取方法參數
        final Object[] args = joinPoint.getArgs();
        //獲取Method對象上所有的註解
        final Parameter[] parameters = method.getParameters();
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<parameters.length;i++){
            final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class);
            //如果屬性不是CacheParam註解,則不處理
            if(cacheParams == null){
                continue;
            }
            //如果屬性是CacheParam註解,則拼接 連接符(:)+ CacheParam
            sb.append(cacheLock.delimiter()).append(args[i]);
        }
        //如果方法上沒有加CacheParam註解
        if(StringUtils.isEmpty(sb.toString())){
            //獲取方法上的多個註解(爲什麼是兩層數組:因爲第二層數組是隻有一個元素的數組)
            final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            //循環註解
            for(int i=0;i<parameterAnnotations.length;i++){
                final Object object = args[i];
                //獲取註解類中所有的屬性字段
                final Field[] fields = object.getClass().getDeclaredFields();
                for(Field field : fields){
                    //判斷字段上是否有CacheParam註解
                    final CacheParam annotation = field.getAnnotation(CacheParam.class);
                    //如果沒有,跳過
                    if(annotation ==null){
                        continue;
                    }
                    //如果有,設置Accessible爲true(爲true時可以使用反射訪問私有變量,否則不能訪問私有變量)
                    field.setAccessible(true);
                    //如果屬性是CacheParam註解,則拼接 連接符(:)+ CacheParam
                    sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object));
                }
            }
        }
        //返回指定前綴的key
        return cacheLock.prefix() + sb.toString();
    }
}

7、分佈式註解實現

import com.example.demo.service.CacheKeyGenerator;
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.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

@Aspect
@Configuration
public class CacheLockMethodInterceptor {



    @Autowired
    public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){
        this.cacheKeyGenerator = cacheKeyGenerator;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private final StringRedisTemplate stringRedisTemplate;
    private final CacheKeyGenerator cacheKeyGenerator;

    @Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)")
    public Object interceptor(ProceedingJoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);
        if(StringUtils.isEmpty(cacheLock.prefix())){
            throw new RuntimeException("前綴不能爲空");
        }
        //獲取自定義key
        final String lockkey = cacheKeyGenerator.getLockKey(joinPoint);
        final Boolean success = stringRedisTemplate.execute(
                (RedisCallback<Boolean>) connection -> connection.set(lockkey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit())
                        , RedisStringCommands.SetOption.SET_IF_ABSENT));
        if (!success) {
            // TODO 按理來說 我們應該拋出一個自定義的 CacheLockException 異常;這裏偷下懶
            throw new RuntimeException("請勿重複請求");
        }
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            throw new RuntimeException("系統異常");
        }
    }
}

8、主函數調整

  主函數引入key生成策略

@Bean
public CacheKeyGenerator cacheKeyGenerator(){
     return new CacheKeyGeneratorImp();
}

9、Controller

@ResponseBody
@PostMapping(value ="/cacheLock")
@ApiOperation(value="重複提交驗證測試--使用redis鎖")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
//@CacheLock
@CacheLock()
public String cacheLock(String token){
    return "sucess====="+token;
}

@ResponseBody
@PostMapping(value ="/cacheLock1")
@ApiOperation(value="重複提交驗證測試--使用redis鎖")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
//@CacheLock
@CacheLock(prefix = "redisLock.test",expire = 20)
public String cacheLock1(String token){
    return "sucess====="+token;
}

@ResponseBody
@PostMapping(value ="/cacheLock2")
@ApiOperation(value="重複提交驗證測試--使用redis鎖")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
//@CacheLock
@CacheLock(prefix = "redisLock.test",expire = 20)
public String cacheLock2(@CacheParam(name = "token") String token){
    return "sucess====="+token;
}

 


人生無常大腸包小腸

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章