Android Retrofit修改baseUrl不生效问题

背景

后台大佬:我们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不生效的坑,打完收工。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Retrofit是一个用于构建网络请求的开源库,通常用于Android开发。在某些情况下,我们需要访问不止一个基本URL来获取数据或访问不同的API。 对于这种情况,Retrofit提供了多种解决方案来支持多个base URL。以下是其中一种实现方式: 首先,我们需要为每个base URL创建一个单独的Retrofit实例。然后可以为每个实例分别设置自定义的OkHttpClient和Converter工厂。 在创建Retrofit实例时,我们可以通过`Retrofit.Builder()`来设置各种属性,例如OkHttpClient、Converter工厂等。我们可以为每个base URL创建一个Retrofit实例,并为每个实例设置不同的配置。 接下来,我们需要为每个base URL创建一个对应的API接口。可以通过在不同的Retrofit实例上调用`create()`方法来创建这些API接口。每个base URL都对应着其相应的Retrofit实例,这样每个实例都会将请求发送到正确的base URL。 最后,我们就可以使用相应的API接口来发送请求和获取数据了。根据需要选择不同的API接口即可,每个接口对应着不同的base URL。 这样,通过创建多个Retrofit实例并为每个实例设置不同的base URL,我们就可以在一个应用中访问多个不同的API。 总结起来,retrofitbaseurl的实现方式是通过为每个base URL创建一个独立的Retrofit实例,并为每个实例设置不同的配置,然后使用相应的API接口来发送请求和获取数据。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值