解决幂等问题的Demo

上篇文章描述了幂等问题的解决方案,接下来,我们通过toke机制来解决接口的幂等问题。
1、Toke支持本地缓存和redis缓存,
首先根据配置决定生成哪个实现类。

package com.sinosoft.idempotence.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.sinosoft.idempotence.service.impl.CacheToken;
import com.sinosoft.idempotence.service.impl.RedisToken;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
@ConfigurationProperties(prefix = "idempotence")
@Data
public class TokenConfig {

    /**
     * 处理幂等性缓存方式 local/redis
     */
    private String lockType = "local";

    @Bean
    @ConditionalOnProperty(name = "idempotence.type", havingValue = "local", matchIfMissing = true)
    public CacheToken getCacheToken() {
        Cache<String, Object> cache = Caffeine.newBuilder()
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(Integer.MAX_VALUE)
                .build();
        return new CacheToken(cache);
    }

    @Bean
    @ConditionalOnProperty(name = "idempotence.type", havingValue = "redis")
    public RedisToken getRedisToken(RedisTemplate redisTemplate) {
        return new RedisToken(redisTemplate);
    }
}

定义接口

package com.sinosoft.idempotence.service;

public interface CheckTokenService {

    /**
     * 判断token是否合法
     * @param token boolean
     * @return
     */
    boolean checkToken(String token) throws Exception;

    /**
     * 获取token
     * @return token
     */
    String getToken();
}


本地缓存和redis缓存实现定义的接口

package com.sinosoft.idempotence.service.impl;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.sinosoft.idempotence.service.CheckTokenService;
import lombok.extern.slf4j.Slf4j;
import com.github.benmanes.caffeine.cache.Cache;

import java.util.UUID;

@Slf4j
public class CacheToken implements CheckTokenService {

    private Cache cache;

    public CacheToken(Cache cache) {
        this.cache = cache;
    }

    @Override
    public boolean checkToken  (String token) throws Exception{
        //判断传入的token是否为空
        if(StringUtils.isBlank(token)){
            throw new Exception("token为空,请先获取token");
        }
        Object o = cache.getIfPresent(token);
        //判断本地缓存中是否存传入的token,存在说明是第一次访问接口,不存在说明不是第一次
        if(o==null){
            throw new Exception("请不要重复请求接口!");
        }
        //判断成功删除token
        cache.invalidate(token);
        return true;
    }

    @Override
    public String getToken() {
        String token= UUID.randomUUID().toString();
        cache.put(token, token);
        log.info("获取token:{},保存本地缓存中。",token);
        return token;
    }
}

package com.sinosoft.idempotence.service.impl;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.sinosoft.idempotence.service.CheckTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.UUID;

@Slf4j
public class RedisToken implements CheckTokenService {

    private RedisTemplate redisTemplate;

    public RedisToken(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean checkToken(String token) throws Exception{
        //判断传入的token是否为空
        if(StringUtils.isBlank(token)){
            throw new Exception("token为空,请先获取token");
        }
        ValueOperations valueOperations = redisTemplate.opsForValue();
        Object o = valueOperations.get(token);
        //判断redis中是否存传入的token,存在说明是第一次访问接口,不存在说明不是第一次
        if(o==null){
            throw new Exception("请不要重复请求接口!");
        }
        //判断成功删除token
        valueOperations.getOperations().delete(token);
        return true;
    }

    @Override
    public String getToken() {
        String token= UUID.randomUUID().toString();
        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set(token,token);
        log.info("获取token:{},保存redis中。",token);
        return token;
    }
}


2、自定义注解
因为我们的controller中有很多接口,并不是每个接口都需要幂等,在需要幂等的接口上增加该注解。

/**
 * 自定义幂等注解
 * @author lsh
 * @date 2022/3/9
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {

}

3、定义拦截器
调用方法前,校验方法上是否有定义的注解,如果有注解校验toke是否生效。

package com.sinosoft.idempotence.interceptor;

import com.sinosoft.idempotence.config.AutoIdempotent;
import com.sinosoft.idempotence.service.CheckTokenService;
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.lang.reflect.Method;

/**
 * 幂等拦截器
 * @author lsh
 * @date 2022/3/24
 */
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    private CheckTokenService tokenService;


    @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();
        //被ApiIdempotment标记的扫描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                // 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
                return tokenService.checkToken(request.getHeader("token"));
            }catch (Exception ex){
                throw ex;
            }
        }
        //必须返回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 {

    }
}


3、配置拦截器生效

package com.sinosoft.idempotence.config;

import com.sinosoft.idempotence.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import javax.annotation.Resource;

/**
 * 配置拦截器
 * @author lsh
 * @date 2022/3/24
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

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

4、测试
首先在postMan调用获取Toke接口获取token
在这里插入图片描述
然后请求接口,接口上增加了我们自定义的@AutoIdempotent注解
在这里插入图片描述
因为是通过请求头获取toke,与前端约定好将toke的key是“token”,所以请求接口时在header增加参数,值是获取的token值。
第一次请求接口
在这里插入图片描述
第二次请求接口
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值