文章目录
一. 什么是幂等性
简单的说就是对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。
例如:在一次用户的支付订单的操作中,第一次支付时因一些原因显示网络异常,但是后台实际已经扣款了,那么用户支付第二次时,就会给用户显示扣款成功.
实现接口幂等性的设计方案可以有很多种,今天我们就用自定义注解的方式来解决
二. 基于拦截器实现
2.1 实现思路
- 自定义注解,在每个需要控制幂等性的接口上加上此注解
- 后台提供生成token的接口供前台调用,并在生成时将之存入redis中
- 前台每次请求接口时,必须带有从后台拿到的token,每个token只能用一次
- 后台在处理前台的一次请求后,将携带的token在redis中删除
- 自定义拦截器,对前台的每次请求进行token校验(校验redis中是否有此token),校验成功则处理逻辑,校验失败则不进行任何处理(返回错误信息提示前台)
2.2 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
2.3 配置redis
spring.redis.port=6300
spring.redis.database=4
spring.redis.host=10.16.64.30
spring.redis.password=123456
2.4 封装redis工具类
@Component
public class RedisService {
@Autowired
StringRedisTemplate redisTemplate;
/**
* 给key设置过期时间
*
* @param key
* @param value
* @param time
* @return
*/
public boolean setExp(String key, String value, Long time) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(key, value);
Boolean expire = redisTemplate.expire(key, time, TimeUnit.SECONDS);
if (expire) {
return true;
}
return false;
}
/**
* 判读key是否存在
*
* @param key
* @return
*/
public boolean exists(String key) {
return redisTemplate.hasKey(key);
}
/**
* 删除key
*
* @param key
* @return
*/
public boolean remove(String key) {
if (exists(key)) {
return redisTemplate.delete(key);
}
return false;
}
}
2.5 检验token服务层
@Component
public class TokenService {
@Autowired
RedisService redisService;
/**
* 创建token 并存入redis
* @return
*/
public String createToken() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
if (redisService.setExp(token, token, 1000L)) {
return token;
}
return null;
}
/**
* 校验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
return false;
}
}
if (!redisService.exists(token)) {
return false;
}
if (!redisService.remove(token)) {
return false;
}
return true;
}
}
2.6 自定义注解
@Target(ElementType.METHOD) //注解作用于方法上
@Retention(RetentionPolicy.RUNTIME) //保留到运行时
public @interface Idempotence {
}
2.7 自定义拦截器
/*
* @className: IdempotenceInterceptor
* @description 对有此注解的相关接口作拦截处理,没有此注解的接口直接放行
* @since JDK1.8
* @author ljh
* @createdAt 2020/9/3 0003
* @version 1.0.0
**/
@Component
public class IdempotenceInterceptor implements HandlerInterceptor {
@Autowired
TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
Method method = ((HandlerMethod) handler).getMethod();
Idempotence annotation = method.getAnnotation(Idempotence.class);
if (annotation != null) {
if (tokenService.checkToken(request)) {
return true;
} else {
return false;
}
}
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 {
}
}
2.8 配置拦截器
@Configuration
public class WebMvcConfing implements WebMvcConfigurer {
@Autowired
IdempotenceInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
2.9测试
@RestController
public class TestController {
@Autowired
TokenService tokenService;
@Idempotence
@PostMapping("/test1")
public String test1(){
return "111"; //被拦截器拦截作校验处理
}
@PostMapping("/test2")
public String test2(){
return "222"; //直接返回结果
}
@GetMapping("/getToken")
public String getToken(){
return tokenService.createToken();
}
}
三. 基于aop实现
这里就可以直接删除拦截器相关的类了IdempotenceInterceptor和WebMvcConfing
3.1 定义切面
@Component
@Aspect
public class IdempotenceAspect {
@Autowired
TokenService tokenService;
@Pointcut("@annotation(com.cicro.annotation.idempotence.Idempotence)")
public void pointCut() {
}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
//拿到当前请求request
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (!tokenService.checkToken(request)){
throw new IdempotenceException("请求重复处理"); //没有检验通过直接抛出异常
}
}
}
3.2 自定义异常类
public class IdempotenceException extends RuntimeException{
public IdempotenceException(String message) {
super(message);
}
}
3.3 定义全局异常处理类
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(IdempotenceException.class)
public String e(IdempotenceException e) {
return e.getMessage();
}
}