接口幂等性设计-拦截器+过滤器+redis

接口幂等性设计-拦截器+过滤器+redis

所需依赖:

  <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

一、Idempotent-接口幂等性自定义注解

package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;

import java.lang.annotation.*;

/**
 * @Classname NoRepeatSubmit
 * @Description:接口幂等性自定义注解:使用拦截器+过滤器的方式实现
 * @Date: 2023/4/14 0014 14:38
 * @AUTHOR: 无泪之城
 * @Version 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {

    /**
     * 是否把 require 数据用来计算幂等key
     *
     * @return
     */
    boolean require() default false;

    /**
     * 参与幂等性计算的字段,默认所有字段
     */
    String[] values() default {};

    /**
     * 幂等性校验失效时间(毫秒)
     */
    long expiredTime() default 10000;
}

二、RequestWrapper

package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;

/**
 * @Classname RequestWrapper
 * @Description:HttpServletRequest实现类
 * 实现 HttpServletRequestWrapper ,将 request 中的 @RequestBody 取出来,
 * 并重写 getReader() 和 getInputStream(),
 * 重写 getInputStream() 方法时,
 * 将标志位还原,让系统无感知我们取过值;
 * @Date: 2023/4/14 0014 16:49
 * @AUTHOR: 无泪之城
 * @Version 1.0
 */
public class RequestWrapper extends HttpServletRequestWrapper {

    private String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {

        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        body = stringBuilder.toString();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), StandardCharsets.UTF_8));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
                body.getBytes(StandardCharsets.UTF_8));
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;
    }

    public String getBody() {
        return this.body;
    }
}

三、ResponseResultInterceptor

package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;

import com.alibaba.fastjson.JSONObject;
import com.hanjuzi.hotel.common.exception.RenException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Classname ResponseResultInterceptor
 * @Description:HandlerIntercepter的实现类
 * 这一步,做的就是幂等性校验的具体步骤;
 * 结合注解,把请求中的参数、IP,通过摘要加密的方式,生成一个唯一的key,保存到 redis 中,并设置过期时间,这样就可以在过期时间内,保证该请求指挥出现一次;
 * @Date: 2023/4/14 0014 16:50
 * @AUTHOR: 无泪之城
 * @Version 1.0
 */
@Slf4j
@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    private final static String REQUEST_URL = "url";


    /**
     * Controller逻辑执行之前 可以幂等性校验,防止重复提交
     * <p>
     * 注意:ServletRequest 中 getReader() 和 getInputStream() 只能调用一次,也就是 request 值取了一次,就无法再取
     *
     * @param request
     * @param response
     * @param handler
     * @return boolean
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        if (request instanceof RequestWrapper) {
            // 获取@RequestBody注解参数
            RequestWrapper requestWrapper = (RequestWrapper) request;
            String body = requestWrapper.getBody();
            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Class<?> clazz = handlerMethod.getBeanType();
                Method method = handlerMethod.getMethod();
                if (clazz.isAnnotationPresent(Idempotent.class)) {
                    Idempotent annotation = clazz.getAnnotation(Idempotent.class);
                    validUrl(body, request, annotation);
                } else if (method.isAnnotationPresent(Idempotent.class)) {
                    Idempotent annotation = method.getAnnotation(Idempotent.class);
                    validUrl(body, request, annotation);
                }
            }
        }
        return true;
    }

    /**
     * 校验url,重复提交
     *
     * @param body
     * @param request
     * @param annotation
     **/
    private void validUrl(String body, HttpServletRequest request, Idempotent annotation) {
        if (annotation.require()) {
            String[] values = annotation.values();
            Map<String, String[]> parameterMap = request.getParameterMap();
            JSONObject jsonObject = JSONObject.parseObject(body);
            jsonObject.put(REQUEST_URL, request.getRequestURL());
            jsonObject.putAll(parameterMap);
            Map<String, Object> stringObjectMap = sortByKey(jsonObject, values);
            // 摘要加密
            long expiredTime = annotation.expiredTime();
            Boolean bool = redisTemplate.opsForValue().setIfAbsent(stringObjectMap.toString(), "1", expiredTime, TimeUnit.MILLISECONDS);
            if (!bool){
                throw new RenException("提交太频繁了,请勿重复提交!");
            }
        }
    }

    /**
     * map 按 key 升序排序,只取 values 字段,values为空时,代表全部字段
     *
     * @param map
     * @param values
     */
    private Map<String, Object> sortByKey(Map<String, Object> map, String[] values) {
        Boolean bool = values.length < 1;
        Map<String, Object> result = new LinkedHashMap<>(map.size());
        map.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEachOrdered(e -> {
                    if (bool || isCheckKey(e.getKey(), values)) {
                        result.put(e.getKey(), e.getValue());
                    }
                });
        return result;
    }

    /**
     * 校验 key 是否存在 keys数组中
     *
     * @param key
     * @param keys
     * @return java.lang.Boolean
     **/
    private Boolean isCheckKey(String key, String[] keys) {
        for (String value : keys) {
            if (key.equals(value) || key.equals(REQUEST_URL)) {
                return true;
            }
        }
        return false;
    }
}

四、ResubmitFilter

package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;

/**
 * @Classname ResubmitFilter
 * @Description:Filter实现类
 *这一步的作用,就是拦截请求,将我们自定义的 Request 注入进去,避开 getReader() 和 getInputStream() 只能调用一次的情况,因为做拦截,只是取值校验,不能影响后面的实际业务;
 * @Date: 2023/4/14 0014 16:49
 * @AUTHOR: 无泪之城
 * @Version 1.0
 */
@Slf4j
public class ResubmitFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        RequestWrapper requestWrapper = null;
        try {
            HttpServletRequest req = (HttpServletRequest) request;
            // 排除GET请求,不做幂等性校验
            if (!HttpMethod.GET.name().equals(req.getMethod())) {
                requestWrapper = new RequestWrapper(req);
            }
        } catch (Exception e) {
            log.warn("RequestWrapper Error:", e);
        }
        chain.doFilter((Objects.isNull(requestWrapper) ? request : requestWrapper),
                response);
    }
}

五、WebConfigurer

package com.hanjuzi.hotel.common.utils.NoRepeatSubmit.annotate;

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.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * @Classname WebConfigurer
 * @Description:将过滤器、拦截器注入到系统
 * @Date: 2023/4/14 0014 17:01
 * @AUTHOR: 无泪之城
 * @Version 1.0
 */
@Configuration
public class WebConfigurer implements WebMvcConfigurer {

    @Resource
    private ResponseResultInterceptor responseResultInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加自定义拦截器
        registry.addInterceptor(responseResultInterceptor).addPathPatterns("/**");
    }

    @Bean
    public FilterRegistrationBean servletRegistrationBean() {
        //通过FilterRegistrationBean实例设置优先级可以生效
        ResubmitFilter resubmitFilter = new ResubmitFilter();
        FilterRegistrationBean<ResubmitFilter> bean = new FilterRegistrationBean<>();
        //注册自定义过滤器
        bean.setFilter(resubmitFilter);
        //过滤器名称
        bean.setName("resubmitFilter");
        //过滤所有路径
        bean.addUrlPatterns("/*");
        //优先级,越低越优先
        bean.setOrder(Ordered.LOWEST_PRECEDENCE);
        return bean;
    }
}

六、使用示例

@Idempotent(require = true)

 @PostMapping("/add")
    @ApiOperation("【酒店】-新增")
    @Idempotent(require = true)
    public Result<String> add(@RequestBody HotelDTO dto) throws IOException {
        String msg=hotelService.add(dto);
        return new Result<String>().ok(msg);
    }

    @PutMapping("/update")
    @ApiOperation("【酒店】-修改")
    @Idempotent(require = true)
    public Result<String> update(@RequestBody HotelDTO dto) throws IOException {
        String msg=hotelService.updateHotel(dto);
        return new Result<String>().ok(msg);
    }
posted @ 2023-04-18 09:33  青喺半掩眉砂  阅读(44)  评论(0编辑  收藏  举报