细聊代理模式

代理模式在生活中其实很常见,比如,你一觉睡到中午不想去食堂打饭,于是你委托阿黄让他帮你把饭带回来,这就产生了一种代理关系,其中委托人是你,代理人是阿黄,而事件就是打饭,你为了足不出户就能吃到午饭而想到的这馊主意,就是一种代理方法。又比如,你女朋友从外地回来下飞机了,而你又因为一些不可描述的事情不能去接机,这时候你就可以叫你的好基友去机场帮你把女朋友接回来啊,于是你和你好基友就形成了一种代理关系,委托人是你,代理人是好基友,而事件就是接女朋友回来,这种很有可能戴绿帽子的方法就是一种代理方法。然后,将这些代理方法总结规律,抽象出来,上升到一个通用层面的高度,就是代理模式。什么是代理模式呢?通俗地讲,有些事情本应该是你做的,但是你不想做或者不方便做的时候,叫别人替你完成。生活中种种“帮帮忙”的事件,都有代理模式的影子,所以说代理模式在生活中很常见。

静态代理

那么我们如何使用代码来描述代理模式呢?我们将上面的例子总结规律,抽象出来,就大概有下面两点:

  • 委托人和代理人都要完成同样的事情(共同实现事件的接口)。
  • 代理人要做的这件事其实是委托人请求的,也就是说,代理人本身并不知道如何做这件事,只有委托人告诉他了,代理人才知道怎么做。所以,代理人需要持有委托人的引用,在做这件事的时候就按照委托人的意愿去做。

就比如第一个例子,“打饭”这件事情,由于你和阿黄都能做,所以可以写成一个接口,让你和阿黄都能实现。

1
2
3
4
5
6
7
8
9
10
/**
* 一个抽象的午饭类, 具体要吃什么样午饭, 看子类的心情。
*/
public interface Lunch {

/**
* 吃什么样的午饭, 糖醋排骨?宫保鸡丁?。。。。
*/
void catagory();
}

然后就要创建“你”这个对象了,由于你要吃午饭,就需要实现Lunch类:

1
2
3
4
5
6
7
8
9
10
/**
* 你, 需要考虑午饭吃什么
*/
public class You implements Lunch {
@Override
public void catagory() {
// 你想吃宫保鸡丁
System.out.println("我要吃宫保鸡丁盖饭");
}
}

然后你很懒啊,需要找阿黄这个代理人帮你带午饭回来,所以还需要一个代理人对象,注意,由于代理事件是委托人(“你”)下发的,所以代理人对象里面还需要持有委托人的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 代理人, 由于需要帮委托人带饭, 也必须实现午饭接口
*/
public class ProxyMan implements Lunch {
/**
* 必须是要有委托人的存在才行,没有委托人还需要代理人干嘛?
*/
private You mYou;
/**
* 在创建代理人对象的时候就传入一个委托人进来,这样就使得代理人可以持有委托人的引用了。
* 当然这只是一种方法,你也可以使用其他方法让代理人持有委托人的引用。
*/
public ProxyMan(You you) {
mYou = you;
}
@Override
public void catagory() {
// "你"要让代理人帮你做的事情。
mYou.catagory();
}
}

OK,大功告成,然后我们创建一个场景(客户端)来模拟一下这个事情:

1
2
3
4
5
6
7
8
9
10
11
public class Proxy {

public static void main(String[] args) {
// 创建一个委托人
You you = new You();
// 创建一个代理人, 可以是阿黄, 也可以是其他人
ProxyMan aHuang = new ProxyMan(you);
// 让阿黄帮你带饭。
aHuang.catagory();
}
}

以上便是代理模式的一种模版,结合代码,我们可以总结出代理模式的类图如下:

Class Diagram (1).png-12.5kB

好了,扯了这么多,该给代理模式来下一个正式的定义了:为其他对象提供一种代理以控制对这个对象的访问。

另外,我们现在看到的代理模式,又称之为静态代理,因为代理类是需要我们自己手动编写的,后面我们会谈谈如何不用手动编写代理类,来实现代理模式。

静态代理的作用

当然,你会说,这种小事,委托类自己就能完成的嘛,非要多增加一些类干嘛。的确,如果单是这种简单的例子,使用任何设计模式都是多此一举。但在实际工作中,使用代理模式就会有如下的有点:

  • 解耦。这几乎是设计模式解决的根本问题。在这里,委托类可以专心地做好自己,麻烦的事情让别人代劳。
  • 拦截、扩展方法。代理类在实现接口方法的时候,除了直接调用委托类的方法外,还可以在不修改委托类的情况下,增加一些其他的功能,比如顺便带瓶可乐回来?

为了能让我们的代理人阿黄能带瓶可乐回来,我们只需要在代理类中对catagory()方法增加一个“带瓶可乐回来”的功能即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProxyMan implements Lunch {

private You mYou;

public ProxyMan(You you) {
mYou = you;
}
@Override
public void catagory() {
// "你"要让代理人帮你做的事情。
mYou.catagory();

// 打完饭后再去买瓶可乐
System.out.println("嗯,买瓶可乐来喝");
}
}

现在你应该能明显地感受到代理类的作用了。对,就是拦截方法、对委托的方法进行加工处理。

动态代理

然后有一天,当你中午醒来的时候,发现寝室里除了你以外空无一人,你为了双倍经验双倍金币所以不想浪费时间下楼去食堂打饭,但是你饿啊,要吃饭啊,这时你就只有等寝室再进来个人,让他帮你带饭(假设你不会叫外卖)。但是这会有一个问题,之前一醒来就可以让阿黄帮你带饭,是因为阿黄在你醒来之前就在寝室的(程序编译阶段就存在一个代理类),并且设定了阿黄“能打饭”(实现了Lunch接口)这个属性,所以简单的几行代码就能搞定。但是现在,你醒来之后(程序开始运行),发现并没有人可以帮你带饭(没有一个在编译阶段就实现了Lunch接口的代理类),在你等待的过程中也并不知道会是谁进来,阿黄,阿猫还是阿三(我们需要在程序运行的时候动态生成一个对象)?而且最重要的是,进来的那个家伙能帮你带饭才行(运行阶段生成的对象要实现Lunch接口)。

那么我们再梳理一遍,现在的你应该怎么做才能吃到午饭呢?

  • 首先,“你”这个对象和“午饭”这个事件是必须存在的。
  • 其次,创建一个代理人。由于这个代理人是帮你做事情的,所以创建这个代理人的方法必须和“你”这个对象以及“午饭”事件接口相关。
  • 最后,让代理人去帮你打饭回来。

让我们来用代码实现一下,You这个委托类和Lunch接口是要保留的,而ProxyMan这个代理类就不需要了。在场景中(client客户端),我们可以使用代理的一个静态方法newProxyInstance来创建一个代理对象,这个代理对象当然是实现了Lunch这个接口的。那么client中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 创建一个委托人
You you = new You();
// 创建一个代理人, 第一个参数是委托类的ClassLoader, 第二个参数是要实现的接口数组,
// 第三个参数是一个匿名内部类, 对于代理人将要做的任何事情做拦截处理。
Lunch somebodyCouldTakeLunch = (Lunch) Proxy.newProxyInstance(you.getClass().getClassLoader(), new Class[]{Lunch.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(you, args);
}
});
// 动态生成的代理人去带饭了
somebodyCouldTakeLunch.catagory();
}

当动态生成的代理人对象调用方法去带饭时(somebodyCouldTakeLunch.catagory()),就会触发匿名内部类InvocationHandler中的invoke()方法,这里面没有做其他处理,直接返回了“你”这个对象的对应方法。也就是说,动态生成的代理人是安装你的要求带回来宫爆鸡丁盖饭!

一个简单的动态代理就这么完成了。然而动态代理是想当强大的,为了显示这种强大,这里再举一个例子,List集合大家都用过,这个接口有很多方法,如果我们想要屏蔽remove这个方法,这时我们就可以使用动态代理,来生成一个代理类List,每当调用到代理类的remove()方法时,就跑出异常,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static List getList(List list) {
return (List) Proxy.newProxyInstance(List.class.getClassLoader(), new Class[]{List.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("remove")) {
throw new UnsupportedOperationException();
} else {
return method.invoke(list, args);
}
}
});
}

当我们传入一个List对象进去,返回的就是经过处理后的代理List类,这时如果调用代理List的remove方法时就会报错了。

估计你已经知道了如何使用动态代理了,但是你肯定有很多疑惑,Proxy.newProxyInstance这个静态方法是怎么完成这件事的呢?我们看一下源码(以下分析的源码是基于jdk 1.6,相比起来比较简单易懂):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Object newProxyInstance(ClassLoader loader, 
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException {

// 检查 h 不为空,否则抛异常
if (h == null) {
throw new NullPointerException();
}

// 获得与制定类装载器和一组接口相关的代理类类型对象
Class cl = getProxyClass(loader, interfaces);

// 通过反射获取构造函数对象并生成代理类实例
try {
Constructor cons = cl.getConstructor(constructorParams);
return (Object) cons.newInstance(new Object[] { h });
} catch (NoSuchMethodException e) { throw new InternalError(e.toString());
} catch (IllegalAccessException e) { throw new InternalError(e.toString());
} catch (InstantiationException e) { throw new InternalError(e.toString());
} catch (InvocationTargetException e) { throw new InternalError(e.toString());
}
}

其他都好懂,可能就是getProxyClass()这个方法没见过。同时该方法也是动态代理中最核心的东西。由于该方法比较长,去除注释后也有120+行代码,所以我就不粘贴代码了,你可以自己去Java源码中去寻找,或者你可以查看这个链接: getProxyClass(),里面大概的流程是:先将传入的接口数组进行一系列检查,并存储所有接口名称,然后创建一个HashMap的缓存表,里面以不同的键值对形式来存放着待创建的代理类、正在被创建的代理类。然后根据一系列的规则确定包名,调用ProxyGenerator.generateProxyClass()方法创建代理类,这个方法会调用ProxyGenerator对象的generateClassFile()方法来生成代理类类文件的字节数组,具体生成方法可以参见这个链接中的源码: generateClassFile(),我就不再深入阐述了。生成代理类对象后,将其记录到proxyClasses表中,然后就是更新缓存表、清楚缓存表等一系列善后工作了。

由于所有的代理类都有一个共同的父类Proxy,Java 的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在 Java 中本质上就行不通。所以Java只能进行接口的动态代理,这不得不说是一个遗憾。

静态代理和动态代理的联系

然而在大多数情况下,我们的代理类在敲代码的时候就能确定下来,也就是说,我们日常开发的大多数情况都能使用静态代理。那么能不能用动态代理呢?当然能!为什么要用动态代理呢?

当委托类实现的接口方法比较多的时候,写一个代理类一个个地处理委托方法就太麻烦了。这时我们可以在invoke()方法中对委托类的所有方法进行判断、拦截、扩展等处理。减少代码量。

Retrofit中的动态代理

Retrofit可谓是当下最火的Android网络请求框架之一了,我们在Retrofit的wiki上可以看到Retrofit的使用方法如下:

1.创建一个请求方法的接口:

1
2
3
4
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

2.生成Retrofit对象,并且创建一个实现了GitHubServiece接口的实体类:

1
2
3
4
5
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();

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

3.发起网络请求:

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

我们看第二步,传入了一个接口对象,然后就创建了一个实现了GitHubService接口的实体类?怎么实现的?Excuse me?这时候,就需要轻轻地点击去看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}
});
}

一个大写的动态代理!!!当然这里用的并不是规范的代理模式,因为我们知道,代理模式是需要有一个具体的委托类的,但是这里并没有一个具体的委托类,它是直接将传入的GitHubService接口,既作为事件接口,又作为代理类使用,显然这里拦截了接口中的方法,并不能对该接口的方法本身进行任何操作,这里使用动态代理,纯粹是为了拦截方法,获取接口方法上的注解信息,然后返回一个Adapter。这种可以算得上一个动态代理的扩展应用吧。

静态代理和动态代理的应用场景很多,常见的框架都有用到(比如Spring框架、OrmLite框架),希望读完本篇文章能帮你理解静态代理和动态代理。

如果觉得文章对你有帮助,请我喝杯可乐吧