Spring Boot实现接口幂等

2023-12-15 17:54:56

Spring Boot实现接口幂等

1、pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>idempotent_demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>idempotent_demo</name>
    <description>idempotent_demo</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- StringUtils工具类 -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.5</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.25</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、Redis工具类

package com.example.idempotent_demo.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @author tom
 * Redis工具类
 */
@Slf4j
@Component
public class RedisUtil {

    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    public void setRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将key和value存入redis
     *
     * @param key        redis的key
     * @param value      redis的value
     * @param expireTime key过期时间
     * @return 保存进redis是否成功
     */
    public boolean save(String key, String value, Long expireTime) {
        try {
            // 存储Token到Redis,且设置过期时间为5分钟
            stringRedisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.MINUTES);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 验证key和value并删除key
     *
     * @param key   redis的key
     * @param value redis的value
     * @return 验证是否成功
     */
    public boolean valid(String key, String value) {
        // 设置Lua脚本,其中KEYS[1]是key,KEYS[2]是value
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        // 执行Lua脚本
        Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根据返回结果判断是否成功成功匹配并删除Redis键值对,若果结果不为空和0,则验证通过
        if (null != result && result != 0L) {
            log.info("验证 key={},value={} 成功", key, value);
            return true;
        }
        log.error("验证 key={},value={} 失败", key, value);
        return false;
    }
}

3、Token服务类

token 服务,里面主要是两个方法,一个用来创建 token,一个用来验证 token。

package com.example.idempotent_demo.service;

import javax.servlet.http.HttpServletRequest;

/**
 * @author tom
 */
public interface TokenService {

    /**
     * 创建token
     *
     * @return
     */
    String generateToken();

    /**
     * 检验token
     *
     * @param request
     * @return
     */
    boolean validToken(HttpServletRequest request);

}
package com.example.idempotent_demo.service.impl;

import com.example.idempotent_demo.constant.Constant;
import com.example.idempotent_demo.exception.NoTokenException;
import com.example.idempotent_demo.exception.ValidTokenException;
import com.example.idempotent_demo.service.TokenService;
import com.example.idempotent_demo.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

/**
 * @author tom
 */
@Service
@Slf4j
public class TokenServiceImpl implements TokenService {

    private RedisUtil redisUtil;

    @Autowired
    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    /**
     * 创建token
     *
     * @return
     */
    @Override
    public String generateToken() {
        // 实例化生成ID工具对象
        String uuid = UUID.randomUUID().toString();
        String token = Constant.IDEMPOTENT_TOKEN_PREFIX + uuid;
        boolean success = redisUtil.save(token, token, 5L);
        if (success) {
            log.info("save token {} to redis success", token);
            return token;
        }
        log.error("save token {} to redis fail", token);
        return null;
    }

    /**
     * 检验token
     *
     * @param request
     * @return
     */
    @Override
    public boolean validToken(HttpServletRequest request) {
        String token = request.getHeader(Constant.IDEMPOTENT_TOKEN_HEADER);
        // header中不存在token
        if (StringUtils.isBlank(token)) {
            log.error("用户未携带token!");
            throw new NoTokenException();
        }
        // 验证token失败
        if (!redisUtil.valid(token, token)) {
            log.error("重复提交!");
            throw new ValidTokenException();
        }
        return true;
    }
}

redis.get(token) 、token.equals 、redis.del(token) 如果这几个操作不是原子,可能导致,高并发下,都get到同

样的数据,判断都成功,继续业务并发执行。这里 redis 使用 lua 脚本完成这个操作:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

package com.example.idempotent_demo.exception;

/**
 * 用户为携带token
 * @author tom
 */
public class NoTokenException extends RuntimeException {

    public NoTokenException() {
        super();
    }
}
package com.example.idempotent_demo.exception;

/**
 * @author
 * 验证token失败
 */
public class ValidTokenException extends RuntimeException{
    public ValidTokenException(){
        super();
    }
}
package com.example.idempotent_demo.util;

/**
 * @author 结果集返回封装
 */
public class ResponseResult {

    /**
     * 响应业务状态
     */
    private Integer code;

    /**
     * 响应消息
     */
    private String msg;

    /**
     * 响应中的数据
     */
    private Object data;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    /**
     * 无参构造方法
     */
    public ResponseResult() {
    }

    /**
     * 全参构造方法
     *
     * @param code
     * @param msg
     * @param data
     */
    public ResponseResult(Integer code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

}
package com.example.idempotent_demo.constant;

/**
 * @author tom
 */
public class Constant {

    /**
     * 存入Redis的Token键的前缀
     */
    public static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";

    /**
     * 请求头的token名称
     */
    public static final String IDEMPOTENT_TOKEN_HEADER = "idempotent_token";
}

4、Redis配置

spring:
  redis:
    ssl: false
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 1000
    password:
    lettuce:
      pool:
        max-active: 100
        max-wait: -1
        min-idle: 0
        max-idle: 20
server:
  servlet:
    encoding:
      charset: UTF-8

5、自定义注解

自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现

自动幂等。

后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解 ElementType.METHOD 表示它

只能放在方法上,EetentionPolicy.RUNTIME 表示它在运行时。

package com.example.idempotent_demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author tom
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

6、拦截器配置

主要的功能是拦截扫描到 AutoIdempotent 注解的方法,然后调用 TokenService 的 validToken方法校验 token

是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

package com.example.idempotent_demo.config;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.idempotent_demo.annotation.AutoIdempotent;
import com.example.idempotent_demo.exception.NoTokenException;
import com.example.idempotent_demo.exception.ValidTokenException;
import com.example.idempotent_demo.service.TokenService;
import com.example.idempotent_demo.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;

/**
 * @author tom
 */
@Slf4j
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    private TokenService tokenService;

    @Autowired
    public void setTokenService(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    /**
     * 预处理
     *
     * @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 handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 被AutoIdempotent注解标记的方法
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                // 幂等性校验,校验通过则放行,校验失败则抛出异常,并通过统一异常处理返回友好提示
                return tokenService.validToken(request);
            } catch (NoTokenException ex) {
                log.error("用户未携带token!");
                returnJson(response, JSON.toJSONString(new ResponseResult(10001, "用户未携带token!", null), SerializerFeature.WriteMapNullValue));
                return false;
            } catch (ValidTokenException ex) {
                log.error("重复提交!");
                returnJson(response, JSON.toJSONString(new ResponseResult(10002, "重复提交!", null), SerializerFeature.WriteMapNullValue));
                return false;
            }
        }
        //必须返回true,否则会被拦截一切请求
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

    private void returnJson(HttpServletResponse response, String json) {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(json);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}

7、注册拦截器

添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,

这样在容器启动是时候就可以添加进入context中。

package com.example.idempotent_demo.config;

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
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
    }
}

8、启动类

package com.example.idempotent_demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author tom
 */
@SpringBootApplication
public class IdempotentDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(IdempotentDemoApplication.class, args);
    }

}

9、测试

9.1 生成token

在这里插入图片描述

请求生成了 token。

9.2 redis查看生成的token

在这里插入图片描述

redis 中生成了 token。

9.3 无header请求

在这里插入图片描述

请求需要携带token。

9.4 正常请求

在这里插入图片描述

请求成功。

9.5 再次查看redis

在这里插入图片描述

发现该 token 已经被删除了。

9.5 再次请求

在这里插入图片描述

返回重复请求。

文章来源:https://blog.csdn.net/qq_30614345/article/details/134885081
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。