当前,许多网络库基于Retrofit或OkHttp开发,但实际项目中常需要定制化,并且需要添加类似双向认证等安全功能。这意味着每个项目都可能需要二次开发。那么,有没有一种通用的封装方式,可以满足大多数项目需求?本文将介绍一种通用化的封装方法,帮助你用最少的代码量开发出自己的网络库。
框架简介
FlexNet 网络库是基于 Square 公司开源的 Retrofit 网络框架进行封装的。Retrofit 底层采用 OkHttp 实现,但相比于 OkHttp,Retrofit 更加便捷易用,尤其适合用于 RESTful API 格式的请求。
在网络库内部,我们实现了双向认证功能。在初始化时,您可以选择是否开启双向认证,框架会自动切换相应的 URL,而业务方无需关注与服务端认证的具体细节。
接入方式
1. 本地aar依赖
下载aar到本地(下载地址见文末),copy到app的libs目录下,如图:
implementation(files("libs/flex-net.aar"))
然后sync只会即可
2. 通过Maven远程依赖
FlexNet目前已上传Maven,可通过Maven的方式引入,在app的build.gradle中加入以下依赖:
implementation("com.max.android:flex-net:3.0.0")
sync之后即可拉到Flex-Net
快速上手
网络库中默认打开了双向认证,并根据双向认证开关配置了相应的 baseUrl,大多数场景下只需要控制双向认证开关,其余配置走默认即可。
在发起网络请求之前(建议在Application
的onCreate()
中),调用:
fun initialize(
app: Application,
logEnable: Boolean = BuildConfig.LOG_DEBUG,
sslParams: SSLParams? = null,
)
- application: Application类型,传入当前App的Application实例;
- logEnable: Boolean类型,网络日志开关,会发打印Http的Request和Resonpse信息,可能涉及敏感数据,release包慎用;(仅限网络请求日志,和双向认证的日志不同)
- sslParams: 双向认证相关参数,可选,为空则关闭双向认证。具体描述见下文。
当App需要双向认证功能时,需要在initialize()
方法中传递sslParams参数,所有双向认证相关的参数都放在sslParams当中,传此参数默认打开双向认证。
SSLParams的定义如下:
data class SSLParams(
/** App 是否在白名单之中。默认不在 */
val inWhiteList: Boolean = false,
/** 双向认证日志开关,可能涉及隐私,release版本慎开。默认关 */
val logSwitch: Boolean = true,
/** 是否开启双向认证。默认开 */
val enable: Boolean = true,
/** 双向认证回调。默认null */
val callback: MutualAuthCallback = null,
)
- inWhiteList: App是否在白名单中,默认不在
- logSwitch: 双向认证日志开关,可能涉及隐私,release版本慎开。默认关,注意这里仅针对双向认证日志,与
initialize()
方法中的logEnable
不同 - callback : 监听初始化结果回调,true表示成功,反之失败。可选参数,默认为null,仅
enableMutualAuth
为true时有效
在调用了initialize
之后就完成了初始化工作,内部包含了双向认证、网络状态、本地网络缓存等等功能,所有的网络请求都需要在初始化之后发起。
初始化示例代码:
FlexNetManger.initialize(this,
logEnable = true,
SSLParams {
Timber.i("Mutual auth result : $it")
})
PS *: *部分App在启动的时候获取不到证书,所以这里会失败。如果失败了后续可以在合适的时机通过MutualAuthenticate.isSSLReady()
来检查是否认证成功,然后通过MutualAuthenticate.suspendBuildSSL()
来主动触发双向认证,成功之后方可开始网络请求。具体可参见文档“配置项”的内容。
双向认证失败及其相关问题,可参考双向认证文档 : [双向认证])
在请求之前需要根据接口协议的字段定义对应的数据Model,用来做Request或者Response的body。
比如我们需要通过UserId获取对应用户的UserName
后端请求接口参数如下:
{
"userId" : "123456"
}
那么根据参数定义一个UserNameReq类:
data class UserNameReq(
/** 用户id */
var userId: String
)
后端返回数据如下:
{
"userName" : "MC"
}
对应定义一个UserNameRsp:
data class UserNameRsp(
/** 用户id */
var userId: String
)
接口类必须继承自IServerAPI:
interface UserApi: IServerApi
然后在IServerApi的实现类中,每个接口需要用注解的形式标注 Http 方法,通过参数传入 http 请求的 url:
interface UserApi: IServerApi {
/** 获取用户ID */
@POST("api/cloudxcar/atmos/v1/getName")
suspend fun getUserName(@Body request: UserNameReq): ResponseEntity<UserNameRsp>
}
这里需要注意的是,我们的UserNameRsp需要用ResponseEntity封装一层,看一下ResponseEntity的内容:
sealed class ResponseEntity<T>(val body: T?, val code: Int, val msg: String)
有3个参数:
-
body: 消息体,即UserNameReq。仅成功时有效
-
code : 返回码,这里要分多种情况描述。
- Http错误:此时code为Http错误码
- 其他异常:code对应错误原因,后面会附上映射表
- 请求成功:区分网络数据和缓存数据
-
msg : 错误信息
可调用ResponseEntity.isSuccessful()
来判断是否请求成功,然后通过ResponseEntity.body
获取数据,返回的是一个根据服务端返回的 Json 解析而来的UserNameRsp实体类。
如果请求失败,则从ResponseEntity.msg
和ResponseEntity.code
中获取失败ma失败码和失败提示
继承自BaseRepo,泛型参数为步骤3中创建的IserverApi实现类:
class VersionRepo : BaseRepo<VersionAPI>
-
其中需要有1个必覆写的变量:
- baseUrl: 网络接口的baseUrl
-
两个可选项:
- mutualAuthSwitch: 双向认证开关,此开关仅针对当前 baseUrl 生效。默认开
- interceptorList: 需要设置的拦截器列表
-
一个必覆写的方法:
- createRepository(): 创建当前网络仓库
完整的Repo类内容如下:
class UserRepo: BaseRepo<UserApi>() {
// 必填
override val baseUrl = "https://juejin.cn/editor/drafts/7379502040140218422"
// 必填
override fun createRepository(): VersionAPI =
MutualAuthenticate.getServerApi(baseUrl, mutualAuthSwitch, interceptorList)
// 可选:双向认证开关,仅针对当前repo生效
override val mutualAuthSwitch = true
// 可选:Http拦截器
override val interceptorList: List<Interceptor>? = listOf(HeaderInterceptor())
// 请求接口
suspend fun getUserName(): ResponseEntity<UserNameRsp>{
return mRepo.upgradeVersion(UserNameReq("123456"))
}
}
注: 其中拦截器的设置interceptorList,如果声明的时候提示错误,可以尝试加上完整的类型声明:
interceptorList: List<Interceptor>?
5 发起网络请求
最后就可以在业务代码中通过Repo类完成网络请求的调用了:
lifecycleScope.launch {
val entity= UserRepo().getUserName()
Timber.i("Get responseEntity: $entity")
if (entity.isSuccessful()) {
val result = entity.body
Timber.i("Get user name result: $result")
} else {
val code = entity.code
val msg = entity.msg
Timber.i("Get user name failed: code->$code; msg->$msg")
}
}
到这里,就可以发起一次基础的网络请求接口了。
依赖项
目前引入的双向认证版本为1.6.0,如果需要切换版本,或者编译出现依赖冲突,可以尝试使用exclude的方式自行依赖。当然也请自行确保功能正常。
implementation("com.jakewharton.timber:timber:4.7.0")
组件库中的日志库。FlexNet推荐宿主使用Timber进行日志输出,但是需要宿主App在初始化FlexNet之前对Timber做plant操作。
// Net
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
底层网络请求目前依赖OkHttp完成。
implementation("com.tencent:mmkv:1.2.14")
网络库中的本地存储,主要用于保存网络缓存,目前采用MMKV-1.2.14版本,同样如果有冲突,或者需要另换版本,可通过exclude实现。
api(core.network.retrofit.gson) {
exclude(module = "okio")
exclude(module = "okhttp")
}
依赖Gson,用于做数据结构和Json的相互转化
错误码对照表
CODE_SUCCESS | 10000 | 请求成功,数据来源网络 |
---|---|---|
CODE_SUCCESS_CACHE | 10001 | 返回成功,数据来源于本地缓存 |
CODE_SUCCESS_BODY_NULL | 10002 | 请求成功,但消息体为空 |
CODE_ERROR_UNKNOWN | -200 | 未知错误 |
CODE_ERROR_UNKNOWN_HOST | -201 | host解析失败,无网络也属于其中 |
CODE_ERROR_NO_NETWORK | -202 | 无网络 |
日志管理
从FlexNet 2.0.5开始,对接入方使用的日志库不再限制(2.0.5以下必须用Timber,否则无日志输出)。可以通过以下接口来设置日志监视器:
setLogMonitor(log: ILog)
设置之后所有的网络日志都会回调给ILog,即可由接入方自行决定如何处理日志数据。
如果没有设置LogMonitor
,则会使用Timber
或者Android
原生Log
来进行日志输出。当宿主App的Timber挂载优先于FlexNet的初始化,则会采用Timber做日志输出,反之使用Android Log。
文件下载
网络库内置了下载功能,可配置下载链接和下载目录。注意外部存储地址需要自行申请系统权限。
1 构建下载器
使用Downloader.builder()
来构建你的下载器,Builder需要传入以下参数:
- url:待下载文件的url
- filePath:下载文件路径
- listener:下载状态回调。可选参数,空则无回调
示例代码如下:
Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
File(requireContext().filesDir, "MC").absolutePath)
2 回调监听
builder()
最后一个参数,可传入下载监听器接口DownloadListener
,内部有3个方法需要实现:
- onFinish(file: File): 下载完成,返回下载完成的文件对象
- onProgress( progress : Int, downloadedLengthKb: Long, totalLengthKb: Long): 下载进度回调,回传进度百分比、已下载的大小、总大小
- onFailed(errMsg: String?): 下载失败,回调失败信息
示例代码如下:
val downloader = Downloader.Builder("https://juejin.cn/editor/drafts/7379502040140218422.zip",
File(Environment.getExternalStorageDirectory(), "MC").absolutePath,
object : DownloadListener {
override fun onFinish(file: File) {
Timber.e("下载的文件地址为:${file.absolutePath}".trimIndent())
}
override fun onProgress(
progress: Int,
downloadedLengthKb: Long,
totalLengthKb: Long,
) {
runOnUiThread {
textView.text =
"文件文件下载进度:${progress}% \n\n已下载:%${downloadedLengthKb}KB | 总长:${totalLengthKb}KB"
}
}
override fun onFailed(errMsg: String?) {
Timber.e("Download Failed: $errMsg")
}
}).build()
PS : 这里要注意,FlexNet会在业务方调用下载的线程返回下载回调,所以绝大部分时候回调是发生在子线程,此时如果有线程敏感的功能(比如刷新UI),需要自行处理线程切换。
3 触发下载
通过Builder.build()
创建 Downloader 下载器,最后调用Downloader.download()
方法即可开始下载。
和Http Request一样,download()
是一个suspend方法,需要在协程中使用:
lifecycleScope.launch(Dispatchers.IO) {
downloader.download()
}
整体架构
设置配置项
1. 设置双向认证开关
在初始化的时候控制双向认证开关:
fun init(context: Application, needMutualAuth: Boolean = true)
方法内部会根据开关值来切换不同的后端服务器,但是有些App不能过早的获取证书,这样会有双向认证失败的风险,FlexNet同时支持懒汉式的主动双向认证
2. 主动双向认证接口
在确定拿到证书,或者确定可以双向认证的时机,可随时发起双向认证请求:
MutualAuthenticate.suspendBuildSSL()
可通过
MutualAuthenticate.isSSLReady()
接口来检查当前双向认证是否成功。
主动触发示例代码如下:
MutualAuthenticate.suspendBuildSSL {
if (it) {
Toast.makeText(context, "双向认证成功,可以开始访问加密资源", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "双向认证失败", Toast.LENGTH_SHORT).show()
}
}
3. 数据缓存
在前面发起请求调用httpRequest
顶层函数的时候,可以传入一个可选参数cacheKey
,这个key不为空则网络库会在本地保存当前请求的返回数据。Key作为缓存的唯一标识,在无网络或请求失败的时候,会通知调用方错误,并返回缓存的数据。
缓存部分流程如下:
4. 错误及异常处理
在发起请求的顶层函数 httpRequest
中,有两个参数用来提供给调用方处理错误和异常。
首先区分一下错误和异常:
错误通常是发起了网络请求,且网络请求有响应,只是由于接口地址或者参数等等原因导致服务端解析失败,最终返回错误码及错误信息。
而异常是指在发起网络请求的过程中出现了 Exception,导致整个网络请求流程被中断,所以当异常发生的时候,网络库是不会返回错误码和错误信息的,只能返回异常信息供调用方定位问题。
回调的使用方式很简单,只需要在httpRequest
中传入两个回调:fail
和error
,下面分别看看二者的处理方式:
1. 错误处理
fai的定义如下:
fail: (response: ResponseEntity<T>) -> Unit = {
onFail(it)
}
传入的回调有一个 ResponseEntity 参数,这是网络请求返回的响应实体,内部包含errorCode
和errorMessage
,不传则默认打印这两个字段,可以在 Logcat 中通过Tag:Http Request
**过滤出来。
2. 异常处理
error的定义如下:
error: (e: Exception) -> Unit = {
onError(it)
} ,
回调函数只有一个 Exeption 对象,和前面的定义相符,在异常的时候将异常返回供调用方定位问题。不传网络库默认打印异常,可以在 Logcat 中通过Tag:Http Request
**过滤出来。
扩展接口:发起请求并处理返回结果
网络库定义了一个顶层函数用来发起请求并接收返回结果或处理异常:
fun <reified T> httpRequest(block, fail, error, cacheKey): T?
- block: 实际请求体,必填。可以传入步骤 4 中实现的接口
- fail: 请求错误回调,非必填。用来处理服务端返回的请求错误,会携带错误码及错误信息
- error: 请求异常回调,非必填。用来处理请求中发生的异常,此时没有response返回
- cacheKey: 数据缓存唯一标识,非必填
httpRequest 中的泛型 T 就是接入步骤2定义的 Response 实体,正常返回会在方法内部自动解析出 UserNameRsp
,到此就完成了一次网络请求。
以上是基本的使用方式,涵盖了安全、数据请求、缓存、异常处理等功能,可以适应于多种项目场景。应大家的建议,后续会完善几篇文章拆解具体的原理及开发思路,从源码的角度教你如何从0开发一套完善的网络库
需要体验的同学可以在评论区留下联系方式,我给你发送aar以及源码。有问题欢迎随时探讨