搞定代理模式,看懂Retrofit动态代理的骚操作

代理模式

看完文章你能学到什么?搞懂代理模式,Retrofit代理模式的使用(其实我就是因为没看懂,才学的),文章有点长但是逻辑很简单

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。 值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。----Frank909 (并不是我总结的,但是我挺认同的)

简单来说就是:你朋友圈的小花卖面膜,面膜100块一盒,但是经过小花的代理变300了,此时面膜还是那个面膜,就是到手的时候总感觉有点贵!

静态代理

把上面说所的,用程序来表达:

定义一个产品的接口,产品的属性是价格

public interface IProduct {
    void price();
}

创建面膜类实现产品接口

public class FacialMask implements IProduct {

    @Override
    public void price() {
        System.out.println("出厂价格100块");
    }
}

最后创建一个代理类:小花

public class XiaoHuaProxy implements IProduct {

    private IProduct product;

    public XiaoHuaProxy(IProduct product) {
        this.product = product;
    }

    @Override
    public void price() {
        System.out.println("我是小fafa");
        product.price();
        System.out.println("面膜现在300块,100不存在的");
    }
}

main 方法

FacialMask facialMask = new FacialMask();
XiaoHuaProxy xiaoHuaProxy = new XiaoHuaProxy(facialMask);
xiaoHuaProxy.price();

运行一下

我是小fafa
出厂价格100块
面膜现在300块,100不存在的

然后这就是静态代理,因为代理类是静态生成的,以上要知道的一点就是:

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。

有人可能会杠一下,老子就不实现同一个接口,同一个继承对象,照样能实现,所以下面看下动态代理

动态代理

动态代理和静态代理功能上是没有任何区别的,动态代理我的理解:动态生成代理类,完成代理的功能。小花不再被显示创建出来,并不是面膜可以被小花代理,那么也可以被小明代理,然后动态代理就是为了生成多个不同样的代理对象。

但是在这之前要补充一个反射的小知识,才能更好的看下去

反射 Method 方法的执行:

我们在应用反射的时候往往是想执行一些我们无法使用的类或者方法;
Method.invoke(Object obj, Object... args);

public Object invoke(Object obj, Object... args) {}

invoke() 方法中第一个参数 Object 实质上是 Method 所依附的 Class 对应的类的实例,如果这个方法是一个静态方法,那么 ojb 为 null,后面的可变参数 Object 对应的自然就是参数。这里看不懂没事,下面有例子;

举个例子,FacialMask(面膜类)

//不使用反射,获取对象和执行方法
FacialMask facialMask = new FacialMask();
facialMask.price();
//使用反射实例化获取类对象,有很多种方式
FacialMask reflectMaxk = FacialMask.class.newInstance();
//不使用反射直接调用
reflectMaxk.price();
//突然发现price是个私有方法,可以通过反射执行这个方法
Method price = FacialMask.class.getMethod("price");
price.setAccessible(true);//把类的可见性打开,即使是私有也能调用
//参数一,就是要调用类的对象,静态方法可以传null,参数二:调用方法的参数,一定是要按顺寻的,没有可以不写
 Object invoke = price.invoke(reflectMaxk, new Class[]{});//并且返回方法的返回值

了解这么多看下面这个文章就够了,如果想深入了解推荐看《细说反射,Java 和 Android 开发者必须跨越的坎》


Proxy.newProxyInstance 生成代理类,完成代理的功能

下面用程序演示

public static void main(String[] args) {
       FacialMask facialMask = new FacialMask();
        IProduct proxyInstance = (IProduct) Proxy.newProxyInstance(FacialMask.class.getClassLoader(), new Class[]{IProduct.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("我是小fafa");
                Object invoke = method.invoke(facialMask, args);
                System.out.println("面膜现在300块,100不存在的");
                return invoke;
            }
        });
        proxyInstance.price();
    }

我们使用Proxy.newProxyInstance()取代了XiaoHuaProxy类,看下输出结果

我是小fafa
出厂价格100块
面膜现在300块,100不存在的

源码中是这样描述这个类的,返回一个指定接口的代理类实例,这个实例的方法都会去调用InvocationHandler。

  • 参数一:ClassLoader 类加载器,这里可以传代理类要实现的接口的ClassLoader

这里大致介绍下,类加载器默认有三个:Bootstrap ClassLoader 最顶层的加载类;Extention ClassLoader 扩展的类加载器;Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类,一般你写的程序都是由这个类加载的,自定义类加载器默认的父类(并不是继承关系)是Appclass Loader
,详细可以看: 《一看你就懂,超详细java中的ClassLoader详解》

  • 参数二:Class<?>[] 要实现的接口,因为一个类可以实现多个接口,所以这里是个数组
  • 参数三:InvocationHandler 这个就是接口代理类执行方法的回调
//源码 jdk 8 基础上
 public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        //删除一些校验参数合法的代码

        /**
         * Look up or generate the designated proxy class.
         * 生成代理类或者查出已生成代理类
         */
         
        Class<?> cl = getProxyClass0(loader, intfs);

        /**
         * Invoke its constructor with the designated invocation handler.
         * 把代理类实例化,返回去
         */
      
        //删除一些try catch
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        //删除一些校检
        //注意这个地方,实例化代理类,把== h ==当参数传进去了,这个== h ==就是咱们实现InvocationHandler的方法
        return cons.newInstance(new Object[]{h});
      
    }

获取生成的字节码文件

getProxyClass0()获取代理类字节码文件,主要使用了ProxyGenerator.generateProxyClass()生成了代理类,默认在内存中,也可以设置System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true") 获取本地文件, 通过此语句可以立个flag让生成字节码文件的时候,输出本地字节码文件,注意这句话一定要在获取代理类实例之前,
生成的文件在当前项目的根目录,对应的包名文件夹里


 private static final boolean saveGeneratedFiles = 
 (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));//这就是那个flag的获取

 if (saveGeneratedFiles) {//这就是写入本地的地方
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    try {
                        int var1 = var0.lastIndexOf(46);
                        Path var2;
                        if (var1 > 0) {
                            Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
                            Files.createDirectories(var3);
                            var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
                        } else {
                            var2 = Paths.get(var0 + ".class");
                        }

                        Files.write(var2, var4, new OpenOption[0]);//写到文件里
                        return null;
                    } catch (IOException var4x) {
                        throw new InternalError("I/O exception saving generated file: " + var4x);
                    }
                }
            });

废了这么大劲儿,看下动态生成的代理类字节码文件吧!

public final class $Proxy0 extends Proxy implements IProduct {
   
   //删除一些不重要的成员变量

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        //删除了一些tryCatch
        return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
    }

    public final void price() throws  {
      //删除了一些tryCatch,
      super.h.invoke(this, m3, (Object[])null);
    }

    public final String toString() throws  {
        //删除了一些tryCatch
        return (String)super.h.invoke(this, m2, (Object[])null);
    }

    public final int hashCode() throws  {
        //删除了一些tryCatch
        return (Integer)super.h.invoke(this, m0, (Object[])null);
    }

    static {
        //删除了一些tryCatch
        m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
        m3 = Class.forName("com.proxy.demo.IProduct").getMethod("price");
        m2 = Class.forName("java.lang.Object").getMethod("toString");
        m0 = Class.forName("java.lang.Object").getMethod("hashCode");
    }
}

咱们最主要看的是
构造方法

//在这里把val1也就是InvocationHandler传给了父类Proxy,回忆一下 
//在Proxy.newProxyInstance() 源码中我让大家注意的地方 return cons.newInstance(new Object[]{h});这个h其实就是
//newProxyInstance(,,h)的第三个参数,也就是InvocationHandler回调。
 public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

和这个代理类所有方法中都会调用的super.h.invoke(this, m3, (Object[])null);

//这个方法的super.h 其实就是刚刚咱们从$Proxy0(InvocationHandler var1)
//传进去的h,h在这里被invoke,这里是invoke的实现
super.h.invoke(this, m3, (Object[])null);
//也就是它
 new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("我是小fafa");
                Object invoke = method.invoke(facialMask, args);
                System.out.println("面膜现在300块,100不存在的");
                return invoke;
            }
        });

总结一下,代理类执行的任何一个方法都会回调你的InvocationHandler实现类。而且通过ProxyGenerator.generateProxyClass()确实动态生成了字节码文件!
那么看完这些咱们也明白了InvocationHandler中invoke(Object proxy, Method method, Object[] args) 这三个参数分别是什么,

  • 参数一:Object 生成的代理类对象
  • 参数二:Method 代理类调用的方法
  • 参数三:args 调用方法时的参数

好了,上面咱们了解了所有关于生成代理类的一些必要方法,一定要记好这三个参数,因为要为下面讲解Retrofit的create()方法做铺垫。

疑问

我们会有一个疑问,动态代理有什么用,因为最后执行的还是 面膜的price方法,所以感觉很鸡肋,但是他把面膜的方法毫无侵略性的增加了其它方法,在这里用的最多是AOP 面向切面编程领域,拦截个日志什么的。

Retrofit中的动态代理

终于写到了大Boss o(╥﹏╥)os

先看一段代码,先了解Retrofit用动态代理解决了什么问题?下面代码是OkHttp一个GET请求实例,为什么扯上了OkHttp,因为Retrofit就是为了封装OkHttp

//第一步:初始化
OkHttpClient client = new OkHttpClient();
//第二步获取请求对象
  Request request = new Request.Builder()
      .url(url)
      .build();
//执行请求获取服务器响应对象
  Response response = client.newCall(request).execute();
  response.body().string();
}

再瞅一下Retrofit如何发起请求的

//第一步,初始化,配置需要的物料,例如请求地址的根路径,转化工厂,拦截器什么的
 Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(API_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

// Create an instance of our GitHub API interface.
//第二步使用动态代理创建代理类对象
GitHub github = retrofit.create(GitHub.class);

// Create a call instance for looking up Retrofit contributors.
// 然后调用代理类的方法获取请求对象
Call<List<Contributor>> call = github.contributors("square", "retrofit");

// Fetch and print a list of the contributors to the library.
//第三步 执行请求获取服务器响应对象
List<Contributor> contributors = call.execute().body();
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}

都是3步走,初始化,创建请求对象,发送请求获取数据.

Retrofit用这两行替代了OkHttp的这两行

GitHub github = retrofit.create(GitHub.class);
Call<List<Contributor>> call = github.contributors("square", "retrofit");

public interface GitHub {
    @GET("/repos/{owner}/{repo}/contributors")
    Call<List<Contributor>> contributors(@Path("owner") String owner,@Path("repo") String repo);
}

okttp

//String url ="baseUrl"+/repos/"+"square"+"/"+"retrofit"+"/contributors"
 Request request = new Request.Builder()
      .url(url)
      .build();
  Response response = client.newCall(request).execute();
  String result=response.body().string()
  return GsonUtlis.from(result);

其实优势已经出来了,okhttp url的拼接,是不是由我们来做,body的数据是字符串或者其它,是不是还要手动处理成gson或者其它,这还只是get请求,Post请求呢?创建请求对象的时候还要创建请求体,但是Retrofit不管你这些,按照暴露的接口,传参就好了,帮你返回你想要对象,你什么都不用做,只用处理传入参数,获取结果.实现了一个黑盒!用户只需关心服务器需要传递什么参数,然后拿到结果.


进入正题

刚刚演示的是Retrofit干了些什么,现在看如何干的

  • 如何动态产生请求对象Request,弄明白这两行我们就算大功告成了.
GitHub github = retrofit.create(GitHub.class);
Call<List<Contributor>> call = github.contributors("square", "retrofit");

如果你站在作者的角度出发,你会如何处理,通过一定的规则产生不同的请求对象呢?而且不能出现很多用户写的代码,因为用户就是为了省力,才使用这个封装.

看看作者怎么做的!

  1. 首先定义了一个接口,让用户告诉我,你是什么请求,传了那些参数,想获得什么对象.
    public interface GitHub {
        @GET("/repos/{owner}/{repo}/contributors")
        Call<List<Contributor>> contributors(
                @Path("owner") String owner,
                @Path("repo") String repo);
    }
  1. 拿到这个接口,处理这个数据,如何通过接口获取数据呢?还记得动态代理这几句代码么?
new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("我是小fafa");
                Object invoke = method.invoke(facialMask, args);
                System.out.println("面膜现在300块,100不存在的");
                return invoke;
            }
        });
  • 参数一:Object 生成的代理类对象
  • 参数二:Method 代理类调用的方法,Method方法可以获取方法上的注解,主要用的这个
  • 参数三:args 调用方法时的参数,
  • 包括方法的返回值

在这里是不是恍然大悟,我想要的东西都有啦.啊哈哈哈哈~

接下来看具体实现:

// 源码2.4.1基础上
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();//获取用户哪个平台,有android和java8

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {
              //删除一些其他跟主逻辑无关的代码,主要删了两行,一个是执行继承Object的方法:tostring,requals,一个是public 非接口的代码
            return loadServiceMethod(method).invoke(args);
          }
        });
  }

这里最核心的就是loadServiceMethod(method)获取被代理对象,也就是获取咱们上面说的面膜,这里你可要注意,这里loadServiceMethod返回的是ServiceMethod,并不是咱们面膜的那个Method了!!!,这里其实就是想借助你调用的时机获得一个饱满的请求对象

ServiceMethod<?> loadServiceMethod(Method method) {
    //从缓存中获取,因为重新获取一遍还是挺麻烦的
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);//这里又获取了一次
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method);//生成ServiceMethod的地方
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

上面主要看的是 ServiceMethod.parseAnnotations(this, method);

abstract class ServiceMethod<T> {
  //静态方法,
  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    Type returnType = method.getGenericReturnType();//获取你方法的返回值类型
    //删除一些校验合法性代码,
    return new HttpServiceMethod.Builder<Object, T>(retrofit, method).build();
  }

  abstract T invoke(@Nullable Object[] args);
}

上面主要瞅的是new HttpServiceMethod.Builder<Object, T>(retrofit, method).build();

下面是最终的实现!!!!

  HttpServiceMethod<ResponseT, ReturnT> build() {
      requestFactory = RequestFactory.parseAnnotations(retrofit, method);//获取方法体上面的注解,获取请求方式,还有retrofit对象获取请求拼接的url

      callAdapter = createCallAdapter();//获取CallAdapter,通过这个adapter可以获取最终的请求对象Call,这里使用适配器模式
      responseType = callAdapter.responseType();//返回类型
     //删除一些校验代码
      responseConverter = createResponseConverter();//获取responseConverter,它的作用就是采用你提前设置的Converter,转换出你想要的结果,例如string->gson
     //面膜终于快出来了,我的泪也出来了
      return new HttpServiceMethod<>(this);
    }

面膜要出来了,这可不是Method的Invoke,而是刚刚new 出来的 HttpServiceMethod.invoke(args)

  @Override ReturnT invoke(@Nullable Object[] args) {
    return callAdapter.adapt(//动态产生请求对象,(咱们的面膜)
        new OkHttpCall<>(requestFactory, args, callFactory, responseConverter));
  }

这里就获得了咱们的 Call<List<Contributor>> call,这里没有详细展开去分析,获取requestFactory,callFactory,responseConverter,callAdapter.adapt()的具体实现,因为这里和我们动态代理没有关系了,如果想看的话,要看下篇文章了,但是我还没写好.O(∩_∩)O哈哈~,最终会分析完Retrofit源码.

最后

  1. Retrofit 使用动态产生的代理,产生了动态的被代理对象----请求对象(面膜),真特么活学活用呀!
  2. 其实看到这里的时候也解决了我的疑惑,Retrofit到底凭什么那么受欢迎,它最大的特点应该就是灵活,通过小的改变,适配你想要的功能.
  3. 能力有限,肯定会有一些错误还请批评指正!
  4. 最后:给你一个么么哒(づ ̄ 3 ̄)づ


    此处应该有签名

参考

轻松学,Java 中的代理模式及动态代理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容