日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?(上篇)

發布時間:2025/3/19 javascript 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 你了解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 請求時,是如何找到 對應的 接口的。哪里才又是我們的一個重點。

三、小結流程

  • 掃描所有注冊的Bean
  • 遍歷這些Bean,依次判斷是否是處理器,并檢測其HandlerMethod
  • 遍歷Handler中的所有方法,找出其中被@RequestMapping注解標記的方法。
  • 獲取方法method上的@RequestMapping實例。
  • 檢查方法所屬的類有沒有@RequestMapping注解
  • 將類層次的RequestMapping和方法級別的RequestMapping結合 (createRequestMappingInfo)
  • 循環注冊進去,請求的時候會再用到
  • 四、后續

    后續文章👉從瀏覽器發送請求給SpringBoot后端時,是如何準確找到哪個接口的?

    若不是小伙伴提起那三問,我想我也不會有如此興致,去一步一步Debug閱讀相關源碼,此文多半可能會胎死腹中了。

    在此非常感謝 @小宇。不瞞大家,他又邀請我一起去讀 ORM 框架源碼了。不過得好好等上一段時間了。

    個人所談:

    閱讀源碼的過程中,其實真的是充滿有趣和枯燥的。

    讀懂了一些關鍵東西,就開心的不得了;而像“又忘記debug到哪了,思路又涼了",就會開始滿心抱怨(我常常想罵上一兩句)。然后就繼續苦逼的去看。

    大家好,我是博主寧在春:主頁

    一名喜歡文藝卻踏上編程這條道路的小青年。

    希望:我們,待別日相見時,都已有所成。

    另外就只能說是在此提供一份個人見解。因文字功底不足、知識缺乏,寫不出十分術語化的文章。

    如果覺得本文讓你有所收獲,希望能夠點個贊,給予一份鼓勵。

    也希望大家能夠積極交流。如有不足之處,請大家及時批正,在此感謝大家。

    掘友可以點點這👉 寧在春 | 掘金

    總結

    以上是生活随笔為你收集整理的你了解SpringBoot启动时API相关信息是用什么数据结构存储的吗?(上篇)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。