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
模式的必要性还是较强的。