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'
这样算是完成了, 再次出现二次请求时直接会被拦截。