Java使用@Idempotent注解处理幂等问题,防止二次点击

Java 使用自定义注解 @Idempotent 处理幂等问题, 防止二次点击

幂等实现原理就是利用 AOP 面向切面编程, 在执行业务逻辑之前插入一个方法, 生成一个 token, 存入 redis 并插入到 response 中返回给前台,

然后前台再拿着这个 token 发起请求, 经过判断, 只执行第一次请求, 多余点击的请求都拦截下来.

创建自定义注解 @Idempotent

package org.jeecg.common.annotation;

import java.lang.annotation.*;

//注解信息会被添加到 Java 文档中
@Documented
//注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
@Retention(RetentionPolicy.RUNTIME)
//注解作用的位置,ElementType.METHOD 表示该注解仅能作用于方法上
@Target(ElementType.METHOD)
public @interface Idempotent {
}

创建自定义注解 @IdempotentToken

 

package org.jeecg.common.annotation;

import java.lang.annotation.*;

//注解信息会被添加到 Java 文档中
@Documented
//注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
@Retention(RetentionPolicy.RUNTIME)
//注解作用的位置,ElementType.METHOD 表示该注解仅能作用于方法上
@Target(ElementType.METHOD)
public @interface IdempotentToken {
}

 

@Idempotent 注解的配置类 IdempotentInterceptor

 

 

package org.jeecg.config.idempotent;

import cn.hutool.core.util.ObjectUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.annotation.Idempotent; import org.jeecg.common.annotation.IdempotentToken; import org.jeecg.common.util.RedisUtil; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.UUID;

/**

  • @author zbw
    */
    @Slf4j
    @Component
    public class IdempotentInterceptor implements HandlerInterceptor {
    private static final String VERSION_NAME = "version";

    private static final String TOKEN_NAME = "idempotent_token";
    private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent:token:";

    /private RedisTemplate<String, Object> redisTemplate;/
    private RedisUtil redisUtil;

    public IdempotentInterceptor(RedisUtil redisUtil){
    this.redisUtil = redisUtil;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    if (!(handler instanceof HandlerMethod)) {
    return true;
    }

     HandlerMethod handlerMethod </span>=<span style="color: rgba(0, 0, 0, 1)"> (HandlerMethod) handler;
     Method method </span>=<span style="color: rgba(0, 0, 0, 1)"> handlerMethod.getMethod();
    
     IdempotentToken idempotentTokenAnnotation </span>= method.getAnnotation(IdempotentToken.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);
     </span><span style="color: rgba(0, 0, 255, 1)">if</span>(idempotentTokenAnnotation!=<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">){
         </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">重新更新token</span>
         String token = UUID.randomUUID().toString().replaceAll("-",""<span style="color: rgba(0, 0, 0, 1)">);
         response.addHeader(TOKEN_NAME,token);
         </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">解决后端传递token前端无法获取问题</span>
         response.addHeader("Access-Control-Expose-Headers"<span style="color: rgba(0, 0, 0, 1)">,TOKEN_NAME);
         redisUtil.set(IDEMPOTENT_TOKEN_PREFIX</span>+<span style="color: rgba(0, 0, 0, 1)">token,token);
     }
     Idempotent idempotentAnnotation </span>= method.getAnnotation(Idempotent.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);
     </span><span style="color: rgba(0, 0, 255, 1)">if</span>(idempotentAnnotation!=<span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">){
         checkIdempotent(request);
     }
     </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
    

    }

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

    }

    private void checkIdempotent(HttpServletRequest request) {
    //首先到 request 中去拿 TOKEN_NAME
    String token = request.getHeader(TOKEN_NAME);

     </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)">(ObjectUtil.isEmpty(token)){
         token </span>= ""<span style="color: rgba(0, 0, 0, 1)">;
     }
     </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isBlank(token)) {<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> header中不存在token</span>
         token =<span style="color: rgba(0, 0, 0, 1)"> request.getParameter(TOKEN_NAME);
         </span><span style="color: rgba(0, 0, 255, 1)">if</span> (StringUtils.isBlank(token)) {<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> parameter中也不存在token</span>
             <span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> IllegalArgumentException("幂等token丢失,请勿重复提交"<span style="color: rgba(0, 0, 0, 1)">);
         }
     }
     </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!redisUtil.hasKey(IDEMPOTENT_TOKEN_PREFIX+<span style="color: rgba(0, 0, 0, 1)">token)) {
         </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> IllegalArgumentException("请勿重复提交"<span style="color: rgba(0, 0, 0, 1)">);
     }
     </span><span style="color: rgba(0, 0, 255, 1)">boolean</span> bool = redisUtil.delete(IDEMPOTENT_TOKEN_PREFIX+<span style="color: rgba(0, 0, 0, 1)">token);
     </span><span style="color: rgba(0, 0, 255, 1)">if</span>(!<span style="color: rgba(0, 0, 0, 1)">bool){
         </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> IllegalArgumentException("没有删除对应的token"<span style="color: rgba(0, 0, 0, 1)">);
     }
    

    }
    }

 

 

 

这个注解配置是基于 redis 的, 在这里 redis 的配置略过, 本章只是讲解如何简单的使用

 

 后端的话, 需要在该页面的 list 查询上加上 @IdempotentToken 注解, 原理是涉及到查询成功后会刷新一下页面, 重新获取 token

@Idempotent 注解配置完成就可以直接加在 controller 的对应方法上, 如图:

 

 前端的一些配置 (这里我用的是 ant design vue):

user.js 里面加入

 

idempotentToken:''

SET_IDEMPOTENT_TOKEN:(state,token)=>{
state.idempotentToken
= token
},


getter.js 里面加入

idempotentToken: state => state.user.idempotentToken,

request.js 里面修改

// request interceptor
service.interceptors.request.use(config => {
  const token = Vue.ls.get(ACCESS_TOKEN)
  if (token) {
    config.headers[ 'X-Access-Token' ] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
  }
  const idempotent_token = Vue.ls.get(IDEMPOTENT_TOKEN)
  if(idempotent_token){
    config.headers['idempotent_token'] = idempotent_token
  }
  //update-begin-author:taoyan date:2020707 for: 多租户
  let tenantid = Vue.ls.get(TENANT_ID)
  if (!tenantid) {
    tenantid = 0;
  }
  config.headers[ 'tenant_id' ] = tenantid
  //update-end-author:taoyan date:2020707 for: 多租户
  if(config.method=='get'){
    if(config.url.indexOf("sys/dict/getDictItems")<0){
      config.params = {
        _t: Date.parse(new Date())/1000,
        ...config.params
      }
    }
  }
  return config
},(error) => {
  return Promise.reject(error)
})

// response interceptor
service.interceptors.response.use((response) => {
let idempotent_token
= response.headers[IDEMPOTENT_TOKEN]
if(idempotent_token){
Vue.ls.set(IDEMPOTENT_TOKEN,idempotent_token,
7 * 24 * 60 * 60 * 1000)
store.commit(
'SET_IDEMPOTENT_TOKEN', idempotent_token)
}
return response.data
}, err)

const installer = {
vm: {},
install (Vue, router
= {}) {
Vue.use(VueAxios, router, service)
}

mutation-type.js 加入定义的常量

export const IDEMPOTENT_TOKEN='idempotent_token'

 

 

 这样算是完成了, 再次出现二次请求时直接会被拦截。