1、pom.xml:主要是引入了redis相关依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><!-- spring-boot-starter-data-redis --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId></dependency><!-- jedis --><dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency><!-- commons-lang3 --><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId></dependency><!-- org.json/json --><dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20190722</version></dependency>
2、application.yml:主要是配置redis
server: port: 6666spring: application: name: idempotent-api redis: host: 192.168.2.43 port: 6379
3、业务代码:
•新建一个枚举,列出常用返回信息,如下:
@Getter@AllArgsConstructorpublic enum ResultEnum { REPEATREQUEST(405, "重复请求"), OPERATEEXCEPTION(406, "操作异常"), HEADERNOTOKEN(407, "请求头未携带token"), ERRORTOKEN(408, "token正确") ; private Integer code; private String msg;}
•新建一个JsonUtil,当请求异常时往页面中输出json:
public class JsonUtil { private JsonUtil() {} public static void writeJsonToPage(HttpServletResponse response, String msg) { PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); try { writer = response.getWriter(); writer.print(msg); } catch (IOException e) { } finally { if (writer != null) writer.close(); } }}
•新建一个RedisUtil,用来操作redis:
@Component
public class RedisUtil {
private RedisUtil() {}
private static RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置序列化Value的实例化对象
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
RedisUtil.redisTemplate = redisTemplate;
}
/**
* 设置key-value,过期时间为timeout秒
* @param key
* @param value
* @param timeout
*/
public static void setString(String key, String value, Long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 设置key-value
* @param key
* @param value
*/
public static void setString(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 获取key-value
* @param key
* @return
*/
public static String getString(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 判断key是否存在
* @param key
* @return
*/
public static boolean isExist(String key) {
return redisTemplate.hasKey(key);
}
/**
* 删除key
* @param key
* @return
*/
public static boolean delKey(String key) {
return redisTemplate.delete(key);
}
}
•新建一个TokenUtil,用来生成和校验token:生成token没什么好说的,这里为了简单直接用uuid生成,然后放入redis中。校验token,如果用户没有携带token,直接返回false;如果携带了token,但是redis中没有这个token,说明已经被删除了,即已经访问了,返回false;如果redis中有,但是redis中的token和用户携带的token不一致,也返回false;有且一致,说明是第一次访问,就将redis中的token删除,然后返回true。
public class TokenUtil {
private TokenUtil() {}
private static final String KEY = "token";
private static final String CODE = "code";
private static final String MSG = "msg";
private static final String JSON = "json";
private static final String RESULT = "result";
/**
* 生成token并放入redis中
* @return
*/
public static String createToken() {
String token = UUID.randomUUID().toString();
RedisUtil.setString(KEY, token, 60L);
return RedisUtil.getString(KEY);
}
/**
* 校验token
* @param request
* @return
* @throws JSONException
*/
public static Map<String, Object> checkToken(HttpServletRequest request) throws JSONException {
String headerToken = request.getHeader(KEY);
JSONObject json = new JSONObject();
Map<String, Object> resultMap = new HashMap<>();
// 请求头中没有携带token,直接返回false
if (StringUtils.isEmpty(headerToken)) {
json.put(CODE, ResultEnum.HEADERNOTOKEN.getCode());
json.put(MSG, ResultEnum.HEADERNOTOKEN.getMsg());
resultMap.put(RESULT, false);
resultMap.put(JSON, json.toString());
return resultMap;
}
if (StringUtils.isEmpty(RedisUtil.getString(KEY))) {
// 如果redis中没有token,说明已经访问成功过了,直接返回false
json.put(CODE, ResultEnum.REPEATREQUEST.getCode());
json.put(MSG, ResultEnum.REPEATREQUEST.getMsg());
resultMap.put(RESULT, false);
resultMap.put(JSON, json.toString());
return resultMap;
} else {
// 如果redis中有token,就删除掉,删除成功返回true,删除失败返回false
String redisToken = RedisUtil.getString(KEY);
boolean result = false;
if (!redisToken.equals(headerToken)) {
json.put(CODE, ResultEnum.ERRORTOKEN.getCode());
json.put(MSG, ResultEnum.ERRORTOKEN.getMsg());
} else {
result = RedisUtil.delKey(KEY);
String msg = result ? null : ResultEnum.OPERATEEXCEPTION.getMsg();
json.put(CODE, 400);
json.put(MSG, msg);
}
resultMap.put(RESULT, result);
resultMap.put(JSON, json.toString());
return resultMap;
}
}
}
新建一个注解,用来标注需要进行幂等的接口:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface NeedIdempotent {}
接着要新建一个拦截器,对有@NeedIdempotent注解的方法进行拦截,进行自动幂等。
public class IdempotentInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,Object object) throws JSONException {
// 拦截的不是方法,直接放行
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
// 如果是方法,并且有@NeedIdempotent注解,就自动幂等
if (method.isAnnotationPresent(NeedIdempotent.class)) {
Map<String, Object> resultMap = TokenUtil.checkToken(httpServletRequest);
boolean result = (boolean) resultMap.get("result");
String json = (String) resultMap.get("json");
if (!result) {
JsonUtil.writeJsonToPage(httpServletResponse, json);
}
return result;
} else {
return true;
}
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, Object o,ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object o, Exception e) {
}
}
然后将这个拦截器配置到spring中去: @Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(idempotentInterceptor())
.addPathPatterns("/**");
}
@Bean
public IdempotentInterceptor idempotentInterceptor() {
return new IdempotentInterceptor();
}
}
•最后新建一个controller,就可以愉快地进行测试了:
@RestController
@RequestMapping("/idempotent")
public class IdempotentApiController {
@NeedIdempotent
@GetMapping("/hello")
public String hello() {
return "are you ok?";
}
@GetMapping("/token")
public String token() {
return TokenUtil.createToken();
}
}
访问/token
,不需要什么校验,访问/hello
,就会自动幂等,每一次访问都要先获取token,一个token不能用两次。