1. 程式人生 > >你知道@RequestMapping的name屬性有什麼用嗎?【享學Spring MVC】

你知道@RequestMapping的name屬性有什麼用嗎?【享學Spring MVC】

每篇一句

牛逼架構師:把複雜問題簡單化,把簡單問題搞沒
菜逼架構師:把簡單問題複雜化

前言

不知這個標題能否勾起你的好奇心和求知慾?在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();
    }

}

簡單總結這部分邏輯如下:

  1. 類上的name值 + '#' + 方法的name值
  2. 類上若沒指定,預設值是:類名所有大寫字母拼裝
  3. 方法上若沒指定,預設值是:方法名

name屬性有什麼用(如何使用)?

說了這麼多,小夥伴可能還是一頭霧水?有什麼用?如何用?

其實在介面的JavaDoc裡有提到了它的作用:應用程式可以在下面這個靜態方法的幫助下按名稱構建控制器方法的URL,它藉助的是MvcUriComponentsBuilderfromMappingName方法實現:

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);
    }

說明:MethodArgumentBuilderMvcUriComponentsBuilder的一個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等等。此抽象類有兩個實現類:OpaqueUriComponentsHierarchicalUriComponents

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還是非常方便的,它的容錯性非常高,寫法靈活且不容易出錯,完全面向模組化思考,值得推薦。

  1. URI構建的任意部分(包括查詢引數、scheme等等)都是可以用{}這種形式的模版引數的
  2. 被替換的模版中還支援這麼來寫:/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,差異化如何體現出來呢?我覺得有一個方向就是向他/她展示這些"真正的技術"~

== 若對Spring、SpringBoot、MyBatis等原始碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==
== 若對Spring、SpringBoot、MyBatis等原始碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛