本文已授权微信公众号:郭霖  在微信公众号平台原创首发。会用Retrofit了?你也能自己动手写一个!

前言

想封装一套网络请求,不想直接上来就用别人写好的,或者说对项目可以更好的掌控,所以自己模仿着Retrofit来写一套.

想要有如下实现:

  1. 快捷的网络请求调用
  2. 声明式的定义网络请求函数
  3. 可以很灵活的变更网络请求的方式(http,https,socket等)
  4. 可以使用自己的线程池或者协程进行线程调度

定义网络请求函数(如果不使用key来判断,甚至不需要定义companion object中的LOGIN),示例:

调用网络请求和接收返回数据,示例:

this回调

或者匿名内部类回调:

准备和前提

需要读者有如下技能,否则阅读会比较吃力

  1. java编程基础
  2. kotlin编程基础(java经验好可能也无所谓) (kotlin下面简称kt)
  3. 网络请求常识

阅读完本篇文章可以看到(或学到)的知识点

  1. 动态代理的使用和工作原理
  2. java和kotlin的部分反射的使用和区别
  3. 声明和使用运行时注解
  4. dsl的创建,使用和原理
  5. 封装的思想(我遇到某些代码时是怎么想的)

正式开始(从空项目开始,所以每一步都会提及,使用kt写)

1.测试网络和url是否通(不然后面没法验证到底是哪的问题)

这里测试的url使用玩安卓的开放api

清单文件加入权限

<uses-permission android:name="android.permission.INTERNET" />

封装ROOT_URL

object HttpConfig {const val ROOT_URL = "https://www.wanandroid.com/"
}

测试如下url是否可用(使用了kt系统库的扩展函数,和自己定义了一个打印log的函数)

import java.net.URL
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)thread {URL(HttpConfig.ROOT_URL + "article/list/0/json?cid=1").readText().print()}//这里,如果没问题的话就会在logcat中打印出本次网络请求返回的数据}}fun Any?.print() = Log.w("lllttt", this.toString())

2.开始仿照Retrofit的声明式接口,自己定义一个接口

网络回调的接口

interface ObserverCallBack {/*** @param data     返回的数据 json* @param encoding 网络请求的状态(成功,失败,网络失败等)* @param method   判断是哪个接口返回的数据*/fun handleResult(data: String?, encoding: Int, method: Int)
}

声明的网络接口

interface HttpFunctions {/*** 获取玩安卓的json数据* @param cid 这个接口的参数(虽然不知道有什么用emmm)*/fun getJson(_callback: ObserverCallBack?,cid: String)
}

显然上面所声明的网络接口是没法直接调用的,想要调用一个接口的方法,必须有其实现类,而实现该接口对于便捷的网络封装是不现实的,而使用动态代理,就可以在运行时动态生成一个实现类,并且还可以使用代码动态的控制其函数的逻辑

3.使用动态代理获取获取运行时的接口实现类,并获取运行时数据

动态代理平时说的挺玄乎,其实使用和理解起来还是很简单的

大体原理可以这么理解:动态的实现一个类,继承Proxy,并实现所有传入的接口,然后通过反射创建出来这个类,方法都是默认空实现,并且每次调用方法都会经过InvocationHandler的invoke方法,invoke方法里有调用方法的Method对象,可以反射Method对象来实现代理.

原理和字节码解析:https://mp.weixin.qq.com/s/DMnYWXVx0Gf3Mjs38pfOiA

主要api:

Proxy.newProxyInstance()

该方法一共三个参数,第一个是类加载器,第二个就是被代理的接口class集合,第三个是处理方法的InvocationHandler

我们可以这样生成动态代理:

interface HttpFunctions {companion object {/*** 动态代理单例对象*/val instance: HttpFunctions = getHttpFunctions()//获取动态代理实例对象private fun getHttpFunctions(): HttpFunctions {val clazz = HttpFunctions::class.java//拿到我们被代理接口的class对象return Proxy.newProxyInstance(//调用动态代理生成的方法来生成动态代理clazz.classLoader,//类加载器对象arrayOf(clazz),//因为我们的接口不需要继承别的接口,所以直接传入接口的class就行HttpFunctionsHandler()//InvocationHandler接口的实现类,用来处理代理对象的方法调用) as HttpFunctions}}
}

接下来我们实现InvocationHandler接口,可以发现只有一个方法,重写后打印动态代理对象调用的方法名称和方法参数(由于使用kt的接口做为被代理,所以可以返回Unit对象)

/*** 动态代理类方法处理对象*/
class HttpFunctionsHandler : InvocationHandler {override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {method?.name.print()//打印方法名args?.forEach { it.print() }//打印参数值return Unit}
}

接下来我们调用动态代理,测试一下

HttpFunctions.instance.getJson(null, "1")
打印如下:
W/lllttt: getJson
W/lllttt: null
W/lllttt: 1

可以看到我们确实拿到了方法名称和参数的值

4.动态代理结合反射实现网络请求

现在我们修改HttpFunctionsHandler的代码来通过反射拿到参数名

    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {method?.name.print()//打印方法名args?.forEach { it.print() }//打印参数值method?.parameters?.forEach { it.name.print() }//打印参数名return Unit}
但是发现打印如下:
W/lllttt: getJson
W/lllttt: null
W/lllttt: 1
W/lllttt: arg0
W/lllttt: arg1

参数名变成了argx(而且在安卓项目上需要api26以上才能使用),这是为什么呢?

原来java8之前的版本因为某些原因没有支持保留方法参数名的功能,直到java8才支持,且需要手动设置编译参数,所以此种方案无法实现

ps:安卓项目使用如下方式只能开启java8的部分能力(如lambda和stream),不能开启全部

    compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}

那Retrofit是怎么绕开这个限制的呢?使用参数注解,如下:

    //发表评论@FormUrlEncoded@POST("v1/comment/create")Observable<NetBean<Boolean>> commentCreate(@Field("scene") String scene,@Field("scene_id") Long scene_id,@Field("reply_id") Long reply_id,@Field("content") String content);

这也太麻烦了,每一个参数都得对应一个注解,而方法上还需要加两个注解

所以我们使用一种更便捷的方式:kt反射

首先引入kt的反射库(大小几百k)

implementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.71'

然后改造HttpFunctionsHandler

    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {
//        method?.name.print()//打印方法名
//        args?.forEach { it.print() }//打印参数值method?.kotlinFunction?.parameters?.forEach {"${it.type} - ${it.name}".print()//打印参数类型和参数名}return Unit}
打印结果如下:
W/lllttt: com.lt.retrofitdemo.http.HttpFunctions - null
W/lllttt: com.lt.retrofitdemo.http.ObserverCallBack? - _callback
W/lllttt: kotlin.String - cid

我们成功的获取到了参数名,现在可以再次改造HttpFunctionsHandler,使调用HttpFunctions的方法就相当于调用网络请求

改造HttpFunctionsHandler (为了方便演示,只适配get请求,且网络请求方式比较简单)

/*** 动态代理类方法处理对象*/
class HttpFunctionsHandler : InvocationHandler {val handler = Handler(Looper.getMainLooper())override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {thread {val kotlinFunction = method?.kotlinFunction//获取到KFunction对象val url = StringBuilder(HttpConfig.ROOT_URL).append("article/list/0/json?")var callback: ObserverCallBack? = nullkotlinFunction?.parameters?.forEachIndexed { index, kParameter ->when (kParameter.name) {null -> {//HttpFunctions对象,我们不需要}"_callback" -> {//回调对象,ps:index-1是因为parameters的第0位置是代理类对象callback = args?.get(index - 1) as? ObserverCallBack}else -> {//其他的就是参数了//进行拼接urlurl.append(kParameter.name).append('=').append(args?.get(index - 1)).append('&')}}}if (url.endsWith('&'))url.deleteCharAt(url.length - 1)//清除最后一个&url.print()val data = URL(url.toString()).readText()//请求网络handler.post {callback?.handleResult(data, 0, 0)//在主线程回调}}return Unit}
}

然后调用封装后的方法:

        HttpFunctions.instance.getJson(object : ObserverCallBack {override fun handleResult(data: String?, encoding: Int, method: Int) {data.print()}}, "1")
打印如下:
W/lllttt: https://www.wanandroid.com/article/list/0/json?cid=1
W/lllttt: {"data":{"curPage":1,"datas":[],"offset":0,"over":true,"pageCount":0,"size":20,"total":0},"errorCode":0,"errorMsg":""}

可以看到网络请求调用很方便,不用使用参数注解就可以,那kt反射是怎么实现的呢?我们来看一下kt文件反编译后的字节码

使用kt后会出现上面这个选项,使用该选项可以看到kt文件生成的字节码,然后点击Decompile按钮可以生成反编译后的java文件,这样就能看到我们HttpFunctions.kt类到底有什么

@Metadata(mv = {1, 1, 16},bv = {1, 0, 3},k = 1,d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\bf\u0018\u0000 \b2\u00020\u0001:\u0001\bJ\u001a\u0010\u0002\u001a\u00020\u00032\b\u0010\u0004\u001a\u0004\u0018\u00010\u00052\u0006\u0010\u0006\u001a\u00020\u0007H&¨\u0006\t"},d2 = {"Lcom/lt/retrofitdemo/http/HttpFunctions;", "", "getJson", "", "_callback", "Lcom/lt/retrofitdemo/http/ObserverCallBack;", "cid", "", "Companion", "app_debug"}
)
public interface HttpFunctions {HttpFunctions.Companion Companion = HttpFunctions.Companion.$$INSTANCE;void getJson(@Nullable ObserverCallBack var1, @NotNull String var2);
//只展示我们需要的

可以看到,kt自动为我们的.kt类生成了@Metadata注解(元数据注解),其中d2的元数据中把我们的类签名,方法名和参数名等都列了出来,所以kt反射取到的参数名就是从这里面取出来的

5.使用注解来增强功能

现在我们的HttpFunctions只支持get请求,url也没地方设置,并且自定义化还没法做,所以我们使用注解,并搭配运行时反射来增强功能

创建GET和POST两个注解

/*** creator: lt  2020/3/26  lt.dygzs@qq.com** get请求* @param url               请求链接* @param isEncryption      是否加密,一般网络请求都是需要加密的,所以设置了默认参数为true* @param callbackName      回调的参数名*/
@Target(AnnotationTarget.FUNCTION)//表示该注解作用于方法上
@Retention(AnnotationRetention.RUNTIME)//表示该注解保留到运行时
annotation class GET(//在kt中 annotation class 表示注解类,而在java中使用 @interfaceval url: String,val isEncryption: Boolean = true,val callbackName: String = "_callback"
)/*** creator: lt  2020/3/26  lt.dygzs@qq.com** post请求* @param url               请求链接* @param isEncryption      是否加密* @param callbackName      回调的参数名*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class POST(val url: String,val isEncryption: Boolean = true,val callbackName: String = "_callback"
)

接下来我们改造HttpFunctionsHandler的invoke方法,加入注解的判断

/*** 动态代理类方法处理对象*/
class HttpFunctionsHandler : InvocationHandler {val handler = Handler(Looper.getMainLooper())override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any {if (method.declaringClass == Any::class.java) {//处理Object类的方法return (if (args == null) method.invoke(this) else method.invoke(this, *args))}//ps:这里为了方便就直接new Thread了,如果是使用的话可以使用线程池或者kt协程,消耗会低很多,一般项目中是不允许直接new Thread的thread {//获取方法的注解,先获取get注解,如果为空就获取post注解; ps:自己用的时候可以先获取常用的注解,这样就不用判断两次了,比如项目里大部分都是post请求,那就先获取POSTval annotation =method?.getAnnotation(GET::class.java)?: method?.getAnnotation(POST::class.java)//代码不要都堆到一块,而是应该拆成方法或者类,这样调用的时候只调用一个方法,逻辑会清晰很多; ps:这里其实也可以先判断常用的,因为kt的when函数的字节码其实也是if elsewhen (annotation) {is GET -> startGet(proxy, method, args, annotation)is POST -> startPost(proxy, method, args, annotation)else -> throw RuntimeException("亲,${method?.name}方法是不是忘加注解了?")//如果出现异常情况,最好不要藏着,及时告诉开发人员,不然出了问题也不知道是怎么回事,得找好长时间}}return Unit}//post请求private fun startPost(proxy: Any?, method: Method?, args: Array<out Any>?, post: POST) {//post就不写了,大家可以在这里二次封装网络请求,比如使用okhttp,或者使用Socket,甚至可以用别人二次或者三次封装好的网络请求}//get请求private fun startGet(proxy: Any?, method: Method?, args: Array<out Any>?, get: GET) {//获取url并拼接val url = StringBuilder(HttpConfig.ROOT_URL).append(get.url)val callbackName = get.callbackNamevar callback: ObserverCallBack? = nullvar isAddQuestionMark = false//是否追加了'?'method?.kotlinFunction?.parameters?.forEachIndexed { index, kParameter ->when (kParameter.name) {null -> {//HttpFunctions对象,我们不需要}callbackName -> {//回调对象,ps:index-1是因为parameters的第0位置是代理类对象callback = args?.get(index - 1) as? ObserverCallBack}else -> {//其他的就是参数了if (get.isEncryption) {//加密操作} else {//进行拼接urlif (!isAddQuestionMark) {url.append('?')isAddQuestionMark = true}url.append(kParameter.name).append('=').append(args?.get(index - 1)).append('&')}}}}if (url.endsWith('&'))url.deleteCharAt(url.length - 1)//清除最后一个&url.print()val data = URL(url.toString()).readText()//请求网络handler.post {callback?.handleResult(data, 0, 0)//在主线程回调}}
}

然后改变网络请求方法,再调用测试成功

//修改网络请求
@GET("article/list/0/json", isEncryption = false)
fun getJson(_callback: ObserverCallBack?,cid: String
)

6.使用dsl封装回调,使其更方便的处理

写一个简单的dsl,里面参数比较少,可以根据业务需求自行添加参数

import com.alibaba.fastjson.JSONObject
import com.lt.retrofitdemo.print/*** creator: lt  2020/3/26  lt.dygzs@qq.com* effect : 网络请求回调的sdl封装* warning:*/
/*** 使用dsl的callback* ps: CallBackDsl.()这种语法相当于CallBackDsl的一个扩展函数,把CallBackDsl当做这个函数的this,所以该函数中可以不用this.就可以调用CallBackDsl的参数和方法*/
inline fun <reified T> callbackOf(initDsl: CallBackDsl<T>.() -> Unit): ObserverCallBack {val dsl = CallBackDsl<T>()dsl.initDsl()//初始化dslif (dsl.isAutoShowLoading)"Show loading dialog".print()return object : ObserverCallBack {override fun handleResult(data: String?, encoding: Int, method: Int) {if (dsl.isAutoShowLoading)"Dismiss loading dialog".print()//可以在这里根据业务判断是否请求成功//引入fastjson来解析json    implementation 'com.alibaba:fastjson:1.2.67'val bean = JSONObject.parseObject(data, T::class.java)if (bean != null) {dsl.mSuccess?.invoke(bean)} else {dsl.mFailed?.invoke(data)}}}
}class CallBackDsl<T> {/*** 网络请求成功的回调*/var mSuccess: ((bean: T) -> Unit)? = nullfun success(listener: (bean: T) -> Unit) {mSuccess = listener}/*** 网络请求失败的回调*/var mFailed: ((data: String?) -> Unit)? = nullfun failed(listener: (data: String?) -> Unit) {mFailed = listener}/*** 是否自动弹出和关闭loading*/var isAutoShowLoading = true
}

改造后的回调

7.扩展

其实还有一个Retrofit很常用的功能我没有实现出来,那就是方法的返回值,其实我们实现起来也很简单(当然实现Retrofit那么强很难....)

我们可以使用反射来创建返回值,如下所示

改造HttpFunctionsHandler.invokeval returnType = method?.returnTypeval newInstance = returnType?.newInstance()returnType?.fields?.forEach {it.set(newInstance, "根据业务逻辑来判断设置什么内容")}return newInstance!!

8.混淆

如果打开了混淆的话,不配置以下内容会导致运行时报错;如果不开启混淆则可以忽略

-keepclassmembers public interface com.lt.retrofitdemo.http.HttpFunctions {*;}#防止自定的接口方法名被混淆
-keepclasseswithmembernames public interface com.lt.retrofitdemo.http.ObserverCallBack {*;}#因为使用到了反射,所以回调的类名称也不能被混淆
-keep class kotlin.reflect.jvm.internal.impl.load.java.**{*; }#防止kt反射被混淆
-keep class kotlin.Metadata{*; }#防止kt元注解被混淆

结语

这样一个网络请求的封装基本就搞定了,声明和调用都很方便

中间由于演示,有很多功能都没有实现或者实现的不完全,大家可以在实现自己的框架的时候可以自行完善,并且可以添加更多的功能

而且这样封装比较灵活,因为具体的逻辑都在HttpFunctionsHandler的startGet和startPost中,所以要更改网络请求的框架或者切换http和Socket很简单

如果想直接这么简单的使用,又不想自己封装,可以使用我修改Retrofit使其更易于使用的框架,文章地址:https://blog.csdn.net/qq_33505109/article/details/108767068

demo链接如下:https://github.com/ltttttttttttt/RetrofitDemo

模仿Retrofit封装一个使用更简单的网络请求框架相关推荐

  1. Android中网络请求框架的封装-Retrofit+RxJava+OkHttp

    Retrofit注解 请求方法 注解代码 请求格式 @GET GET请求 @POST POST请求 @DELETE DELETE请求 @HEAD HEAD请求 @OPTIONS OPTIONS请求 @ ...

  2. Retrofit+kotlin Coroutines(协程)+mvvm(Jetpack架构组件)实现更简洁的网络请求

    前言 使用kotlin协程也有一段时间了,给我最大的感受就是完全可以替代Rxjava了,并且写起来更加的简洁. 6月份Retrofit发布的2.6.0版本内部支持了kotlin协程中的挂起(suspe ...

  3. 一步步搭建Retrofit+RxJava+MVP网络请求框架(二),个人认为这次封装比较强大了

    在前面已经初步封装了一个MVP的网络请求框架,那只是个雏形,还有很多功能不完善,现在进一步进行封装.添加了网络请求时的等待框,retrofit中添加了日志打印拦截器,添加了token拦截器,并且对Da ...

  4. 一步步搭建Retrofit+RxJava+MVP网络请求框架(二),个人认为这次封装比较强大了...

    在前面已经初步封装了一个MVP的网络请求框架,那只是个雏形,还有很多功能不完善,现在进一步进行封装.添加了网络请求时的等待框,retrofit中添加了日志打印拦截器,添加了token拦截器,并且对Da ...

  5. Retrofit + Kotlin + MVVM 的网络请求框架的封装尝试

    1.前言 之前在学习郭霖<第一行代码>时按部就班地写过一个彩云天气 App,对里面的网络请求框架的封装印象非常深刻,很喜欢这种 Retrofit + Kotlin + 协程的搭配使用.随后 ...

  6. Xamarin.Android之封装个简单的网络请求类

    http://doc.okbase.net/catcher1994/archive/220195.html Catcher8 2016/4/23 0:28:50 阅读(72) 评论(0) 一.前言 回 ...

  7. Android肝帝战纪之网络请求框架封装(Retrofit的封装)

    网络请求框架封装(OkHttp3+Retrofit+loading的封装) Retrofit的Github链接 点此链接到Github AVLoadingIndicatorView的Github链接( ...

  8. Kotlin使用Coroutine+ViewModel+retrofit构建一个网络请求框架

    Kotlin使用Coroutine+ViewModel+retrofit构建一个网络请求框架 公司里的老代码用的网络请求框架技术都比较老,为了快速搭建一个网络请求框架,提高工作效率,记录一下用jetp ...

  9. 一款基于RxJava2+Retrofit2实现简单易用的网络请求框架

    本库是一款基于RxJava2+Retrofit2实现简单易用的网络请求框架,结合android平台特性的网络封装库,采用api链式调用一点到底,集成cookie管理,多种缓存模式,极简https配置, ...

最新文章

  1. 【青少年编程(第32周)】李老师太给力了!
  2. Golang实现简单爬虫框架(4)——队列实现并发任务调度
  3. 刷新存储器的容量单位是什么_存储系统 半导体存储器
  4. 快速理解https是如何保证安全的
  5. 带你快速了解 Docker 和 Kubernetes
  6. 2021.NET大会日程首发!行程亮点全曝光!
  7. 收藏表数据库_选择您的收藏库
  8. async/await 顺序执行和并行
  9. NEC向格鲁吉亚提供基于面部识别技术的城市监控系统
  10. MySQL高级-视图
  11. python查看方法作用_python中有帮助函数吗
  12. SpringBoot使用thymefeal出现No mapping for GET /xxx的解决办法
  13. 解决 Composer 运行时的 Xdebug 冲突
  14. ACCESS数据库程序设计
  15. 第4章 网络安全体系与网络安全模型
  16. 常用html标签及其属性
  17. Java代码分层规范
  18. No current assignment for partition 解决
  19. 周报8.22-8.28
  20. 集体照的拍摄与后期合成处理

热门文章

  1. 欧拉角推算旋转矩阵的问题
  2. 企业数字化转型,一文通读什么是数字化中台?
  3. tableau实战系列(四十七)-Tableau快速生成可视化视图
  4. MATLAB从入门到精通-机械动力学仿真-Amesim仿真实例:对于任意的外力作用下的机械动力学仿真
  5. 使用Python绘制热图的库
  6. WEB开发中的会话控制
  7. matlab 小波变换_matlab小波工具箱实例(二):时频分析和连续小波变换
  8. LeetCode题组:第13题-罗马数字转整数
  9. Qt控件与按钮颜色透明
  10. Python/WSGI 应用快速入门--转