Retrofit中的动态代理

1. 动态代理机制分析

Retrofit已经诞生好几年了,从诞生开始一直都是Android应用开发最流行的网络请求框架,准确来说,是网络请求框架一个巧妙的包装。

动态代理可以说是Retrofit中的核心机制,正如官网所说,Retrofit最大的特点,在于可以用一个Java interface通过注解去表示一个Http请求。

我们先来看一个最简单的动态代理例子,感受下通过动态代理将一个普通Java接口转化为一个代理对象:

一: 首先创建一个 JavaInterface

public interface HttpService {
    void requestCsdnHomePage();
}

二: 通过Proxy生成HttpService的代理对象的Class对象

 Class<?> httpServiceProxy = Proxy.getProxyClass(HttpService.class.getClassLoader(), HttpService.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 nonpublic. 

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。

通过添加一行代码:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

可以将运行时生成的动态代理Class文件保存下来,使用AndroidStudio打开该文件(已自动反编译)

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements HttpService {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    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 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 String request(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 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"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("HttpService").getMethod("request", Class.forName("java.lang.String"));
            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,实现了我们的HttpService接口。
  2. 用4个Method引用保存equals、hashCode、toString以及HttpService接口的read方法的Method对象。
  3. 在equals、hashCode、toString以及HttpService接口的read方法的实现都为调用super.h的invoke方法并传入2中对应的Method对象和参数。
  4. 通过查看Proxy的代码可知super.h为一个InvocationHandler 引用,而该InvocationHandler 引用就是从$Proxy0的构造方法中传入。

所以说白了,该代理类就是构造方法传入的InvocationHandler 对象的代理,无论调用什么方法,都会调用InvocationHandler 对象的invoke方法并传入方法对应的Method对象和参数。

三: 通过反射从Class对象创建对应的具体代理的实例对象进行反射创建对象

Class proxyClass = Proxy.getProxyClass(HttpService.class.getClassLoader(), HttpService.class);

		try {
			Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);
			HttpService httpService = (HttpService) constructor.newInstance(new InvocationHandler() {
				@Override
				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
					return " I Am Iron man : " + args[0];
				}
			});

			String result = httpService.request("Nelson");

			System.out.println(result);

		} catch (Exception e) {
			e.printStackTrace();
		}

这就是我们经常见到的动态代理的代码,结合Retrofit来讲,就是在具体的Http还未知的情况下,就确定了Http的请求代码

2. Retrofit中的动态代理

我们拿一段简单的Retrofit代码来解释上文中的动态代理机制。

HttpService service = retrofit.create(HttpService.class);

这一行的时候,虚拟机内部生成了代理类的Class二进制流,并且加载到虚拟机中形成代理类的Class对象,再反射得到代理类对象,而该代理类即实现了HttpService,并且持有了InvocationHandler的引用。

当调用

Call<List<Repo>> repos = service.listRepos("octocat");

就会调用代理对象的listRepos方法,通过上文的分析,它会调用
该InvocationHandler引用的对象的invoke方法,并传入HttpService的listRepos的Method对象。

InvocationHandler引用的对象的invoke方法会通过该Method对象,得到方法的注解以及参数,得到Http请求的链接、请求方法、请求路径、参数等请求信息,构建一个OkHttp的请求并执行。

3. 实现一个简易的Retrofit

所谓纸上得来终觉浅,绝知此事要躬行。 上文的叙述恐怕可能还是让人听得云里雾里,那么接下来,我就来重点围绕动态代理实现一个迷你的Retrofit。这个迷你Retrofit唯一的功能,就是通过Get请求请求我的csdn首页数据,主要是为了将Retrofit动态代理相关的的主要流程阐述清楚

一: 创建一个Get注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Get {

    String value();
}

二: 实现Retrofit客户端

public class Retrofit {

    String mBaseUrl;

    public Retrofit(String baseUrl) {
        mBaseUrl = baseUrl;
    }

    public <T> T create(Class<T> service) {
        T proxyInstance = (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和BaseUrl拼接
                        String path = mBaseUrl;
                        if (annotation instanceof Get) {
                            path += ((Get) annotation).value();
                        }

                        OkHttpClient okHttpClient = new OkHttpClient();
                        Builder builder = new Builder();
                        builder.url(path);
                        Request request = builder.build();
                        call = okHttpClient.newCall(request);
                    }

                    return call;
                }
            });
        return proxyInstance;
    }

}

外层代码

		Retrofit retrofit = new Retrofit("https://blog.csdn.net/");
        HttpService service = retrofit.create(HttpService.class);
        service.requestHomePage().enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.i(TAG,"[nelson] -- onFailure");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String result = response.body().string();
                Log.i(TAG, "[nelson] -- onResponse : " + result);
            }
        });

执行结果:

10-11 02:19:08.326 10215-10228/com.nelson.demogroup I/ProxyActivity: [nelson] -- onResponse : <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <link rel="canonical" href="https://blog.csdn.net/newMan1024"/>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <meta name="renderer" content="webkit"/>
        <meta name="force-rendering" content="webkit"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <meta name="apple-mobile-web-app-status-bar-style" content="black">
        <meta name="report" content='{"pid":"blog"}'>
        <meta name="referrer" content="always">
        <meta http-equiv="Cache-Control" content="no-siteapp" /><link rel="alternate" media="handheld" href="#" />
        <meta name="shenma-site-verification" content="5a59773ab8077d4a62bf469ab966a63b_1497598848">
            <meta name="csdn-baidu-search"  content='{"autorun":true,"install":true,"keyword":"【newMan1024的博客】linux_idea-2016_spring-boot"}'>
        
        <link href="https://csdnimg.cn/public/favicon.ico" rel="SHORTCUT ICON">
        <title>【newMan1024的博客】linux_idea-2016_spring-boot - CSDN博客</title>
            
                        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/list-b5b4c84896.min.css">
                <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/column_pc-c79c31ef12.min.css" />
                
        
                <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/themes/skin3-template/skin3-template-c9d2f651cc.min.css">
        
    <!--    自定义皮肤样式-->
        
        <script type="text/javascript">
            var username = "newMan1024";
            var blog_address = "https://blog.csdn.net/newman1024";
            var static_host = "https://csdnimg.cn/release/phoenix/";
            var currentUserName = "";
            var isShowAds = true;
            var isOwner = false;
            var loginUrl = "http://passport.csdn.net/account/login?from=https://blog.csdn.net/newMan1024"
            var blogUrl = "https://blog.csdn.net/";
    
            var curSkin = "skin3-template";
            // 收藏所需数据
            var articleTitle = "newMan1024的博客";
            var articleDesc = "CSDN博主 newMan1024的博客  主页提供丰富的内容介绍,包含博客等级、博主粉丝、积分、排名等内容,查找最新 newMan1024的博客 博文更新信息,请上CSDN博客频道.";
            // 第四范式所需数据
            var articleTitles = "【newMan1024的博客】linux_idea-2016_spring-boot";
            
            var nickName = "Nelson-KK";
            var isCorporate = false;
            var subDomainBlogUrl = "https://blog.csdn.net/"
            var digg_base_url = "https://blog.csdn.net/newman1024/phoenix/comment";
            var articleDetailUrl = "";
        </script>
        <script src="https://csdnimg.cn/public/common/libs/jquery/jquery-1.9.1.min.js" type="text/javascript"></script>
        <!--js引用-->
                <script src="//g.csdnimg.cn/??fixed-sidebar/1.1.6/fixed-sidebar.js,report/1.0.6/report.js" type="text/javascript"></script>
        <link rel="stylesheet" href="https://csdnimg.cn/public/sandalstrap/1.4/css/sandalstrap.min.css">
        <style>
            .MathJax, .MathJax_Message, .MathJax_Preview{
                display: none
            }
        </style>
    </head>
    <body class="nodata " > 
        <link rel="stylesheet" href="https://csdnimg.cn/public/common/toolbar/content_toolbar_css/content_toolbar.css">
        <script id="toolbar-tpl-scriptId" src="https://csdnimg.cn/public/common/toolbar/js/content_toolbar.js" type="text/javascript" domain="https://blog.csdn.net/"></script>
        <script>
        (function(){
            var bp = document.createElement('script');
            var curProtocol = window.location.protocol.split(':')[0];
            if (curProtocol === 'https') {
                bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
            }
            else {
                bp.src = 'http://push.zhanzhang.baidu.com/push.js';
            }
            var s = document.getElementsByTagName("scri

这样一个简易的Retrofit就完成啦。

你可能感兴趣的:(android)