Spring Boot 定義介面的方法是否可以宣告為 private?

捋是什麼意思

我們在 Controller 中定義介面的時候,一般都是像下面這樣:

@GetMapping(“/01”)public String hello(Map map){ map。put(“name”,“javaboy”);return“forward:/index”;}

估計很少有人會把介面方法定義成 private 的吧?那我們不禁要問,如果非要定義成 private 的方法,那能執行起來嗎?

帶著這個疑問,我們開始今天的原始碼解讀~

在我們使用 Spring Boot 的時候,經常會看到 HandlerMethod 這個型別,例如我們在定義攔截器的時候,如果攔截目標是一個方法,則 preHandle 的第三個引數就是 HandlerMethod(以下案例選自松哥之前的影片:手把手教你 Spring Boot 自定義註解):

@ComponentpublicclassIdempotentInterceptorimplementsHandlerInterceptor{ @Autowired TokenService tokenService;@OverridepublicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception { if(!(handler instanceofHandlerMethod)){ returntrue;}//省略。。。returntrue;}//。。。}

我們在閱讀 SpringMVC 原始碼的時候,也會反覆看到這個 HandlerMethod,那麼它到底是什麼意思?今天我想和小夥伴們捋一捋這個問題,把這個問題搞清楚了,前面的問題大家也就懂了。

1。概覽

Spring Boot 定義介面的方法是否可以宣告為 private?

可以看到,HandlerMethod 體系下的類並不多:

HandlerMethod

封裝 Handler 和具體處理請求的 Method。

InvocableHandlerMethod

在 HandlerMethod 的基礎上增加了呼叫的功能。

ServletInvocableHandlerMethod

在 InvocableHandlerMethod 的基礎上增了對 @ResponseStatus 註解的支援、增加了對返回值的處理。

ConcurrentResultHandlerMethod

在 ServletInvocableHandlerMethod 的基礎上,增加了對非同步結果的處理。

基本上就是這四個,接下來松哥就來詳細說一說這四個元件。

2。HandlerMethod

2。1 bridgedMethod

在正式開始介紹 HandlerMethod 之前,想先和大家聊聊 bridgedMethod,因為在 HandlerMethod 中將會涉及到這個東西,而有的小夥伴可能還沒聽說過 bridgedMethod,因此松哥在這裡做一個簡單介紹。

首先考考大家,下面這段程式碼編譯會報錯嗎?

publicinterfaceAnimal{ voideat(T t);}publicclassCatimplementsAnimal{ @Overridepublicvoideat(String s){ System。out。println(“cat eat ”+ s);}}publicclassDemo01{ publicstaticvoidmain(String[] args){ Animal animal =newCat(); animal。eat(newObject());}}

首先我們定義了一個 Animal 介面,裡邊定義了一個 eat 方法,同時聲明瞭一個泛型。Cat 實現了 Animal 介面,將泛型也定義為了 String。當我呼叫的時候,宣告型別是 Animal,實際型別是 Cat,這個時候調 eat 方法傳入了 Object 物件大家猜猜會怎麼樣?如果呼叫 eat 方法時傳入的是 String 型別那就肯定沒問題,但如果不是 String 呢?

松哥先說結論:編譯沒問題,執行報錯。

如果小夥伴們在自己電腦上寫出上面這段程式碼,你會發現這樣一個問題,開發工具中提示的引數型別竟然是 Object,以松哥的 IDEA 為例,如下:

Spring Boot 定義介面的方法是否可以宣告為 private?

大家看到,在我寫程式碼的時候,開發工具會給我提示,這個引數型別是 Object,有的小夥伴會覺得奇怪,明明是泛型,怎麼變成 Object 了?

我們可以透過反射檢視 Cat 類中到底有哪些方法,程式碼如下:

publicclassDemo01{ publicstaticvoidmain(String[] args){ Method[] methods = Cat。class。getMethods();for(Method method : methods){ String name = method。getName(); Class<?>[] parameterTypes = method。getParameterTypes(); System。out。println(name+“(”+ Arrays。toString(parameterTypes)+“)”);}}}

執行結果如下:

Spring Boot 定義介面的方法是否可以宣告為 private?

可以看到,在實際執行過程中,竟然有兩個 eat 方法,一個的引數為 String 型別,另一個引數為 Object 型別,這是怎麼回事呢?

這個引數型別為 Object 的方法其實是 Java 虛擬機器在執行時創建出來的,這個方法就是我們所說的 bridge method。本節的小標題叫做 bridgedMethod,這是 HandlerMethod 原始碼中的變數名,bridge 結尾多了一個 d,含義變成了被 bridge 的方法,也就是引數為 String 的原方法,大家在接下來的原始碼中看到了 bridgedMethod 就知道這表示引數型別不變的原方法。

2。2 HandlerMethod 介紹

接下來我們來簡單看下 HandlerMethod。

在我們前面分析 HandlerMapping 的時候(參見:SpringMVC 九大元件之 HandlerMapping 深入分析),裡邊有涉及到 HandlerMethod,建立 HandlerMethod 的入口方法是 createWithResolvedBean,因此這裡我們就從該方法開始看起:

public HandlerMethod createWithResolvedBean(){ Object handler =this。bean;if(this。bean instanceofString){ String beanName =(String)this。bean; handler =this。beanFactory。getBean(beanName);}returnnewHandlerMethod(this, handler);}

這個方法主要是確認了一下 handler 的型別,如果 handler 是 String 型別,則根據 beanName 從 Spring 容器中重新查詢到 handler 物件,然後構建 HandlerMethod:

privateHandlerMethod(HandlerMethod handlerMethod, Object handler){ this。bean = handler;this。beanFactory = handlerMethod。beanFactory;this。beanType = handlerMethod。beanType;this。method = handlerMethod。method;this。bridgedMethod = handlerMethod。bridgedMethod;this。parameters = handlerMethod。parameters;this。responseStatus = handlerMethod。responseStatus;this。responseStatusReason = handlerMethod。responseStatusReason;this。resolvedFromHandlerMethod = handlerMethod;this。description = handlerMethod。description;}

這裡的引數都比較簡單,沒啥好說的,唯一值得介紹的地方有兩個:parameters 和 responseStatus。

parameters

parameters 實際上就是方法引數,對應的型別是 MethodParameter,這個類的原始碼我這裡就不貼出來了,主要和大家說一下封裝的內容包括:引數的序號(parameterIndex),引數巢狀級別(nestingLevel),引數型別(parameterType),引數的註解(parameterAnnotations),引數名稱查詢器(parameterNameDiscoverer),引數名稱(parameterName)等。

HandlerMethod 中還提供了兩個內部類來封裝 MethodParameter,分別是:

HandlerMethodParameter:這個封裝方法呼叫的引數。

ReturnValueMethodParameter:這個繼承自 HandlerMethodParameter,它封裝了方法的返回值,返回值裡邊的 parameterIndex 是 -1。

注意,這兩者中的 method 都是 bridgedMethod。

responseStatus

這個主要是處理方法的 @ResponseStatus 註解,這個註解用來描述方法的響應狀態碼,使用方式像下面這樣:

@GetMapping(“/04”)@ResponseBody@ResponseStatus(code = HttpStatus。OK)publicvoidhello4(@SessionAttribute(“name”) String name){ System。out。println(“name = ”+ name);}

從這段程式碼中大家可以看到,其實 @ResponseStatus 註解靈活性很差,不實用,當我們定義一個介面的時候,很難預知到該介面的響應狀態碼是 200。

在 handlerMethod 中,在呼叫其構造方法的時候,都會呼叫 evaluateResponseStatus 方法處理 @ResponseStatus 註解,如下:

privatevoidevaluateResponseStatus(){ ResponseStatus annotation =getMethodAnnotation(ResponseStatus。class);if(annotation == null){ annotation = AnnotatedElementUtils。findMergedAnnotation(getBeanType(), ResponseStatus。class);}if(annotation != null){ this。responseStatus = annotation。code();this。responseStatusReason = annotation。reason();}}

可以看到,這段程式碼也比較簡單,找到註解,把裡邊的值解析出來,賦值給相應的變數。

這下小夥伴們應該明白了 HandlerMethod 大概是個怎麼回事。

3。InvocableHandlerMethod

看名字就知道,InvocableHandlerMethod 可以呼叫 HandlerMethod 中的具體方法,也就是 bridgedMethod。我們先來看下 InvocableHandlerMethod 中宣告的屬性:

private HandlerMethodArgumentResolverComposite resolvers =newHandlerMethodArgumentResolverComposite();private ParameterNameDiscoverer parameterNameDiscoverer =newDefaultParameterNameDiscoverer();@Nullableprivate WebDataBinderFactory dataBinderFactory;

主要就是這三個屬性:

resolvers:這個不用說,引數解析器,前面的文章中松哥已經和大家聊過這個問題了。

parameterNameDiscoverer:這個用來獲取引數名稱,在 MethodParameter 中會用到。

dataBinderFactory:這個用來建立 WebDataBinder,在引數解析器中會用到。

具體的請求呼叫方法是 invokeForRequest,我們一起來看下:

@Nullablepublic Object invokeForRequest(NativeWebRequest request,@Nullable ModelAndViewContainer mavContainer, Object。。。 providedArgs)throws Exception { Object[] args =getMethodArgumentValues(request, mavContainer, providedArgs);returndoInvoke(args);}@Nullableprotected Object doInvoke(Object。。。 args)throws Exception { Method method =getBridgedMethod(); ReflectionUtils。makeAccessible(method);try{ if(KotlinDetector。isSuspendingFunction(method)){ return CoroutinesUtils。invokeSuspendingFunction(method,getBean(), args);}return method。invoke(getBean(), args);}catch(InvocationTargetException ex){ // 省略 。。。}}

首先呼叫 getMethodArgumentValues 方法按順序獲取到所有引數的值,這些引數值組成一個數組,然後呼叫 doInvoke 方法執行,在 doInvoke 方法中,首先獲取到 bridgedMethod,並設定其可見(

意味著我們在 Controller 中定義的介面方法也可以是 private 的

),然後直接透過反射呼叫即可。當我們沒看 SpringMVC 原始碼的時候,我們就知道介面方法最終肯定是透過反射呼叫的,現在,經過層層分析之後,終於在這裡找到了反射呼叫程式碼。

最後松哥再來說一下負責引數解析的 getMethodArgumentValues 方法:

protected Object[]getMethodArgumentValues(NativeWebRequest request,@Nullable ModelAndViewContainer mavContainer, Object。。。 providedArgs)throws Exception { MethodParameter[] parameters =getMethodParameters();if(ObjectUtils。isEmpty(parameters)){ return EMPTY_ARGS;} Object[] args =newObject[parameters。length];for(int i =0; i < parameters。length; i++){ MethodParameter parameter = parameters[i]; parameter。initParameterNameDiscovery(this。parameterNameDiscoverer); args[i]=findProvidedArgument(parameter, providedArgs);if(args[i]!= null){ continue;}if(!this。resolvers。supportsParameter(parameter)){ thrownewIllegalStateException(formatArgumentError(parameter,“No suitable resolver”));}try{ args[i]=this。resolvers。resolveArgument(parameter, mavContainer, request,this。dataBinderFactory);}catch(Exception ex){ // 省略。。。}}return args;}

首先呼叫 getMethodParameters 方法獲取到方法的所有引數。

建立 args 陣列用來儲存引數的值。

接下來一堆初始化配置。

如果 providedArgs 中提供了引數值,則直接賦值。

檢視是否有引數解析器支援當前引數型別,如果沒有,直接丟擲異常。

呼叫引數解析器對引數進行解析,解析完成後,賦值。

是不是,很 easy!

4。ServletInvocableHandlerMethod

ServletInvocableHandlerMethod 則是在 InvocableHandlerMethod 的基礎上,又增加了兩個功能:

對 @ResponseStatus 註解的處理

對返回值的處理

Servlet 容器下 Controller 在查詢介面卡時發起呼叫的最終就是 ServletInvocableHandlerMethod。

這裡的處理核心方法是 invokeAndHandle,如下:

publicvoidinvokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object。。。 providedArgs)throws Exception { Object returnValue =invokeForRequest(webRequest, mavContainer, providedArgs);setResponseStatus(webRequest);if(returnValue == null){ if(isRequestNotModified(webRequest)||getResponseStatus()!= null || mavContainer。isRequestHandled()){ disableContentCachingIfNecessary(webRequest); mavContainer。setRequestHandled(true);return;}}elseif(StringUtils。hasText(getResponseStatusReason())){ mavContainer。setRequestHandled(true);return;} mavContainer。setRequestHandled(false);try{ this。returnValueHandlers。handleReturnValue( returnValue,getReturnValueType(returnValue), mavContainer, webRequest);}catch(Exception ex){ throw ex;}}

首先呼叫父類的 invokeForRequest 方法對請求進行執行,拿到請求結果。

呼叫 setResponseStatus 方法處理 @ResponseStatus 註解,具體的處理邏輯是這樣:如果沒有新增 @ResponseStatus 註解,則什麼都不做;如果添加了該註解,並且 reason 屬性不為空,則直接輸出錯誤,否則設定響應狀態碼。這裡需要注意一點,如果響應狀態碼是 200,就不要設定 reason,否則會按照 error 處理。

接下來就是對返回值的處理了,returnValueHandlers#handleReturnValue 方法松哥在之前的文章中和大家專門介紹過,這裡就不再贅述,傳送門:Spring Boot 中如何統一 API 介面響應格式?。

事實上,ServletInvocableHandlerMethod 還有一個子類 ConcurrentResultHandlerMethod,這個支援非同步呼叫結果處理,因為使用場景較少,這裡就不做介紹啦。

5。小結

現在大家可以回答文章標題提出的問題了吧?

,https://blog。csdn。net/u012702547/article/details/115365730