我们在使用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合并使用的时候需要注意以下几点:
- baseUrl必须以“/”结尾,否则主机最后面的部分路径将被忽略掉
- endpoint以“/”开始的话表示绝对路径,也就是baseURL只有服务器主机地址之前的部分将被继续使用
- endpoint包含主机地址时,最终主机地址将使用endpoint的地址,而scheme将沿用baseUrl的
- 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不会发送到服务器,只是在客户端中使用 - 组合拼接流程:
- 先通过查找Endpoint中的第一个':'的位置,当存在且长度为5则为"http:",为6则是"https:";当找不到时则使用baseUrl的scheme
- 当Endpoint剩余部分包含两个以上'/' '\'符号,或baseUrl为空或者baseUrl的scheme与Endpoint不一致时,Authority及主机端口路径等都将从Endpoint中解析读取;否则直接取baseUrl中的值
- Authority信息的处理,通过查找'@'符号的存在以及该符号前是否包含账号密码分隔符':'来确认Endpoint是否有用户账号及密码信息
- 当前位置到下一个':'部分就是主机部分了
- 主机部分后到下一个分隔符若可以解析为整型且在(0,65535]范围内则为端口号,否则端口号为默认值,按照scheme来负值
- 解析路径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++; } }
- 分割符'?'往后'#'之前部分则为查询参数QueryParameter
- 剩下的分隔符'#'开始往后部分就是fragment部分了
由于这部分代码比较多就不贴出来了,更多源码内容可以见: