Retrofit源码设计模式解析(上)

时间:2022-12-20 17:36:56

Retrofit通过注解的方法标记HTTP请求参数,支持常用HTTP方法,统一返回值解析,支持异步/同步的请求方式,将HTTP请求对象化,参数化。真正执行网络访问的是Okhttp,Okhttp支持HTTP&HTTP2,因此,使用Retrofit可以支持REST、HTTPS及SPDY。

行业内分析Retrofit的使用方法的文章已经比较丰富,这里不再赘述,如想了解这部分内容,请参考如下链接。

用 Retrofit 2 简化 HTTP 请求

Retrofit 源码解析

本文主要从设计模式的角度分享对Retrofit源码的一些理解。

  1. 外观模式
  2. 建造者模式
  3. 代理模式
  4. 简单工厂模式
  5. 工厂模式
  6. 抽象工厂模式

一、外观模式

在封装某些特定功能的子系统时,外观模式是一种很好的设计规范。即该子系统的外部与内部通信时通过一个统一的对象进行。Retrofit是整个库的一个入口类,Retrofit库的使用基本都是围绕着这个类。外观模式具有高内聚、低耦合的特性,对外提供简单统一的接口,隐蔽了子系统具体的实现、隔离变化。

Retrofit的外观模式的UML类图如下所示。

Retrofit源码设计模式解析(上)

Retrofit对客户端模块(Client1、Client2……)提供统一接口,Retrofit类内部封装了ServiceMethod、CallAdapter和Converter等组件。并且,CallAdapter和Converter都是抽象为接口,用户可以扩展自定义的实现。正如官方文档中的示例:

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();

附:举个栗子,说明下外观模式在封装条形码/二维码扫描功能时的应用。

Android中的条形码/二维码扫描功能通常会基于zxing库进行封装,定义CaptureActivity,统一提供扫码功能,并返回扫描结果。客户端使用该封装只需两步:首先,通过Intent启动CaptureActivity;然后,在onActivityResult中处理扫描结果。

Intent intent = new Intent(MainActivity.this, CaptureActivity.class);
startActivityForResult(intent, REQUESTCODE);
if (requestCode == REQUESTCODE && resultCode == RESULT_OK) {
Bundle bundle = data.getExtras();
String scanResult = bundle.getString(CaptureActivity.RESULT);
helloWorld.setText(scanResult);
}

客户端不需要处理任何跟摄像头控制、调焦、图片处理、条形码解析等相关的问题。CaptureActivity提供了所有扫描相关的功能。

二、建造者模式

设计模式分为三种类型:创建型模式、结构型模式和行为型模式。建造者模式属于创建型模式,将构建复杂对象的过程和它的部件解耦,使构建过程和部件的表示隔离。Retrofit内部包含Retrofit.Builder,Retrofit包含的域都能通过Builder进行构建。经典设计模式(《设计模式:可复用面向对象软件的基础》)中建造者模式有四种角色:

  • Product产品类——该类为一般为抽象类,定义Product的公共属性配置;
  • Builder建造类——该类同样为抽象类,规范Product的组建,一般由子类实现具体Product的构建过程;
  • ConcreteBuilder实际建造类——继承自Builder,构建具体的Product;
  • Director组装类——统一组装过程。

在Retrofit类中,Retrofit直接对应Product,并没有基于抽象Product进行扩展;Retrofit.Builder对应ConcreteBuilder,也没有基于抽象Builder进行扩展,同时省略了Director,并在Retrofit.Builder每个setter方法都返回自身,使得客户端代码可以链式调用,整个构建过程更加简单。

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();

举个栗子:笔者基于Retrofit封装适合自身业务的库时,由于需要配置基础URL、默认超时时间、拦截器以及是否添加转换器等,也采用相同的建造者模式。

public class NetWork {

    // 基础URL设置
private String baseUrl;
// 默认超时时间
private long timeout; /**
* NetWork构建者
*/
public static class Builder {
// 基础URL设置
private String baseUrl;
// 默认超时时间
private long timeout = 5; /**
* baseUrl为必填项
*
* @param baseUrl 基础Url
*/
public Builder(String baseUrl) {
this.baseUrl = baseUrl;
} public Builder timeout(long timeout) {
this.timeout = timeout;
return this;
} public NetWork build() {
return new NetWork(this);
}
} /**
* 构造器
*
* @param builder 构造builder
*/
private NetWork(Builder builder) {
this.baseUrl = builder.baseUrl;
this.timeout = builder.timeout;
}

三、代理模式

代理模式属于上述提到的结构型模式。当无法或不想直接访问某个对象,或者访问某个对象比较复杂的时候,可以通过一个代理对象来间接访问,代理对象向客户端提供和真实对象同样的接口功能。经典设计模式中,代理模式有四种角色:

  • Subject抽象主题类——申明代理对象和真实对象共同的接口方法;
  • RealSubject真实主题类——实现了Subject接口,真实执行业务逻辑的地方;
  • ProxySubject代理类——实现了Subject接口,持有对RealSubject的引用,在实现的接口方法中调用RealSubject中相应的方法执行;
  • Cliect客户端类——使用代理对象的类。

代理模式分为静态代理和动态代理,严格按照上述角色定义编写的代码属于静态代理,即在代码运行前ProxySubject代理类的class编译文件就已存在。Retrofit使用的是动态代理,是通过反射机制来动态生成方法接口的代理对象的。动态代理的实现是通过JDK提供的InvocationHandler接口,实现该接口重写其调用方法invoke。

public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(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, Object... args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}

Proxy.newProxyInstance返回接口的动态代理类,InvocationHandler的invoke方法处理method分为三种情况:1)Object的方法,直接返回Object的实现;2)判断是否Java8支持的DefaultMethod;3)创建OkHttpCall,通过ServiceMethod转换为接口的动态代理类。

使用Retrofit的客户端通过create方法获取自定义HTTP请求的动态代理类,是客户端代码中最重要的部分之一。这里有三个重要组件:

  • ServiceMethod
  • OKHttpCall
  • ServiceMethod.callAdapter

ServiceMethod用于处理Api Service上定义的注解,参数等,得到这个ServiceMethod之后,传给OkHttpCall,这个OkHttpCall就是对Okhttp的网络请求封装的一个类。

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

关于同步,这里有两点值得学习:

  1. 采用synchronized将锁加在serviceMethodCache上,而不是加到方法上。(直接synchronized加到方法可能会引起Dos,当然,你可以说客户端不用考虑这种问题);
  2. serviceMethodCache是用于缓存HTTP请求方法的,初始方法采用LinkedHashMap而不是普通的HashMap。
private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();

LinkedHashMap中有一个字段accessOrder,表示是否按照访问顺序进行重排序。默认为false,表示按插入时间顺序排序,如果设置为true,则进行重排序。最近最少访问的元素会被放到队尾,最先删除,而最常访问的元素,则放到队头,最后删除。

把ServiceMethod传给OkHttpCall实际上就是把网络接口所需要的URL,参数等条件传给了OkHttpCall,就可以进行网络请求了。ServiceMethod中如果没有配置CallAdapter,则使用默认的DefaultCallAdapterFactory, 得到的结果是Call<?>。

回到正题,你可能已经发现,create方法采用的代理模式和正常的代理模式并不一样,正常的代理模式只是对真实对象的一层控制,这个真实对象是实现对应的接口的,而这里并没有真实的对象,它把方法调用最终全部转发到OKHttp了。

四、简单工厂模式

本文后面的部分将集中在简单工厂模式、工厂模式和抽象工厂模式,它们都属于创建型模式,其主要功能都是将对象的实例化部分抽取出来。简单工厂模式也称为静态工厂模式,包含三种角色:

  • Factory工厂角色——负责实现创建所有实例的内部逻辑;
  • Product抽象产品角色——创建的所有对象的父类,负责描述所有实例所共有的公共接口;
  • ConcreteProduct具体产品角色——继承自Product,负责具体产品的创建。

简单工厂模式是一种很常见、很简单的设计模式,以Platform类为例,其首先包含静态域PLATFORM,并通过静态返回供客户端调用。

private static final Platform PLATFORM = findPlatform();

static Platform get() {
return PLATFORM;
}

findPlatform其实就是一个静态工厂方法,根据Class.forName是否抛出ClassNotFoundException来判断不同的平台。

private static Platform findPlatform() {
try {
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
}
try {
Class.forName("java.util.Optional");
return new Java8();
} catch (ClassNotFoundException ignored) {
}
try {
Class.forName("org.robovm.apple.foundation.NSObject");
return new IOS();
} catch (ClassNotFoundException ignored) {
}
return new Platform();
}

而Android、Java8、IOS相当于ConcreteProduct的角色,继承自抽象产品类Platform。

Java8:

static class Java8 extends Platform {}

Android:

static class Android extends Platform {}

IOS:

static class IOS extends Platform {}

五、工厂模式

上述简单工厂模式中的工厂方法类只有一个Factory,仍以Platform为例,如果在增加一种平台:Windows Phone,那么就需要修改findPlatform方法,添加Windows Phone类的创建。

这种修改模式不符合“开闭原则”,即对扩展开放,对修改封闭。本着可扩展的原则,抽象Factory类的公共部分为抽象类,然后不同的平台工厂继承自抽象的Factory。需要不用的平台就调用不同的工厂方法,这就是工厂模式。即对简单工厂中的工厂类进行抽象:

  • Factory抽象工厂类——负责工厂类的公共部分;
  • ConcreteFactory具体工厂类——继承自Factory,实现不同特性的工厂。

如果按照工厂模式,通过PlatformFactory类抽象工厂方法,那么大概会是这样:

public abstract class PlatformFactory {
abstract Platform findPlatform();
}

Android、Java8、IOS或者可能新增的Windows Phone工厂继承自PlatformFactory。

public class AndroidFactory extends PlatformFactory {

    @Override
Platform findPlatform() {
try {
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
}
return new Platform();
}
}

客户端需要不同的平台对象就调用不同的工厂,但客户端如果调用错误,比如在Android上调用了IOS的工厂,那么就会得到一个Platform的实例,这并不符合要求,这种写法增加了客户端的难度,同时,需要引入抽象层,增加多个具体工厂类,维护成本也更大。

所以,在使用工厂设计模式时,一定需要衡量利弊,在特定的场景选择最合适的设计模式。那么,Retrofit中使用工厂模式的经典例子又是什么呢?CallAdapter!

public interface CallAdapter<T> {

    Type responseType();
<R> T adapt(Call<R> call); abstract class Factory {
public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit); protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
} protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
}

CallAdapter是什么呢?见名知义,对Call进行适配,这里涉及到适配器模式,下节会着重说明。这里关注CallAdapter.Factory,CallAdapter.Factory对应上述角色中的Factory抽象工厂类,包含两个静态工具方法getParameterUpperBound、getRawType和抽象方法get。

get方法返回不同类型的CallAdapter,RxJavaCallAdapterFactory返回CallAdapter<Observable<?>>,DefaultCallAdapterFactory返回CallAdapter<Call<?>>。

public final class RxJavaCallAdapterFactory extends CallAdapter.Factory {

    // 省略代码
@Override
public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) { // 省略代码
CallAdapter<Observable<?>> callAdapter = getCallAdapter(returnType, scheduler);
// 省略代码
return callAdapter;
} private CallAdapter<Observable<?>> getCallAdapter(Type returnType, Scheduler scheduler) {
// 省略代码
}
}

如果需要增加新的CallAdapter,继承自CallAdapter.Factory,覆盖get方法即可。符合面向对象软件设计的“开闭原则”。

六、抽象工厂模式

在配置Retrofit时,除了上述提到的CallAdapter,还需要addConverterFactory。Retrofit调用Okhttp时,将请求内容由T转换为okhttp3.RequestBody,将返回内容由okhttp3.ResponseBody转换为T,Converter是就是负责转换的类。Retrofit官方文档中给出了多种不同实现的转换器类,如下:

Gson: com.squareup.retrofit2:converter-gson
Jackson: com.squareup.retrofit2:converter-jackson
Moshi: com.squareup.retrofit2:converter-moshi
Protobuf: com.squareup.retrofit2:converter-protobuf
Wire: com.squareup.retrofit2:converter-wire
Simple XML: com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

其中包含JSON转换的Gson、Jackson,负责PB解析的Protobuf,负责XML解析的Simple XML,这些类具有相同点,均继承自Converter.Factory。

public interface Converter<F, T> {
T convert(F value) throws IOException; abstract class Factory {
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
               Retrofit retrofit) {
return null;
} public Converter<?, RequestBody> requestBodyConverter(Type type,
               Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return null;
} public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}
}
}

这里注意下Converter.Factory与CallAdapter.Factory的区别,CallAdapter.Factory只有一个抽象的get方法返回CallAdapter<?>,Converter.Factory有三个方法,分别返回Converter<ResponseBody, ?>、Converter<?, RequestBody>和Converter<?, String>。

相比于工厂模式,具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象,例如:上述Converter.Factory需要同时提供请求内容和返回内容的转换类,这时,就需要考虑抽象工厂模式。抽象工厂模式同样包含四种角色:

  • AbstractFactory:抽象工厂
  • ConcreteFactory:具体工厂
  • AbstractProduct:抽象产品
  • Product:具体产品

标准抽象工厂模式的UML图如下:(图片来自互联网)

Retrofit源码设计模式解析(上)

这里以GsonConverterFactory为例进行说明。GsonConverterFactory对应ConcreteFactory具体工厂,表示Gson转换类的工厂,GsonConverterFactory继承自AbstractFactory抽象工厂——Converter.Factory,重写了requestBodyConverter方法和responseBodyConverter方法,相当于上图中的createProductA和createProductB。

public final class GsonConverterFactory extends Converter.Factory {
    // 省略代码
@Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
} @Override
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonRequestBodyConverter<>(gson, adapter);
}
}

这里GsonRequestBodyConverter对应ProductA,GsonResponseBodyConverter对应ProductB。

final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
// 省略代码
}
final class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
// 省略代码
}

Converter<T, RequestBody>对应抽象产品AbstractProductA,Converter<ResponseBody, T>对应抽象产品AbstractProductB。

最后思考下:为什么Converter.Factory需要用抽象工厂模式,用工厂模式可以吗?如果用工厂模式,客户端需要怎么配置?

Retrofit源码设计模式解析(上)的更多相关文章

  1. Retrofit源码设计模式解析(下)

    本文将接着<Retrofit源码设计模式解析(上)>,继续分享以下设计模式在Retrofit中的应用: 适配器模式 策略模式 观察者模式 单例模式 原型模式 享元模式 一.适配器模式 在上 ...

  2. Retrofit源码解析(上)

    简介Retrofit是Square公司开发的一款针对Android网络请求的框架,官网地址http://square.github.io/retrofit/ ,在官网上有这样的一句话介绍retrofi ...

  3. 还怕问源码?Github上神级Android三方源码解析手册,已有7&period;6 KStar

    或许对于许多Android开发者来说,所谓的Android工程师的工作"不过就是用XML实现设计师的美术图,用JSON解析服务器的数据,再把数据显示到界面上"就好了,源码什么的,看 ...

  4. spring源码深度解析— IOC 之 默认标签解析(上)

    概述 接前两篇文章  spring源码深度解析—Spring的整体架构和环境搭建  和  spring源码深度解析— IOC 之 容器的基本实现 本文主要研究Spring标签的解析,Spring的标签 ...

  5. &lbrack;置顶&rsqb;&NewLine; 【Android实战】----从Retrofit源码分析到Java网络编程以及HTTP权威指南想到的

    一.简介 接上一篇[Android实战]----基于Retrofit实现多图片/文件.图文上传中曾说非常想搞明白为什么Retrofit那么屌.最近也看了一些其源码分析的文章以及亲自查看了源码,发现其对 ...

  6. Retrofit源码分析&lpar;一&rpar;

    1.基本用法 创建接口 public interface GitHubService { @GET("users/{user}/repos") Observable<List ...

  7. mybatis 3&period;x源码深度解析与最佳实践(最完整原创)

    mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...

  8. Kotlin系列之序列&lpar;Sequences&rpar;源码完全解析

    Kotlin系列之序列(Sequences)源码完全解析 2018年06月05日 22:04:50 mikyou 阅读数:179 标签: Kotlin序列(sequence)源码解析Androidja ...

  9. Tomcat处理HTTP请求源码分析(上)

    Tomcat处理HTTP请求源码分析(上) 作者 张华 发布于 2011年12月8日 | 8 讨论 分享到: 微博 微信 Facebook Twitter 有道云笔记 邮件分享 稍后阅读 我的阅读清单 ...

随机推荐

  1. window&period;frameElement的使用

    window.frameElement的使用: 返回嵌入当前window对象的元素(比如 <iframe> 或者 <object>),如果当前window对象已经是顶层窗口,则 ...

  2. web项目存数据到数据库,中文乱码,解决过程

    first: 排查原因: 打断点,查看到底是在执行存数据库操作之前就已经乱码了,还是存数据库操作后乱码的. 前者解决方案: 在web.xml里面加上: <filter> <filte ...

  3. 学习PYTHON之路, DAY 5 - PYTHON 基础 5 &lpar;装饰器,字符格式化,递归,迭代器,生成器&rpar;

    ---恢复内容开始--- 一 装饰器 1 单层装饰器 def outer(func): def inner(): print('long') func() print('after') return ...

  4. C&num;操作office进行Excel图表创建,保存本地,word获取

    ,新建C#控制台应用程序(Excel创建图表) using System; using System.Collections.Generic; using System.Linq; using Sys ...

  5. POJ2253——Frogger&lpar;Floyd变形&rpar;

    Frogger DescriptionFreddy Frog is sitting on a stone in the middle of a lake. Suddenly he notices Fi ...

  6. ongl三种符号的使用

    1.#符号 访问非根对象属性,由于Struts2中值栈被视为根对象,所以访问其他非根对象时,需要加#前缀.实际上,#相当于ActionContext.getContext(): 用于过滤和投影(pro ...

  7. Git-删除文件后找回-比较文件差异

    #前提:删除前,文件存在是的状态提交到了本地库#操作: git reset --hard 指针位置 删除操作已近提交到本地库:指针指向历史记录 linxianli@VM-QS- MINGW64 /c/ ...

  8. AOP中使用Aspectj对接口访问权限进行访问控制

    切面编程的应用案例比较多,在统一的日志处理,鉴权过程中都会用的AOP原理,本文主要针对对进口的访问权限进行控制为例,说明 切面编程的使用: 1.使用Aspectj的方式进行切面编程: 2.编码环境,s ...

  9. Sqlcmd使用详解

    Sqlcmd实用工具,可以输入 TRANSACT-SQL 语句. 系统过程和脚本文件,通过各种可用模式: 通过命令提示符. 在中查询编辑器在 SQLCMD 模式下. 在 Windows 脚本文件. 在 ...

  10. Mac安装mysql8&period;0&period;12

    ···shell 下载 wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.12-macos10.13-x86_64.tar.gz ...