您好,登錄后才能下訂單哦!
SpringBoot
?是為了簡化?Spring
?應用的創建、運行、調試、部署等一系列問題而誕生的產物,自動裝配的特性讓我們可以更好的關注業務本身而不是外部的XML配置,我們只需遵循規范,引入相關的依賴就可以輕易的搭建出一個 WEB 工程
在平時開發中,如果網速比較慢的情況下,用戶提交表單后,發現服務器半天都沒有響應,那么用戶可能會以為是自己沒有提交表單,就會再點擊提交按鈕重復提交表單,我們在開發中必須防止表單重復提交….
字面意思就是提交了很多次,這種情況一般都是前端給你挖的坑….
前段時間在開發中遇到一個這樣的問題;前端小哥哥調用接口的時候存在?循環調用?的問題,正常情況下發送一個請求添加一條數據,結果變成了同一時刻并發的發送了 N 個請求,服務端瞬間懵逼的插入了 N 條一模一樣的數據,前端小哥哥也不知道問題在哪里(恩...坑就這樣挖好了,反正不填坑,氣死你
) 這時候咋辦呢;后端干唄,反正臟活累活,背鍋的事情也沒少干了,多一件也不多….
利用?自定義注解
、Spring Aop
、Guava Cache
?實現表單防重復提交(不適用于分布式哦,后面會講分布式方式...
)
非常簡單…
在?pom.xml
?中添加上?spring-boot-starter-web
?的依賴即可
<dependencies>
<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>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
</dependencies>
創建一個?LocalLock
?注解,簡單點就一個?key
?可以了,由于暫時未用到?redis
?所以?expire
?是擺設….
package com.battcn.annotation;
import java.lang.annotation.*;
/**
* 鎖的注解
*
* @author Levin
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
/**
* @author fly
*/
String key() default "";
/**
* 過期時間 TODO 由于用的 guava 暫時就忽略這屬性吧 集成 redis 需要用到
*
* @author fly
*/
int expire() default 5;
}
首先通過?CacheBuilder.newBuilder()
?構建出緩存對象,設置好過期時間;其目的就是為了防止因程序崩潰鎖得不到釋放(當然如果單機這種方式程序都炸了,鎖早沒了;但這不妨礙我們寫好點)
在具體的?interceptor()
?方法上采用的是?Around(環繞增強)
?,所有帶?LocalLock
?注解的都將被切面處理;
如果想更為靈活,key 的生成規則可以定義成接口形式(可以參考:org.springframework.cache.interceptor.KeyGenerator),這里就偷個懶了;
package com.battcn.interceptor;
import com.battcn.annotation.LocalLock;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* 本章先基于 本地緩存來做,后續講解 redis 方案
*
* @author Levin
* @since 2018/6/12 0012
*/
@Aspect
@Configuration
public class LockMethodInterceptor {
private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
// 最大緩存 100 個
.maximumSize(1000)
// 設置寫緩存后 5 秒鐘過期
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.LocalLock)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
LocalLock localLock = method.getAnnotation(LocalLock.class);
String key = getKey(localLock.key(), pjp.getArgs());
if (!StringUtils.isEmpty(key)) {
if (CACHES.getIfPresent(key) != null) {
throw new RuntimeException("請勿重復請求");
}
// 如果是第一次請求,就將 key 當前對象壓入緩存中
CACHES.put(key, key);
}
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("服務器異常");
} finally {
// TODO 為了演示效果,這里就不調用 CACHES.invalidate(key); 代碼了
}
}
/**
* key 的生成策略,如果想靈活可以寫成接口與實現類的方式(TODO 后續講解)
*
* @param keyExpress 表達式
* @param args 參數
* @return 生成的key
*/
private String getKey(String keyExpress, Object[] args) {
for (int i = 0; i < args.length; i++) {
keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
}
return keyExpress;
}
}
在接口上添加?@LocalLock(key = "book:arg[0]")
;意味著會將?arg[0]
?替換成第一個參數的值,生成后的新 key 將被緩存起來;
package com.battcn.controller;
import com.battcn.annotation.LocalLock;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* BookController
*
* @author Levin
* @since 2018/6/06 0031
*/
@RestController
@RequestMapping("/books")
public class BookController {
@LocalLock(key = "book:arg[0]")
@GetMapping
public String query(@RequestParam String token) {
return "success - " + token;
}
}
package com.battcn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Levin
*/
@SpringBootApplication
public class Chapter21Application {
public static void main(String[] args) {
SpringApplication.run(Chapter21Application.class, args);
}
}
完成準備事項后,啟動?Chapter21Application
?自行測試即可,測試手段相信大伙都不陌生了,如?瀏覽器
、postman
、junit
、swagger
,此處基于?postman
,如果你覺得自帶的異常信息不夠友好,那么配上巧用SpringBoot輕松搞定全局異常?可以輕松搞定…
第一次請求
第二次請求
本文的重點是你有沒有收獲與成長,其余的都不重要,希望讀者們能謹記這一點。同時我經過多年的收藏目前也算收集到了一套完整的學習資料,包括但不限于:分布式架構、高可擴展、高性能、高并發、Jvm性能調優、Spring,MyBatis,Nginx源碼分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多個知識點高級進階干貨,希望對想成為架構師的朋友有一定的參考和幫助
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。