优雅使用Retrofit,在协程时代遨游安卓网络请求(二)

关于上一篇的一些补全

  在上一篇文章中,笔者提出的NetworkResult缺乏了部分关键代码,首先我们重新回顾这部分代码。

sealed class NetworkResult<T> {

    /**
     * 网络请求成功
     */
    class Success<T>(private val response: Response<T>) : NetworkResult<T>(){}
    
    /**
     * 网络请求失败
     */
    sealed class Failure<T> : NetworkResult<T>() {
        /**
         * 服务器内部错误
         */
        data class ServerError<T>(private val response: Response<T>) : Failure<T>() {}

        /**
         * 网络请求出现异常
         */
        data class Exception<T> constructor(
            val exception: Throwable
        ) : Failure<T>() {}
    }
}
 
复制代码

  可以看到,Response转成NetworkResult的过程,实际上就是用NetworkResultResponse一部分或者全部包裹起来的过程,当然我们并不希望将原始的Response暴露给调用者,我们希望做一些接口上的屏蔽,把对Response的取值交给接口属性,接下来定义一套接口。

private interface ResponseGetter {
    //http状态码
    val code: Int
    //响应头
    val headers: Headers
    //请求Url(部分场景用于判断)
    val url: String
}
复制代码

注:可以扩充你希望得到的属性

  接下来,让Result子类实现这个接口,都用lazy来委任取值(避免反复调用取值函数降低性能),需要注意的是Success和ServerError分别拥有一个单独的属性:responseBody和ResponseErrorMessage。

  responseBody是接口的返回值对应的泛型类型,responseErrorMessage则是服务器内部错误的信息,一般是一个HTML(具体看后台框架)

/**
 * 网络请求成功
 */
class Success<T>(private val response: Response<T>) : NetworkResult<T>(), ResponseGetter {
    val responseBody by lazy { response.body()!! }
    override val code by lazy { response.code() }
    override val headers: Headers by lazy { response.headers() }
    override val url by lazy { response.raw().request.url.toString() }
}

/**
 * HTTP协议错误
 */
data class ServerError<T>(val response: Response<T>) : Failure<T>(), ResponseGetter {
    val responseErrorMessage: String by lazy { response.errorBody()?.string().orEmpty() }
    override val code by lazy { response.code() }
    override val headers: Headers by lazy { response.headers() }
    override val url by lazy { response.raw().request.url.toString() }
}

复制代码

  因为Exception并没有Response,所以我们不需要实现接口方法,独立给他增加一个异常信息。所谓的“异常信息”并不是指异常本身的错误堆栈,这些堆栈是给程序员阅读的,用户并不知道是什么含义,所以我们需要针对特定的异常去翻译一套用户能够识别的错误信息,例如将网络中断异常翻译成“Network Error”。至于如何实现我们下文讲解。

/**
 * 网络请求出现异常
 */
data class Exception<T>(val exception: Throwable) : Failure<T>() {
    //分析异常类型,返回自然语言错误信息
    val exceptionMessage:String by lazy {
        //TODO 下文讲解
    }
}
复制代码

好了,上一篇的坑已经补完了,我们接下来继续讲解实际开发中会遇到的问题。

实际开发中会遇到的问题

  这些问题的解决过程肯定离不开okhttp(毕竟框架底层就是okhttp),因此笔者希望你对okhttp有一定的理解,特别是拦截器的层面。

公共参数

  几乎所有的项目都会遇到添加公参的问题,这里以POST请求为例,讲解一下如何添加公参:众所周知,POST请求的请求体是放在body中的,因此我们只需要以下几步:

  1. 新增一个okhttp拦截器
  2. 通过chain获取request,通过request获取到原始的body,将body中的字节流转成字符串或者其他格式(例如JSON,这个具体看你们公司项目)
  3. 对转换后的对象进行添加公参操作,例如JSON就是添加一些字段
  4. 将对象转回request,通过chain传递到下一个拦截器中

  依然是废话不多说,看代码!

/**
 * 公参基类,重写方法来增加公参
 */
abstract class BaseCommonParamsInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originRequest = chain.request()
        //1.body转成json,如果body为空则构建空json,只填充公参
        val jsonBody = (originRequest.body?.toJson() ?: JSONObject()).apply {
            //2.添加公参
            addCommonParams()
        }
        //3.构建新的requestBody,传递给下一个拦截器
        return chain.proceed(originRequest.newBuilder().apply {
            method(
                originRequest.method,
                jsonBody.toString().toRequestBody(originRequest.body?.contentType())
            )
        }.build())
    }

    //公参
    protected abstract fun getCommonParams(): Map<String, String>

    //给JSON添加参数
    private fun JSONObject.addCommonParams() {
        getCommonParams().forEach { entry ->
            put(entry.key, entry.value)
        }
    }

}

//将RequestBody中的字节流转成字符串
fun RequestBody.string(): String {
    val buffer = Buffer()
    writeTo(buffer)
    return buffer.readUtf8()
}

fun RequestBody.toJson(): JSONObject {
    return string().toJSONObject()
}

复制代码

  originRequest会被转成Json对象,然后对json对象进行添加公参,逻辑非常简单。需要注意的是RequestBody.string()这个扩展方法,通过它转成字符串后,你就可以根据你们公司的请求格式来转成其他实体类了,笔者演示的是转成json。

扫描二维码关注公众号,回复: 14251048 查看本文章

  接下来重写,然后添加到okhttp的构造方法链中就好了。

class MyCommonParamsInterceptor : BaseCommonParamsInterceptor() {
    override fun getCommonParams(): Map<String, String> = CommHttpParams.getInstance().urlParamsMap
}
复制代码

  实际上大部分的定制逻辑(接近99%),都可以通过添加拦截器来实现,okhttp的拦截器真的是神器。

test4 (2).png

请求参数加密and响应报文解密

  为了数据安全,大多数请求和响应报文都是AES加密的,因此我们也需要Retrofit帮我们完成这部分逻辑,实际上这部分逻辑依然是交给Okhttp完成。和添加公参的过程类似,我们依然是新增拦截器,只不过这次还需要对响应报文进行解密处理。

  依然是废话不多说,看代码!

/**
 * 加解密拦截器基类
 */
abstract class BaseEncryptAndDecryptInterceptor : Interceptor{

    final override fun intercept(chain: Interceptor.Chain): Response {
        val originRequest = chain.request()
        return decryptResponse(chain.proceed(
            originRequest.body.run {
                //如果body为空,则直接往下传
                if (this == null) {
                    originRequest
                }
                //如果body不为空,则解密
                else {
                    originRequest.newBuilder().apply {
                        method(
                            originRequest.method,
                            encrypt(this@run)
                        )
                    }.build()
                }
            }
        ))
    }
    
    //加密参数
    @Throws(IOException::class)
    abstract fun encrypt(requestBody: RequestBody): RequestBody
    
    //解密响应报文
    @Throws(IOException::class)
    abstract fun decryptResponse(response: Response): Response

}
复制代码

  使用起来也非常简单,只要实现了那两个抽象方法即可,RequestBody和Response都有一个类似克隆的方法,以便于返回一个全新的对象供修改。以下是笔者的实现(省略了部分代码,主要是具体加密部分,你可以替代为你们公司的密钥工具)。


override fun encrypt(requestBody: RequestBody): RequestBody {
    return requestBody.string()//1.转成String
        .encrypt()//2.加密
        .toRequestBody(requestBody.contentType())//3.转成requestBody,保留原来的contentType
}

override fun decryptResponse(response: Response): Response {
    //如果不成功则不尝试解密
    if (!response.isSuccessful) {
        return response
    }
    val responseBody = response.body ?: return response
    return response.newBuilder()
        .body(responseBody.encrypt())
        .build()
}
复制代码

  讲了2个关于okhttp的拦截器的逻辑定制的做法,想必聪明的读者已经可以举一反三了,利用okhttp的拦截器我们可以实现很多客制化逻辑,例如重试逻辑等,发挥你的想象力,动手尝试吧!等等,好像我们还没有来到Retrofit层面呢。

  别急,下面即将带领你进入Retrofit的自定义注解+自定义注解逻辑的世界!

Retrofit自定义注解

  Retrofit给我们提供了好多有用的注解,但是Retrofit却没有给我们增加自定义注解的机会,所有Retrofit预设的注解的解析过程都是硬编码在一堆if-else语句里面的,也就是说:Retrofit并没有在这个层面增加类似CallAdapter的扩展性。

  那我们该怎么办呢?实则Retrofit官方早就已经想到了这一层了,于是在Retrofit构建OKhttp的Request的过程中,通过给Request新增一个tag,把方法通过tag传到这个Request上面,具体我们可以看源码,具体在Retrofit源码的RequestFactory.java中。

okhttp3.Request create(Object[] args) throws IOException {
    //...省略部分源码
    RequestBuilder requestBuilder =
        new RequestBuilder(
            httpMethod,
            baseUrl,
            relativeUrl,
            headers,
            contentType,
            hasBody,
            isFormEncoded,
            isMultipart);

    //...省略部分源码 
    //新增一个tag,把方法添加到tag里面去
    return requestBuilder.get().tag(Invocation.class, new Invocation(method, argumentList)).build();
}
复制代码

  既然Retrofit给Request添加了一个tag,tag中包含了当前的方法,那么我们就可以通过解析request来拿到我们想要的注解,然后通过注解本身的信息,来完成一些逻辑定制了。

  同样的,让我们重新回到okhttp的拦截器中去,直接看代码!

/**
 * 返回某个Retrofit定义在方法上的注解,例如[POST],[GET]
 */
fun <T : Annotation> Request.getMethodAnnotation(annotationClass: Class<T>): T? {
    return tag(Invocation::class.java)?.method()?.getAnnotation(annotationClass)
}
复制代码

  可恶,代码居然如此简单,实际上就是通过类型来找到某个tag,再通过tag来找到我们定义的方法,紧接着找到方法上面的注解即可!

  为了讲明白如何定制逻辑,我们先假设一个场景,我们有一部分的接口是需要给url后面接一个参数,例如?version=2,那么我们应该怎么做呢?只需要三步即可:

  1. 新增一个注解,包含一个属性versionCode
/**
 * 用于标记retrofit接口方法,声明当前请求的new_versioncode的值
 */
@Target(AnnotationTarget.FUNCTION)
annotation class VersionCode(
    //版本号,
    val versionCode: Int,
)
复制代码
  1. 在Retrofit方法中使用该注解。
@POST("/friend/list")
@VersionCode(versionCode = 2)
suspend fun requestFriend(
    @Body friendRequestParam: FriendRequestParam
): NetworkResult<FriendBean>
复制代码
  1. 在拦截器中获取到该注解,并修改Request
/**
 * 后台加密版本号控制,主要修改new_versioncode字段的值
 */
class VersionControlInterceptor @Inject constructor() : Interceptor {

    companion object {
        //需要添加的前缀
        private const val VERSION_CODE_PREFIX = "?versioncode="
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val originRequest = chain.request()
        val annotation = originRequest.getMethodAnnotation(VersionCode::class.java)
        return chain.proceed(
            //没加注解,跳过处理
            if (annotation == null)
                originRequest
            //在原url中拼接?new_versioncode=x字符串
            else
                originRequest.newBuilder()
                    .url("${originRequest.url}$VERSION_CODE_PREFIX${annotation.versionCode}")
                    .build()
        )
    }

}
复制代码

  大功告成!我们通过查找request中的tag的方式找到原始的method,然后再通过method找到了我们需要的注解,这样就打通了Retrofit和okhttp在定制逻辑上的关系了,通过在接口方法上增加注解的方式,让逻辑定制更加直观!

很感谢你看到这里,这篇已经结束了,下一篇我将继续讲讲Retrofit的一些全局操作。

猜你喜欢

转载自juejin.im/post/7106832453578260494