SpringBoot實(shí)現(xiàn)接口等冪次校驗(yàn)的示例代碼
接口等冪性通俗的來(lái)說(shuō)就是同一時(shí)間內(nèi),發(fā)起多次請(qǐng)求只有一次請(qǐng)求成功;其目的時(shí)防止多次提交,數(shù)據(jù)重復(fù)入庫(kù),表單驗(yàn)證網(wǎng)絡(luò)延遲重復(fù)提交等問(wèn)題。
比如:
- 訂單接口, 不能多次創(chuàng)建訂單
- 支付接口, 重復(fù)支付同一筆訂單只能扣一次錢(qián)
- 支付寶回調(diào)接口, 可能會(huì)多次回調(diào), 必須處理重復(fù)回調(diào)
- 普通表單提交接口, 因?yàn)榫W(wǎng)絡(luò)超時(shí)等原因多次點(diǎn)擊提交, 只能成功一次
等等
主流的實(shí)現(xiàn)方案如下:
1、唯一索引:給表加唯一索引,該方法最簡(jiǎn)單,當(dāng)數(shù)據(jù)重復(fù)插入時(shí),直接報(bào)sql異常,對(duì)應(yīng)用影響不大;
alter table 表名 add unique(字段)
示例,兩個(gè)字段為唯一索引,如果出現(xiàn)完全一樣的order_name,create_time就直接重復(fù)報(bào)異常;
alter table 'order' add unique(order_name,create_time)
2、先查詢后判斷:入庫(kù)時(shí)先查詢是否有該數(shù)據(jù),如果沒(méi)有則插入。否則不插入;
3、token機(jī)制:發(fā)起請(qǐng)求的時(shí)候先去redis獲取token,將獲取的token放入請(qǐng)求的hearder,當(dāng)請(qǐng)求到達(dá)服務(wù)端的時(shí)候攔截請(qǐng)求,對(duì)請(qǐng)求的hearder中的token,進(jìn)行校驗(yàn),如果校驗(yàn)通過(guò)則放開(kāi)攔截,刪除token,否則使用自定義異常返回錯(cuò)誤信息。
第一步:書(shū)寫(xiě)redis工具類(lèi)
?
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 判斷key是否存在
* @param key 鍵
* @return
*/
public boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 刪除key
* @param key 鍵
* @return
*/
public Boolean del(String key){
if (key != null && key.length() > 0){
return redisTemplate.delete(key);
}else {
return false;
}
}
/**
* 普通緩存獲取
* @param key 鍵
* @return
*/
public Object get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通緩存放入并設(shè)置時(shí)間
* @param key 鍵
* @param value 值
* @param time 時(shí)間(秒) time要大于0 如果time小于等于0 將設(shè)置無(wú)限期
* @return
*/
public boolean set(String key,Object value,long time){
try {
if (time > 0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
}
?第二步、書(shū)寫(xiě)token工具類(lèi)
import com.smile.project.exception.utils.CodeMsg;
import com.smile.project.exception.utils.CommonException;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
/**
* 使用uuid生成隨機(jī)字符串,
* 通過(guò)md5加密防止token被解密,保證token的唯一性與安全性
* 設(shè)置過(guò)期時(shí)間為30秒,即在30秒內(nèi)之惡能提交成功一次請(qǐng)求
*/
@Component
public class TokenUtils {
@Autowired
RedisUtils redisUtils;
//token過(guò)期時(shí)間為30秒
private final static Long TOKEN_EXPIRE = 30L;
private final static String TOKEN_NAME = "token";
/**
* 生成token放入緩存
*/
public String generateToken(){
String uuid = UUID.randomUUID().toString();
String token = DigestUtils.md5DigestAsHex(uuid.getBytes());
redisUtils.set(TOKEN_NAME,token,TOKEN_EXPIRE);
return token;
}
/**
* token校驗(yàn)
*/
public boolean verifyToken(HttpServletRequest request){
String token = request.getHeader(TOKEN_NAME);
//header中不存在token
if (StringUtils.isEmpty(token)){
//拋出自定義異常
System.out.println("token不存在");
throw new CommonException(CodeMsg.NOT_TOKEN);
}
//緩存中不存在
if (!redisUtils.hasKey(TOKEN_NAME)){
System.out.println("token已經(jīng)過(guò)期");
throw new CommonException(CodeMsg.TIME_OUT_TOKEN);
}
String cachToken = (String) redisUtils.get(TOKEN_NAME);
if (!token.equals(cachToken)){
System.out.println("token檢驗(yàn)失敗");
throw new CommonException(CodeMsg.VALIDA_ERROR_TOKEN);
}
//移除token
Boolean del = redisUtils.del(TOKEN_NAME);
if (!del){
System.out.println("token刪除失敗");
throw new CommonException(CodeMsg.DEL_ERROR_TOKEN);
}
return true;
}
}
第三步:定義注解,使用在方法上,當(dāng)控制層的方法上被注釋時(shí),表示該請(qǐng)求為等冪性請(qǐng)求
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 當(dāng)控制層的方法上被注釋時(shí),表示該請(qǐng)求為等冪性請(qǐng)求
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
}
第四步:攔截器配置。選擇前置攔截器,每次請(qǐng)求都校驗(yàn)到達(dá)的方法上是否有等冪性注解,如果有則進(jìn)行token校驗(yàn)
import com.smile.project.redis.utils.Idempotent;
import com.smile.project.redis.utils.TokenUtils;
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;
?
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
?
? ? @Autowired
? ? private TokenUtils tokenUtils;
?
? ? @Override
? ? public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
? ? ? ? if (!(handler instanceof HandlerMethod)){
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? //對(duì)有Idempotent注解的方法進(jìn)行攔截校驗(yàn)
? ? ? ? HandlerMethod handlerMethod = (HandlerMethod) handler;
? ? ? ? Method method = handlerMethod.getMethod();
? ? ? ? Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
? ? ? ? if (methodAnnotation != null){
? ? ? ? ? ? //token校驗(yàn)
? ? ? ? ? ? tokenUtils.verifyToken(request);
? ? ? ? }
? ? ? ? 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 {
? ? }
}第五步:對(duì)攔截器進(jìn)行url模式匹配,并注入spring容器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 對(duì)攔截器進(jìn)行url模式匹配,并注入spring容器
*/
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired
IdempotentInterceptor idempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//攔截所有請(qǐng)求
registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**");
}
}
第六步:控制層
對(duì)控制層進(jìn)行編寫(xiě),發(fā)起請(qǐng)求時(shí)通過(guò)getToken方法獲取token,將獲取的token放入hearder后,再請(qǐng)求具體方法。正常請(qǐng)求具體方法的時(shí)候注意在hearder中加入token,否則是失敗
import com.alibaba.fastjson.JSONObject;
import com.smile.project.exception.utils.CodeMsg;
import com.smile.project.exception.utils.ResultPage;
import com.smile.project.redis.utils.Idempotent;
import com.smile.project.redis.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SmileController {
@Autowired
TokenUtils tokenUtils;
@GetMapping("smile/token")
public ResultPage getToken(){
String token = tokenUtils.generateToken();
JSONObject jsonObject = new JSONObject();
jsonObject.put("token",token);
return ResultPage.success(CodeMsg.SUCCESS,jsonObject);
}
@Idempotent
@GetMapping("smile/test")
public ResultPage testIdempotent(){
return ResultPage.success(CodeMsg.SUCCESS,"校驗(yàn)成功");
}
}
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)接口等冪次校驗(yàn)的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot實(shí)現(xiàn)接口等冪次校驗(yàn) 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis日期格式自動(dòng)轉(zhuǎn)換需要用到的兩個(gè)注解說(shuō)明
這篇文章主要介紹了Mybatis日期格式自動(dòng)轉(zhuǎn)換需要用到的兩個(gè)注解說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
SpringBoot 啟動(dòng)報(bào)錯(cuò)Unable to connect to 
這篇文章主要介紹了SpringBoot 啟動(dòng)報(bào)錯(cuò)Unable to connect to Redis server: 127.0.0.1/127.0.0.1:6379問(wèn)題的解決方案,文中通過(guò)圖文結(jié)合的方式給大家講解的非常詳細(xì),對(duì)大家解決問(wèn)題有一定的幫助,需要的朋友可以參考下2024-10-10
Java中的StringJoiner類(lèi)使用示例深入詳解
這篇文章主要為大家介紹了Java中的StringJoiner類(lèi)使用示例深入詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
詳解spring boot應(yīng)用啟動(dòng)原理分析
這篇文章主要介紹了詳解spring boot應(yīng)用啟動(dòng)原理分析,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06
無(wú)感NullPointerException的值相等判斷方法
當(dāng)我們需要去判斷一個(gè)?入?yún)?查庫(kù)?返回的開(kāi)關(guān)變量(通常是個(gè)Integer類(lèi)型的)時(shí),常常會(huì)寫(xiě)如下的if-else判斷語(yǔ)句。但又會(huì)為在生產(chǎn)環(huán)境看到的「NullPointerException」感到困擾,遇到這個(gè)問(wèn)題如何處理呢,下面小編通過(guò)本文給大家詳細(xì)講解,需要的朋友參考下吧2023-02-02

