[动态代理三部曲:下] - 从动态代理,看Retrofit的源码实现

前言

关于动态代理的系列文章,到此便进入了最后的“一出好戏”。前俩篇内容分别展开了:从源码上,了解JDK实现动态代理的原理;以及从动态代理切入,学会看class文件结构的含义。

如果还没有看过这俩篇文章的小伙伴,可以看一看呦(前俩篇是一个小伙伴总结的,这一篇由我来续上。至于他会不会结合动态代理捋一捋Java中的AOP,那就看他了,emmmmmm~)

[动态代理三部曲:中] - 从动态代理,看Class文件结构定义

[动态代理三部曲:上] - 动态代理是如何"坑掉了"我4500块钱

不扯这些没用的直接开整!

上源码

构建Retrofit对象

毫无疑问,分析源码要先从使用凡是入手。对于我们正常的Retrofit套路,我们会先构建一个接口,这里我们使用一个post请求(这个接口已经不能用了,很久没有倒腾我的服务器了~):

public interface RetrofitApi {String URL = "https://www.ohonor.xyz/";@POST("retrofitPost")@FormUrlEncodedCall<ResponseBody> postRetrofit(@Field("username") String username, @Field("password") String password);
}

然后,我们会通过Builder构建一个Retrofit:

Retrofit retrofit = new Retrofit.Builder().baseUrl(RetrofitApi.URL).addConverterFactory(ScalarsConverterFactory.create()).build();

对于构建Retrofit来说,从外部看就是通过Builder模式去构建。但是细节之处,并非如此,让我们看一下baseUrl的内部实现。

public static @Nullable HttpUrl parse(String url) {Builder builder = new Builder();Builder.ParseResult result = builder.parse(null, url);return result == Builder.ParseResult.SUCCESS ? builder.build() : null;}

内部很简单,通过builder.parase()的返回值来判断是否应该去调用build()方法。因此很明显,大量的逻辑是在parse()方法之中处理的。让我们进去一睹芳泽:

此方法内容非常的长,本质就是对url进行准确性的校验。这里我截取了一些较为关键的内容。

//这里是对HTTP请求类型的判断,是http还是https,并且记录一个下标pos。
if (input.regionMatches(true, pos, "https:", 0, 6)) {this.scheme = "https";pos += "https:".length();
} else if (input.regionMatches(true, pos, "http:", 0, 5)) {this.scheme = "http";pos += "http:".length();
} else {return ParseResult.UNSUPPORTED_SCHEME; 
}//接下来的内容,代码过于的长,这里就不贴出来啦。主要内容就是对我们url常见的分隔符进行解码。
//比如@和%40的相爱先杀。大家有兴趣的话,可以自行查看一下源码

url构建之前有一个比较经典的校验过程:"baseUrl must end in /: " + baseUrl。这个异常大家都不陌生吧?~baseUrl必须以/结尾。这里的过程,大家有兴趣可以自己看一下呦,原理是通过切割“/”字符串,来判断是不是以“/”结尾。这里切的url并非是咱们的baseUrl,而是构建完毕的url。因为篇幅原因,这里就不贴代码了。

动态代理部分

让我们进入下一个过程,动态代理开始的地方。构建了Retrofit对象直接,我们就开始生成我们的接口对象啦,点进入之后,我们就能看到,属于的动态代理的方法。还是熟悉的配方,熟悉的味道:

RetrofitApi retrofitApi = retrofit.create(RetrofitApi.class);public <T> T create(final Class<T> service) {//省略一些判断代码return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },new InvocationHandler() {private final Platform platform = Platform.get();@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)throws Throwable {// 如果该方法是来自Object的方法,则遵循正常调用。(正常来说,咱们也不会传一个Object进来)if (method.getDeclaringClass() == Object.class) {return method.invoke(this, args);}//判断是否是默认方法,这是1.8新增的内容。下文简单展开一些:if (platform.isDefaultMethod(method)) {return platform.invokeDefaultMethod(method, service, proxy, args);}//这里才是我们重点关注的地方:ServiceMethod<Object, Object> serviceMethod =(ServiceMethod<Object, Object>) loadServiceMethod(method);OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);return serviceMethod.callAdapter.adapt(okHttpCall);}});}

默认方法: 是JDK1.8增加的接口中的内容。其关键字为default。(如果感兴趣这个新特性,小伙伴们可以自行了解~)

官网解释:如果此方法是默认方法,则返回true; 否则返回false。 默认方法:即在在接口类型中,声明的具有主体的非静态方法(有具体实现的)。(Returns true if this method is a default method; returns false otherwise. A default method is a public non-abstract instance method, that is, a non-static method with a body, declared in an interface type.)

俩种类型判断结束,让我们重点看一下:ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);这行代码做了什么。我们点进去loadSerivceMethod()方法。

ServiceMethod<?, ?> loadServiceMethod(Method method) {ServiceMethod<?, ?> result = serviceMethodCache.get(method);if (result != null) return result;synchronized (serviceMethodCache) {result = serviceMethodCache.get(method);if (result == null) {result = new ServiceMethod.Builder<>(this, method).build();serviceMethodCache.put(method, result);}}return result;
}

很明显,这里做了一次缓存。如果没有ServiceMethod对象,那么就通过Builder的方式去构建这个对象。那么Buidler的过程是什么样子的呢?

build()方法相对比较的长,这里我们看一些比较关键的地方。

关键点1:

拿到方法上的所有注解,然后遍历:

for (Annotation annotation : methodAnnotations) {parseMethodAnnotation(annotation);
}

parseMethodAnnotation()方法:

private void parseMethodAnnotation(Annotation annotation) {if (annotation instanceof DELETE) {parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);} else if (annotation instanceof GET) {parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);} else if (annotation instanceof HEAD) {parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);if (!Void.class.equals(responseType)) {throw methodError("HEAD method must use Void as response type.");}} else if (annotation instanceof PATCH) {parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);} else if (annotation instanceof POST) {parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);} //省略一些注解类型
}

parseHttpMethodAndPath()方法中,主要做了一件事情:通过传进来的注解对应的value去判断是否有?,如果有,那么?后边不能包含{}(通过正则表达式实现),否则抛异常。如果没有抛异常,那么通过正则切割{},存到一个Set之中,后续进行处理,也就是和参数中的Path注解的内容进行替换。(下文会涉及替换过程)

关键点2:

遍历过所有方法上的注解后,接下来就是参数注解了。

参数类型校验:

到达这里,第一步进行的操作,是判断参数类型。如果参数类型是TypeVariable(类型变量:T、V...)、WildcardType (通配符;?)则直接抛异常:

Type parameterType = parameterTypes[p];if (Utils.hasUnresolvableType(parameterType)) {throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",parameterType);
}static boolean hasUnresolvableType(Type type) {if (type instanceof Class<?>) {return false;}//省略递归遍历的过程if (type instanceof GenericArrayType) {return hasUnresolvableType(((GenericArrayType) type).getGenericComponentType());}if (type instanceof TypeVariable) {return true;}if (type instanceof WildcardType) {return true;}
}

参数类型完毕后,便进入参数注解类型的判断。

参数注解类型校验:

正式校验参数注解类型的时候,会先判断是否有不含注解的参数,这里就会直接抛异常(也就是我们为什么不能在参数中传不用注解修饰参数报错的原因):

Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
if (parameterAnnotations == null) {throw parameterError(p, "No Retrofit annotation found.");
}

接下来便是校验参数注解类型。不过,这一部分实在没办法贴出来了,核心的判断方法大概有400行。为啥这么多?因为参数注解类型太多了,每一种都有自己的规则,所以判断内容很多。如果小伙伴有感兴趣的,可以自行去ServiceMethod类中的parseParameterAnnotation()方法查看。

请求接口Api类中,注解使用的异常。基本都是在这里处理的。如果小伙伴们遇到什么奇怪的异常,不妨不着急去百度/Google;让我们看看源码是怎么说的~~

Path替换{}的内容

这里我们解决一个疑问:那就是我们最开始处理url的时候,通过正则切割{},我们都知道,这里会通过Path注解去替换。那么这里就让我们看一看Retrofit是如何处理Path类型的注解的。

else if (annotation instanceof Path) {//省略部分内容Path path = (Path) annotation;String name = path.value();validatePathName(p, name);Converter<?, String> converter = retrofit.stringConverter(type, annotations);return new ParameterHandler.Path<>(name, converter, path.encoded());
}

这里我们能看到,想进行接下来的操作。必然和Converter这个类有着密不可分的关系。

public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotations) {// 省略判空及缓存取值操作。return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
}

我们可以看到,第一次一定是没有Converter对象的。点进INSTANCE之后我们会发现这里构建了一个ToStringConverter类。初始化之后,再让我们回到Path类型中的判断里。最终我们会return一个return new ParameterHandler.Path<>(name, converter, path.encoded());很明显这是一个内部类。其实它是一个封装类。对应封装了所有注解对应的java类。用于在请求网络的时候统一管理。而这个类只需要重写了apply方法。

static final class Path<T> extends ParameterHandler<T> {//省略构造方法@Override void apply(RequestBuilder builder, @Nullable T value) throws IOException {//省略抛异常。我们Path替换{}的过程就在下面这个方法中。builder.addPathParam(name, valueConverter.convert(value), encoded);}
}void addPathParam(String name, String value, boolean encoded) {//省略抛异常,看到replace应该很清楚了吧。relativeUrl = relativeUrl.replace("{" + name + "}", canonicalizeForPath(value, encoded));}

当然,执行replace势必要引起apply方法的调用。很显然目前在动态代理的这个过程中,我们没有办法看到apply被调用。因此现在先按住不表,让我们先把动态代理部分整完。

newProxyInstance的return

我们上面看了,校验接口方法的参数类型/参数注解类型。这个逻辑过后,就是调用build,构建ServiceMethod。

public ServiceMethod build() {// 省略上诉的检验过程return new ServiceMethod<>(this);
}

构建完了ServiceMethod之后,让我们再把目光转移到Retrofit.create()中newProxyInstance的最后一点内容:

ServiceMethod<Object, Object> serviceMethod =(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

走到这,就通过动态代理构建出了我们接口方法中的Call对象。从这三行代码中,我们很明显看不出来猫腻,让我们走进OkHttpCall中:

final class OkHttpCall<T> implements Call<T>

这其中重写了Call中我们常用的方法,比如:enqueue()。内部是转发给okhttp3.Call(OkHttp)去处理真正的网络请求。
接下来让我们重点看一下return的serviceMethod.callAdapter.adapt(okHttpCall)方法。这里callAdapter的初始化就不展开,默认的是DefaultCallAdapterFactory:

这里我们因为没有设置适配的Adapter,比如:RxJava的。

final class DefaultCallAdapterFactory extends CallAdapter.Factory {//省略构造方法@Overridepublic CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {//省略判空final Type responseType = Utils.getCallResponseType(returnType);return new CallAdapter<Object, Call<?>>() {@Override public Type responseType() {return responseType;}@Override public Call<Object> adapt(Call<Object> call) {return call;}};}
}

看到这个类,我们就可以明确,这了返回的Call实际上就是我们动态代理中传递的OkHttpCall。

return serviceMethod.callAdapter.adapt(okHttpCall);
有了它,我们就可以执行我们想执行的网络请求的方法了。

那么此时我们就可以这么做了:

Call<ResponseBody> call = retrofitApi.postRetrofit(username,password);
call.enqueue(....);

动态代理部分接近尾声

走到这里,动态代理部分就结束了。不过我们还有一些问题没有看到结果。最简单的,上面所说的apply方式是谁调用的?其实这个问题很好解答。

我们通过上面的梳理,可以明确动态代理的部分仅仅是为了构建我们的接口类,而真正的调用并非在此。因此我们可以推断出apply的调用时机应该是正在去请求网络的时候。

因为本篇的主题是梳理Retrofit中动态代理的部分。所以关于真正请求的部分,就简单的进行总结下见谅了,各位

我们知道,我们正真请求网络是调用了Call中的方法:

public interface Call extends Cloneable {Request request();Response execute() throws IOException;void enqueue(Callback responseCallback);
}

那么Call的实现类是怎么被创建出来的呢?其中,上文我们已经看到,在newProxyInstance方法中return的时候,初始化的OkHttpCall。既然知道了Call的实现类是什么,那么我们就取其中比较有代表性的方法,来展开apply被调用的过程。

这里我们展开enqueue()方法做代表吧:

@Override public void enqueue(final Callback<T> callback) {checkNotNull(callback, "callback == null");okhttp3.Call call;Throwable failure;//省略判空,同步等操作call = rawCall = createRawCall();//省略真正发起请求的过程。
}private okhttp3.Call createRawCall() throws IOException {//apply就在此方法中被调用Request request = serviceMethod.toRequest(args);okhttp3.Call call = serviceMethod.callFactory.newCall(request);//省略抛异常return call;
}Request toRequest(@Nullable Object... args) throws IOException {//省略无关的代码for (int p = 0; p < argumentCount; p++) {//到此我们的apply就被调用了。handlers[p].apply(requestBuilder, args[p]);}return requestBuilder.build();
}

在这我们就很清晰的看到apply方法被调用~

总结

我们的Retrofit,通过动态代理,构建我们所需要的接口方法,其中校验我们的接口方法的注解,参数类型,参数注解类型;构建ServiceMethod对象,最终通过OkHttpCall,return出我们所需要的Call类型对象。
有了Call,我们就可以开始网络请求,当然网络请求的过程,在OkHttpCall中是被转发给OkHttp框架中的okhttp3.Call去执行的。

到此,从动态代理,看Retrofit的源码实现就结束了。这篇文章重点是去分析Retrofit中的动态代理的思路,所以在网络请求的源码过程并没有过多的涉猎。有机会的话,在Retrofit的源码实现中去总结吧。

在看源码的过程中,最大的感慨是框架设计上的巧妙。自己最近在重构公司的相机库,越来越感觉整体设计的重要性!唉,好难。


这里是一个应届生/初程序员公众号~~欢迎围观

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

img_39e544ceb47ae92ede4c13f05f18ed09.png
个人公众号:IT面试填坑小分队

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/253727.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字后端——电源规划

电源规划是给整个芯片的供电设计出一个均勻的网络&#xff0c;它是芯片物理设计中非常关键的一部分。电源规划在芯片布图规划后或在布图规划过程中交叉完成,它贯穿于整个设计中&#xff0c;需要在芯片设计的不同阶段对电源的供电网络进行分析并根据要求进行修改。&#xff0c;主…

逆向project实战--Acid burn

0x00 序言 这是第二次破解 crackme 小程序&#xff0c;感觉明显比第一次熟练。破解过程非常顺利&#xff0c;差点儿是分分钟就能够找到正确的 serial&#xff0c;可是我们的目标是破解计算过程。以下将具体介绍。 0x01 初次执行 刚開始拿到 crackme 先执行程序。看看有哪些明显…

PyCharm使用技巧(六):Regullar Expressions的使用

2019独角兽企业重金招聘Python工程师标准>>> PyCharm v2018.2最新版本下载 使用正则表达式查找和替换文件中的文本 示例代码 使用正则表达式查找和替换字符串 假设您想用扩展标记<title> </title>替换元素&#xff08;title&#xff09;中的属性&#x…

jQuery笔记总结

来源于&#xff1a;http://blog.poetries.top/2016/10/20/review-jQuery/ http://www.jianshu.com/p/f8e3936b34c9 首先&#xff0c;来了解一下jQuery学习的整体思路 第一节 jQuery初步认知 jQuery概述 JQuery概念 javascript概念 基于Js语言的API和语法组织逻辑&#xff0c;通…

芯片生产流程

每个半导体产品的制造都需要数百个工艺&#xff0c;泛林集团将整个制造过程分为八个步骤&#xff1a;晶圆加工-氧化-光刻-刻蚀-薄膜沉积-互连-测试-封装。 一、晶圆加工 所有半导体工艺都始于一粒沙子&#xff01;因为沙子所含的硅是生产晶圆所需要的原材料。晶圆是将硅(Si)或砷…

GRE Sub math 报名

Step1 注册ETS帐号 Step2 登录帐号&#xff0c;点击Register/Find Test Centers, Dates Step3 按照提示查询考场 如果没有结果而是出现了如下提示&#xff0c;意味着这个地方没有考位了&#xff0c;需要选择其他地方的考位 Step 4 接下来就和GRE general test的过程一样了&…

示例解读 Python 2 和 Python 3 之间的主要差异

开发四年只会写业务代码&#xff0c;分布式高并发都不会还做程序员&#xff1f; 每门编程语言在发布更新之后&#xff0c;主要版本之间都会发生很大的变化。 在本文中&#xff0c;Vinodh Kumar 通过示例解释了 Python 2 和 Python 3 之间的一些重大差异&#xff0c;以帮助说明…

数字后端——时钟树综合

在数字集成电路设计中&#xff0c;时钟信号是数据传输的基准&#xff0c;它对于同步数字系统的功能、性能和稳定性起决定性作用&#xff0c;所以时钟信号的特性及其分配网络尤被人们关注。时钟信号通常是整个芯片中有最大扇出、通过最长距离、以最高速度运行的信号。时钟信号必…

52次课(mysql用户管理、常用sql语句、 mysql数据库备份恢复)

MySQL创建用户以及授权 默认用户是root用户&#xff0c;不可能所有人员都用root用户&#xff0c;创建用户防止误删除&#xff0c;因为mysql里边有多个库每个库里有很多表&#xff0c;所以需要给单独的用户做一些授权我只需要它对某一个数据库有权限&#xff0c;或者说对某个数据…

20145225 《信息安全系统设计基础》第14周学习总结

第九章 虚拟存储器 虚拟存储器是计算机系统最重要的概念之一&#xff0c;它是对主存的一个抽象 三个重要能力&#xff1a; 它将主存看成是一个存储在磁盘上的地址空间的高速缓存&#xff0c;在主存中只保存活动区域&#xff0c;并根据需要在磁盘和主存之间来回传送数据&#xf…

数字后端——布线

布线是继布局和时钟树综合之后的重要物理实施任务&#xff0c;其内容是将分布在芯片核内的模块、标准单元和输入输出接口单元( I /O pad&#xff09;按逻辑关系进行互连&#xff0c;其要求是百分之百地完成它们之间的所有逻辑信号的互连&#xff0c;并为满足各种约束条件进行优…

streamsets 集成 minio s3测试

具体streamsets crate 集成可以参考 streamsets crate 以下文档只关注minio 集成的配置 minio 服务 搭建 具体搭建参考&#xff1a; https://www.cnblogs.com/rongfengliang/p/9197315.html 创建bucket &#xff08;crate 集成使用&#xff09; 测试的csv 文件从https://www.s…

【codecombat】 试玩全攻略 第十四关 已知敌人

第十四关 已知敌人 在这一关里&#xff0c;我们的英雄获得了一副可以看见敌人的眼镜&#xff0c;所以他很强势的学会了“发现敌人”的技能。 hero.findNearestEnemy()命令&#xff0c;单词多了&#xff0c;首字母都要大写了&#xff0c;不然分不出来。玩过wow的小伙伴用过 宏命…

数字后端——信号完整性分析

随着光刻和集成电路制造工艺的不断进步&#xff0c;以及芯片的特征尺寸从深亚微米到纳米的迅速采用&#xff0c;人们一方面因为芯片的功能极大提高而受益&#xff0c;另一方面&#xff0c;当逻辑门的沟道长度减小时&#xff0c;门的开关时间会减小&#xff0c;这意味着输出驱动…

CMOS图像传感器——TDI CIS

一、面阵与线阵图像传感器 人们在日常生活中见到的相机大多基于普通的面阵图像传感器,这种相机多用来拍摄静止的物体。即使用它们來拍摄运动的物体,也仅仅是缩短了相邻两次拍摄的时间间隔,无需对所拍摄图像进行额外操作,对物体的运动方向和速度也没有限定条件。 除此之外,…

做好数据挖掘模型的9条经验总结

爱数据学习社 welcome数据挖掘是利用业务知识从数据中发现和解释知识(或称为模式)的过程&#xff0c;这种知识是以自然或者人工形式创造的新知识。当前的数据挖掘形式&#xff0c;是在20世纪90年代实践领域诞生的&#xff0c;是在集成数据挖掘算法平台发展的支撑下适合商业分析…

数字后端——低功耗设计物理实施

一、低功耗设计方案综述 为了实现集成电路的低功耗设计目标&#xff0c;我们需要在系统设计阶段就采用低功耗设计方案&#xff0c;因为随着设计流程的逐步推进&#xff0c;到了芯片设计实现阶段&#xff0c;降低芯片功耗的方法将越来越少&#xff0c;可节省功耗的百分比将不断下…

H5项目开发分享——用Canvas合成文字

以前曾用Canvas合成、裁剪、图片等《用H5中的Canvas等技术制作海报》。这次用Canvas来画文字。 下图中“老王考到驾照后”这几个字是画在Canvas上的&#xff0c;与在PS中打入的字非常接近&#xff0c;毫无违和感。 前面一段时间也在研读JavaScript设计模式相关的知识&#xff0…

CMOS图像传感器——SNR计算

图像质量评价在计算机视觉,人工智能,高清视频传输上面有很广泛的应用。目前,图像质量评价主要分为三个方向,有参考图像的质量评价,半参考的图像质量评价,以及无参考的图像质量评价。许多时候,我们利用CIS采集的RAW DATA本身就是含噪信号,因为我们往往不知道感兴趣的像素…

Java this 关键字的用法

this 关键字的用法 this 在类中就是代表当前对象&#xff0c;可以通过 this 关键字完成当前 对象的成员属性、成员方法和构造方法的调用。 那么何时用 this? 当在定义类中的方法时&#xff0c;如果需要调用该类对象&#xff0c;就可以用 this 来表示这个对象。也就是说&#x…