基于SpringBoot,Redis分布式幂等完整代码。可直接用

项目没有,自己整一个,废话不多,直接写。

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 等各种方法只要能拿到参数,都可以。 

做个记录,以后直接搬,有问题请自行查询解决。 不是最完整版,但也可以用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值