Spring MVC入门(二)—— URI Builder模式
阅读原文时间:2023年07月08日阅读:2

URI Builder

Spring MVC作为一个web层框架,避免不了处理URI、URL等和HTTP协议相关的元素,因此它提供了非常好用、功能强大的URI Builder模式来完成,这就是本文重点需要讲述的脚手架~

Spring MVC从3.1开始提供了一种机制,可以通过UriComponentsBuilderUriComponents面向对象的构造和编码URI。

它表示一个不可变的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:不透明的

由于在实际使用中会使用构建器来创建实例,所以都是面向抽象类编程,并不需要关心具体实现,因此实现类部分此处省略~

从命名中就可以看出,它使用了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容器的适配,也非常值得一提

它主要是扩展了一些静态工厂方法,用于创建一些相对路径(相当于当前请求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模式的必要性还是较强的。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章