寫了一套優雅介面之後,領導讓我給大家講講這背後的技術原理
阿新 • • 發佈:2020-12-02
Hello,各位小夥伴們,早上好~
上週文章[年輕人不講武德,竟然重構出這麼優雅後臺 API 介面](https://studyidea.cn/spring-reactor)我們使用 `@ControllerAdvice`與 `ResponseBodyAdvice` 重構後端的 API 介面,降低了複雜度,減少了重複程式碼,後續介面開發非常簡潔優雅。
知其然而知其所以然,今天這篇文章來聊聊這個註解背後的原理,讓我們徹底掌握這個註解,避免後續踩坑。
另外,有個小夥伴看完上篇文章,覺得這個註解的跟 `Spring Interceptor` 功能很類似,再加上之前還學習了 `Servlet` 體系 `Filter` 功能,不知道這幾個有什麼區別,感覺很混亂。
所以今天這篇文章下面兩個部分出發,詳細解釋一下。
1. `@ControllerAdvice`與 `ResponseBodyAdvice` 註解原理
2. `Filter`,`Interceptor`,`ResponseBodyAdvice` 區別
> 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyidea.cn)
## 從原始碼解析背後的原理
上篇文章中我們看到 `ResponseBodyAdvice`的子類使用 `@ControllerAdvice`註解,大家有沒有好奇,如果我將`@ControllerAdvice`換成 `@Controller` 註解,還能達到上篇文章的效果嗎?
感興趣的小夥伴可以自己嘗試下,這裡小黑哥自己告訴大家結果了,實際測試結果是不行的。
那為什麼一定要與`@ControllerAdvice` 搭配才會生效?
首先我們先檢視一下 `@ControllerAdvice` 的原始碼:
![image-20201128152447563](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085639042-517543487.jpg)
可以看到這個註解上還存在一個我們非常熟悉的 `@Component` 註解。這裡我們可以將 `@ControllerAdvice` 理解成`@Component` 子類,所以其修飾的類也會成為 Spring 中 `Bean`。
> ps:大家可以看下 `@Controller`/`@Service`/`@Repository`,其實也是這個原理。
Spring 容器初始化過程,如果掃描到 `@ControllerAdvice` 註解,將會將其生成一個 `ControllerAdviceBean` Bean。
這個過程程式碼主要位於 `RequestMappingHandlerAdapter#initControllerAdviceCache`:
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085639281-88415342.jpg)
這段程式碼主要分為兩步:
第一步使用 `ControllerAdviceBean#findAnnotatedBeans`獲取所有被 `@ControllerAdvice`修飾的類。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085639670-1521883550.jpg)
第二步將所有實現了`ResponseBodyAdvice` 介面的 Bean 放入到 `requestResponseBodyAdviceBeans` 集合中,後續將會使用該集合。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085640003-726968884.jpg)
這就解釋了為什麼實現 `ResponseBodyAdvice`介面的子類一定要與`@ControllerAdvice`一起使用的原因了。
接下來我們來看下 `ResponseBodyAdvice` 的執行流程。
這裡教給大家一個程式碼除錯的小技巧,當我們不知道一個類在原始碼中如何被呼叫的時候,我們可以使用 IDEA 程式碼除錯功能,然後檢視程式碼呼叫棧。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085640282-1475841540.jpg)
如上面的所示,我們可以很清楚觀察 `ResponseBodyAdvice` 呼叫關係。這裡的類呼叫關係相對還是比較複雜,下面給大家簡化一下。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085640533-135505846.jpg)
前面的邏輯就不說了,就是 Spring MVC 通用流程。重點邏輯位於 `RequestResponseBodyAdviceChain`,我們具體看下原始碼:
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085640676-111976884.jpg)
> 嗯吶嗯吶,請忽略上圖的 ③
其實邏輯非常簡單,遍歷所有的 `ResponseBodyAdvice` 的子類,首先呼叫其 `supports`判斷是否支援,如果支援的呼叫的 `beforeBodyWrite`修改返回資訊。
## `Filter`、`Interceptor`、`ResponseBodyAdvice` 區別
`Filter`屬於 Servlet 元件,所有請求將會先進入 `Filter` ,判斷通過之後才會在進入到真正的具體的請求中。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085640893-1265455715.jpg)
上圖代表是用 Spring MVC 的一個 Web 專案,所有請求將會先進入到 `Filter`,通過之後才會進入到 SpringMVC 中最重要的元件 `DispatchServlet`。
而 `Interceptor` 是 SpringMVC 的元件,它的作用實際上與 `Filter`類似, 只不過的它的作用是位於自定義的 `Controller` 前後。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085641125-174262714.jpg)
不管是 `Filter` 還是 `Interceptor`,它們的作用方法域內只能拿到 `ServletResponse` 的引數,這個時候返回值已經被寫入 `ServletResponse`,我們很難再去修改。
而 `ResponseBodyAdvice`作用時機位於寫入之前,所以這個時候可以很容易拿到原值進行修改。
![](https://img2020.cnblogs.com/other/1419561/202012/1419561-20201202085641394-365034754.jpg)
## 總結
SpringMVC 初始化的過程中,將會掃描所有帶有 `@ControllerAdvice`註解的類,將其生成為 `ControllerAdviceBean`。如果這類剛好為 `ResponseBodyAdvice`介面的子類,Spring 將會為其單獨儲存起來,後續將會封裝到的 `RequestResponseBodyAdviceChain`,使用責任鏈的模式對請求、響應進行處理。
最後我們解釋了一下 `Filter`,`Interceptor`,`ResponseBodyAdvice`區別,從作用範圍上來講:
```
Filter>Interceptor>ResponseBodyAdvice
```
但是前兩者沒辦法修改返回值(時機太晚),只有後者才可以真正在返回值返回之前做到修改。
好了,今天文章就到這裡了,下次我們分享一下如何寫出優雅的 Dubbo 介面,下次見。
> 歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:[studyidea.cn](https://studyidea.cn)