Spring Boot+Redis+攔截器+自定義Annotation實現接口自動冪等

2020-12-17 黑馬程式設計師

在實際的開發項目中,一個對外暴露的接口往往會面臨很多次請求,我們來解釋一下冪等的概念:任意多次執行所產生的影響均與一次執行的影響相同。按照這個含義,最終的含義就是 對資料庫的影響只能是一次性的,不能重複處理。如何保證其冪等性,通常有以下手段:

資料庫建立唯一性索引,可以保證最終插入資料庫的只有一條數據token機制,每次接口請求前先獲取一個token,然後再下次請求的時候在請求的header體中加上這個token,後臺進行驗證,如果驗證通過刪除token,下次請求再次判斷token悲觀鎖或者樂觀鎖,悲觀鎖可以保證每次for update的時候其他sql無法update數據(在資料庫引擎是innodb的時候,select的條件必須是唯一索引,防止鎖全表)先查詢後判斷,首先通過查詢資料庫是否存在數據,如果存在證明已經請求過了,直接拒絕該請求,如果沒有存在,就證明是第一次進來,直接放行。redis實現自動冪等的原理圖:

一:搭建redis的服務Api

1:首先是搭建redis伺服器。

2:引入springboot中到的redis的stater,或者Spring封裝的jedis也可以,後面主要用到的api就是它的set方法和exists方法,這裡我們使用springboot的封裝好的redisTemplate

/*** redis工具類 */@Componentpublic class RedisService { @Autowired private RedisTemplate redisTemplate; /** * 寫入緩存 * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 寫入緩存設置時效時間 * @param key * @param value * @return */ public boolean setEx(final String key, Object value, Long expireTime) { boolean result = false; try { ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); operations.set(key, value); redisTemplate.expire(key, expireTime, TimeUnit.SECONDS); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * 判斷緩存中是否有對應的value * @param key * @return */ public boolean exists(final String key) { return redisTemplate.hasKey(key); } /** * 讀取緩存 * @param key * @return */ public Object get(final String key) { Object result = null; ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * 刪除對應的value * @param key */ public boolean remove(final String key) { if (exists(key)) { Boolean delete = redisTemplate.delete(key); return delete; } return false; }}

二:自定義註解AutoIdempotent

自定義一個註解,定義此註解的主要目的是把它添加在需要實現冪等的方法上,凡是某個方法註解了它,都會實現自動冪等。後臺利用反射如果掃描到這個註解,就會處理這個方法實現自動冪等,使用元註解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在運行時

@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface AutoIdempotent {}

三:token創建和檢驗

1:token服務接口

我們新建一個接口,創建token服務,裡面主要是兩個方法,一個用來創建token,一個用來驗證token。創建token主要產生的是一個字符串,檢驗token的話主要是傳達request對象,為什麼要傳request對象呢?主要作用就是獲取header裡面的token,然後檢驗,通過拋出的Exception來獲取具體的報錯信息返回給前端

public interface TokenService {/** * 創建token * @return */ public String createToken(); /** * 檢驗token * @param request * @return */ public boolean checkToken(HttpServletRequest request) throws Exception;}

2:token的服務實現類

token引用了redis服務,創建token採用隨機算法工具類生成隨機uuid字符串,然後放入到redis中(為了防止數據的冗餘保留,這裡設置過期時間為10000秒,具體可視業務而定),如果放入成功,最後返回這個token值。checkToken方法就是從header中獲取token到值(如果header中拿不到,就從paramter中獲取),如若不存在,直接拋出異常。這個異常信息可以被攔截器捕捉到,然後返回給前端。

@Servicepublic class TokenServiceImpl implements TokenService {@Autowired private RedisService redisService; /** * 創建token * * @return */ @Override public String createToken() { String str = RandomUtil.randomUUID(); StrBuilder token = new StrBuilder(); try { token.append(Constant.Redis.TOKEN_PREFIX).append(str); redisService.setEx(token.toString(), token.toString(),10000L); boolean notEmpty = StrUtil.isNotEmpty(token.toString()); if (notEmpty) { return token.toString(); } }catch (Exception ex){ ex.printStackTrace(); } return null; } /** * 檢驗token * * @param request * @return */ @Override public boolean checkToken(HttpServletRequest request) throws Exception { String token = request.getHeader(Constant.TOKEN_NAME); if (StrUtil.isBlank(token)) {// header中不存在token token = request.getParameter(Constant.TOKEN_NAME); if (StrUtil.isBlank(token)) {// parameter中也不存在token throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100); } } if (!redisService.exists(token)) { throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200); } boolean remove = redisService.remove(token); if (!remove) { throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200); } return true; }}

四:攔截器的配置

1:web配置類,實現WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置類中,這樣我們到攔截器才能生效,注意使用@Configuration註解,這樣在容器啟動是時候就可以添加進入context中

@Configurationpublic class WebConfiguration extends WebMvcConfigurerAdapter {@Resource private AutoIdempotentInterceptor autoIdempotentInterceptor; /** * 添加攔截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(autoIdempotentInterceptor); super.addInterceptors(registry); }}

2:攔截處理器:主要的功能是攔截掃描到AutoIdempotent到註解到方法,然後調用tokenService的checkToken()方法校驗token是否正確,如果捕捉到異常就將異常信息渲染成json返回給前端

/*** 攔截器 */@Componentpublic class AutoIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; /** * 預處理 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //被ApiIdempotment標記的掃描 AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class); if (methodAnnotation != null) { try { return tokenService.checkToken(request);// 冪等性校驗, 校驗通過則放行, 校驗失敗則拋出異常, 並通過統一異常處理返回友好提示 }catch (Exception ex){ ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage()); writeReturnJson(response, JSONUtil.toJsonStr(failedResult)); throw ex; } } //必須返回true,否則會被攔截一切請求 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 { } /** * 返回的json值 * @param response * @param json * @throws Exception */ private void writeReturnJson(HttpServletResponse response, String json) throws Exception{ PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=utf-8"); try { writer = response.getWriter(); writer.print(json); } catch (IOException e) { } finally { if (writer != null) writer.close(); } }}

五:測試用例

1:模擬業務請求類

首先我們需要通過/get/token路徑通過getToken()方法去獲取具體的token,然後我們調用testIdempotence方法,這個方法上面註解了@AutoIdempotent,攔截器會攔截所有的請求,當判斷到處理的方法上面有該註解的時候,就會調用TokenService中的checkToken()方法,如果捕獲到異常會將異常拋出調用者,下面我們來模擬請求一下:

@RestControllerpublic class BusinessController {@Resource private TokenService tokenService; @Resource private TestService testService; @PostMapping("/get/token") public String getToken(){ String token = tokenService.createToken(); if (StrUtil.isNotEmpty(token)) { ResultVo resultVo = new ResultVo(); resultVo.setCode(Constant.code_success); resultVo.setMessage(Constant.SUCCESS); resultVo.setData(token); return JSONUtil.toJsonStr(resultVo); } return StrUtil.EMPTY; } @AutoIdempotent @PostMapping("/test/Idempotence") public String testIdempotence() { String businessResult = testService.testIdempotence(); if (StrUtil.isNotEmpty(businessResult)) { ResultVo successResult = ResultVo.getSuccessResult(businessResult); return JSONUtil.toJsonStr(successResult); } return StrUtil.EMPTY; }}

2:使用postman請求

首先訪問get/token路徑獲取到具體到token:

利用獲取到到token,然後放到具體請求到header中,可以看到第一次請求成功,接著我們請求第二次:

第二次請求,返回到是重複性操作,可見重複性驗證通過,再多次請求到時候我們只讓其第一次成功,第二次就是失敗:

六:總結

本篇博客介紹了使用springboot和攔截器、redis來優雅的實現接口冪等,對於冪等在實際的開發過程中是十分重要的,因為一個接口可能會被無數的客戶端調用,如何保證其不影響後臺的業務處理,如何保證其只影響數據一次是非常重要的,它可以防止產生髒數據或者亂數據,也可以減少並發量,實乃十分有益的一件事。而傳統的做法是每次判斷數據,這種做法不夠智能化和自動化,比較麻煩。而今天的這種自動化處理也可以提升程序的伸縮性。

作者:慕容千語

相關焦點

  • Springboot-Redis分布式鎖
    分布式鎖的實現有很多種,比如基於資料庫、 zookeeper 等,本文主要介紹使用 Redis 做分布式鎖的方式,並封裝成spring boot starter,方便使用一. Redis 分布式鎖的實現以及存在的問題鎖是針對某個資源,保證其訪問的互斥性,在實際使用當中,這個資源一般是一個字符串。
  • 接口文檔從Swagger升級成knife4j使用教程
    dependency部分引入了具體的knife4j-spring-boot-starter包。;import com.google.common.base.Predicates;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration
  • Redis是如何實現點讚、取消點讚的?
    一個牛逼的多級緩存實現方案基於redis分布式鎖實現「秒殺」(含代碼)百億數據量下,掌握這些Redis技巧你就能Hold全場9個提升逼格的redis命令get個新技能:redis實現自動補全利用 Redis 實現「附近的人」功能!
  • Spring boot 基於註解方式配置datasource
    Spring boot 基於註解方式配置datasourceXml配置我們先來回顧下,使用xml配置數據源。-- 讀取參數配置 --><list><value>classpath:dbconfig.properties</value><value>classpath:redis.properties
  • Spring Boot實現定時任務新解,你是否能get到?
    在啟動類上加@EnableScheduling註解,如下,package com.example.demo;import com.example.demo.properties.ApplicationPro;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication
  • Spring Boot面試題(2020最新版)
    server.port = 8090安全如何實現 Spring Boot 應用程式的安全性?為了實現 Spring Boot 的安全性,我們使用 spring-boot-starter-security 依賴項,並且必須添加安全配置。它只需要很少的代碼。
  • 「純手打」2萬字長文從0開始Spring Boot(上)
    第一次連接資料庫上面的例子讓我們實現了接口返回數據,舉一反三你可以寫出很多複雜的接口,但是,沒有資料庫的支持,都是死數據,沒意思,對吧,廢話不多說,不搞 JDBC 不搞 hibernate ,直接上現代化 mybatis 框架Spring 在之前集成 mybatis 相當複雜,需要配置很多的xml。
  • 一篇帶給你SpringBoot + Spring Security入門
    >                 <groupId>org.springframework.boot</groupId>                 <artifactId>spring-boot-starter-security</artifactId>
  • 基礎篇:Spring Boot入門體驗(圖文教程)
    在 【main/java/com.xpwi.springboot】包下新建一個【HelloController】:packagecom.xpwi.springboot;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController
  • 程式設計師:一步一步輕鬆實現SpringBoot整合Redis緩存,看了不後悔
    前言隨著技術的發展,程序框架也越來越多,非常考驗程序的學習能力,但是框架的產生在程式設計師開發過程中也提供很多便利,框架可以為程式設計師減少許多工作量,比如spring boot 、mybatis等都可以提高開發人員的工作效率,今天就來聊聊spring boot與redis 的整合。
  • Spring Boot 啟動事件和監聽器,太強大了!
    Spring Boot 基礎的構建這裡就不介紹了,如果你對 Spring Boot 還不是很熟悉,或者只是會簡單的使用,那還是建議你深入學習下吧,推薦這個 Spring Boot 學習倉庫,歡迎 Star 關注:https://github.com/javastacks/spring-boot-best-practice1、新建監聽器
  • 芋道 Spring Boot JPA 入門(一)之快速入門
    根據約定好的方法名規則,自動生成對應的查詢操作。使用 @Query 註解,自定義 SQL 。所以,絕大多數情況下,我們無需編寫代碼,直接調用 JPA 的 API 。也因此,在我們使用的 Spring Data JPA 的項目中,如果想要替換底層使用的 JPA 實現框架,在未使用到相關 JPA 實現框架的特殊特性的情況下,可以透明替換。
  • 史上最全spring boot實戰文檔,吃透這些,面試幹掉80%對手
    最大的重要性是:springcloud是一個基於springboot實現的一系 列框架的集合,用來提供全局的服務治理方案。springcloud要基於springboot來實現,離不開springboot。如果要學習源碼,當然還是SpringBoot最適合不過了。
  • Springboot+MybatisPlus高效實現增刪改查
    <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>
  • redis +aop+註解優雅的寫代碼,挑戰百萬年薪
    定義註解類:定義redis切面類業務代碼改造帥傑當時就抬槓,你這也沒有省代碼,怎麼代碼還比以前更多了呢。那我們來看我們整個系統省了多少代碼,我們有上萬個查詢接口,每個接口省10行代碼,我們省了多少行?那麼這些底層是怎麼實現的呢Aop參照我寫的其他文章,這裡不做解答,註解的底層:從反編譯的信息來看,註解繼承了java.lang.annotation.Annotation這個類是被動的元數據
  • Spring Boot優雅地處理404異常
    2020-11-19 19:04:04.280 [http-nio-8888-exec-7] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping:313 - Looking up handler method for path /error2020-11-19
  • 5步學完spring boot單元測試,與postman有什麼優點?
    目前大多數項目已經實現了前後端分離。作為後端開發,在開發完成後很有必要進行接口的自測。目前主流的postman,現在還有新出來postwoman,實際上idea也有支持HTTP測試。平時一般都是用上面的工具自測也就夠了,但是他們都是基於接口層面的http請求測試!對於單元測試,他們都不能很好的實現,還是springboot的單元測試最合適的。