文章目录
使用拦截器+Redis实现接口幂等
1.思路分析
接口幂等有很多种实现方式,拦截器/AOP+Redis,拦截器/AOP+本地缓存等等,本文讲解一下通过拦截器+Redis实现幂等的方式。
其原理就是在拦截器中拦截请求,然后根据一个标识符
去redis中查询是否已经存在,如果存在,则说明当前请求正在处理,抛出异常告诉前端请勿重复请求。
标识符:一般可以使用token+methodType+uri
作为标识符,具体业务具体分析。
2.具体实现
2.1 创建redis工具类
java
import com.yunling.sys.config.exception.ParamValidateException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类
*
* @author 谭永强
* @date 2023-08-15
*/
@Component
public class RedisUtils {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 写入缓存
*
* @param key 建
* @param value 值
* @return 成功/失败
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存设置时效时间
*
* @param key 键
* @param value 值
* @return 成功/失败
*/
public boolean set(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 判断缓存中是否有对应的value
*
* @param key 键
* @return 成功/失败
*/
public boolean exists(final String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 读取缓存
*
* @param key 键
* @return 成功/失败
*/
public Object get(final String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 删除对应的value
*
* @param key 键
* @return 成功/失败
*/
public boolean remove(final String key) {
if (exists(key)) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
return false;
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return 结果
*/
public Long incr(String key, long delta) {
if (ObjectUtils.isEmpty(key)) {
throw new ParamValidateException("key值不能为空");
}
if (delta < 0) {
throw new ParamValidateException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return 结果
*/
public Long decr(String key, long delta) {
if (ObjectUtils.isEmpty(key)) {
throw new ParamValidateException("key值不能为空");
}
if (delta < 0) {
throw new ParamValidateException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
}
2.2 自定义幂等注解
自定义幂等注解,将seconds
设置为该注解的属性,在拦截器中判断方法上是否有该注解,如果有该注解,则说明当前方法需要做幂等校验。
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自动幂等
* 该注解加在需要幂等的方法上,即可自动上线方法的幂等。
*
* @author 谭永强
* @date 2023-08-15
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
/**
* 限定时间(秒)
* 限制多少秒内,每个用户只能请求一次该接口。
*/
long seconds() default 1;
}
2.2 自定义幂等拦截器
定义幂等接口用于拦截处理请求。
java
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.yunling.sys.annotate.AutoIdempotent;
import com.yunling.sys.common.RedisUtils;
import com.yunling.sys.common.ResultData;
import com.yunling.sys.common.ReturnCode;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 自动幂等拦截器
*
* @author 谭永强
* @date 2023-08-15
*/
@Component
public class AutoIdempotentInterceptor extends HandlerInterceptorAdapter {
@Resource
private RedisUtils redisUtils;
/**
* @param request 请求
* @param response 响应
* @param handler 处理
* @return 结果
* @throws Exception 异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断请求是否为方法的请求
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod method = (HandlerMethod) handler;
//获取方法中是否有幂等性注解
AutoIdempotent anno = method.getMethodAnnotation(AutoIdempotent.class);
//若注解为空则直接返回
if (Objects.isNull(anno)) {
return true;
}
//限定时间
long seconds = anno.seconds();
//token
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (Objects.isNull(token)) {
ResultData<String> resultData = ResultData.fail(ReturnCode.ACCESS_DENIED.getCode(), "token不能为空");
write(response, JSON.toJSONString(resultData));
return false;
}
//此处转MD5的原因就是token长度太长了,转成md5短一些,此操作并不是必须的
String md5 = MD5Utils.md5Hex(token, StandardCharsets.UTF_8.toString());
//使用token+method+uri作为key值,此处需要通过key值确定请求的唯一性,也可以使用其他的组合,只要保证唯一性即可
String key = md5 + ":" + request.getMethod() + ":" + request.getRequestURI();
Object requestCountObj = redisUtils.get(key);
if (!ObjectUtils.isEmpty(requestCountObj)) {
//不为空,说明不是第一次请求,直接报错
ResultData<String> resultData = ResultData.fail(ReturnCode.RC206.getCode(), "请求已提交,请勿重复请求");
write(response, JSON.toJSONString(resultData));
return false;
}
//若为空则为第一次请求
return redisUtils.set(key, 1, seconds);
}
/**
* 返回结果到前端
*
* @param response 响应
* @param body 结果
* @throws IOException 异常
*/
private void write(HttpServletResponse response, String body) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ServletOutputStream os = response.getOutputStream();
os.write(body.getBytes());
os.flush();
os.close();
}
}
2.3 注入拦截器到容器
将拦截器注册到容器中。
java
package com.yunling.sys.config;
import com.yunling.sys.config.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* 将拦截器注入到容器中
*
* @author 谭永强
* @date 2023-08-15
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
}
}
3.测试
java
@RestController
@RequestMapping("user")
public class SysUserController {
/**
* 用户新增
*
* @param user 用户信息
*/
@AutoIdempotent(seconds = 60)
@PostMapping("add")
public void add(@RequestBody SysUser user) {
//业务代码.....
}
}
请求该接口,如果在60s内再次请求,就会返回重复请求的结果。seconds具体值设置多少由该接口的实际响应时间为标准,默认值为1秒。