本篇文章已授权微信公众号 玉刚说 (任玉刚)独家发布
retrofit(Retrofit官方)已经诞生好几年了,从诞生开始一直都是Android应用开发最流行的网络请求框架,准确来说,是网络请求框架一个巧妙的包装。
正如官网所说,retrofit最大的特点,在于可以用一个Java interface通过注解去表示一个Http请求。
1.比如定义一个GET请求的Java interface:
public interface GitHubService {
@GET("users/{user}/repos")
Call> listRepos(@Path("user") String user);
}
2.然后 通过Retrofit创建一个GitHubService实例:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
3.再获取GitHubService实例对应的Call,通过Call就可以发起Http请求了:
Call> repos = service.listRepos("octocat");
call.enqueue(new Callback>()
{
@Override
public void onResponse(Call> call, Response> response)
{
Log.e(TAG, "normalGet:" + response.body() + "");
}
@Override
public void onFailure(Call> call, Throwable t)
{
}
});
Retrofit底层默认是用OkHttp,正如前面所说它只是一个包装,我们之所以使用Retrofit的目的是为了简化和优化代码的调用,优化开发效率。
关于Retrofit的使用不是本文的重点,本文重点谈谈Retrofit中的动态代理机制。
动态代理可以说是Retrofit中的核心机制,其使用体现在上面第2步,即生成一个我们定义的请求Java interface的请求实例,将原来一个普通的Java Interface转化为一个可以进行Http请求的对象。
要了解Retrofit中动态代理机制的运用,还是让我们从动态代理本身讲起吧。
建议先看下官网关于动态代理的基本说明,毕竟是权威资料而且详细:
Oracle动态代理说明
我们先来看一个最简单的动态代理例子,感受下通过动态代理将一个普通Java接口转化为一个代理对象:
一.首先创建一个Java Interface,对应上文的Retrofit请求接口GitHubService :
public interface Book {
String read(String s);
}
二.通过Proxy生成Book 的代理对象的Class对象:
Class bookProxyClass = Proxy.getProxyClass(Book.class.getClassLoader(),Book.class);
我们知道,一个Java Interface是不可以直接创建一个对象的,所以动态代理所做的是在运行时生成一个实现了该Interface的类的Class对象。
Proxy的getProxyClass方法注释已经解释清楚了:
Returns the {@code java.lang.Class} object for a proxy class given a class loader and an array of interfaces. The proxy class will be defined by the specified class loader and will implement all of the supplied interfaces. If any of the given interfaces is non-public, the proxy class will be non-public. If a proxy class for the same permutation of interfaces has already been defined by the
class loader, then the existing proxy class will be returned; otherwise,a proxy class for those interfaces will be generated dynamically and defined by the class loader.
简单来说,就是在运行时生成一个代理Class二进制流,并通过传入的ClassLoader去加载成一个代理Class对象,该Class实现了传入的第二个参数对应的Interface。
具体怎么生成Class二进制流就不在这里讲了,这里看下生成的Class具体啥样子:
通过添加一行代码:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
可以将运行时生成的动态代理Class文件保存下来(关于Class文件,详细可以看我这篇博文:从字节码角度仔细剖析一个HelloWorld程序)
用Idea打开该Class文件(自动进行了反编译):
public final class $Proxy0 extends Proxy implements Book {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String read(String var1) throws {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("proxy.Book").getMethod("read", Class.forName("java.lang.String"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
代码很简单,从该代码可以提取以下几个要点:
1.代理类$Proxy0继承Proxy,实现了我们的Book接口。
2.用4个Method引用保存equals、hashCode、toString以及Book接口的read方法的Method对象
3.在equals、hashCode、toString以及Book接口的read方法的实现都为调用super.h的invoke方法并传入2中对应的Method对象和参数。
4.通过查看Proxy的代码可知super.h为一个InvocationHandler 引用。而该InvocationHandler 引用就是从 $Proxy0的构造方法中传入。
所以说白了,该代理类就是构造方法传入的InvocationHandler 对象的代理,无论调用什么方法,都会调用InvocationHandler 对象的invoke方法并传入方法对应的Method对象和参数。
三:通过反射从Class对象创建对应的具体Book代理的实例对象:
有了代理类的Class对象,就可以通过反射创建对象了。
Constructor constructor = bookProxyClass.getConstructor(InvocationHandler.class);
//注意到上文的代理类只有一个有参构造方法,参数为InvocationHandler类型
Book bookProxy = (Book) constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String s = (String) args[0];
return "i am proxy of " + s;
}
});
这里终于得到了通过Book转换得到的对象bookProxy。
四.调用Book代理的实例对象的代理方法(即Book接口的方法)
String a = bookProxy.read("红楼梦");
System.out.println(a);
控制台打印出结果:
i am proxy of 红楼梦
正是三传入的InvocationHandler对象invoke方法指定的逻辑。
以上2,3步其实可以合成一步:
Book bookProxy = Proxy.newProxyInstance(Book.class.getClassLoader(),new Class[]{Book.class} ,
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String s = (String) args[0];
return "i am proxy of " + s;
}
});
这就是我们经常见到的动态代理的代码,但是为了解释清楚内在原理流程,所以我拆为了2步。
那么动态代理有什么作用呢?
《深入理解Java虚拟机》一书中给出的答案是:
结合Retrofit来讲,就是在具体的Http请求还未知的情况下,就确定了Http的请求代码。
Retrofit源码因为复杂就不在这里详细叙述了,只要明白了动态代理原理,它的流程就很容易明白。
就拿上文的Retrofit例子来说吧,在执行
GitHubService service = retrofit.create(GitHubService.class);
这一行的时候,虚拟机内部生成了代理类的Class二进制流,并且加载到虚拟机中形成代理类的Class对象,再反射得到代理类对象,而该代理类即实现了GitHubService,并且持有了InvocationHandler的引用。
当调用
Call> repos = service.listRepos("octocat");
就会调用代理对象的listRepos方法,通过上文的分析,它会调用
该InvocationHandler引用的对象的invoke方法,并传入GitHubService的listRepos的Method对象。
InvocationHandler引用的对象的invoke方法会通过该Method对象,得到方法的注解以及参数,得到Http请求的链接、请求方法、请求路径、参数等请求信息,构建一个OkHttp的请求并执行。
所谓纸上得来终觉浅,绝知此事要躬行。上文的叙述恐怕可能还是让人听得云里雾里,那么接下来,我就来重点围绕动态代理实现一个迷你的Retrofit。这个迷你Retrofit唯一的功能,就是通过Get请求请求我的csdn首页数据,主要是为了将Retrofit动态代理相关的的主要流程阐述清楚~
1.创建一个Get注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Get {
String value();
}
2.创建Call接口和CallBack回调接口:
这是代理对象请求方法返回的对象,T表示请求的数据类型。和Retrofit的Call对应。
public interface Call {
void enqueue(CallBack callBack);
}
这是请求的回调接口,T表示请求的数据类型,和Retrofit的CallBack对应。
public interface CallBack {
void onResponse(T response);
void onFail(Exception e);
}
3.GetBaiduService接口添加Get注解和Path:
这里为了简单就不添加参数了。
public interface GetBaiduService {
@Get("sinat_23092639")
Call read();
}
这里为了简单就直接将path写在注解上了。
4.根据Retrofit源码创建MiniRetrofit:
public class MiniRetrofit {
String mBaseUrl;
public MiniRetrofit(String baseUrl) {
mBaseUrl = baseUrl;
}
public T create(final Class service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class>[]{service},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//如果是equals、toString、hashCode方法则直接调用原来的方法
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
Call> call = null;
//获取被代理的接口的方法注解
Annotation[] annotations = method.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
//如果找到对应的Get注解,则将Get注解中的url和mBaseUrl拼接一起
if (annotation instanceof Get) {
String path = ((Get) annotation).value();
String url = mBaseUrl + "/" + path;
System.out.println("url:" + url);
//通过OkHttp请求url,并将结果回调到外部
final OkHttpClient okHttpClient = new OkHttpClient().newBuilder().build();
final Request request = new Request.Builder()
.url(url)
.build();
call = new Call
5.最后添加最外层的请求调用代码:
MiniRetrofit miniRetrofit = new MiniRetrofit.Builder().baseUrl("https://blog.csdn.net").build();
GetBaiduService getBaiduService = miniRetrofit.create(GetBaiduService.class);
Call call = getBaiduService.read();
call.enqueue(new CallBack() {
@Override
public void onResponse(String response) {
System.out.println("success:"+response);
}
@Override
public void onFail(Exception e) {
System.out.println(e.toString());
}
});
成功请求到了我的csdn首页数据。
动态代理还有很多用处,比如我之前写过的通过反射实现的仿ButterKnife功能Demo
一文里就讲过通过动态代理巧妙解决控件绑定点击监听回调方法的方案。总的来说,它提供了很大的灵活性,让我们在运行期根据具体情况做具体的应变。
原创不易,如果你觉得好,随手点赞,也是对笔者的肯定~