设计模式之代理模式

说道Java的代理模式,很可能就想到类Proxy和接口InvocationHandler两个东西,事实上代理模式的思想应用还是不止于此,本篇就做个梳理;

先看看InvocationHandler

//proxy:宿主对象的代理对象
//method:宿主对象的某个方法
//args:method方法所需的参数
 public Object invoke(Object proxy, Method method, Object[] args)

需要注意第二个参数Method,该对象表明的是宿主对象的某个方法。该对象使得我们可以通过其invoke方法执行宿主的某个方法:

//obj:当前方法所属的对象,不能为null
//args:当然方法所需的参数
Object invoke(Object obj, Object... args)

所以我们在执行method的invoke方法时需要这么做(伪代码):

//初始化一个宿主对象
SubjectImpl  subject = new SubjectImpl();

//执行subject对象的某个方法
method.invoke(subject,params);

综合上面的说明,所以网上关于动态代理的例子就很好理解为什么是如下所示了:


/**
 * 实现了InvocationHandler接口的类:
 */
public class SubjectInvocationHandler implements InvocationHandler {
    //宿主对象
    private ISubject subject;

    public SubjectInvocationHandler(ISubject subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--代理类工作:实际是宿主在执行相关的invoke方法--");
        return method.invoke(subject, args);
    }

    /**
     * 为宿主对象subject创建代理类对象
     * @return
     */
    public ISubject createProxy() {
        ISubject subjectProxy = (ISubject) Proxy.newProxyInstance(getClass().getClassLoader(),
                subject.getClass().getInterfaces(),
                this);
        return subjectProxy;
    }

}

public interface ISubject {
    void doSomething();
}

public class SubjectImpl implements ISubject {
    public void doSomething() {
        System.out.println("SubjectImple do Somthing");
    }
}

客户端的调用也很简单:

       SubjectInvocationHandler subjectInvocationHandler = new SubjectInvocationHandler(new SubjectImpl());
       //运行时创建代理类对象
        ISubject proxy = subjectInvocationHandler.createProxy();
        //代理执行doSomething
        proxy.doSomething();

这段代码仿写很容易,<font color="#ff00ff">但是有什么用呢?换句话说我都拿到了SubjectImpl还费劲创建一个代理对象干嘛?从上面的代码来看proxy.doSometing()执行的仍然是SubjectImpl这个宿主类的doSmething方法。何必多此一举不直接使用宿主呢?</font>

回答这个问题之前先回顾一下问题的本质:<font color="#ff00ff">什么是动态代理?</font>代理类或者对象在程序运行时创建的代理被成为 动态代理。动态代理模式理论上的有点就不细说了,关于更多的讨论可以参考知乎的这篇文章。下面就结合实际例子来看看动态代理的强大作用。


作为android开发人员,Retrofit这个网络框架可能都不陌生,该框架就是借助于动态代理实现了这么牛叉的网络框架。使用该框架你需要先定制一个接口:

public interface MyService{
    @FormUrlEncoded
    @POST("you url")
    Call<YourJavaBean>  loadData(@FieldMap Map<String, Object> params);
}

上面的接口很简单,就是配置了三个注解@FormUrlEncoded,@POST,@FieldMap.关键是我们在使用这个接口进行网络请求的时候,没有自己implements MyService,直接如下调用即可:

MyService myService = retrofit.create(MyService.class)
myService.loadData(map);

从上面的代码看,调用Retrofit对象的create方法即可创建一个MyService对象;其实也没啥神秘的地方,就是使用了动态代理模式:

public <T> T create(final Class<T> service) {
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
             
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

看到了吧,在create方法中主要是Proxy.newProxyInstance动态创建了MyService的代理对象,在我们执行loadData的时候调用代理对象的invoke方法;通过invoke方法的第二个参数Method对象,我们可以拿到loadData方法配置的注解信息以及loadData方法的map参数的值,从而执行网络请求。只不过Retrofit又将method封装到了ServiceMethod里面而已。从Retrofit的例子中相信你们可以意识到了,<font color="#ff00ff">动态代理其实不需要宿主对象也能发挥其强大的作用</font>。在retrofit我们只是拿到了接口方法的Method对象,然后通过操控method对象获取注解以及参数信息来执行网络请求而已。关于Retrofit的更多细节,可以点此了解更多


实际上动态代理理念的运用不仅仅体现在上面的Proxy和InvocationHandler接口。其思想运用的很广泛,比如android开发程序员EventBus,ButteKinfe等等,都是运用了注解+动态代理的思想;或者从某种程度来说EventBus和ButterKnife运用了AOP的思想。

比如ButterKnife的运用,ButterKnife事先是不知道宿主对象是谁,在程序运行的时候需要拿到具体的宿主对象,然后横向切入到宿主代码中去,这其实就是动态代理和AOP思想的结合。

还是用代码来说话,没使用ButterKnife的代码如下所示

public class MyActivity extends Activity {
  
     TextView helloWoldTxtView;

     protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       //初始化helloWoldTxtView
        helloWoldTxtView = findViewById(R.id.hello_world)
        helloWoldTxtView.setText("Hello  Android")
      }

而使用了ButterKnife的代码则如下所示:

public class MyActivity extends Activity {
     @BindView(R.id.hello_world)
     TextView helloWoldTxtView;

     protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       //绑定butterknife
        ButterKnife.bind(this)
        //直接使用(看起来没有初始化)
        helloWoldTxtView.setText("Hello  Aop")
      }
}

从上面代码来看这就是代理模式的体现,比如把初始化控件的方法代理给了ButterKnife,由ButterKnife代劳android组件的初始化工作。那么ButterKnife是怎么做到的呢?其原理也很简单,在用apt或者annotationProcessor这种技术,在程序编译期间扫描程序,对标有@BindView注解的类进行扫描,然后生成一个Activity的代理类其命名规则为MyActivity_ViewBinding:

public class MyActivity_ViewBinding implements Unbinder {
   //宿主类
    private MyActivity target;
    
      @UiThread
     public MyActivity_ViewBinding (MyActivity  target) {
        this(target, target.getWindow().getDecorView());
    }
    
      @UiThread
  public ActiveWebActivity_ViewBinding(ActiveWebActivity target, View source) {
    this.target = target;
    target.helloWoldTxtView= Utils.findRequiredViewAsType(source, R.id.helloWoldTxtView, "field 'helloWoldTxtView'", TextView.class);
  }

}

可以看出宿主组件helloWoldTxtView的初始化代理给了MyActivity_ViewBinding 这个类。该类调用 Utils.findRequiredViewAsType这个方法,其内部也即是调用了android的findViewById(R.id.helloWoldTxtView)方法来完成了初始化。
从上面代码可以看出,编译期间动态生成的代码MyActivity_ViewBinding 有两个构造函数,那么什么时候初始化这个代理类呢?答案是调用ButterKnife.bind(this)的时候,会调用createBind方法:

//target就是宿主Activity
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }

所以进入createBinding里看看发生了什么:

  private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    //根据Activity所在的class所在的路径拼接_ViewBinding查抄对应代理类的Class对象
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    
    //代理类初始化
      return constructor.newInstance(target, source);
 
  }

 private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
   //获取你的Activity的class的路径
    String clsName = cls.getName();

   //拼接代理类所在的路径
      Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
     //获取代理类的构造函数
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
 
    return bindingCtor;
  }

上面代码思路也很清晰:
1、获取宿主类的Class对象
2、根据该Class对象的 cls.getName()来拼接出编译期间动态生成的代理类的路径
3、根据代理类所在的路径获取其构造器,然后初始化代理对象
4、在构造器对android组件进行初始化


到此为止,代理模式的分析讲解完毕。从中也体会到了注解+代理模式的强大作用。如有不当之处欢迎批评指正。

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

推荐阅读更多精彩内容