背景
后台大佬:我们api目前不够安全,不能直接通过原有地址(https://xxx.xxx.x.x/#/#)访问了,要通过网关访问,所有域名后面加多个路径(https://xxx.xxx.x.x/1/#/#);
前端大鸟:简单,我们是采用retrofit+okhttp的网络框架,基础url是配置的,改下就可以,敲代码…
//原有Retrofit配置
new Retrofit
.Builder()
.baseUrl("https://xxx.xxx.x.x")
.client(getOkHttpClient())
//修改后Retrofit配置
new Retrofit
.Builder()
.baseUrl("https://xxx.xxx.x.x/1/") //带path最后必须加/否则报错
.client(getOkHttpClient())
//其中api声明
@POST("/#/#")
Observable A();
但是运行起来后会发现baseurl新增的**/1/是没生效的**,实际访问地址还是https://xxx.xxx.x.x/#/#,并不是我们期望的https://xxx.xxx.x.x/1/#/#
是不是第一反应是编译器的问题,反正我是这样以为,甚至关机重启后问题依旧,其实这里面是由于retrofit导致的,
解决方案就是删除api声明最前面的/
//修改后的api声明 少了最开始的/
@POST("#/#")
Observable A();
以下是我的问题分析,
首先是看下baseUrl设置有没异常,这时候看下Retrifit的baseUrl()方法
/**
* Set the API base URL.
*/
public Builder baseUrl(String baseUrl) {
return baseUrl(HttpUrl.get(baseUrl));
}
//我们传进来的base url字符串最后以HttpUrl变量保存在retrofit实例中
public Builder baseUrl(HttpUrl baseUrl) {
this.baseUrl = baseUrl;
return this;
}
这里面有个不懂的问题-项目代码是用java写的,照理说HttpUrl.get(String)方法应该也是java类型的,但是HttpUrl中并没有get(String)类型,断点后发现是直接调用kotlin的HttpUrl.get(String)方法,那该类就按kotlin分析
其中HttpUrl.get(String)方法构建一个HttpUrl对象
@JvmStatic
@JvmName("get") fun String.toHttpUrl(): HttpUrl = Builder().parse(null, this).build()
我们先先看parse(null, this)方法,其中null是base,this是传进来的baseUrl,也就是 https://xxx.xxx.x.x/1/#/#
省略代码如下
internal fun parse(base: HttpUrl?, input: String): Builder {
val slashCount = input.slashCount(pos, limit)//0
if (slashCount >= 2 || base == null || base.scheme != this.scheme) {//1
//...
if (portColonOffset + 1 < componentDelimiterOffset) {//2
host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost()
port = parsePort(input, portColonOffset + 1, componentDelimiterOffset)
} else {
host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost()
port = defaultPort(scheme!!)
}
//...
} else {
this.host = base.host
this.port = base.port
this.encodedPathSegments.clear()
this.encodedPathSegments.addAll(base.encodedPathSegments)
}
val pathDelimiterOffset = input.delimiterOffset("?#", pos, limit)
resolvePath(input, pos, pathDelimiterOffset)//3
.....
首先baseUrl进来,slashCount()方法是计算url中"\\和"/"的个数(也就是base url是否包含//或者\),并且传进来的base也为null,故标记1处的if判断会为true ,进入标记2处设置域名和端口,然后我们接着往下看,resolvePath方法
private fun resolvePath(input: String, startPos: Int, limit: Int) {
val c = input[pos]
if (c == '/' || c == '\\') {
// Absolute path: reset to the default "/".
encodedPathSegments.clear()
encodedPathSegments.add("")
pos++
} else {
encodedPathSegments[encodedPathSegments.size - 1] = ""
}
var i = pos
while (i < limit) {//1
val pathSegmentDelimiterOffset = input.delimiterOffset("/\\", i, limit)
val segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit
push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true)//1
i = pathSegmentDelimiterOffset
if (segmentHasTrailingSlash) i++
}
}
其中在1处会while循环找出我们的path位置,然后调用push方法 如下:
/** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */
private fun push(pos:String,...省略) {
val segment = input.canonicalize(
pos = pos,
limit = limit,
encodeSet = PATH_SEGMENT_ENCODE_SET,
alreadyEncoded = alreadyEncoded
)
if (encodedPathSegments[encodedPathSegments.size - 1].isEmpty()) {
encodedPathSegments[encodedPathSegments.size - 1] = segment
} else {
encodedPathSegments.add(segment)
}
if (addTrailingSlash) {
encodedPathSegments.add("")
}
}
input.canonicalize方法会根据传进来的pos找出path名称,插入encodedPathSegments集合中,也就是我们新增的path(/1)在此处被保存
其中encodedPathSegments字段要关注了(敲黑板),就是我们要分析的东西
/**
* A list of encoded path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This
* list is never empty though it may contain a single empty string.
*
* | URL | `encodedPathSegments()` |
* | :---------------------- | :---------------------- |
* | `http://host/` | `[""]` |
* | `http://host/a/b/c` | `["a", "b", "c"]` |
* | `http://host/a/b%20c/d` | `["a", "b%20c", "d"]` |
*/
@get:JvmName("encodedPathSegments") val encodedPathSegments: List
注释已经蛮明显了,就是存储我们传进来的url中包含的path集合,到此parse部分就完成了。
@JvmStatic
@JvmName("get") fun String.toHttpUrl(): HttpUrl = Builder().parse(null, this).build()
接下来就是build()方法
fun build(): HttpUrl {
@Suppress("UNCHECKED_CAST") // percentDecode returns either List or List.
return HttpUrl(
scheme = scheme ?: throw IllegalStateException("scheme == null"),
username = encodedUsername.percentDecode(),
password = encodedPassword.percentDecode(),
host = host ?: throw IllegalStateException("host == null"),
port = effectivePort(),
pathSegments = encodedPathSegments.map { it.percentDecode() },
queryNamesAndValues = encodedQueryNamesAndValues?.map { it?.percentDecode(plusIsSpace = true) },
fragment = encodedFragment?.percentDecode(),
url = toString()
)
}
都是之前配置的一些参数,没提到的也不要紧毕竟我们关注的是path为什么不生效,这里可以看到url是通过toString函数生成,我们跟进去看下
override fun toString(): String {
return buildString {
append(scheme)
append("://")
append(host)
encodedPathSegments.toPathString(this)//添加path
}
}
encodedPathSegments.toPathString(this)方法如下,就是拼装进去
internal fun List.toPathString(out: StringBuilder) {
for (i in 0 until size) {
out.append('/')
out.append(this[i])
}
}
至此baseurl已经设置上去,这时候url还是https://xxx.xxx.x.x/1/ 这时候还没问题的,
那问题到底在哪里呢?
我们看下请求接口时 Request对象的构建,因为最终的url是在此处赋值。
由于源码一步步看太长了,我们直接从关键类代码看
RequestFactory类-OkHttpCall构建Request时会调用该类的create方法,
okhttp3.Request create(Object[] args) throws IOException {
//...
RequestBuilder requestBuilder =
new RequestBuilder(
httpMethod,
baseUrl,
relativeUrl,
headers,
contentType,
hasBody,
isFormEncoded,
isMultipart);
return requestBuilder.get().tag(Invocation.class, new Invocation(method, argumentList)).build();
}
我们看下requestBuilder.get()方法,简洁代码…
Request.Builder get() {
HttpUrl url;
url = baseUrl.resolve(relativeUrl);
return requestBuilder.url(url).headers(headersBuilder.build()).method(method, body);
}
可以看出baseUrl是之前设置的对象,relativeUrl是我们在api声明的路径(https://xxx.xxx.x.x/1/#/#中的**/#/#**)继续跟进resolve方法
public @Nullable HttpUrl resolve(String link) {
Builder builder = newBuilder(link);
return builder != null ? builder.build() : null;
}
还是构造模式,继续根据newBuilder,提醒下newBuilder是HttpUrl内部方法
Builder builder = new Builder();
Builder.ParseResult result = builder.parse(this, link);
return result == Builder.ParseResult.SUCCESS ? builder : null;
what?? 又是parse方法,不是又跟前面的分析一样了吗? 其实这时候还是不一样,这时候parse的第一个参数是this,也就是我们一开始创建的HttpUrl对象,我们这时候挑不同的说,就
internal fun parse(base: HttpUrl?, input: String): Builder {
val slashCount = input.slashCount(pos, limit)
if (slashCount >= 2 || base == null || base.scheme != this.scheme) {//1
//...
if (portColonOffset + 1 < componentDelimiterOffset) {
host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost()
port = parsePort(input, portColonOffset + 1, componentDelimiterOffset)
} else {
host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost()
port = defaultPort(scheme!!)
}
//...
} else {//2
this.host = base.host
this.port = base.port
this.encodedPathSegments.clear()
this.encodedPathSegments.addAll(base.encodedPathSegments)
}
val pathDelimiterOffset = input.delimiterOffset("?#", pos, limit)
resolvePath(input, pos, pathDelimiterOffset)//3
.....
这时会走标记2处,因为path不包含"\"和含有base(HttpUrl)
然后会把baseurl的path加进当前的encodedPathSegments,既然把baseurl的path加进来了,为什么最终的的ur不包含呢?笔者也是在这里卡了好久。
我们接着看标记3
private fun resolvePath(input: String, startPos: Int, limit: Int) {
val c = input[pos]
if (c == '/' || c == '\\') {
// Absolute path: reset to the default "/".
encodedPathSegments.clear()//1
encodedPathSegments.add("")
pos++
} else {
encodedPathSegments[encodedPathSegments.size - 1] = ""
}
var i = pos
while (i < limit) {
val pathSegmentDelimiterOffset = input.delimiterOffset("/\\", i, limit)
val segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit
push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true)//1
i = pathSegmentDelimiterOffset
if (segmentHasTrailingSlash) i++
}
}
看到这里就清楚了,这时候inputs是/#/#,startPos是0,也就是说
path里面是/开头 则把baseurl的encodedPathSegments(path集合)给clear了,所以我们设置的会没有效果。
//其中api声明
@POST("/#/#")
Observable A();
下面的就是添加声明的path到encodedPathSegments,最后toString生成请求的api,详细的话就看源码了
记录一下一次修改baseUrl不生效的坑,打完收工。