SpringBoot關(guān)于自定義注解實(shí)現(xiàn)接口冪等性方式
自定義注解實(shí)現(xiàn)接口冪等性方式
近期需要對(duì)接口進(jìn)行冪等性的改造,特此記錄下。
背景
在微服務(wù)架構(gòu)中,冪等是一致性方面的一個(gè)重要概念。
一個(gè)冪等操作的特點(diǎn)是指其多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。在業(yè)務(wù)中也就是指的,多次調(diào)用方法或者接口不會(huì)改變業(yè)務(wù)狀態(tài),可以保證重復(fù)調(diào)用的結(jié)果和單次調(diào)用的結(jié)果一致。
常見場(chǎng)景
1.用戶重復(fù)操作
在產(chǎn)品訂購下單過程中,由于網(wǎng)絡(luò)延遲或者用戶誤操作等原因,導(dǎo)致多次提交。這時(shí)就會(huì)在后臺(tái)執(zhí)行多條重復(fù)請(qǐng)求,導(dǎo)致臟數(shù)據(jù)或執(zhí)行錯(cuò)誤等。
2.分布式消息重復(fù)消費(fèi)
消息隊(duì)列中由于某種原因消息二次發(fā)送或者被二次消費(fèi)的時(shí)候,導(dǎo)致程序多次執(zhí)行,從而導(dǎo)致數(shù)據(jù)重復(fù),資源沖突等。
3.接口超時(shí)重試
由于網(wǎng)絡(luò)波動(dòng),引起的重復(fù)請(qǐng)求,導(dǎo)致數(shù)據(jù)的重復(fù)等。
常見解決方案
1.token機(jī)制實(shí)現(xiàn)
由客戶端發(fā)送請(qǐng)求獲取Token,服務(wù)端生成全局唯一的ID作為token,并保存在redis中,同時(shí)返回ID給客戶端。
客戶端調(diào)用業(yè)務(wù)端的請(qǐng)求的時(shí)候需要攜帶token,由服務(wù)端進(jìn)行校驗(yàn),校驗(yàn)成功,則允許執(zhí)行業(yè)務(wù),不成功則表示重復(fù)操作,直接返回給客戶端。
2.mysql去重
建立一個(gè)去重表,當(dāng)客戶端請(qǐng)求的時(shí)候,將請(qǐng)求信息存入去重表進(jìn)行判斷。由于去重表帶有唯一索引,如果插入成功則表示可以執(zhí)行。如果失敗則表示已經(jīng)執(zhí)行過當(dāng)前請(qǐng)求,直接返回。
3.基于redis鎖機(jī)制
在redis中,SETNX表示 SET IF NOT EXIST的縮寫,表示只有不存在的時(shí)候才可以進(jìn)行設(shè)置,可以利用它實(shí)現(xiàn)鎖的效果。
客戶端請(qǐng)求服務(wù)端時(shí),通過計(jì)算拿到代表這次業(yè)務(wù)請(qǐng)求的唯一字段,將該值存入redis,如果設(shè)置成功表示可以執(zhí)行。失敗則表示已經(jīng)執(zhí)行過當(dāng)前請(qǐng)求,直接返回。
實(shí)現(xiàn)方法
基于種種考慮,本文將基于方法3實(shí)現(xiàn)冪等性方法。其中有兩個(gè)需要注意的地方:
1.如何實(shí)現(xiàn)唯一請(qǐng)求編號(hào)進(jìn)行去重?
本文將采用用戶ID:接口名:請(qǐng)求參數(shù)進(jìn)行請(qǐng)求參數(shù)的MD5摘要,同時(shí)考慮到請(qǐng)求時(shí)間參數(shù)的干擾性(同一個(gè)請(qǐng)求,除了請(qǐng)求參數(shù)都相同可以認(rèn)為為同一次請(qǐng)求),排除請(qǐng)求時(shí)間參數(shù)進(jìn)行摘要,可以在短時(shí)間內(nèi)保證唯一的請(qǐng)求編號(hào)。
2.如何保證最小的代碼侵入性?
本文將采用自定義注解,同時(shí)采用切面AOP的方式,最大化的減少代碼的侵入,同時(shí)保證了方法的易用性。
代碼實(shí)現(xiàn)
1.自定義注解
實(shí)現(xiàn)自定義注解,同時(shí)設(shè)置超時(shí)時(shí)間作為重復(fù)間隔時(shí)間。在需要使用冪等性校驗(yàn)的方法上面加上注解即可實(shí)現(xiàn)冪等性。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
?
/**
?* @create 2021-01-18 16:40
?* 實(shí)現(xiàn)接口冪等性注解
?**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
?
? ? long expireTime() default 10000;
?
}2.MD5摘要輔助類
通過傳入的參數(shù)進(jìn)行MD5摘要,同時(shí)去除需要排除的干擾參數(shù)生成唯一的請(qǐng)求ID。
import com.google.gson.Gson;
import com.hhu.consumerdemo.model.User;
import lombok.extern.slf4j.Slf4j;
?
import javax.xml.bind.DatatypeConverter;
import java.security.MessageDigest;
import java.util.*;
?
/**
?* @create 2021-01-14 10:12
?**/
@Slf4j
public class ReqDedupHelper {
?
?
? ? private Gson gson = new Gson();
? ? /**
? ? ?*
? ? ?* @param reqJSON 請(qǐng)求的參數(shù),這里通常是JSON
? ? ?* @param excludeKeys 請(qǐng)求參數(shù)里面要去除哪些字段再求摘要
? ? ?* @return 去除參數(shù)的MD5摘要
? ? ?*/
? ? public String dedupParamMD5(final String reqJSON, String... excludeKeys) {
? ? ? ? String decreptParam = reqJSON;
?
? ? ? ? TreeMap paramTreeMap = gson.fromJson(decreptParam, TreeMap.class);
? ? ? ? if (excludeKeys!=null) {
? ? ? ? ? ? List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
? ? ? ? ? ? if (!dedupExcludeKeys.isEmpty()) {
? ? ? ? ? ? ? ? for (String dedupExcludeKey : dedupExcludeKeys) {
? ? ? ? ? ? ? ? ? ? paramTreeMap.remove(dedupExcludeKey);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
?
? ? ? ? String paramTreeMapJSON = gson.toJson(paramTreeMap);
? ? ? ? String md5deDupParam = jdkMD5(paramTreeMapJSON);
? ? ? ? log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
? ? ? ? return md5deDupParam;
? ? }
?
? ? private static String jdkMD5(String src) {
? ? ? ? String res = null;
? ? ? ? try {
? ? ? ? ? ? MessageDigest messageDigest = MessageDigest.getInstance("MD5");
? ? ? ? ? ? byte[] mdBytes = messageDigest.digest(src.getBytes());
? ? ? ? ? ? res = DatatypeConverter.printHexBinary(mdBytes);
? ? ? ? } catch (Exception e) {
? ? ? ? ? ? log.error("",e);
? ? ? ? }
? ? ? ? return res;
? ? }
?
? ? //測(cè)試方法
? ? public static void main(String[] args) {
? ? ? ? Gson gson = new Gson();
? ? ? ? User user1 = new User("1","2",18);
? ? ? ? Object[] objects = new Object[]{"sss",11,user1};
?
? ? ? ? Map<String, Object> maps = new HashMap<>();
? ? ? ? maps.put("參數(shù)1",objects[0]);
? ? ? ? maps.put("參數(shù)2",objects[1]);
? ? ? ? maps.put("參數(shù)3",objects[2]);
? ? ? ? String json1 = gson.toJson(maps);
? ? ? ? System.out.println(json1);
? ? ? ? TreeMap paramTreeMap = gson.fromJson(json1, TreeMap.class);
? ? ? ? System.out.println(gson.toJson(paramTreeMap));
?
? ? }?
}3.redis輔助Service
生成唯一的請(qǐng)求ID作為token存入redis,同時(shí)設(shè)置好超時(shí)時(shí)間,在超時(shí)時(shí)間內(nèi)的請(qǐng)求參數(shù)將作為重復(fù)請(qǐng)求返回,而校驗(yàn)成功插入redis的請(qǐng)求Token將作為首次請(qǐng)求,進(jìn)行放通。
本文采用的spring-redis版本為2.0以上,使用2.0以下版本的需要主要沒有setIfAbsent方法,需要自己實(shí)現(xiàn)。
import com.xxx.xxx.utils.ReqDedupHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
?
import java.util.concurrent.TimeUnit;
?
/**
?* @create 2021-01-18 17:44
?**/
@Service
@Slf4j
public class TokenService {
?
? ? private static final String TOKEN_NAME = "request_token";
?
? ? @Autowired
? ? private StringRedisTemplate stringRedisTemplate;?
?
? ? public boolean checkRequest(String userId, String methodName, long expireTime, String reqJsonParam, String... excludeKeys){
? ? ? ? final boolean isConsiderDup;
? ? ? ? String dedupMD5 = new ReqDedupHelper().dedupParamMD5(reqJsonParam, excludeKeys);
? ? ? ? String redisKey = "dedup:U="+userId+ "M="+methodName+"P="+dedupMD5;
? ? ? ? log.info("redisKey:{}", redisKey);
?
? ? ? ? long expireAt = System.currentTimeMillis() + expireTime;
? ? ? ? String val = "expireAt@" + expireAt;
?
? ? ? ? // NOTE:直接SETNX不支持帶過期時(shí)間,所以設(shè)置+過期不是原子操作,極端情況下可能設(shè)置了就不過期了
? ? ? ? if (stringRedisTemplate.opsForValue().setIfAbsent(redisKey, val)) {
? ? ? ? ? ? if (stringRedisTemplate.expire(redisKey, expireTime, TimeUnit.MILLISECONDS)) {
? ? ? ? ? ? ? ? isConsiderDup = ?false;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? isConsiderDup = ?true;
? ? ? ? ? ? }
? ? ? ? } else {
? ? ? ? ? ? log.info("加鎖失敗 failed??!key:{},value:{}",redisKey,val);
? ? ? ? ? ? return true;
? ? ? ? }
? ? ? ? return isConsiderDup;
? ? }
?
}4.AOP切面輔助類
aop切面,切住所有帶有冪等注解的方法。進(jìn)行冪等性的操作。
import com.google.gson.Gson;
import com.xxx.xxx.annotation.AutoIdempotent;
import com.xxx.xxx.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
?
import java.util.HashMap;
import java.util.Map;
?
/**
?* @author:?
?* @date: 2020-04-28 14:20
?*/
@Aspect
@Component
@Slf4j
public class AutoIdempontentHandler {
?
? ? private Gson gson = new Gson();
?
? ? private static final String excludeKey = "";
? ? private static final String methodName = "methodName";
?
? ? @Autowired
? ? private TokenService tokenService;
?
? ? @Pointcut("@annotation(com.xxx.xxx.annotation.AutoIdempotent)")
? ? public void autoIdempontentHandler() {
? ? }
?
? ? @Before("autoIdempontentHandler()")
? ? public void doBefore() throws Throwable {
? ? ? ? log.info("idempontentHandler..doBefore()");
? ? }
?
? ? @Around("autoIdempontentHandler()")
? ? public Object doAround(ProceedingJoinPoint joinpoint) throws Throwable {
?
? ? ? ? boolean checkres = this.handleRequest(joinpoint);
? ? ? ? if(checkres){
? ? ? ? ? ? //重復(fù)請(qǐng)求,提示重復(fù) 報(bào)錯(cuò)
? ? ? ? ? ? log.info("重復(fù)性請(qǐng)求..");
? ? ? ? ? ? throw new Exception();
? ? ? ? }
? ? ? ? return joinpoint.proceed();
? ? }
?
? ? private Boolean handleRequest(ProceedingJoinPoint joinpoint) {
? ? ? ? Boolean result = false;
? ? ? ? log.info("========判斷是否是重復(fù)請(qǐng)求=======");
? ? ? ? MethodSignature methodSignature = (MethodSignature) joinpoint.getSignature();
? ? ? ? //獲取自定義注解值
? ? ? ? AutoIdempotent autoIdempotent = methodSignature.getMethod().getDeclaredAnnotation(AutoIdempotent.class);
? ? ? ? long expireTime = autoIdempotent.expireTime();
? ? ? ? // 獲取參數(shù)名稱
? ? ? ? String methodsName = methodSignature.getMethod().getName();
? ? ? ? String[] params = methodSignature.getParameterNames();
? ? ? ? //獲取參數(shù)值
? ? ? ? Object[] args = joinpoint.getArgs();
? ? ? ? Map<String, Object> reqMaps = new HashMap<>();
? ? ? ? for(int i=0; i<params.length; i++){
? ? ? ? ? ? reqMaps.put(params[i], args[i]);
? ? ? ? }
? ? ? ? String reqJSON = gson.toJson(reqMaps);
? ? ? ? result = tokenService.checkRequest("user1", methodsName, expireTime, reqJSON, excludeKey);
? ? ? ? return result;
? ? }
?
? ? @AfterReturning(returning = "retVal", pointcut = "autoIdempontentHandler()")
? ? public void doAfter(Object retVal) throws Throwable {
? ? ? ? log.debug("{}", retVal);
? ? }
}5.注解的使用
在需要冪等性的方法上進(jìn)行注解,同時(shí)設(shè)置參數(shù)保證各個(gè)接口的超時(shí)時(shí)間的不一致性??梢钥吹皆?秒內(nèi)是無法再次請(qǐng)求方法1的。
import com.xxx.xxx.annotation.AutoIdempotent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
?
/**
?* @author?
?* @Date: 2020-01-03 14:16
?*/
@RestController
public class ConsumerController {?
?
? ? @AutoIdempotent(expireTime = 5000)
? ? @GetMapping("/start/{index}")
? ? public String setValue( @PathVariable("index") ?String index){
? ? ? ? return index + "1";
? ? }
?
? ? @GetMapping("/start2/{index}")
? ? public String setValue2( @PathVariable("index") ?String index){
? ? ? ? return index + "2";
? ? }?
}思考與不足
微服務(wù)架構(gòu)中,冪等操作的特點(diǎn)是指任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。但在實(shí)際設(shè)計(jì)的時(shí)候,卻簡單的進(jìn)行所有請(qǐng)求進(jìn)行重復(fù)。
然而,重試是降低微服務(wù)失敗率的重要手段。因?yàn)榫W(wǎng)絡(luò)波動(dòng)、系統(tǒng)資源的分配不確定等因素會(huì)導(dǎo)致部分請(qǐng)求的失敗。而這部分的請(qǐng)求中大部分實(shí)際上只需要進(jìn)行簡單的重試就可以保證成功。這才是冪等性真正需要實(shí)現(xiàn)的。暫時(shí)我并沒有更好的解決方法,只能通過短時(shí)間的禁用,以及人為的決定何種方法進(jìn)行冪等性校驗(yàn)來達(dá)到目的。歡迎有想法的和我一起探討交流~
SpringBoot接口冪等性設(shè)計(jì)
MVC方案
多版本并發(fā)控制,該策略主要使用 update with condition(更新帶條件來防止)來保證多次外部請(qǐng)求調(diào)用對(duì)系統(tǒng)的影響是一致的。在系統(tǒng)設(shè)計(jì)的過程中,合理的使用樂觀鎖,通過 version 或者 updateTime(timestamp)等其他條件,來做樂觀鎖的判斷條件,這樣保證更新操作即使在并發(fā)的情況下,也不會(huì)有太大的問題。
例如
select * from tablename where condition=#condition# // 取出要跟新的對(duì)象,帶有版本 versoin update tableName set name=#name#,version=version+1 where version=#version#
在更新的過程中利用 version 來防止,其他操作對(duì)對(duì)象的并發(fā)更新,導(dǎo)致更新丟失。為了避免失敗,通常需要一定的重試機(jī)制。
Token機(jī)制,防止頁面重復(fù)提交
業(yè)務(wù)要求:頁面的數(shù)據(jù)只能被點(diǎn)擊提交一次。
發(fā)生原因:由于重復(fù)點(diǎn)擊或者網(wǎng)絡(luò)重發(fā),或者 nginx 重發(fā)等情況會(huì)導(dǎo)致數(shù)據(jù)被重復(fù)提交
解決辦法:
集群環(huán)境:采用 token 加 redis(redis 單線程的,處理需要排隊(duì))
單 JVM 環(huán)境:采用 token 加 redis 或 token 加 jvm 內(nèi)存
處理流程:
數(shù)據(jù)提交前要向服務(wù)的申請(qǐng) token,token 放到 redis 或 jvm 內(nèi)存,token 有效時(shí)間
提交后后臺(tái)校驗(yàn) token,同時(shí)刪除 token,生成新的 token 返回
token 特點(diǎn):要申請(qǐng),一次有效性,可以限流
基于Token方式防止API接口冪等
客戶端每次在調(diào)用接口的時(shí)候,需要在請(qǐng)求頭中,傳遞令牌參數(shù),每次令牌只能用一次。
一旦使用之后,就會(huì)被刪除,這樣可以有效防止重復(fù)提交。
步驟:
- 生成令牌接口
- 接口中獲取令牌驗(yàn)證
實(shí)戰(zhàn)教程
要用到aop跟Redis , 所以在pom中添加
<!-- Redis-Jedis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- springboot-aop 技術(shù) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>這是通過注解實(shí)現(xiàn)接口冪等性,先寫Redis邏輯
@Component
public class BaseRedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, Object data, Long timeout) {
if (data instanceof String) {
String value = (String) data;
// 往Redis存值
stringRedisTemplate.opsForValue().set(key, value);
}
if (timeout != null) {
// 帶時(shí)間緩存
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
/**
* 查看是否有值
* @param key 值
* @return
*/
public Object getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 刪除Redis
* @param key 值
*/
public void delKey(String key) {
stringRedisTemplate.delete(key);
}
}然后寫怎么生成token,保證每個(gè)token只用一次
@Component
public class RedisToken {
@Autowired
private BaseRedisService baseRedisService;
/** 緩存指定時(shí)間200秒 */
private static final long TOKENTIMEOUT = 200;
/**
* 生成Token
*/
public String getToken(){
String token = UUID.randomUUID().toString();
// 將token放到Redis中,用UUID保證唯一性
baseRedisService.setString(token, token, TOKENTIMEOUT);
return token;
}
public synchronized boolean findToken(String tokenKey) {
String tokenValue = (String) baseRedisService.getString(tokenKey);
// 如果能夠獲取該(從redis獲取令牌)令牌(將當(dāng)前令牌刪除掉) 就直接執(zhí)行該訪問的業(yè)務(wù)邏輯
if (StringUtils.isEmpty(tokenValue)) {
return false;
}
// 保證每個(gè)接口對(duì)應(yīng)的token 只能訪問一次,保證接口冪等性問題,用完直接刪掉
baseRedisService.delKey(tokenValue);
return true;
}
}寫一個(gè)工具類 請(qǐng)求是通過http請(qǐng)求還是from提交過來的,大部分都是form提交來的
public interface ConstantUtils {
/**
* http 中攜帶的請(qǐng)求
*/
static final String EXTAPIHEAD = "head";
/**
* from 中提交的請(qǐng)求
*/
static final String EXTAPIFROM = "from";
}寫好了 現(xiàn)在就寫我們的注解了,沒帶參數(shù)的是前后端不分離,直接跳頁面,獲取到token,帶參數(shù)前后端不分離的
- 帶參數(shù)的
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
String value();
}- 不帶參數(shù)的
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {
}寫好這個(gè) 要是aop切點(diǎn),要把注解切入進(jìn)去
@Aspect
@Component
public class ExtApiAopIdempotent {
@Autowired
private RedisToken redisToken;
// 1.使用AOP環(huán)繞通知攔截所有訪問(controller)
@Pointcut("execution(public * com.yuyi.controller.*.*(..))")
public void rlAop() {
}
/**
* 封裝數(shù)據(jù)
*/
public HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
}
/**
* 前置通知
*/
@Before("rlAop()")
public void before(JoinPoint point) {
// 獲取被增強(qiáng)的方法相關(guān)信息 - 查看方法上是否有次注解
MethodSignature signature = (MethodSignature) point.getSignature();
ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);
if (extApiToken != null) {
// 可以放入到AOP代碼 前置通知
getRequest().setAttribute("token", redisToken.getToken());
}
}
/**
* 環(huán)繞通知
*/
@Around("rlAop()")
public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 獲取被增強(qiáng)的方法相關(guān)信息 - 查看方法上是否有次注解
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
ExtApiIdempotent declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
if (declaredAnnotation != null) {
String values = declaredAnnotation.value();
String token = null;
HttpServletRequest request = getRequest();
if (values.equals(ConstantUtils.EXTAPIHEAD)) {
token = request.getHeader("token");
} else {
token = request.getParameter("token");
}
// 獲取不到token
if (StringUtils.isEmpty(token)) {
return ResultTool.error(ExceptionNume.PARAMETER_ERROR);
}
// 接口獲取對(duì)應(yīng)的令牌,如果能夠獲取該(從redis獲取令牌)令牌(將當(dāng)前令牌刪除掉) 就直接執(zhí)行該訪問的業(yè)務(wù)邏輯
boolean isToken = redisToken.findToken(token);
// 接口獲取對(duì)應(yīng)的令牌,如果獲取不到該令牌 直接返回請(qǐng)勿重復(fù)提交
if (!isToken) {
return ResultTool.error(ExceptionNume.REPEATED_SUBMISSION);
}
}
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}
}controller層 大家可以測(cè)一下
@Autowired
private OrderInfoDAO infoDAO;
@Autowired
private RedisToken token;
// @Autowired
// private RedisTokenUtils redisTokenUtils;
//
// 從redis中獲取Token
@RequestMapping("/redisToken")
public String getRedisToken() {
return token.getToken();
}
@RequestMapping("/addOrderExtApiIdempotent")
@ExtApiIdempotent(ConstantUtils.EXTAPIFROM)
public ResultBO<?> addOrderExtApiIdempotent(
@RequestParam String orderName,
@RequestParam String orderDes
) {
int result = infoDAO.addOrderInfo(orderName, orderDes);
return ResultTool.success(result);
}


保證了只能請(qǐng)求一次。前后端沒有分離的,@ExtApiToken帶上注解會(huì)自動(dòng)吧token攜帶過去

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于maven環(huán)境的安裝及maven集成idea環(huán)境的問題
Maven 是一個(gè)基于 Java 的工具,所以要做的第一件事情就是安裝 JDK。本文重點(diǎn)給大家介紹關(guān)于maven環(huán)境的安裝及和idea環(huán)境的集成問題,感興趣的朋友一起看看吧2021-09-09
java結(jié)合prometheus如何實(shí)現(xiàn)自定義數(shù)據(jù)監(jiān)控
文章介紹了如何配置Prometheus監(jiān)控系統(tǒng),包括配置文件prometheus.yml、被監(jiān)控應(yīng)用的指標(biāo)暴露配置以及自定義監(jiān)控指標(biāo)的實(shí)現(xiàn),同時(shí),還詳細(xì)說明了監(jiān)控應(yīng)用如何通過Prometheus API獲取數(shù)據(jù)、處理數(shù)據(jù)并返回結(jié)果2024-12-12
java?Springboot對(duì)接開發(fā)微信支付詳細(xì)流程
最近要做一個(gè)微信小程序,需要微信支付,所以研究了下怎么在java上集成微信支付功能,下面這篇文章主要給大家介紹了關(guān)于java?Springboot對(duì)接開發(fā)微信支付的相關(guān)資料,需要的朋友可以參考下2024-08-08
java 查找list中重復(fù)數(shù)據(jù)實(shí)例詳解
這篇文章主要介紹了java 查找list中重復(fù)數(shù)據(jù)實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下2017-01-01
SpringMVC架構(gòu)的項(xiàng)目 js,css等靜態(tài)文件導(dǎo)入有問題的解決方法
下面小編就為大家?guī)硪黄猄pringMVC架構(gòu)的項(xiàng)目 js,css等靜態(tài)文件導(dǎo)入有問題的解決方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-10-10

