本文项目基于以下教程的代码版本: https://javaxbfs.blog.csdn.net/article/details/135224261
代码仓库: springboot一些案例的整合_1: springboot一些案例的整合
我们需要redis、aop的依赖。
<dependency>
????<groupId>org.springframework.boot</groupId>
????<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
????<groupId>org.aspectj</groupId>
????<artifactId>aspectjrt</artifactId>
????<version>1.9.6</version>
</dependency>
<dependency>
????<groupId>org.aspectj</groupId>
????<artifactId>aspectjweaver</artifactId>
????<version>1.9.6</version>
</dependency>
redis的访问参数,默认的就行,不需要特别配置。
我们最终希望的效果是,你想要哪个方法有防止重复提交的功能,直接加上@RepeatSubmit
注解即可。 代码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public?@interface?RepeatSubmit?{
????/**
?????*?加锁过期时间,默认是5秒
?????*?@return
?????*/
????long?lockTime()?default?5L;
}
这段代码定义了一个Java注解(Annotation)叫做RepeatSubmit。注解是Java提供的一种元数据机制,它可以被用于为代码提供附加的信息,这些信息可以被编译器用于生成代码、生成文档、代码检查等。
下面是对这段代码的详细解释:
@Target(ElementType.METHOD)
: 这个注解指定RepeatSubmit只能被用于方法上。ElementType.METHOD表示这个注解只能用于方法。
@Retention(RetentionPolicy.RUNTIME)
: 这个注解指定了RepeatSubmit的保留策略是RUNTIME。这意味着在运行时,这个注解仍然可以被读取。
@Documented
: 这个注解表明RepeatSubmit应当被Java文档生成器包含在生成文档中。
public @interface RepeatSubmit
: 定义了一个名为RepeatSubmit的公共注解。
long lockTime() default 5L
: 这是RepeatSubmit注解的一个元素,名为lockTime。这个元素返回一个长整型值,并且有一个默认值5秒(5L表示5秒)。
这个注解是为了防止方法的重复提交,并提供了一个默认的加锁过期时间为5秒。如果一个方法被这个注解标记,那么在特定的加锁过期时间内,这个方法只会被执行一次。
关于Spring Aop切面,可以看我之前的文章。
NoRepeatSubmitAspect
package?com.it.demo.aspect;
import?cn.hutool.core.util.StrUtil;
import?cn.hutool.crypto.digest.DigestUtil;
import?com.it.demo.annotation.RepeatSubmit;
import?lombok.extern.slf4j.Slf4j;
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.springframework.data.redis.core.RedisTemplate;
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.HttpServletRequest;
import?java.util.Objects;
import?java.util.concurrent.TimeUnit;
@Component
@Slf4j
@Aspect
public?class?NoRepeatSubmitAspect?{
????@Resource
????private?RedisTemplate?redisTemplate;
????@Pointcut("@annotation(repeatSubmit)")
????public?void?pointCutNoRepeatSubmitAspect(RepeatSubmit?repeatSubmit)?{
????}
????@Around("pointCutNoRepeatSubmitAspect(repeatSubmit)")
????public?Object?around(ProceedingJoinPoint?joinPoint,RepeatSubmit?repeatSubmit)?throws?Throwable?{
????????String?reqSignature?=?buildReqSignature(joinPoint);
????????//加锁时间
????????long?lockTime?=?repeatSubmit.lockTime();
????????Boolean?res?=?redisTemplate.opsForValue().setIfAbsent(reqSignature,?1,?lockTime,?TimeUnit.SECONDS);
????????if(!res){
????????????throw?new?RuntimeException("重复请求!");
????????}
????????return?joinPoint.proceed();
????}
????/**
?????*?构建请求签名
?????*?@param?joinPoint
?????*?@return
?????*/
????private?String?buildReqSignature(ProceedingJoinPoint?joinPoint)?{
????????ServletRequestAttributes?requestAttributes?=?(ServletRequestAttributes)?RequestContextHolder.getRequestAttributes();
????????if(Objects.isNull(requestAttributes)){
????????????throw?new?RuntimeException("请求参数异常!");
????????}
????????HttpServletRequest?request?=?requestAttributes.getRequest();
????????String?remoteAddr?=request.getRemoteAddr()??+?":"?+??request.getRemoteHost();
????????String?method?=?request.getMethod();
????????String?requestURI?=?request.getRequestURI();
????????StringBuffer?sb?=?new?StringBuffer();
????????for?(Object?arg?:?joinPoint.getArgs())?{
????????????sb.append(arg);
????????}
????????//请求签名(MD5加密)
????????return?DigestUtil.md5Hex(StrUtil.format("{}-{}-{}-{}",remoteAddr,method,requestURI,sb));
????}
}
这段代码是一个切面(Aspect),用于处理重复提交的情况。以下是对代码中的主要部分的解释:
导入包及注解:代码首先导入了一些类,并使用了一些注解,例如 @Component
, @Aspect
, @Resource
, @Slf4j
等。
NoRepeatSubmitAspect 类:这是一个切面类,用于实现对重复提交的处理逻辑。
成员变量:代码中使用了 @Resource
注解注入了一个 RedisTemplate
对象,用于操作 Redis。
切入点:使用 @Pointcut
注解定义了一个切入点,这里使用了 @annotation(repeatSubmit)
表达式,表示会拦截所有标注了 @RepeatSubmit
注解的方法。
Around 通知:使用 @Around
注解定义了一个环绕通知,在拦截的方法执行前后会执行这里定义的逻辑。
around() 方法:在该方法中,首先调用了 buildReqSignature(joinPoint)
方法构建了请求的签名信息,然后根据 RepeatSubmit
注解中的配置,在 Redis 中设置了一个锁,以防止重复提交。如果设置锁失败,则抛出了一个运行时异常,表示重复请求,否则执行被拦截的方法。
buildReqSignature() 方法:该方法用于构建请求的签名信息,首先获取了请求相关的信息,然后将这些信息进行拼接,并使用 MD5 加密生成请求签名。
这段代码是一个使用 AOP 实现的防止重复提交的切面,在方法执行前通过生成请求签名并利用 Redis 进行锁定的方式,来避免重复提交。
环绕通知实现逻辑如下图所示
构建 Redis 加锁需要使用的 key,加锁key由指定前缀 + MD5(请求签名)组成,对请求签名进行MD5,一方面减少Key的长度,另一方面保证签名信息中的敏感信息不可见,其实现逻辑如下图所示:
/**
?*?构建请求签名
?*?@param?joinPoint
?*?@return
?*/
private?String?buildReqSignature(ProceedingJoinPoint?joinPoint)?{
????ServletRequestAttributes?requestAttributes?=?(ServletRequestAttributes)?RequestContextHolder.getRequestAttributes();
????if(Objects.isNull(requestAttributes)){
????????throw?new?RuntimeException("请求参数异常!");
????}
????HttpServletRequest?request?=?requestAttributes.getRequest();
????String?remoteAddr?=request.getRemoteAddr()??+?":"?+??request.getRemoteHost();
????String?method?=?request.getMethod();
????String?requestURI?=?request.getRequestURI();
????StringBuffer?sb?=?new?StringBuffer();
????for?(Object?arg?:?joinPoint.getArgs())?{
????????sb.append(arg);
????}
????//请求签名(MD5加密)
????return?DigestUtil.md5Hex(StrUtil.format("{}-{}-{}-{}",remoteAddr,method,requestURI,sb));
}
查询方法加一个@RepeatSubmit
@GetMapping("/list")
@RepeatSubmit
public?List<Device>?list(){
????List<Device>?list?=?deviceService.list();
????System.out.println(list);
????return?deviceService.list();
}
访问localhost:8080/v1/device/list
五秒内再访问,报错:
{
????"code":?400,
????"msg":?"重复请求!",
????"data":?null
}
验证通过。