关于上一篇的一些补全
在上一篇文章中,笔者提出的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的过程,实际上就是用NetworkResult将Response一部分或者全部包裹起来的过程,当然我们并不希望将原始的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中的,因此我们只需要以下几步:
- 新增一个okhttp拦截器
- 通过chain获取request,通过request获取到原始的body,将body中的字节流转成字符串或者其他格式(例如JSON,这个具体看你们公司项目)
- 对转换后的对象进行添加公参操作,例如JSON就是添加一些字段
- 将对象转回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。
接下来重写,然后添加到okhttp的构造方法链中就好了。
class MyCommonParamsInterceptor : BaseCommonParamsInterceptor() {
override fun getCommonParams(): Map<String, String> = CommHttpParams.getInstance().urlParamsMap
}
复制代码
实际上大部分的定制逻辑(接近99%),都可以通过添加拦截器来实现,okhttp的拦截器真的是神器。
请求参数加密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,那么我们应该怎么做呢?只需要三步即可:
- 新增一个注解,包含一个属性versionCode
/**
* 用于标记retrofit接口方法,声明当前请求的new_versioncode的值
*/
@Target(AnnotationTarget.FUNCTION)
annotation class VersionCode(
//版本号,
val versionCode: Int,
)
复制代码
- 在Retrofit方法中使用该注解。
@POST("/friend/list")
@VersionCode(versionCode = 2)
suspend fun requestFriend(
@Body friendRequestParam: FriendRequestParam
): NetworkResult<FriendBean>
复制代码
- 在拦截器中获取到该注解,并修改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的一些全局操作。