上篇文章描述了幂等问题的解决方案,接下来,我们通过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值。
第一次请求接口
第二次请求接口