首页 > 数据库 >接口幂等性设计-拦截器+过滤器+redis

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

时间:2023-04-18 09:48:09浏览次数:36  
标签:拦截器 return request redis annotation new 过滤器 import public

接口幂等性设计-拦截器+过滤器+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);
    }

标签:拦截器,return,request,redis,annotation,new,过滤器,import,public
From: https://www.cnblogs.com/xiaoguo-java/p/17328435.html

相关文章

  • Redis 报”OutOfDirectMemoryError“(堆外内存溢出)
    Redis报错“OutOfDirectMemoryError(堆外内存溢出)”问题如下:一、报错信息:使用Redis的业务接口,产生OutOfDirectMemoryError(堆外内存溢出),如图:格式化后的报错信息:{ "timestamp":"2023-04-1722:46:36", "status":500, "error":"InternalServerError&q......
  • redis介绍,redis linux安装,redis启动,redis经典场景,redis通用命令,数据结构和内部编码,red
    目录redis介绍,redislinux安装,redis启动,redis经典场景,redis通用命令,数据结构和内部编码,redis字符串类型昨日内容回顾今日内容1redis介绍2redislinux下安装3redis启动方式3.1最简启动3.2动态参数启动3.3配置文件启动3.4客户端连接命令4redis经典场景5redis通用命令6数据......
  • 【redis】 redis linux下安装 redis启动方式 redis典型场景 redis通用命令 数据结构
    目录上节回顾今日内容1redis介绍2redislinux下安装3redis启动方式3.1最简启动3.2动态参数启动3.3配置文件启动3.4客户端连接命令4redis典型场景5redis通用命令6数据结构和内部编码7redis字符串类型练习上节回顾#dockerfile命令 RUNCOPYADDENV......
  • 【Azure Redis 缓存】Azure Redis 遇见的连接不上问题和数据丢失的情况解答
    问题描述PHP应用再连接AzureRedis服务时,出现ConnectionTimedout。当通过升级提高AzureRedis的性能时候,发现之前的数据丢失了。 问题解答当Redis服务出现Timeout的情况时,可以从Redis服务的指标(Metrics)开始查看,如果出现负载(ServiceLoad)很高的情况,表明当前Redis服务的资源已......
  • 【Azure Redis 缓存】Azure Redis 遇见的连接不上问题和数据丢失的情况解答
    问题描述PHP应用再连接AzureRedis服务时,出现ConnectionTimedout。当通过升级提高AzureRedis的性能时候,发现之前的数据丢失了。 问题解答当Redis服务出现Timeout的情况时,可以从Redis服务的指标(Metrics)开始查看,如果出现负载(ServiceLoad)很高的情况,表明当前Redis服务的......
  • Redis---数据持久化之RDB与AOF
    一、数据持久化之RDB1、RDB介绍Redis数据库文件,全称RedisDataBase,数据持久化方式之一,数据持久化默认方式,按照指定时间间隔,将内存中的数据及快照写入硬盘定义RDB文件名 dbfilename"dump.rdb"RDB指dump.rdb文件;redis数据每次存盘,如果不指定持久化的方式,数据就会默认......
  • redis
    今日内容1redis介绍#特性Redis特性(8个)#速度快:10wops(每秒10w读写),数据存在内存中,c语言实现,单线程模型#持久化:rdb和aof#多种数据结构: 5大数据结构BitMaps位图:布隆过滤器本质是字符串HyperLogLog:超小内存唯一值计数,12kbHyperLogLog本质是字符串GEO:地......
  • Redis --- 数据类型之列表 数据类型之hash
    一、数据类型之列表列表简介Redis的list是一个字符队列,先进后出,一个key可以有多个值列表操作lpushkeyvalues[value...]将一个或多个值value插入到列表key的表头,Key不存在,则创建key127.0.0.1:6379>FLUSHALLOK#lpush命令,创建变量student,类型为列表类型,值为bob......
  • Redis中的Hash类型常用命令
    一、hset命令作用:设置hash类型值;格式:hsetkeyfieldvalue案例:192.168.0.111:0>hsetproductname苹果"1"192.168.0.111:0>hsetproductprice5"1"二、hget命令作用:获取hash类型某个key下的某个field的值格式:hgetkeyfield案例:192.168.0.111:0>hgetproductname"苹果&qu......
  • 记录 RedisTemplate.executePipelined 使用问题
    需求,向redis写入2000万个key@Slf4j@Component("job2")publicclassToRedis2implementsIJob{privateAtomicLongcount=newAtomicLong(0);privateLongoldCount=0L;privateList<String>userIdList=newArrayList<>();pri......