利用SpringAOP实现单实例系统防重复提交

本文介绍了如何使用SpringAOP来防止单实例系统中的重复提交问题,确保系统健壮性。首先,阐述了实现思路,即记录首次请求并在后续请求中检查是否在限制时间内。接着,提到了需要引入的jar包,特别是用于支持AOP编程和客户端访问标识缓存的库。然后,自定义了一个注解标记需要防重复提交的接口,并展示了如何通过AOP拦截器来实现这一功能。最后,进行了测试,展示在5秒内重复请求会被拦截并返回特定异常。
摘要由CSDN通过智能技术生成

        如果没有防重复提交,当用户在做一个新增操作时,多次点击新增按钮,那么会在数据库生成多条一模一样的数据。前端处理的方式就是,当用户点击新增后,禁用按钮,直到服务端响应成功。有句话说得好,作为一个服务端开发人员,不能相信客户端的任何输入。所以,一个健壮的系统,不仅要有前端校验,服务端校验更是必不可少。

实现思路

1、当客户端发起第一次请求,记录下该次请求。

2、当客户端发起第二次请求的时候,校验上次请求是否在指定的限制重复请求时间内。如果在,抛出指定的异常;如果不在,则放行请求。

引入jar包

说明:本示例基于springboot

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 该jar包主要作用是支持aop编程

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <!--使用最新版本即可-->
    <version>last-version</version>
</dependency>

该jar包主要用来作客户端访问标识的缓存工具。如果要用于集群系统,使用redis

自定义注解

针对于读操作的接口,不管如何请求,都不会对数据产生任何改变。所以,在一个系统中,不可能一刀切的对所有接口进行防重复提交,这里就自定义一个注解,用于标识需要防重复提交的接口。

// 指定当前注解只能用于方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SubmitLock {

	/**
	 * 对接口指定唯一标识
	 * */
	String key();
	
}

利用aop编写拦截

@Aspect
@Configuration
public class SubmitLockInterceptor {

	// 声明缓存服务:利用本地缓存,省去网络传输
	private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
			// 最大缓存 1000个
			.maximumSize(1000)
			// 设置缓存5 秒钟过期:即5s内禁止重复提交
			.expireAfterWrite(5, TimeUnit.SECONDS).build();

	/**
	 * 切入目标:公共方法且带有@SubmitLock注解
	 */
	@Around("execution(public * *(..)) && @annotation(com.zepal.lock.annotation.SubmitLock)")
	public Object interceptor(ProceedingJoinPoint pjp) {
		MethodSignature signature = (MethodSignature) pjp.getSignature();
		// 获取当前执行方法
		Method method = signature.getMethod();
		// 获取当前方法的@SubmitLock注解
		SubmitLock submitLock = method.getAnnotation(SubmitLock.class);

		RequestAttributes ra = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes sra = (ServletRequestAttributes) ra;
		HttpServletRequest request = sra.getRequest();

		// 获取当前登录用户的唯一标识(用户标识的参数名可能不尽相同)
		String token = request.getHeader("token");
		if (!StringUtils.hasText(token)) {
			token = request.getParameter("token");
		}
		if (!StringUtils.hasText(token)) {
			// 客户端未传递token参数
			throw new RuntimeException("参数:token不能为空");
		}
		// 利用token参数生成唯一key
		String key = submitLock.key() + token;
		if (CACHES.getIfPresent(key) != null) {
			// 利用double check lock,保证线程安全
			synchronized (this) {
				if (CACHES.getIfPresent(key) != null) {
					// 缓存中存在相应的token
					throw new RuntimeException("请勿重复请求");
				}
			}
		}
		// 如果是第一次请求,就将当前请求唯一标识压入缓存中
		CACHES.put(key, key);
		try {
			// 回调目标方法(即放行请求后执行原有的业务逻辑)
			return pjp.proceed();
		} catch (Throwable throwable) {
			// 被aop代理的目标方法发生了异常(当然是程序出bug了)
			throw new RuntimeException("系统异常:" + throwable.getMessage(), throwable);
		}
	}
}

测试

@Controller
public class TestController {

	// 指定@SubmitLock的标识要保证整个系统唯一,否则会导致多个不同功能的接口被防重复拦截
	@SubmitLock(key = "submitLockTest")
	@PostMapping("/submitLockTest")
	@ResponseBody
	public String submitLockTest(String token) {
		
		return "success";
	}
	
}

5s内重复对目标接口发起请求,会响应以下异常(项目中,这里可以使用全局异常拦截将异常信息处理得更加优雅)

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTM4MTg2Mw==,size_16,color_FFFFFF,t_70

 

 

 

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值