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
// @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
模式的必要性還是較強的。