javascript
你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?(上篇)
封面:學校籃球場上的云
紙上得來終覺淺,絕知此事要躬行
注意: 本文 SpringBoot 版本為 2.5.2; JDK 版本 為 jdk 11.
后續文章👉 從瀏覽器發送請求給SpringBoot后端時,是如何準確找到哪個接口的?(下篇)
前言:
在寫文章的時候,我都會習慣性的記錄下,是什么因素促使我去寫的這篇文章。并竟對于感興趣的東西,寫起來也上心,也更得心應手,文章質量相應也更高。當然更多的是想和更多人分享自己的看法,與更多的人一起交流。“三人行,必有我師焉” ,歡迎大家留言評論交流。
寫這篇文章的原因是在于昨天一個學 Go 語言的后端小伙伴,問了我一個問題。
問題大致如下:
為什么瀏覽器向后端發起請求時,就知道要找的是哪一個接口?采用了什么樣的匹配規則呢?
SpringBoot 后端是如何存儲 API 接口信息的?又是拿什么數據結構存儲的呢?
@ResponseBody @GetMapping("/test") public String test(){return "test"; }說實話,聽他問完,我感覺我又不夠卷了,簡直靈魂拷問,我一個答不出來。我們一起去看看吧。
我對于SpringBoot的框架源碼閱讀經驗可能就一篇👉SpringBoot自動裝配原理算是吧,所以在一定程度上我個人對于SpringBoot 框架理解的還是非常淺顯的。
如果文章中有不足之處,請你一定要及時批正!在此鄭重感謝。
一、注解派生概念
在java體系中,類是可以被繼承,接口可以被實現。但是注解沒有這些概念,而是有一個派生的概念。舉例,注解A。被標記了在注解B頭上,那么我們可以說注解B就是注解A的派生。
如:
就像 注解 @GetMapping 上就還有一個 @RequestMapping(method = RequestMethod.GET) ,所以我們本質上也是使用了 @RequestMapping注解。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @RequestMapping(method = RequestMethod.GET) public @interface GetMapping {}還有 @Controller 和 @RestController 也是如此。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { }廢話不多說,直接肝啦。
二、啟動流程
更前面的不去做探究了,我們直接到這個入口處。
做了一個大致的分析流程圖給大家做參考,也是我個人探究的路線。
2.1、AbstractHandlerMethodMapping
/** HandlerMapping實現的抽象基類,定義了請求和HandlerMethod之間的映射。 對于每個注冊的處理程序方法,一個唯一的映射由定義映射類型<T>細節的子類維護 */ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {// .../**在初始化時檢測處理程序方法。 可以說是入口處啦*/@Overridepublic void afterPropertiesSet() {initHandlerMethods();}/**掃描 ApplicationContext 中的 bean,檢測和注冊處理程序方法。 */protected void initHandlerMethods() {//getCandidateBeanNames() :確定應用程序上下文中候選 bean 的名稱。for (String beanName : getCandidateBeanNames()) {if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {//確定指定候選 bean 的類型,如果標識為處理程序類型,則調用detectHandlerMethods // 這里的處理程序 就為我們在controller 中書寫的那些接口方法processCandidateBean(beanName);}}// 這里的邏輯不做討論啦handlerMethodsInitialized(getHandlerMethods());}// ... }只有當掃描到 是由@RestController 或@RequestMapping 注解修飾時,進入 processCandidateBean 方法,這個時候才是我們要找的東西。其他的bean我們不是我們討論的點,不做討論。
我們來接著看看 processCandidateBean的處理邏輯,它做了一些什么事情。
/** 確定指定候選 bean 的類型,如果標識為處理程序類型,則調用detectHandlerMethods 。 */ protected void processCandidateBean(String beanName) {Class<?> beanType = null;try {// 確定注入的bean 類型beanType = obtainApplicationContext().getType(beanName);}catch (Throwable ex) {// 無法解析的beanif (logger.isTraceEnabled()) {logger.trace("Could not resolve type for bean '" + beanName + "'", ex);}}//isHandler 方法判斷是否是web資源類。if (beanType != null && isHandler(beanType)) {// 算是這條線路上重點啦detectHandlerMethods(beanName);} }isHandler 方法判斷是否是web資源類。當一個類被標記了 @Controller 或者@RequestMapping。 注意 @RestController 是@Controller的派生類。所以這里只用判斷 @Controller 或者@RequestMapping就行了。
另外 isHandler 定義在 AbstractHandlerMethodMapping< T > ,實現在 RequestMappingHandlerMapping
/** 給定類型是否是具有處理程序方法的處理程序。處理程序就是我們寫的 Controller 類中的接口方法 期望處理程序具有類型級別的Controller注釋或類型級別的RequestMapping注釋。 */ @Override protected boolean isHandler(Class<?> beanType) {return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }繼續往下:
2.2、detectHandlerMethods() 方法
這個方法detectHandlerMethods(beanName);它是做什么的呢?
它的方法注釋為:在指定的處理程序 bean 中查找處理程序方法。
其實 detectHandlerMethods方法就是真正開始解析Method的邏輯。通過解析Method上的@RequestMapping或者其他派生的注解。生成請求信息。
/** 在指定的處理程序 bean 中查找處理程序方法。*/protected void detectHandlerMethods(Object handler) {Class<?> handlerType = (handler instanceof String ?obtainApplicationContext().getType((String) handler) : handler.getClass());if (handlerType != null) {//返回給定類的用戶定義類:通常只是給定的類,但如果是 CGLIB 生成的子類,則返回原始類。Class<?> userType = ClassUtils.getUserClass(handlerType);//selectMethods://根據相關元數據的查找,選擇給定目標類型的方法。// 調用者通過MethodIntrospector.MetadataLookup參數定義感興趣的方法,允許將關聯的元數據收集到結果映射中// 簡單理解 :解析RequestMapping信息Map<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {try {//為處理程序方法提供映射。 不能為其提供映射的方法不是處理程序方法return getMappingForMethod(method, userType);}catch (Throwable ex) {throw new IllegalStateException("Invalid mapping on handler class [" +userType.getName() + "]: " + method, ex);}});if (logger.isTraceEnabled()) {logger.trace(formatMappings(userType, methods));}else if (mappingsLogger.isDebugEnabled()) {mappingsLogger.debug(formatMappings(userType, methods));}// 這里將解析的信息,循環進行注冊methods.forEach((method, mapping) -> {Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);registerHandlerMethod(handler, invocableMethod, mapping);});}}2.3、getMappingForMethod
getMappingForMethod定義在 AbstractHandlerMethodMapping< T > ,實現在 RequestMappingHandlerMapping 類下
這里簡單說就是 將類層次的RequestMapping和方法級別的RequestMapping結合 (createRequestMappingInfo)
/** 使用方法和類型級別的RequestMapping注解來創建RequestMappingInfo。 */ @Override @Nullable protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {RequestMappingInfo info = createRequestMappingInfo(method);if (info != null) {RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);if (typeInfo != null) {info = typeInfo.combine(info);}//獲取類上 String prefix = getPathPrefix(handlerType);if (prefix != null) {info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);}}return info; }createRequestMappingInfo:
/** 委托createRequestMappingInfo(RequestMapping, RequestCondition) ,根據提供的annotatedElement是類還是方法提供適當的自定義RequestCondition 。 */ @Nullable private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {//主要是 解析 Method 上的 @RequestMapping 信息RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);RequestCondition<?> condition = (element instanceof Class ?getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); }2.4、MethodIntrospector.selectMethods()方法
根據相關元數據的查找,選擇給定目標類型的方法。
很多雜七雜八的東西在里面,很難說清楚,這里只簡單說了一下。
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {final Map<Method, T> methodMap = new LinkedHashMap<>();Set<Class<?>> handlerTypes = new LinkedHashSet<>();Class<?> specificHandlerType = null;if (!Proxy.isProxyClass(targetType)) {specificHandlerType = ClassUtils.getUserClass(targetType);handlerTypes.add(specificHandlerType);}handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));for (Class<?> currentHandlerType : handlerTypes) {final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);//對給定類和超類(或給定接口和超接口)的所有匹配方法執行給定的回調操作。ReflectionUtils.doWithMethods(currentHandlerType, method -> {Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);T result = metadataLookup.inspect(specificMethod);if (result != null) {// BridgeMethodResolver :給定一個合成bridge Method返回被橋接的Method 。 //當擴展其方法具有參數化參數的參數化類型時,編譯器可能會創建橋接方法。 在運行時調用期間,可以通過反射調用和/或使用橋接Method //findBridgedMethod : 找到提供的bridge Method的原始方法。Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {methodMap.put(specificMethod, result);}}}, ReflectionUtils.USER_DECLARED_METHODS);}return methodMap; }方法上的doc注釋:
根據相關元數據的查找,選擇給定目標類型的方法。
調用者通過MethodIntrospector.MetadataLookup參數定義感興趣的方法,允許將關聯的元數據收集到結果映射中
一眼兩言說不清楚,直接貼一張debug 的圖片給大家看一下。
2.5、registerHandlerMethod 方法
這一段代碼其本質就是 這里將解析出來的信息,循環進行注冊
methods.forEach((method, mapping) -> {//選擇目標類型上的可調用方法:如果實際公開在目標類型上,則給定方法本身,或者目標類型的接口之一或目標類型本身上的相應方法。// 簡單理解返回個方法吧Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);registerHandlerMethod(handler, invocableMethod, mapping); }); protected void registerHandlerMethod(Object handler, Method method, T mapping) {this.mappingRegistry.register(mapping, handler, method); }這里的 this.mappingRegistry 是 AbstractHandlerMethodMapping<T> 的一個內部類。
MappingRegistry : doc注釋:一個注冊表,它維護到處理程序方法的所有映射,公開執行查找的方法并提供并發訪問。
對于它的結構,在這里不做探討啦。感興趣,可以點進去繼續看看。
我們繼續探究我們 register 方法做了什么
public void register(T mapping, Object handler, Method method) {this.readWriteLock.writeLock().lock();try {//創建 HandlerMethod 實例。HandlerMethod handlerMethod = createHandlerMethod(handler, method);//驗證方法映射validateMethodMapping(handlerMethod, mapping);//這里就是直接獲取路徑 mapping 的值是 GET[/login]// 獲取出來后 就是 /loginSet<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);for (String path : directPaths) {//this.pathLookup 它的定義如下:// private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();// 其實new 就是一個 new LinkedHashMap<>();// 這里就是將 path 作為key ,mapping作為value 存起來this.pathLookup.add(path, mapping);}String name = null;// 這里的意思可以歸納為:if (getNamingStrategy() != null) {///確定給定 HandlerMethod 和映射的名稱。name = getNamingStrategy().getName(handlerMethod, mapping);addMappingName(name, handlerMethod);}// 下面幾行是處理跨域問題的,不是我們本章討論的。大家感興趣可以去看看。CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);if (corsConfig != null) {corsConfig.validateAllowCredentials();this.corsLookup.put(handlerMethod, corsConfig);}this.registry.put(mapping,new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));}finally {this.readWriteLock.writeLock().unlock();} } this.registry.put(mapping,new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));這里的 this.registry 的定義如下:private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
不同的方法走到這,其實差別不是很大
其實看完這個啟動流程,對于我們剛開始的三個問題,我們大概率可以找到其中兩個答案了。
2.6、小結
你們 SpringBoot 后端框架是如何存儲API接口的信息的?是拿什么數據結構存儲的呢?
第一個答案:大致就是和MappingRegistry 這個注冊表類相關.
第二個答案:我們之前看到存儲信息時,都是 HashMap 相關的類來存儲的,那么我們可以知道它底層的數據結構就是 數組+鏈表+紅黑樹
注意: 本文 SpringBoot 版本為 2.5.2;JDK 版本 為 jdk 11.
并未針對多個版本進行比較,但是推測下來,多半都是如此.
那么我們的下一步就是去查看 SpringBoot 請求時,是如何找到 對應的 接口的。哪里才又是我們的一個重點。
三、小結流程
四、后續
后續文章👉從瀏覽器發送請求給SpringBoot后端時,是如何準確找到哪個接口的?
若不是小伙伴提起那三問,我想我也不會有如此興致,去一步一步Debug閱讀相關源碼,此文多半可能會胎死腹中了。
在此非常感謝 @小宇。不瞞大家,他又邀請我一起去讀 ORM 框架源碼了。不過得好好等上一段時間了。
個人所談:
閱讀源碼的過程中,其實真的是充滿有趣和枯燥的。
讀懂了一些關鍵東西,就開心的不得了;而像“又忘記debug到哪了,思路又涼了",就會開始滿心抱怨(我常常想罵上一兩句)。然后就繼續苦逼的去看。
大家好,我是博主寧在春:主頁
一名喜歡文藝卻踏上編程這條道路的小青年。
希望:我們,待別日相見時,都已有所成。
另外就只能說是在此提供一份個人見解。因文字功底不足、知識缺乏,寫不出十分術語化的文章。
如果覺得本文讓你有所收獲,希望能夠點個贊,給予一份鼓勵。
也希望大家能夠積極交流。如有不足之處,請大家及時批正,在此感謝大家。
掘友可以點點這👉 寧在春 | 掘金
總結
以上是生活随笔為你收集整理的你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?(上篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mybatis元素类型为 “result
- 下一篇: 你知道从浏览器发送请求给SpringBo