远程代理
场景描述:我们想要一台远程监视器!
我们可能需要引入一点新概念了,比如远程代理。
远程代理就好比“远程对象的本地代表”。何谓有“远程对象”?这是一种对象,活在不同的Java虚拟机(JVM)堆中(更一般的说法为,在不同的地址空间运行的远程对象)。何谓“本地代表”?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。
客户对象所做的就像是在做远程方法的调用,但其实只是调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信的底层细节。
客户辅助对象不是真正的远程服务,它并不拥有方法逻辑。客户辅助对象会联系服务器,传送方法调用信息(如方法名称、变量等),然后等待服务器返回。
RMI将客户辅助对象称为stub(桩),服务辅助对象称为skeleton(骨架)。
我们试着实现一个使用rmi的远程方法调用
public interface MyRemote extends Remote {
public String sayHello() throws RemoteException;
}
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
public String sayHello(){
return "Sever says, 'Hey'";
}
public MyRemoteImpl() throws RemoteException{}
public static void main(String[] args) {
try{
MyRemote service = new MyRemoteImpl();
Naming.rebind("RemoteHello", service);
}catch (Exception e){
e.printStackTrace();
}
}
}
public class MyRemoteClient {
public static void main(String[] args) {
new MyRemoteClient().go();
}
public void go(){
try {
MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
String s = service.sayHello();
System.out.println(s);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
写完代码后,我们需要执行rmic
命令来生成相应的stub文件。
首先需要将jdk/bin
和jre/bin
加入path
路径中,使rmic
命令可以执行;
接着在MyRemoteImpl上层包(我的为remote_proxy)执行命令rmic remote_proxy.MyRemoteImpl
,就会生成对应的MyRemoteImpl_Stub.class
文件;
然后启动rmiregistry
,之后分别启动MyRemoteImpl和MyRemoteClient。
对于RMI,程序员最常犯的三个错误是:
1). 忘了在启动远程服务之前先启动rmiregistry(要用Naming.rebind()注册服务,rmiregistry必须是运行的)。
2). 忘了让变量的返回值的类型成为可序列化的类型(这种错误无法在编译期发现,只会在运行时发现)。
3). 忘了给客户提供stub类。
如果在远程传输时有不想传输的字段,使用transient关键字,可以告诉JVM不序列化相应的字段。
远程代理是一般代理模式的一种实现,这个模式的变体相当多。
代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。
远程代理控制访问远程对象。
虚拟代理控制访问创建开销大的资源。
保护代理基于权限控制对资源的访问。
远程代理的类图如下
虚拟代理
虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
我们写一个使用虚拟代理的例子,它需要加载开销大的对象(通过网络加载图片)。
public class ImageProxy implements Icon {
ImageIcon imageIcon;
URL imageURL;
Thread retrievalThread;
boolean retrieving = false;
public ImageProxy(URL url){
imageURL = url;
}
public int getIconWidth(){
if (imageIcon != null){
return imageIcon.getIconWidth();
}else {
return 800;
}
}
public int getIconHeight(){
if (imageIcon != null){
return imageIcon.getIconHeight();
}else {
return 600;
}
}
public void paintIcon(final Component c, Graphics g, int x, int y){
if (imageIcon != null){
imageIcon.paintIcon(c, g, x, y);
}else {
g.drawString("Loading CD cover, please wait...", x+300, y+190);
if (!retrieving){
retrieving = true;
retrievalThread = new Thread(new Runnable() {
@Override
public void run() {
try {
imageIcon = new ImageIcon(imageURL, "CD Cover");
c.repaint();
}catch (Exception e){
e.printStackTrace();
}
}
});
retrievalThread.start();
}
}
}
}
public class ImageComponent extends JComponent {
private Icon icon;
public ImageComponent(Icon icon){
this.icon = icon;
}
public void setIcon(Icon icon){
this.icon = icon;
}
public void paintComponent(Graphics g){
super.paintComponent(g);
int w = icon.getIconWidth();
int h = icon.getIconHeight();
int x = (800-w)/2;
int y = (600-h)/2;
icon.paintIcon(this, g, x, y);
}
}
public class ImageProxyTestDrive {
ImageComponent imageComponent;
JFrame frame = new JFrame("CD Cover Viewer");
JMenuBar menuBar;
JMenu menu;
Hashtable cds = new Hashtable();
public static void main(String[] args) throws Exception{
ImageProxyTestDrive testDrive = new ImageProxyTestDrive();
}
public ImageProxyTestDrive() throws Exception{
cds.put("Buddha Bar", "http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
cds.put("Ima", "http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZZ.jpg");
URL initialURL = new URL((String) cds.get("Ima"));
menuBar = new JMenuBar();
menu = new JMenu("Favorite CDs");
menuBar.add(menu);
frame.setJMenuBar(menuBar);
for (Enumeration e = cds.keys(); e.hasMoreElements();){
String name = (String) e.nextElement();
JMenuItem menuItem = new JMenuItem(name);
menu.add(menuItem);
menuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
imageComponent.setIcon(new ImageProxy(getCDUrl(e.getActionCommand())));
frame.repaint();
}
});
}
// 建立框架和菜单
Icon icon = new ImageProxy(initialURL);
imageComponent = new ImageComponent(icon);
frame.getContentPane().add(imageComponent);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
frame.setVisible(true);
}
URL getCDUrl(String name){
try {
return new URL((String) cds.get(name));
}catch (MalformedURLException e){
e.printStackTrace();
return null;
}
}
}
测试一下
在真实的世界中,代理模式有许多变体,这些变体都有共通点:都会将客户对主题(Subject)施加的方法调用拦截下来。
动态代理
接下来我们再看另一个常用变体,保护代理。
什么是保护代理?这是一种根据访问权限决定客户可否访问对象的代理。
而我们将使用动态代理来实现保护代理。
动态代理的类图如下
因为Java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy不是你直接实现的。既然这样的代码不能放在Proxy类中,那么要放在哪里?放在InvocationHandler中。InvocationHandler的工作是响应代理的任何调用。你可以把InvocationHandler想成是代理收到方法调用后,请求做实际工作的对象。
接着我们写一个动态代理的例子来看一下
public interface PersonBean {
String getName();
String getGender();
String getInterests();
int getHotOrNotRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setHotOrNotRating(int rating);
}
public class PersonBeanImpl implements PersonBean {
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getGender() {
return gender;
}
@Override
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String getInterests() {
return interests;
}
@Override
public void setInterests(String interests) {
this.interests = interests;
}
@Override
public int getHotOrNotRating() {
if (ratingCount == 0) return 0;
return (rating/ratingCount);
}
@Override
public void setHotOrNotRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
接着就是动态代理的关键,我们需要实现相应的InvocationHandler
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean person;
public OwnerInvocationHandler(PersonBean person){
this.person = person;
}
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException{
try {
if (method.getName().startsWith("get")){
return method.invoke(person, args);
}else if (method.getName().equals("setHotOrNotRating")){
throw new IllegalAccessException();
}else if (method.getName().startsWith("set")){
return method.invoke(person, args);
}
}catch (InvocationTargetException e){
e.printStackTrace();
}
return null;
}
}
public class NotOwnerInvocationHandler implements InvocationHandler {
PersonBean person;
public NotOwnerInvocationHandler(PersonBean person){
this.person = person;
}
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException{
try {
if (method.getName().startsWith("get")){
return method.invoke(person, args);
}else if (method.getName().equals("setHotOrNotRating")){
return method.invoke(person, args);
}else if (method.getName().startsWith("set")){
throw new IllegalAccessException();
}
}catch (InvocationTargetException e){
e.printStackTrace();
}
return null;
}
}
最后测试一下
public class MatchMakingTestDrive {
public static void main(String[] args) {
MatchMakingTestDrive test = new MatchMakingTestDrive();
test.drive();
}
public MatchMakingTestDrive(){
}
public void drive(){
PersonBean joe = getPersonFromDatabase("Joe JavaBean");
PersonBean ownerProxy = getOwnerProxy(joe);
System.out.println("Name is " + ownerProxy.getName());
ownerProxy.setInterests("bowling, Go");
System.out.println("Interests set from owner proxy");
try {
ownerProxy.setHotOrNotRating(10);
}catch (Exception e){
System.out.println("Can't set rating from owner proxy");
}
System.out.println("Rating is " + ownerProxy.getHotOrNotRating());
System.out.println("\n------------\n");
PersonBean nonOwnerProxy = getNonOwnerProxy(joe);
System.out.println("Name is " + nonOwnerProxy.getName());
try {
nonOwnerProxy.setInterests("what the fuck!");
}catch (Exception e){
System.out.println("Can't set interests from non owner proxy");
}
nonOwnerProxy.setHotOrNotRating(3);
System.out.println("Rating set from non owner proxy");
System.out.println("Rating is " + nonOwnerProxy.getHotOrNotRating());
}
PersonBean getOwnerProxy(PersonBean person){
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person)
);
}
PersonBean getNonOwnerProxy(PersonBean person){
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NotOwnerInvocationHandler(person)
);
}
PersonBean getPersonFromDatabase(String name){
PersonBean person = new PersonBeanImpl();
person.setName(name);
person.setHotOrNotRating(7);
return person;
}
}
结果
动态代理之所以被称为动态,是因为运行时才将它的类创建出来。代码开始执行时,还没有proxy类,它是根据需要从你传入的接口集创建的。而对于传入newProxyInstance()的接口类型是有一些限制的。详情研读javadoc相关文件。
其他代理模式变体:
- 防火墙代理(Firewall Proxy);控制网络资源的访问,保护主题免于“坏客户”的侵害。
- 智能引用代理(Smart Reference Proxy);当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数。
- 缓存代理(Caching Proxy);为开销大的运算结果提供暂时存储:它也允许多个客户共享结果,以减少计算或网络延迟。
- 同步代理(Synchronization Proxy);在多线程的情况下为主题提供安全的访问。
- 复杂隐藏代理(Complexity Hiding Proxy);用来隐藏一个类的复杂集合的复杂度,并进行访问控制。有时候也称为外观代理(Facade Proxy),这不难理解。复杂隐藏代理和外观模式是不一样的,因为代理控制访问,而外观模式只提供另一组接口。
- 写入时复制代理(Copy-On-Write Proxy);用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。
代理在结构上类似装饰者,但是目的不同。装饰者模式为对象加上行为,而代理则是控制访问。
总结:关于代理模式的应用,我们主要介绍了三种变体,远程代理,虚拟代理和动态代理;他们的应用场景分别为:远程代理在想要像本地方法调用那样进行远程方法调用时使用,经常在分布式的实现中看到它的身影;虚拟代理在加载大资源对象时,用代理做预先处理,我们经常看到的网页或者视频资源加载等,就是应用了这种技术;动态代理涉及反射,主要是动态生成对象,并通过权限控制方法时使用。关于代理还有静态代理,动态代理和cglib代理,感兴趣的可以查阅相关文章。