Retrofit之baseUrl与Endpoint的combination

我们在使用retrofit的时候通常会使用Retrofit的baseUrl(baseUrl)来设置服务器的地址或者是后台API的公共路径;然后再在具体的API的请求方法上通过@GET、@POST等注解设置不同的相对路径(Endpoint)。

相信不少同学也曾经试过一不小心在复制baseUrl或Endpoint时,baseUrl尾部少了个'/',又或者Endpoint多复制了个'/'开头;然后就出现了类似404这种奇怪的错误。比如面这种情况:

baseUrl = http://example.com/api/
endpoint = /foo/bar/
result = http://example.com/foo/bar/

这字符串拼接的结果应该就是baseUrl + endpoint = " http://example.com/api/foo/bar/",为毛这中间丢失了api这部分路径的呢?这是不是Retrofit出现了Bug啊还是哪里抽风了???
---- 其实这是错误使用endpoint导致跟我们预期结果不一致的现象,因为我们的endpoint是以'/'开头,其含义是endpoint是一个绝对路径,然后在与baseUrl合并的时候就会把baseUrl主机后的部分路径给清除掉。

其实我们在使用baseUrl与Endpoint合并使用的时候需要注意以下几点:

  1. baseUrl必须以“/”结尾,否则主机最后面的部分路径将被忽略掉
  2. endpoint以“/”开始的话表示绝对路径,也就是baseURL只有服务器主机地址之前的部分将被继续使用
  3. endpoint包含主机地址时,最终主机地址将使用endpoint的地址,而scheme将沿用baseUrl的
  4. endpoint是一个完成的路径时,则整个替换baseUrl

在Retrofit的源码里面注解是这么描述关于baseUrl的用法的:

  /**
    * Set the API base URL.
    *
    * <p>The specified endpoint values (such as with {@link GET @GET}) are resolved against this
    * value using {@link HttpUrl#resolve(String)}. The behavior of this matches that of an {@code
    * <a href="">} link on a website resolving on the current URL.
    *
    * <p><b>Base URLs should always end in {@code /}.</b>
    *
    * <p>A trailing {@code /} ensures that endpoints values which are relative paths will correctly
    * append themselves to a base which has path components.
    *
    * <p><b>Correct:</b><br>
    * Base URL: http://example.com/api/<br>
    * Endpoint: foo/bar/<br>
    * Result: http://example.com/api/foo/bar/
    *
    * <p><b>Incorrect:</b><br>
    * Base URL: http://example.com/api<br>
    * Endpoint: foo/bar/<br>
    * Result: http://example.com/foo/bar/
    *
    * <p>This method enforces that {@code baseUrl} has a trailing {@code /}.
    *
    * <p><b>Endpoint values which contain a leading {@code /} are absolute.</b>
    *
    * <p>Absolute values retain only the host from {@code baseUrl} and ignore any specified path
    * components.
    *
    * <p>Base URL: http://example.com/api/<br>
    * Endpoint: /foo/bar/<br>
    * Result: http://example.com/foo/bar/
    *
    * <p>Base URL: http://example.com/<br>
    * Endpoint: /foo/bar/<br>
    * Result: http://example.com/foo/bar/
    *
    * <p><b>Endpoint values may be a full URL.</b>
    *
    * <p>Values which have a host replace the host of {@code baseUrl} and values also with a scheme
    * replace the scheme of {@code baseUrl}.
    *
    * <p>Base URL: http://example.com/<br>
    * Endpoint: https://github.com/square/retrofit/<br>
    * Result: https://github.com/square/retrofit/
    *
    * <p>Base URL: http://example.com<br>
    * Endpoint: //github.com/square/retrofit/<br>
    * Result: http://github.com/square/retrofit/ (note the scheme stays 'http')
    */
   public Builder baseUrl(HttpUrl baseUrl) {
     Objects.requireNonNull(baseUrl, "baseUrl == null");
     List<String> pathSegments = baseUrl.pathSegments();
     if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
       throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
     }
     this.baseUrl = baseUrl;
     return this;
   }

那么baseUrl跟Endpoint它们是怎么组合拼接的呢?

retrofit是通过:url = baseUrl.resolve(relativeUrl);来将baseUrl跟Endpoint组合拼接的,resolve方法是okhttp3中HttpUrl类的一个方法,最终是由HttpUrl的Builder parse(@Nullable HttpUrl base, String input);方法来完成组合拼接任务。

  • 首先我们先来了解下一个url的组成:
    一个完整的URL是这样子的:
    URL = scheme://username:password@host:Port/PathSegment?QueryParameter#fragment
    通常我们平时使用的URL中很少会带有username:password这个Authority部分的,所以我们常见的URL是这个样子的:
    URL = scheme://host:Port/PathSegment?QueryParameter#fragment
    scheme :http/https协议
    Authority :username:password
    host : 主机地址
    Port :端口号;http默认为80,https默认为443
    PathSegment :路径
    QueryParameter :查询参数,我们的get请求参数就是在这个部分
    fragment :fragment不会发送到服务器,只是在客户端中使用
  • 组合拼接流程:
    1. 先通过查找Endpoint中的第一个':'的位置,当存在且长度为5则为"http:",为6则是"https:";当找不到时则使用baseUrl的scheme
    2. 当Endpoint剩余部分包含两个以上'/' '\'符号,或baseUrl为空或者baseUrl的scheme与Endpoint不一致时,Authority及主机端口路径等都将从Endpoint中解析读取;否则直接取baseUrl中的值
    3. Authority信息的处理,通过查找'@'符号的存在以及该符号前是否包含账号密码分隔符':'来确认Endpoint是否有用户账号及密码信息
    4. 当前位置到下一个':'部分就是主机部分了
    5. 主机部分后到下一个分隔符若可以解析为整型且在(0,65535]范围内则为端口号,否则端口号为默认值,按照scheme来负值
    6. 解析路径PathSegment部分,当前剩余部分到分隔符'?''#'间的部分为路径部分,当路径部分以'/' 或'\'开始的话则会将之前的PathSegment部分给reset,否则将之前最后一个PathSegment置为空串 👆上面需要注意的1、2点原因见下代码可知
      private void resolvePath(String input, int pos, int limit) {
        // Read a delimiter.
        if (pos == limit) {
            // Empty path: keep the base path as-is.
            return;
        }
        char c = input.charAt(pos);
        if (c == '/' || c == '\\') {
          // Absolute path: reset to the default "/".
          encodedPathSegments.clear();
          encodedPathSegments.add("");
          pos++;
        } else {
          // Relative path: clear everything after the last '/'.
          encodedPathSegments.set(encodedPathSegments.size() - 1, "");
        }
      
        // Read path segments.
        for (int i = pos; i < limit; ) {
          int pathSegmentDelimiterOffset = delimiterOffset(input, i, limit, "/\\");
          boolean segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit;
          push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true);
          i = pathSegmentDelimiterOffset;
          if (segmentHasTrailingSlash) i++;
        }
      }
      
    7. 分割符'?'往后'#'之前部分则为查询参数QueryParameter
    8. 剩下的分隔符'#'开始往后部分就是fragment部分了

由于这部分代码比较多就不贴出来了,更多源码内容可以见:

https://github.com/square/retrofit

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容