你知道@RequestMapping的name屬性有什麼用嗎?帶你了解URI Builder模式(UriComponents/UriComponentsBuilder)【享學Spring MVC】
- 2019 年 10 月 5 日
- 筆記
版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/f641385712/article/details/100103326
每篇一句
牛逼架構師:把複雜問題簡單化,把簡單問題搞沒 菜逼架構師:把簡單問題複雜化
前言
不知這個標題能否勾起你的好奇心和求知慾?在Spring MVC
的使用中,若我說@RequestMapping
是最為常用的一個註解你應該沒啥意見吧。若你細心的話你能發現它有一個name
屬性(Spring4.1
後新增),大概率你從來都沒有使用過且鮮有人知。
我本人搜了搜相關文章,也幾乎沒有一篇文章較為系統化的介紹它。可能有人會說搜不到就代表不重要/不流行嘛,我大部分統一這個觀點,但這塊知識點我覺得還挺有意思,因此本文就針對性的彌補市面的空白,帶你了解name
屬性的作用和使用。更為重要的是藉此去了解學習Spring MVC
非常重要的URI Builder
模式
@RequestMapping的name屬性
首先看此屬性在@RequestMapping
中的定義:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { // @since 4.1 String name() default ""; ... }
javadoc描述:為此映射分配名稱,它可以使用在類上,也可以標註在方法上。
在分析RequestMappingHandlerMapping
源碼的時候指出過:它的createRequestMappingInfo()
方法會把註解的name
封裝到RequestMappingInfo.name
屬性里。因為它既可以在類上又可以在方法上,因此一樣的它需要combine
,但是它的combine
邏輯稍微特殊些,此處展示如下:
RequestMappingInfo: // 此方法來自介面:RequestCondition @Override public RequestMappingInfo combine(RequestMappingInfo other) { String name = combineNames(other); PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition); RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition); ... return new RequestMappingInfo( ... ); } @Nullable private String combineNames(RequestMappingInfo other) { if (this.name != null && other.name != null) { String separator = RequestMappingInfoHandlerMethodMappingNamingStrategy.SEPARATOR; return this.name + separator + other.name; } else if (this.name != null) { return this.name; } else { return other.name; } }
邏輯不難,就是類+"#"+方法的拼接,但是我們知道其實絕大部分情況下我們都從來沒有指定過name屬性,那麼此處就不得不提這個策略介面:HandlerMethodMappingNamingStrategy
了,它用於預設的名字生成策略器
HandlerMethodMappingNamingStrategy
為處理程式HandlerMethod
方法分配名稱的策略介面。此介面可以在AbstractHandlerMethodMapping
里配置生成name,然後這個name可以用於AbstractHandlerMethodMapping#getHandlerMethodsForMappingName(String)
方法進行查詢~
// @since 4.1 @FunctionalInterface // 函數式介面 public interface HandlerMethodMappingNamingStrategy<T> { // Determine the name for the given HandlerMethod and mapping. // 根據HandlerMethod 和 Mapping拿到一個name String getName(HandlerMethod handlerMethod, T mapping); }
它的唯一實現類是:RequestMappingInfoHandlerMethodMappingNamingStrategy
(目前而言RequestMappingInfo
的唯一實現只有@RequestMapping
,但設計上是沒有強制綁定必須是這個註解~)
RequestMappingInfoHandlerMethodMappingNamingStrategy
此類提供name的默認的生成規則(若沒指定的話)的實現
// @since 4.1 public class RequestMappingInfoHandlerMethodMappingNamingStrategy implements HandlerMethodMappingNamingStrategy<RequestMappingInfo> { // 類級別到方法級別的分隔符(當然你也是可以改的) public static final String SEPARATOR = "#"; @Override public String getName(HandlerMethod handlerMethod, RequestMappingInfo mapping) { if (mapping.getName() != null) { // 若使用者自己指定了,那就以指定的為準 return mapping.getName(); } // 自動生成的策略 StringBuilder sb = new StringBuilder(); // 1、拿到類名 // 2、遍歷每個字母,拿到所有的大寫字母 // 3、用拿到的大寫字母拼接 # 拼接方法名。如:TestController#getFoo()最終結果是:TC#getFoo String simpleTypeName = handlerMethod.getBeanType().getSimpleName(); for (int i = 0; i < simpleTypeName.length(); i++) { if (Character.isUpperCase(simpleTypeName.charAt(i))) { sb.append(simpleTypeName.charAt(i)); } } sb.append(SEPARATOR).append(handlerMethod.getMethod().getName()); return sb.toString(); } }
簡單總結這部分邏輯如下:
- 類上的name值 + 『#』 + 方法的name值
- 類上若沒指定,默認值是:類名所有大寫字母拼裝
- 方法上若沒指定,默認值是:方法名
name屬性有什麼用(如何使用)?
說了這麼多,小夥伴可能還是一頭霧水?有什麼用?如何用?
其實在介面的JavaDoc里有提到了它的作用:應用程式可以在下面這個靜態方法的幫助下按名稱構建控制器方法的URL,它藉助的是MvcUriComponentsBuilder
的fromMappingName
方法實現:
MvcUriComponentsBuilder: // 靜態方法:根據mappingName,獲取到一個MethodArgumentBuilder public static MethodArgumentBuilder fromMappingName(String mappingName) { return fromMappingName(null, mappingName); } public static MethodArgumentBuilder fromMappingName(@Nullable UriComponentsBuilder builder, String name) { ... Map<String, RequestMappingInfoHandlerMapping> map = wac.getBeansOfType(RequestMappingInfoHandlerMapping.class); ... for (RequestMappingInfoHandlerMapping mapping : map.values()) { // 重點:根據名稱找到List<HandlerMethod> handlerMethods // 依賴的是getHandlerMethodsForMappingName()這個方法,它是從MappingRegistry里查找 handlerMethods = mapping.getHandlerMethodsForMappingName(name); } ... HandlerMethod handlerMethod = handlerMethods.get(0); Class<?> controllerType = handlerMethod.getBeanType(); Method method = handlerMethod.getMethod(); // 構建一個MethodArgumentBuilder return new MethodArgumentBuilder(builder, controllerType, method); }
說明:
MethodArgumentBuilder
是MvcUriComponentsBuilder
的一個public靜態內部類,持有controllerType、method、argumentValues、baseUrl等屬性…
它的使用場景,我參考了Spring
的官方文檔,截圖如下:

官方文檔說:它能讓你非常方便的在JSP
頁面上使用它,形如這樣子:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %> ... <a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>
這麼寫至少感覺比你這樣拼裝URL:pageContext.request.contextPath//people/1/addresses/china
要更加靠譜點,且更加面向對象點吧~
說明:使用此
s:mvcUrl
函數是要求你導入Spring
標籤庫的支援的~
此處應有疑問:JSP
早就過時了,現在誰還用呢?難道Spring4.1
新推出來的name屬性這麼快就壽終正寢了? 當然不是,Spring
作為這麼優秀的框架,設計上都是功能都是非常模組化的,該功能自然不是和JSP
強耦合的(Spring僅是提供了對JSP標籤庫而順便內置支援一下下而已~)。 在上面我截圖的最後一段話也講到了,大致意思是: 示例依賴於Spring
標記庫(即META-INF/Spring.tld
)中申明的mvcUrl
函數,此函數的聲明如下:
<function> <description>Helps to prepare a URL to a Spring MVC controller method.</description> <name>mvcUrl</name> <function-class>org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder</function-class> <function-signature>org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodArgumentBuilder fromMappingName(java.lang.String)</function-signature> </function>
可見它最終的處理函數是MvcUriComponentsBuilder.fromMappingName(java.lang.String)()
這個方法而已(文末有詳細介紹,請關聯起來看本文)~ 因為,如果你是其它模版技術(如Thymeleaf
)也是很容易自定義一個這樣類似的函數的,那麼你就依舊可以使用此便捷、強大的功能來幫助你開發。
通過name屬性的引入,就順利過渡到了接下來要將的重點,也是本文的重中之重:Spring MVC
支援的強大的URI Builder
模式。
URI Builder
Spring MVC
作為一個web層框架,避免不了處理URI、URL等和HTTP協議相關的元素,因此它提供了非常好用、功能強大的URI Builder
模式來完成,這就是本文重點需要講述的腳手架~ Spring MVC
從3.1開始提供了一種機制,可以通過UriComponentsBuilder和UriComponents
面向對象的構造和編碼URI。
UriComponents
它表示一個不可變的URI
組件集合,將組件類型映射到字元串值。
URI:統一資源標識符。 URL:統一資源定位符。 還是傻傻分不清楚?這裡我推薦一篇通俗易懂的 文章 供你參考
它包含用於所有組件的方便getter,與java.net.URI
類似,但具有更強大的編碼選項和對URI模板變數的支援。
// @since 3.1 自己是個抽象類。一般構建它我們使用UriComponentsBuilder構建器 public abstract class UriComponents implements Serializable { // 捕獲URI模板變數名 private static final Pattern NAMES_PATTERN = Pattern.compile("\{([^/]+?)\}"); @Nullable private final String scheme; @Nullable private final String fragment; // 唯一構造,是protected 的 protected UriComponents(@Nullable String scheme, @Nullable String fragment) { this.scheme = scheme; this.fragment = fragment; } ... // 省略它倆的get方法(無set方法) @Nullable public abstract String getSchemeSpecificPart(); @Nullable public abstract String getUserInfo(); @Nullable public abstract String getHost(); // 如果沒有設置port,就返回-1 public abstract int getPort(); @Nullable public abstract String getPath(); public abstract List<String> getPathSegments(); @Nullable public abstract String getQuery(); public abstract MultiValueMap<String, String> getQueryParams(); // 此方法是public且是final的哦~ // 注意它的返回值還是UriComponents public final UriComponents encode() { return encode(StandardCharsets.UTF_8); } public abstract UriComponents encode(Charset charset); // 這是它最為強大的功能:對模版變數的支援 // 用給定Map映射中的值替換**所有**URI模板變數 public final UriComponents expand(Map<String, ?> uriVariables) { return expandInternal(new MapTemplateVariables(uriVariables)); } // 給定的是變數數組,那就按照順序替換 public final UriComponents expand(Object... uriVariableValues) {...} public final UriComponents expand(UriTemplateVariables uriVariables) { ... } // 真正的expand方法,其實還是子類來實現的 abstract UriComponents expandInternal(UriTemplateVariables uriVariables); // 規範化路徑移除**序列**,如「path/…」。 // 請注意,規範化應用於完整路徑,而不是單個路徑段。 public abstract UriComponents normalize(); // 連接所有URI組件以返回完全格式的URI字元串。 public abstract String toUriString(); public abstract URI toUri(); @Override public final String toString() { return toUriString(); } // 拷貝 protected abstract void copyToUriComponentsBuilder(UriComponentsBuilder builder); ... // 提供靜態工具方法expandUriComponent和sanitizeSource }
它包含有和Http相關的各個部分:如schema、port、path、query等等。此抽象類有兩個實現類:OpaqueUriComponents
和HierarchicalUriComponents
Hierarchical
:分層的Opaque
:不透明的
由於在實際使用中會使用構建器來創建實例,所以都是面向抽象類編程,並不需要關心具體實現,因此實現類部分此處省略~
UriComponentsBuilder
從命名中就可以看出,它使用了Builder
模式,用於構建UriComponents
。實際應用中我們所有的UriComponents
都應是通過此構建器構建出來的~
// @since 3.1 public class UriComponentsBuilder implements UriBuilder, Cloneable { ... // 省略所有正則(包括提取查詢參數、scheme、port等等等等) ... // 它所有的構造函數都是protected的 // ******************鞋面介紹它的實例化靜態方法(7種)****************** // 創建一個空的bulder,裡面schema,port等等啥都木有 public static UriComponentsBuilder newInstance() { return new UriComponentsBuilder(); } // 直接從path路徑裡面,分析出一個builder。較為常用 public static UriComponentsBuilder fromPath(String path) {...} public static UriComponentsBuilder fromUri(URI uri) {...} // 比如這種:/hotels/42?filter={value} public static UriComponentsBuilder fromUriString(String uri) {} // 形如這種:https://example.com/hotels/42?filter={value} // fromUri和fromHttpUrl的使用方式差不多~~~~ public static UriComponentsBuilder fromHttpUrl(String httpUrl) {} // HttpRequest是HttpMessage的子介面。它的原理是:fromUri(request.getURI())(調用上面方法fromUri) // 然後再調用本類的adaptFromForwardedHeaders(request.getHeaders()) // 解釋:從頭Forwarded、X-Forwarded-Proto等拿到https、port等設置值~~ // 詳情請參見http標準的Forwarded頭~ // @since 4.1.5 public static UriComponentsBuilder fromHttpRequest(HttpRequest request) {} // origin 裡面放的是跨域訪問的域名地址。比如 www.a.com 訪問 www.b.com會形成跨域 // 這個時候訪問 www.b.com 的時候,請求頭裡會攜帶 origin:www.a.com(b服務需要通過這個來判斷是否允許a服務跨域訪問) // 方法可以獲取到協議,域名和埠。個人覺得此方法沒毛卵用~~~ // 和fromUriString()方法差不多,不過比它精簡(因為這裡只需要關注scheme、host和port) public static UriComponentsBuilder fromOriginHeader(String origin) {} // *******************下面都是實例方法******************* // @since 5.0.8 public final UriComponentsBuilder encode() { return encode(StandardCharsets.UTF_8); } public UriComponentsBuilder encode(Charset charset) {} // 調用此方法生成一個UriComponents public UriComponents build() { return build(false); } public UriComponents build(boolean encoded) { // encoded=true,取值就是FULLY_ENCODED 全部編碼 // 否則只編碼模版或者不編碼 return buildInternal(encoded ? EncodingHint.FULLY_ENCODED : (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE) ); } // buildInternal內部就會自己new子類:OpaqueUriComponents或者HierarchicalUriComponents // 以及執行UriComponents.expand方法了(若指定了參數的話),使用者不用關心了 // 顯然這就是個多功能方法了:設置好參數。build後立馬Expand public UriComponents buildAndExpand(Map<String, ?> uriVariables) { return build().expand(uriVariables); } public UriComponents buildAndExpand(Object... uriVariableValues) {} //build成為一個URI。注意這裡編碼方式是:EncodingHint.ENCODE_TEMPLATE @Override public URI build(Object... uriVariables) { return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri(); } @Override public URI build(Map<String, ?> uriVariables) { return buildInternal(EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri(); } // @since 4.1 public String toUriString() { ... } // ====重構/重新設置Builder==== public UriComponentsBuilder uri(URI uri) {} public UriComponentsBuilder uriComponents(UriComponents uriComponents) {} @Override public UriComponentsBuilder scheme(@Nullable String scheme) { this.scheme = scheme; return this; } @Override public UriComponentsBuilder userInfo(@Nullable String userInfo) { this.userInfo = userInfo; resetSchemeSpecificPart(); return this; } public UriComponentsBuilder host(@Nullable String host){ ... } ... // 省略其它部分 // 給URL後面拼接查詢參數(鍵值對) @Override public UriComponentsBuilder query(@Nullable String query) {} // 遇上相同的key就替代,而不是直接在後面添加了(上面query是添加) @Override public UriComponentsBuilder replaceQuery(@Nullable String query) {} @Override public UriComponentsBuilder queryParam(String name, Object... values) {} ... replaceQueryParam // 可以先單獨設置參數,但不expend哦~ public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {} @Override public Object clone() { return cloneBuilder(); } // @since 4.2.7 public UriComponentsBuilder cloneBuilder() { return new UriComponentsBuilder(this); } ... }
API都不難理解,此處我給出一些使用案例供以參考:
public static void main(String[] args) { String url; UriComponents uriComponents = UriComponentsBuilder.newInstance() //.encode(StandardCharsets.UTF_8) .scheme("https").host("www.baidu.com").path("/test").path("/{template}") //此處{}就成 不要寫成${} //.uriVariables(傳一個Map).build(); .build().expand("myhome"); // 此效果同上一句,但推薦這麼使用,方便一些 url = uriComponents.toUriString(); System.out.println(url); // https://www.baidu.com/test/myhome // 從URL字元串中構造(注意:toUriString方法內部是調用了build和expend方法的~) System.out.println(UriComponentsBuilder.fromHttpUrl(url).toUriString()); // https://www.baidu.com/test/myhome System.out.println(UriComponentsBuilder.fromUriString(url).toUriString()); // https://www.baidu.com/test/myhome // 給URL中放添加參數 query和replaceQuery uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中國&age=18").query("&name=二次拼接").build(); url = uriComponents.toUriString(); // 效果描述:&test前面這個&不寫也是木有問題的。並且兩個name都出現了哦~~~ System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中國&name=二次拼接&age=18 uriComponents = UriComponentsBuilder.fromHttpUrl(url).query("name=中國&age=18").replaceQuery("name=二次拼接").build(); url = uriComponents.toUriString(); // 這種夠狠:後面的直接覆蓋前面「所有的」查詢串 System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=二次拼接 //queryParam/queryParams/replaceQueryParam/replaceQueryParams // queryParam:一次性指定一個key,queryParams一次性可以搞多個key url = "https://www.baidu.com/test/myhome"; // 重置一下 uriComponents = UriComponentsBuilder.fromHttpUrl(url).queryParam("name","中國","美國").queryParam("age",18) .queryParam("name","英國").build(); url = uriComponents.toUriString(); // 發現是不會有repalace的效果的~~~~~~~~~~~~~ System.out.println(uriComponents.toUriString()); // https://www.baidu.com/test/myhome?name=中國&name=美國&name=英國&age=18 // 關於repalceParam相關方法,交給各位自己去試驗吧~~~ // 不需要domain,構建局部路徑,它也是把好手 uriComponents = UriComponentsBuilder.fromPath("").path("/test").build(); // .fromPath("/").path("/test") --> /test // .fromPath("").path("/test") --> /test // .fromPath("").path("//test") --> /test // .fromPath("").path("test") --> /test System.out.println(uriComponents.toUriString()); // /test?name=fsx }
使用這種方式來構建URL還是非常方便的,它的容錯性非常高,寫法靈活且不容易出錯,完全面向模組化思考,值得推薦。
- URI構建的任意部分(包括查詢參數、scheme等等)都是可以用
{}
這種形式的模版參數的 - 被替換的模版中還支援這麼來寫:
/myurl/{name:[a-z]}/show
,這樣用expand也能正常賦值
它還有個子類:ServletUriComponentsBuilder
,是對Servlet
容器的適配,也非常值得一提
ServletUriComponentsBuilder
它主要是擴展了一些靜態工廠方法,用於創建一些相對路徑(相當於當前請求HttpServletRequest
)。
// @since 3.1 public class ServletUriComponentsBuilder extends UriComponentsBuilder { @Nullable private String originalPath; // 不對外提供public的構造函數 // initFromRequest:設置schema、host、port(HTTP默認80,https默認443) public static ServletUriComponentsBuilder fromContextPath(HttpServletRequest request) { ServletUriComponentsBuilder builder = initFromRequest(request); // 注意:此處路徑全部替換成了ContextPath builder.replacePath(request.getContextPath()); return builder; } // If the servlet is mapped by name, e.g. {@code "/main/*"}, the path // 它在UriComponentsBuilderMethodArgumentResolver中有用 public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {} public static ServletUriComponentsBuilder fromRequestUri(HttpServletRequest request) { ServletUriComponentsBuilder builder = initFromRequest(request); builder.initPath(request.getRequestURI()); return builder; } private void initPath(String path) { this.originalPath = path; replacePath(path); } public static ServletUriComponentsBuilder fromRequest(HttpServletRequest request) {} // fromCurrentXXX方法... public static ServletUriComponentsBuilder fromCurrentContextPath() {} // 生路其它Current方法 // @since 4.0 移除掉originalPath的後綴名,並且把此後綴名return出來~~ // 此方法必須在UriComponentsBuilder.path/pathSegment方法之前調用~ @Nullable public String removePathExtension() { } }
說明:
Spring5.1
後不推薦使用它來處理X-Forwarded-*
等請求頭了,推薦使用ForwardedHeaderFilter
來處理~
使用UriComponentsBuilder
類的最大好處是方便地注入到Controller
中,在方法參數中可直接使用。詳見UriComponentsBuilderMethodArgumentResolver
,它最終return
的是:ServletUriComponentsBuilder.fromServletMapping(request)
,這樣我們在Controller
內就可以非常容易且優雅的得到URI
的各個部分了(不用再自己通過request
慢慢get)~
MvcUriComponentsBuilder
此類效果類似於ServletUriComponentsBuilder
,它負責從Controller
控制器標註有@RequestMapping
的方法中獲取UriComponentsBuilder
,從而構建出UriComponents
。
// @since 4.0 public class MvcUriComponentsBuilder { // Bean工廠里·UriComponentsContributor·的通用名稱 // 關於UriComponentsContributor,RequestParamMethodArgumentResolver和PathVariableMethodArgumentResolver都是它的子類 public static final String MVC_URI_COMPONENTS_CONTRIBUTOR_BEAN_NAME = "mvcUriComponentsContributor"; // 用於創建動態代理對象 private static final SpringObjenesis objenesis = new SpringObjenesis(); // 支援Ant風格的Path private static final PathMatcher pathMatcher = new AntPathMatcher(); // 參數名 private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); // 課件解析查詢參數、path參數最終是依賴於我們的MethodArgumentResolver // 他們也都實現了UriComponentsContributor介面~~~ private static final CompositeUriComponentsContributor defaultUriComponentsContributor; static { defaultUriComponentsContributor = new CompositeUriComponentsContributor(new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false)); } // final的,只能通過構造器傳入 private final UriComponentsBuilder baseUrl; // 此構造方法是protected的 protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { this.baseUrl = baseUrl; } // 通過BaseUrl創建一個實例 public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { return new MvcUriComponentsBuilder(baseUrl); } // 從控制器里。。。 // 這個一個控制器類里有多個Mapping,那麼只會有第一個會被生效 public static UriComponentsBuilder fromController(Class<?> controllerType) { return fromController(null, controllerType); } // 注意此方法也是public的哦~~~~ builder可以為null哦~~ public static UriComponentsBuilder fromController(@Nullable UriComponentsBuilder builder, Class<?> controllerType) { // 若builder為null,那就用它ServletUriComponentsBuilder.fromCurrentServletMapping(),否則克隆一個出來 builder = getBaseUrlToUse(builder); // 拿到此控制器的pathPrefixes。 // 關於RequestMappingHandlerMapping的pathPrefixes,出門右拐有詳細說明來如何使用 String prefix = getPathPrefix(controllerType); builder.path(prefix); // 找到類上的RequestMapping註解,若沒標註,默認就是'/' // 若有此註解,拿出它的mapping.path(),若是empty或者paths[0]是empty,都返回'/' // 否則返回第一個:paths[0] String mapping = getClassMapping(controllerType); builder.path(mapping); return builder; } // 這個方法應該是使用得最多的~~~~ 同樣的借用了MethodIntrospector.selectMethods這個方法 // 它的path是結合來的:String path = pathMatcher.combine(typePath, methodPath); // fromMethodInternal方法省略,但最後一步調用了applyContributors(builder, method, args)這個方法 // 它是使用`CompositeUriComponentsContributor`來處理賦值URL的template(可以自己配置,也可以使用默認的) // 默認使用的便是PathVariableMethodArgumentResolver和RequestParamMethodArgumentResolver // 當在處理請求的上下文之外使用MvcUriComponentsBuilder或應用與當前請求不匹配的自定義baseurl時,這非常有用。 public static UriComponentsBuilder fromMethodName(Class<?> controllerType, String methodName, Object... args) { Method method = getMethod(controllerType, methodName, args); // 第一個參數是baseUrl,傳的null 沒傳就是ServletUriComponentsBuilder.fromCurrentServletMapping() return fromMethodInternal(null, controllerType, method, args); } // @since 4.2 public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) {} // @since 4.2 public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, @Nullable Class<?> controllerType, Method method, Object... args) {} // info必須是MethodInvocationInfo類型 // Create a {@link UriComponentsBuilder} by invoking a "mock" controller method. 用於mock // 請參見on方法~~ public static UriComponentsBuilder fromMethodCall(Object info) {} public static <T> T on(Class<T> controllerType) { return controller(controllerType); } // 此方法是核心:ControllerMethodInvocationInterceptor是個私有靜態內部類 // 實現了org.springframework.cglib.proxy.MethodInterceptor介面以及 // org.aopalliance.intercept.MethodInterceptor介面 // org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodInvocationInfo介面 // ReflectionUtils.isObjectMethod(method) public static <T> T controller(Class<T> controllerType) { Assert.notNull(controllerType, "'controllerType' must not be null"); return ControllerMethodInvocationInterceptor.initProxy(controllerType, null); } // @since 4.1 // 請看上面對@RequestMapping註解中name屬性的介紹和使用 // ${s:mvcUrl('PC#getPerson').arg(0,"123").build() // 這個標籤s:mvcUrl它對應的解析函數其實就是MvcUriComponentsBuilder.fromMappingName // 也就是這個方法`PC#getPerson`就二十所謂的mappingName,若不指定它由HandlerMethodMappingNamingStrategy生成 // 底層依賴方法:RequestMappingInfoHandlerMapping.getHandlerMethodsForMappingName public static MethodArgumentBuilder fromMappingName(String mappingName) { return fromMappingName(null, mappingName); } // **************以上都是靜態工廠方法,下面是些實例方法************** // 調用的是靜態方法fromController,See class-level docs public UriComponentsBuilder withController(Class<?> controllerType) { return fromController(this.baseUrl, controllerType); } // withMethodName/withMethodCall/withMappingName/withMethod等都是依賴於對應的靜態工廠方法,略 }
MvcUriComponentsBuilder
提供的功能被廣泛應用到Mock
介面中,並且它提供的MvcUriComponentsBuilder#fromMappingName
的API是集成模版引擎的關鍵,我個人認為所想深入了解Spring MVC
或者在此基礎上擴展,了解它的URI Builder
模式的必要性還是較強的。
總結
本文所敘述的內容整體生可能比較冷,可能大多數人沒有接觸過甚至沒有聽過,但並不代表它沒有意義。 你和同伴都使用Spring MVC
,差異化如何體現出來呢?我覺得有一個方向就是向他/她展示這些"真正的技術"~