所谓代购,就是找人帮忙购买自己需要的商品,代购包括两种类型,一种是在当地买不到商品,或者因为当地该商品价格较高,因此托人在其他地区或者国外购买,另一种类型是消费者对想要购买的商品消息缺乏,只能委托中介或者中间商购买。
在软件开发中,有时也需要提供与代购类似的功能,由于某些原因,客户端不想或不能直接访问对象,此时可通过一种叫代理的第三者来实现间接访问,这种方案对应的设计模式称为代理模式。
代理模式是一种应用很广泛的结构型设计模式,而且变化很多。在代理模式中引入了一个新的代理对象,代理对象可以在客户端对象和目标对象之间起到中介的作用,去掉客户不能看到的内容或者增添客户需要的额外服务。
代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
代理模式是一种对象结构型模式。
Subject
(抽象主题角色):声明了真实主题和代理主题的共同接口,客户端通常需要针对抽象主题角色编程Proxy
(代理主题角色):内部包含了对真实主题的引用,从而可以操作真实主题对象。代理主题角色中提供了一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题。代理角色还可以控制对真实主题的使用,在需要的时候创建或删除真实主题对象,并对真实主题的使用加以约束。通常在代理主题角色中,客户端调用之前或之后都需要执行特定操作,比如图中的preRequest
以及postRequest
RealSubject
(真实主题角色):定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理角色间接调用真实主题角色中的操作代理模式根据目的以及实现方式可以分成很多类,常见的几种如下:
代理模式和装饰模式在实现时类似,主要区别如下:
这里简单实现为一个接口:
interface Subject
{
void request();
}
实现抽象主题接口,执行真正的业务操作:
class RealSubject implements Subject
{
public void request()
{
System.out.println("真实主题角色方法");
}
}
同样实现抽象主题接口,一般来说在调用真正的业务方法之前或之后会有相关操作:
class Proxy implements Subject
{
private RealSubject subject = new RealSubject();
public void pre()
{
System.out.println("代理前操作");
}
public void request()
{
pre();
subject.request();
post();
}
public void post()
{
System.out.println("代理后操作");
}
}
客户端针对抽象主题角色进行编程即可,如果不需要代理,则实例化真实主题角色,如果需要代理则实例化代理主题角色:
public static void main(String[] args)
{
Subject subject = new RealSubject();
subject.request();
System.out.println("\n使用代理:\n");
subject = new Proxy();
subject.request();
}
一个已具有搜索功能的系统,需要为搜索添加身份认证以及日志记录功能,使用代理模式设计该系统。
设计如下:
Searcher
RealSearcher
ProxySearcher
代码如下:
public class Test
{
public static void main(String[] args)
{
Searcher subject = new ProxySearcher();
subject.search();
}
}
interface Searcher
{
void search();
}
class RealSearcher implements Searcher
{
public void search()
{
System.out.println("搜索");
}
}
class ProxySearcher implements Searcher
{
private RealSearcher subject = new RealSearcher();
public void validate()
{
System.out.println("身份验证");
}
public void search()
{
validate();
subject.search();
log();
}
public void log()
{
System.out.println("日志记录,查询次数+1");
}
}
进行搜索之前,先验证用户,接着进行搜索,搜索完成后进行日志记录,这是保护代理以及智能引用代理的例子。
通常情况下,每一个代理类编译之后都会生成一个字节码文件,代理所实现的接口和所代理的方法都固定,这种代理称为静态代理。
静态代理中,客户端通过Proxy
调用RealSubject
的request
方法,同时封装其他方法(代理前/代理后操作),比如上面的查询验证以及日志记录功能。
静态代理的优点是实现简单,但是,代理类以及真实主题类都需要事先存在,代理类的接口以及代理方法都明确指定,但是如果需要:
需要增加新的代理类,这会导致系统中类的个数大大增加。
这是静态代理最大的缺点,为了减少系统中类的个数,可以采用动态代理。
动态代理可以让系统根据实际需要动态创建代理类,同一个代理类可以代理多个不同的真实主题类,而且可以代理不同方法,在Java中实现动态代理需要Proxy
类以及InvocationHandler
接口。
Proxy
Proxy
类提供了用于创建动态代理类和实例对象的方法,最常用的方法包括:
public static Class> getProxy(ClassLoader loader,Class> ... interfaces)
:该方法返回一个Class
类型的代理类,在参数中需要提供类加载器并指定代理的接口数组,这个数组应该与真实主题类的接口列表一致public staitc Object newProxyInstance(ClassLoader loader,Class> [] interfaces,InvocationHandler h)
:返回一个动态创建的代理类实例,第一个参数是类加载器,第二个参数表示代理类实现的接口列表,同理与真实主题的接口列表一致,第三个参数表示h
所指派的调用处理程序类InvocationHandler
InvocationHandler
接口是代理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用者(也就是实现了InvocationHandler
的类),该接口中声明以下方法:
public Object invoke(Object proxy,Method method,Object [] args)
:该方法用于处理对代理类实例的方法调用并返回相应结果,当一个代理实例中的业务方法被调用时自动调用该方法。第一个参数表示代理类的实例,第二个参数表示需要代理的方法,第三个参数表示方法的参数数组动态代理类需要在运行时指定所代理的真实主题类的接口,客户端在调用动态代理对象的方法时,调用请求会自动转发到InvocationHandler
的invoke
方法,由invoke
实现对请求的统一处理。
为一个数据访问Dao层增加方法调用日志,记录每一个方法被调用的时间和结果,使用动态代理模式进行设计。
设计如下:
AbstractUserDao
UserDao1
+UserDao2
DAOLogHandler
Proxy.newInstance()
生成首先设计抽象主题角色:
interface AbstarctUserDao
{
void findUserById(String id);
}
接着创建两个具体类实现该接口:
class UserDao1 implements AbstarctUserDao
{
public void findUserById(String id)
{
System.out.println("1号数据库中查找id" +
("1".equals(id) ? "成功" : "失败"));
}
}
class UserDao2 implements AbstarctUserDao
{
public void findUserById(String id)
{
System.out.println("2号数据库中查找id" +
("2".equals(id) ? "成功" : "失败"));
}
}
接着定义请求处理角色:
class DAOLogHandler implements InvocationHandler
{
private Object object;
public DAOLogHandler(Object object)
{
this.object = object;
}
@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
beforeInvoke();
Object result = method.invoke(object, args);
postInvoke();
return result;
}
private void beforeInvoke()
{
System.out.println("记录时间");
}
private void postInvoke()
{
System.out.println("记录结果");
}
}
核心是实现了InvocationHandler
的invoke
方法,该方法在调用抽象主题角色中的方法时自动转发到该方法处理。
也就是说,假设抽象主题角色有A(),B(),C()
三个方法,当调用A()
时,将调用A()
替换掉里面的Object result = method.invoke(object.args)
,也就是实际上相当调用如下函数:
@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
beforeInvoke();
Object result = A(args);
postInvoke();
return result;
}
当调用B()
时,相当于调用以下函数:
@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
beforeInvoke();
Object result = B(args);
postInvoke();
return result;
}
下面是测试客户端的代码:
public static void main(String[] args)
{
AbstarctUserDao userDao1 = new UserDao1();
AbstarctUserDao proxy = null;
InvocationHandler handler = new DAOLogHandler(userDao1);
proxy = AbstarctUserDao.class.cast(
Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(), new Class[]{AbstarctUserDao.class}, handler)
);
proxy.findUserById("2");
AbstarctUserDao userDao2 = new UserDao2();
handler = new DAOLogHandler(userDao2);
proxy = AbstarctUserDao.class.cast(
Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(),new Class[]{AbstarctUserDao.class},handler)
);
proxy.findUserById("2");
}
proxy = AbstarctUserDao.class.cast(
Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(), new Class[]{AbstarctUserDao.class}, handler)
);
其中cast()
方法相当于是对强制类型转换进行了包装,转换前进行了安全检查。
在Proxy.newInstance()
中,第一个参数是抽象主题角色的类加载器,第二个参数表示抽象主题角色的所有方法都转发请求到第三个参数中的invoke
方法处理。第三个参数是自定义的InvocationHandler
,通过构造方法注入抽象主题角色,目的是提供一个抽象主题角色的引用,调用代理方法时自动调用抽象主题角色的方法。
远程代理是一种常见的代理模式,使得客户端程序可以访问在远程主机(或另一个JVM)上的对象,远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户端完全可以认为被代理的远程业务对象是本地的而不是远程的,远程代理对象承担了大部分的网络通信工作,并负责对远程业务的方法调用。
远程业务对象在本地主机中有一个代理对象,该代理对象负责对远程业务对象的访问和网络通信,它对于客户端而言是透明的。客户端无须关心实现的具体业务是谁,只需要按照服务接口所定义的方式直接与本地主机中的代理对象交互即可。
在Java中可以通过RMI(Remote Method Invocation,远程方法调用)
机制来实现远程代理,它能够实现一个JVM中的对象调用另一个JVM中的对象,下面看一个简单的例子。
这个简单的例子有以下四个类:
Hello
HelloImpl
HelloServer
HelloClient
代码如下:
interface Hello extends Remote
{
String sayHello(String name) throws RemoteException;
}
一个简单的sayHello
方法,注意里面的方法需要声明为抛出RemoteException
。
接着是接口实现类:
public class HelloImpl extends UnicastRemoteObject implements Hello{
public HelloImpl() throws RemoteException
{
super();
}
public String sayHello(String name) throws RemoteException
{
System.out.println("Hello");
return "Hello"+name;
}
}
实现sayHello
方法。
接下来是服务端:
public class HelloServer {
public static void main(String[] args) {
try {
Hello hello = new HelloImpl();
LocateRegistry.createRegistry(8888);
System.setProperty("java.rmi.server.hostname", "127.0.0.1");
Naming.bind("rmi://localhost:8888/hello", hello);
System.out.println("远程绑定对象成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端中首先注册了一个本地端口8888
,接着设置系统属性rmi服务的主机名为本地地址,也就是127.0.0.1
,如果是部署在服务器上修改对应ip即可。下一步是通过Naming
的静态方法bind
绑定该URL到RMI服务器上,并命名为hello
。其中rmi:
(RMI协议)可以省略。
最后是客户端:
public class HelloClient {
public static void main(String[] args) {
try
{
Hello hello = Hello.class.cast(
Naming.lookup("rmi://127.0.0.1:8888/hello")
);
System.out.println(hello.sayHello("111"));
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
客户端通过Naming
的lookup
查找参数URL中对应的远程服务对象hello
,找到后返回并强制转换为Hello
,接着即可调用远程对象的方法sayHello
。
首先运行服务端:
接着启动客户端:
可以看到来自服务端的结果。
对于一直占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理。在真实对象创建成功之前虚拟代理扮
演真实对象的替身,当真实对象创建之后,虚拟代理将用户的请求转发给真实对象。
以下两种情况可以考虑使用虚拟代理:
有一批人找老板谈事情,谈事情之前需要先通过老板的助手进行预约,预约这件事只需要助手完成,真正执行预约列表里面的任务时才需要老板出现,使用虚拟代理模式进行设计。
设计如下:
Approvable
Boss
Assistant
代码如下:
//抽象主题角色
interface Approvable
{
void approve();
}
下一步定义真实主题角色Boss
:
class Boss implements Approvable
{
private List<String> orders = new LinkedList<>();
static
{
System.out.println("\n老板来处理了\n");
}
public Boss(List<String> orders)
{
this.orders = orders;
}
public void addOrder(String order)
{
orders.add(order);
}
@Override
public void approve()
{
while(orders.size() > 0)
{
System.out.println("老板处理了<"+orders.remove(0)+">");
}
}
}
使用List
存储待处理的事件,approve
表示处理所有的事件。
代理主题角色如下:
class Assistant implements Approvable
{
private List<String> orders = new LinkedList<>();
private volatile Boss boss;
public void addOrder(String order)
{
if(boss != null)
{
System.out.println("老板将<"+order+">添加到预约列表");
boss.addOrder(order);
}
else
{
System.out.println("助手将<"+order+">添加到预约列表");
orders.add(order);
}
}
@Override
public void approve()
{
if(boss == null)
{
synchronized(this)
{
if(boss == null)
{
boss = new Boss(orders);
}
}
}
boss.approve();
}
}
在添加事件(addOrder
)函数中,首先判断boss
是否为null
,如果为null
表示还没创建老板
对象,这时让助手添加到预约列表中去,如果不为null
表示已经存在老板
对象,直接交由老板加入预约列表。
对于approve
方法,首先判断boss
是否为null
,不为null
表示老板能直接处理所有事件。为null
表示老板
对象还没有创建,新建一个Boss
并将待处理的事件作为参数注入boss
中。
测试类:
public static void main(String[] args)
{
Assistant assistant = new Assistant();
assistant.addOrder("找老板面试");
assistant.addOrder("找老板借钱");
assistant.addOrder("找老板聊天");
assistant.approve();
assistant.addOrder("找老板吃饭");
assistant.addOrder("找老板喝酒");
assistant.approve();
}
输出如下:
缓存代理为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果,在这里使用缓存代理模式模拟YouTube对使用集成的第三方库下载进行缓存。
设计如下:
ThirdPartyYouTubeLib
+ThirdPartyYouTubeClass
Video
YouTubeCacheProxy
YouTubeDownloader
首先是第三方类库,通常情况下是没有源码实现的,其中ThirdPartyYouTubeLib
是一个接口,并且ThirdPartyYouTubeClass
以及YouTubeCacheProxy
实现了它,也就是说:
ThirdPartyYouTubeLib
是抽象主题角色ThirdPartyYouTubeClass
是真实主题角色YouTubeCacheProxy
是代理主题角色首先定义抽象主题角色:
interface ThirdPartyYouTubeLib
{
HashMap<String,Video> popularVideos();
Video getVideo(String videoId);
}
一个是获取热门视频的方法,一个是根据id获取具体视频的方法。
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib
{
private static final String URL = "https://www.youtube.com";
@Override
public HashMap<String,Video> popularVideos()
{
connectToServer(URL);
return getRandomVideos();
}
@Override
public Video getVideo(String id)
{
connectToServer(URL+id);
return getSomeVideo(id);
}
private int random(int min,int max)
{
return min+(int)(Math.random()*((max-min)+1));
}
private void experienceNetworkLatency()
{
int randomLatency = random(5, 10);
for(int i=0;i<randomLatency;++i)
{
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
private void connectToServer(String url)
{
System.out.println("连接到 " + url + " ...");
experienceNetworkLatency();
System.out.println("连接成功!\n");
}
private HashMap<String,Video> getRandomVideos()
{
System.out.println("正在下载热门视频");
experienceNetworkLatency();
HashMap<String,Video> map = new HashMap<>();
map.put("1111111",new Video("1111","1111.mp4"));
map.put("2222222",new Video("2222","2222.avi"));
map.put("3333333",new Video("3333","3333.mov"));
map.put("4444444",new Video("4444","4444.mkv"));
System.out.println("下载完成!\n");
return map;
}
private Video getSomeVideo(String id)
{
System.out.println("正在下载id为"+id+"的视频");
experienceNetworkLatency();
System.out.println("下载完成!\n");
return new Video(id,"title");
}
}
获取热门视频或者某一个视频时,进行了一个模拟连接到服务器的操作,首先输出提示连接到xxx
,接着模拟了网络延迟,最后提示下载完成并返回相应的视频。
class YouTubeCacheProxy implements ThirdPartyYouTubeLib
{
private ThirdPartyYouTubeLib youtubeService = new ThirdPartyYouTubeClass();
private HashMap<String,Video> cachePopular = new HashMap<>();
private HashMap<String,Video> cacheAll = new HashMap<>();
@Override
public HashMap<String,Video> popularVideos()
{
if(cachePopular.isEmpty())
{
cachePopular = youtubeService.popularVideos();
}
else
{
System.out.println("从缓存检索中热门视频");
}
return cachePopular;
}
@Override
public Video getVideo(String id)
{
Video video = cacheAll.get(id);
if(video == null)
{
video = youtubeService.getVideo(id);
cacheAll.put(id,video);
}
else
{
System.out.println("从缓存中检索id为"+id+"的视频");
}
return video;
}
public void reset()
{
cachePopular.clear();
cacheAll.clear();
}
}
这里的缓存代理角色其实就是在调用真实主题角色的获取视频方法之前,首先判断是否存在缓存,存在的话直接从缓存中获取,不存在的话首先调用获取视频方法并存储在缓存中,下次获取时从缓存中获取。
class Video
{
private String id;
private String title;
private String data;
public Video(String id,String title)
{
this.id = id;
this.title = title;
}
//getter+setter...
}
class YouTubeDownloader
{
private ThirdPartyYouTubeLib api;
public YouTubeDownloader(ThirdPartyYouTubeLib api)
{
this.api = api;
}
public boolean useCacheProxy()
{
return api instanceof YouTubeCacheProxy;
}
public void renderVideoPage(String id)
{
Video video = api.getVideo(id);
System.out.println("\n-------------------------------------------");
System.out.println("ID:"+video.getId());
System.out.println("标题:"+video.getTitle());
System.out.println("数据:"+video.getData());
System.out.println("\n-------------------------------------------");
}
public void renderPopularVideos()
{
HashMap<String,Video> list = api.popularVideos();
System.out.println("\n-------------------------------------------");
System.out.println("热门视频");
list.forEach((k,v)->System.out.println("ID:"+v.getId()+"\t标题:"+v.getTitle()));
System.out.println("\n-------------------------------------------");
}
}
public class Test
{
public static void main(String[] args)
{
YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());
long navie = test(naiveDownloader);
long smart = test(smartDownloader);
System.out.println("缓存代理节约的时间:"+(navie-smart)+"ms");
}
private static long test(YouTubeDownloader downloader)
{
long startTime = System.currentTimeMillis();
downloader.renderPopularVideos();
downloader.renderVideoPage("1111");
downloader.renderPopularVideos();
downloader.renderVideoPage("2222");
downloader.renderVideoPage("3333");
downloader.renderVideoPage("4444");
long estimatedTime = System.currentTimeMillis() - startTime;
System.out.println(downloader.useCacheProxy() ? "使用缓存运行时间:" : "不使用缓存运行时间:");
System.out.println(estimatedTime+"ms\n");
return estimatedTime;
}
}
模拟了两个下载器,一个使用原生下载,一个使用缓存代理下载,输出如下:
连接到 https://www.youtube.com ...
连接成功!
正在下载热门视频
下载完成!
-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4
-------------------------------------------
连接到 https://www.youtube.com1111 ...
连接成功!
正在下载id为1111的视频
下载完成!
-------------------------------------------
ID:1111
标题:title
数据:null
-------------------------------------------
连接到 https://www.youtube.com ...
连接成功!
正在下载热门视频
下载完成!
-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4
-------------------------------------------
连接到 https://www.youtube.com2222 ...
连接成功!
正在下载id为2222的视频
下载完成!
-------------------------------------------
ID:2222
标题:title
数据:null
-------------------------------------------
连接到 https://www.youtube.com3333 ...
连接成功!
正在下载id为3333的视频
下载完成!
-------------------------------------------
ID:3333
标题:title
数据:null
-------------------------------------------
连接到 https://www.youtube.com4444 ...
连接成功!
正在下载id为4444的视频
下载完成!
-------------------------------------------
ID:4444
标题:title
数据:null
-------------------------------------------
不使用缓存运行时间:
9312ms
连接到 https://www.youtube.com ...
连接成功!
正在下载热门视频
下载完成!
-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4
-------------------------------------------
连接到 https://www.youtube.com1111 ...
连接成功!
正在下载id为1111的视频
下载完成!
-------------------------------------------
ID:1111
标题:title
数据:null
-------------------------------------------
从缓存检索中热门视频
-------------------------------------------
热门视频
ID:4444 标题:4444.mkv
ID:2222 标题:2222.avi
ID:3333 标题:3333.mov
ID:1111 标题:1111.mp4
-------------------------------------------
连接到 https://www.youtube.com2222 ...
连接成功!
正在下载id为2222的视频
下载完成!
-------------------------------------------
ID:2222
标题:title
数据:null
-------------------------------------------
连接到 https://www.youtube.com3333 ...
连接成功!
正在下载id为3333的视频
下载完成!
-------------------------------------------
ID:3333
标题:title
数据:null
-------------------------------------------
连接到 https://www.youtube.com4444 ...
连接成功!
正在下载id为4444的视频
下载完成!
-------------------------------------------
ID:4444
标题:title
数据:null
-------------------------------------------
使用缓存运行时间:
7611ms
缓存代理节约的时间:1701ms
可以看到缓存代理是能节省时间的,除了第一次获取视频外,随后的获取视频都是从缓存中直接提取。
如果觉得文章好看,欢迎点赞。
同时欢迎关注微信公众号:氷泠之路。