项目没有,自己整一个,废话不多,直接写。
1. 幂等注解定义
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* SpEL 表达式,用于生成幂等键
*/
String key();
/**
* 过期时间(单位:秒)
*/
int expire() default 3;
/**
* 错误提示信息
*/
String message() default "重复请求,请稍后再试";
/**
* HTTP 状态码
*/
int httpStatus() default 400;
/**
* 锁类型 默认可重入锁
*/
LockType lockType() default LockType.REENTRANT;
/**
* 加锁等待时间(秒),默认0表示不等待
*/
int waitTime() default 0;
/**
* 锁超时释放时间(秒),默认0表示使用过期时间
*/
int leaseTime() default 0;
enum LockType {
REENTRANT, FAIR, MULTI, READ, WRITE, SEMAPHORE
}
}
2.参数处理,防止从request里拿了后续接口拿不到
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 支持多次读取请求体的HttpServletRequest包装器
* 解决请求体只能读取一次的问题
*/
public class IdempotentRequestWrapper extends HttpServletRequestWrapper {
private byte[] cachedBody; // 缓存请求体内容
public IdempotentRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
}
/**
* 获取缓存的请求体内容(线程安全)
*/
public byte[] getCachedBody() throws IOException {
// 双重检查锁定确保线程安全
if (cachedBody == null) {
synchronized (this) {
if (cachedBody == null) {
// 从原始请求读取并缓存
cachedBody = readRequestBody(super.getInputStream());
}
}
}
return cachedBody;
}
/**
* 读取请求体到字节数组
*/
private byte[] readRequestBody(InputStream inputStream) throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 返回基于缓存的输入流(可多次读取)
return new CachedServletInputStream(getCachedBody());
}
/**
* 内部类 - 基于缓存的ServletInputStream实现
*/
private static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream source;
public CachedServletInputStream(byte[] body) {
this.source = new ByteArrayInputStream(body);
}
@Override
public int read() {
return source.read();
}
@Override
public int read(byte[] b, int off, int len) {
return source.read(b, off, len);
}
@Override
public boolean isFinished() {
return source.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException("ReadListener not supported");
}
}
}
3. 过滤器,参数包装到上面的warpper
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 过滤器:将HttpServletRequest包装为支持多次读取的IdempotentRequestWrapper
*/
public class IdempotentRequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 只处理HTTP请求
if (request instanceof HttpServletRequest) {
// 包装原始请求
HttpServletRequest httpRequest = (HttpServletRequest) request;
IdempotentRequestWrapper wrappedRequest = new IdempotentRequestWrapper(httpRequest);
// 继续过滤器链(使用包装后的请求)
chain.doFilter(wrappedRequest, response);
} else {
chain.doFilter(request, response);
}
}
}
4.过滤器filter,幂等注册,接口优先级处理
import com.bitsun.ops.pos.api.InterceptorAdapter.IdempotentInterceptor;
import com.bitsun.ops.pos.api.filter.IdempotentRequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<IdempotentRequestFilter> idempotentRequestFilter() {
FilterRegistrationBean<IdempotentRequestFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new IdempotentRequestFilter());
registrationBean.addUrlPatterns("/*");
// 设置最高优先级,确保在其他过滤器之前执行
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
/**
* 注册幂等拦截器
*/
@Bean
public IdempotentInterceptor idempotentInterceptor(RedisTemplate<String, Object> redisTemplate) {
return new IdempotentInterceptor(redisTemplate);
}
}
5. 拦截器 核心方法
import com.bitsun.core.common.exception.AppException;
import com.bitsun.ops.pos.api.annotation.Idempotent;
import com.bitsun.ops.pos.api.filter.IdempotentRequestWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@Configuration
public class IdempotentInterceptor implements HandlerInterceptor, WebMvcConfigurer {
private static final String IDEMPOTENT_PREFIX = "idempotent:";
private final RedisTemplate<String, Object> redisTemplate;
// Spel表达式解析器
private final ExpressionParser parser = new SpelExpressionParser();
// 参数名解析器
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
private final ObjectMapper objectMapper = new ObjectMapper();
public IdempotentInterceptor(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this)
.addPathPatterns("/**")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/swagger-ui.html/**")
.order(Ordered.HIGHEST_PRECEDENCE + 1); // 确保在过滤器之后执行
}
/**
* 请求处理前 - 幂等检查核心逻辑
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 仅处理控制器方法
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Idempotent idempotent = AnnotationUtils.findAnnotation(method, Idempotent.class);
// 没有注解的方法直接放行
if (idempotent == null) {
return true;
}
try {
IdempotentRequestWrapper requestWrapper = new IdempotentRequestWrapper(request);
// 生成幂等键
String idempotentKey = generateIdempotentKey(idempotent, requestWrapper, method);
log.info("生成幂等键: {}", idempotentKey);
// 原子性幂等检查(SETNX操作)
boolean isNew = redisTemplate.opsForValue().setIfAbsent(idempotentKey, "1", idempotent.expire(), TimeUnit.SECONDS);
if (!isNew) {
throw new AppException(idempotent.message());
}
return true;
} catch (AppException e) {
throw e;
} catch (Exception e) {
log.error("幂等处理异常", e);
throw new AppException("幂等处理失败: " + e.getMessage(), String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR));
}
}
/**
* 生成幂等键(核心方法)
*/
private String generateIdempotentKey(Idempotent idempotent,
HttpServletRequest request,
Method method) throws Exception {
// 创建评估上下文
EvaluationContext context = createEvaluationContext(request, method);
// 解析SpEL表达式
Object keyValue = parser.parseExpression(idempotent.key()).getValue(context, Object.class);
// 键不能为空
if (keyValue == null) {
throw new IllegalArgumentException("幂等键表达式结果为空");
}
// 转换为字符串键
String keyString = convertKeyToString(keyValue);
// 构建最终键(确保唯一性)
return IDEMPOTENT_PREFIX + request.getMethod() + ":" + request.getRequestURI() + ":" + method.getName() + ":" + keyString;
}
/**
* 将键值转换为字符串表示
*/
private String convertKeyToString(Object keyValue) {
if (keyValue instanceof String) {
return (String) keyValue;
} else if (keyValue instanceof Number) {
return String.valueOf(keyValue);
} else if (keyValue.getClass().isArray()) {
return "array:" + Arrays.hashCode((Object[]) keyValue);
} else if (keyValue instanceof Collection) {
return "collection:" + ((Collection<?>) keyValue).hashCode();
} else if (keyValue instanceof Map) {
return "map:" + ((Map<?, ?>) keyValue).hashCode();
} else {
// 使用对象的唯一标识
return keyValue.getClass().getSimpleName() + "@" + System.identityHashCode(keyValue);
}
}
/**
* 创建SpEL评估上下文
*/
private EvaluationContext createEvaluationContext(HttpServletRequest request,
Method method) throws Exception {
StandardEvaluationContext context = new StandardEvaluationContext();
// 添加请求相关变量
context.setVariable("request", request);
// 添加请求头相关变量
context.setVariable("headers", extractHeaders(request));
//方法名称
context.setVariable("method", request.getMethod());
//请求路径
context.setVariable("path", request.getRequestURI());
//请求参数
context.setVariable("params", request.getParameterMap());
// 添加方法参数(非请求体)
Map<String, Object> parameters = parseMethodParameters(request, method);
if (!parameters.isEmpty()) {
context.setVariables(parameters);
}
// JSON和map 类型
Object requestBody = parseRequestBody(request, method);
if (requestBody != null) {
context.setVariable("body", requestBody);
context.setVariable("data", requestBody);
}
return context;
}
/**
* 解析方法参数(路径参数和查询参数)
*/
private Map<String, Object> parseMethodParameters(HttpServletRequest request,
Method method) {
Map<String, Object> parameters = new HashMap<>();
// 添加所有查询参数
request.getParameterMap().forEach((key, values) -> {
if (values.length > 0) {
parameters.put(key, values.length == 1 ? values[0] : values);
}
});
// 添加路径参数(Spring MVC会将路径参数放入request属性)
String[] paramNames = nameDiscoverer.getParameterNames(method);
if (paramNames != null) {
for (String name : paramNames) {
Object value = request.getAttribute(name);
if (value != null) {
parameters.put(name, value);
}
}
}
return parameters;
}
/**
* 安全解析请求体(使用缓存内容)
*/
private Object parseRequestBody(HttpServletRequest request,
Method method) {
// 只处理包装后的请求
if (!(request instanceof IdempotentRequestWrapper)) {
return null;
}
IdempotentRequestWrapper wrappedRequest = (IdempotentRequestWrapper) request;
try {
byte[] body = wrappedRequest.getCachedBody();
if (body == null || body.length == 0) {
return null;
}
// 只处理JSON内容
String contentType = request.getContentType();
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
return null;
}
// 检查是否有@RequestBody注解
for (Parameter param : method.getParameters()) {
if (param.isAnnotationPresent(RequestBody.class)) {
return parseBodyAsType(body, param.getType());
}
}
// 默认解析为Map
return objectMapper.readValue(body, Map.class);
} catch (IOException e) {
log.warn("请求体解析失败", e);
return null;
}
}
/**
* 按目标类型解析请求体
*/
private Object parseBodyAsType(byte[] body, Class<?> targetType) throws IOException {
if (targetType == byte[].class) {
return body;
} else if (targetType == String.class) {
return new String(body);
} else if (targetType == Map.class) {
return objectMapper.readValue(body, Map.class);
} else if (targetType == List.class) {
return objectMapper.readValue(body, List.class);
} else {
// 尝试反序列化为目标类型
return objectMapper.readValue(body, targetType);
}
}
/**
* 提取请求头
*/
private Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, request.getHeader(name));
}
return headers;
}
}
6. 使用
/**
* SPEL表达式
* 具体参数存在:com/bitsun/ops/pos/api/InterceptorAdapter/IdempotentInterceptor.java:114
* JSON请求 都从 body.字段名称获取
* 对象模式
* 用 body.字段名称得到参数值
*
* @param request
* @return
*/
@PostMapping("/testByJson")
@Idempotent(key = "#body.orgId", expire = 20, message = "请勿重复提交")
public ResultDTO<> putOrder(@RequestBody @ApiParam("请求参数") ReqDto request) {
return ResultDTO.ok();
}
/**
* SPEL表达式
* 多组合模式:@Idempotent(key = "#data.get('userId') + ':' + #data.get('productId')")
* Map模式 @Idempotent(key = "#data.get('userId') + ':' + #data.get('productId')") 必须为data.get(‘key名称’)
*
* @param data
* @return
*/
@PostMapping("/testByMap")
@Idempotent(key = "#data.get('orgId')", expire = 20, message = "请勿重复提交")
public ResultDTO<> testMap(@RequestBody Map<String, Object> data) {
return ResultDTO.ok();
}
/**
* SPEL表达式
* get方法幂等 直接拿参数作为幂等键, 多个参数 + 号拼接,当做字符串去处理即可。 也可toString()方法
*
* @param orgId
* @param shopCode
* @return
*/
@GetMapping("/testByGet")
@Idempotent(key = "#orgId + #shopCode", expire = 5)
public ResultDTO<> testGet(@RequestParam("orgId") String orgId, @RequestParam("shopCode") String shopCode) {
return ResultDTO.ok();
}
/**
* list参数模式
*/
@PostMapping("/testByList")
@Idempotent(key = "#body[0].get('orgId')", expire = 5, message = "请勿重复提交")
public ResultDTO<> testPost(@RequestBody List<ReqDto> reqDtos) {
return ResultDTO.ok();
}
/**
* 请求头里的参数
* request.getHeader("参数")
*
* @param
*/
@PostMapping("/testByHeader")
@Idempotent(key = "#request.getHeader('x-1-id')", expire = 5, message = "请勿重复提交")
public ResultDTO<> testByHeader() {
return ResultDTO.ok();
}
7. 其他
支持GET,POS,PATCH 等各种方法只要能拿到参数,都可以。
做个记录,以后直接搬,有问题请自行查询解决。 不是最完整版,但也可以用了。