问题的提出
现在生活中,常常在微信朋友圈里面看到代购的信息,你想在国外买什么,香港买什么,但是又懒得自己过去,于是常常委托别人帮忙买奶粉买那啥的。这类问题的缘由是因为客户和原产地没有直接的接触,所以需要一个代理(代购)的第三者来实现间接引用。代理对象可以在客户端和目标对象间起到中介作用,而且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。代理模式是一种很好实现客户对象与代理对象分离的策略。其抽象UML图如下图
代理模式包含如下角色
ISubject:抽象主题角色,是一个接口。该接口是对象和它的代理公用的接口。
RealSubject:真实的主题角色,是实现主题接口的类。
Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以操作真实对象。代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。
买电视为例
1)定义抽象主题-买电视
public interface ITV { public void buyTV(); }
2)定义实际主题-买电视过程
public class Buyer implements ITV { public void buyTV(){ System.out.println("I have bought the TV by buyer proxy"); } }
3)定义代理
public class BuyerProxy implements ITV { private Buyer buyer; public BuyerProxy(Buyer buyer){ this.buyer = buyer; } public void buyTV(){ preProcess(); buyer.buyTV(); postProcess(); } public void preProcess(){ //询问客户需要的电视类型,价位等信息 } public void postProcess(){ //负责把电视送到客户家(售后服务) } }
在这里,一开始看到这样的设计的时候。会有这样的疑问,代理和实际主题为什么都要实现相同的接口呢?而且接口方法的实现完全一样。当从代码段分析,确实感到很累赘。但是,结合实际想想,你需要代理者帮你买东西,那作为代理者,理所因当必须要有和你一样的功能:买东西!代理对象提供与真实对象相同的接口,以便在任何时刻都能替代真实对象。
代理模式最突出的特点:代理角色与实际主题角色有相同的父类接口。常用的代理方式有4类:虚拟代理,远程代理,计数代理,动态代理。
下面分别简单介绍一下这四种代理模式
一.虚拟代理
虚拟代理的关键思想是:如果需要创建的对象资源消耗较大,那就先创建一个消耗相对比较小的对象表示。真正完整的对象只有在需要时才会被创建。当用户请求一个“大”的对象时,虚拟代理在该对象真正被创建出来之前扮演着替身的角色;当对象被创建出来后,虚拟代理将用户请求直接委托给对象。
看下面这个例子,高校本科生科研信息查询功能设计
实验室数据库表字段说明
可以看出,第4,5个字段都是大段的文字,如果直接列出申请所有项目信息,一方面会花费比较多时间,另一方面界面也难设计,因为前三个字段较短,后面较长。良好的查询策略应该分为二级查询,第一级查询显示“账号,姓名,项目名称”三个字段,第二级查询时当鼠标选中表中某一个具体项目时,再进行一次数据库查询,得到项目完整信息。
Mysql数据表简单设计
create table project( account varchar(20), name varchar(20), project varchar(20), content varchar(20), plan varchar(20) )
1)定义抽象主题接口IItem
public interface IItem { String getAccount(); void setAccount(String s); String getName(); void setName(String s); String getProject(); void setProject(String s); String getContent(); String getPlan(); void itemFill() throws Exception; }
注意这里content和plan字段只有getter方法,表明这两个字段的信息仅仅执行第二级查询时候才填充完整,由itemFill()方法完成
2)具体实现主题类RealItem
import BuildModel.Example.DbProc; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; /** * Created by lenovo on 2017/4/21. */ public class RealItem implements IItem { private String account; private String name; private String project; private String content; private String plan; public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getProject() { return project; } public void setProject(String project) { this.project = project; } public String getContent() { return content; } public String getPlan() { return plan; } public void itemFill() throws Exception{ //填充本项目content以及plan字段 第二级查询 System.out.println(account); String strSQL = "select content,plan from project"; DbProc dbProc = new DbProc(); Connection conn = dbProc.connect(); Statement stm = conn.createStatement(); ResultSet rst = stm.executeQuery(strSQL); rst.next(); content = rst.getString("content"); //填充content字段内容 plan = rst.getString("plan"); //填充plan字段 rst.close(); stm.close(); conn.close(); } }
ItemFill方法描述第二级查询时如何获得content,plan字段具体内容的过程,但是什么时候调用,怎么调用是由代理类完成。
3)代理主题类ProxyItem
public class ProxyItem implements IItem{ private RealItem item; boolean bFill = false ; public ProxyItem(RealItem item){ this.item = item; } public String getAccount() { return item.getAccount(); } public void setAccount(String s) { item.setAccount(s); } public String getName() { return item.getName(); } public void setName(String s) { item.setName(s); } public String getProject() { return item.getProject(); } public void setProject(String s) { item.setProject(s); } public String getContent() { return item.getContent(); } public String getPlan() { return item.getPlan(); } public void itemFill() throws Exception { if(!bFill){ item.itemFill(); } bFill = true; } }
初始bFil为false,调用一次item.itemFill方法后,完成对该字段的填充,最后把bFill置为true,所以无论调用代理对象itemFill多少次,只执行一次具体主题类对象的itemFill方法一次。
4)代理项目集合类ManageItems
public class ManageItems { Vector<ProxyItem> v = new Vector(); //代理项目集合 public void firstSearch() throws Exception{ String strSQL = "select account ,name ,project from project" ;//第一级查询SQL语句 DbProc dbProc = new DbProc(); Connection conn = dbProc.connect(); Statement stm = conn.createStatement(); ResultSet rst = stm.executeQuery(strSQL); //获得第一级查询集合 while(rst.next()){ ProxyItem obj = new ProxyItem(new RealItem()); obj.setAccount(rst.getString("account")); obj.setName(rst.getString("name")); obj.setProject(rst.getString("project")); v.add(obj); } rst.close(); stm.close(); conn.close(); } }
对于数据库应用来说,往往是对记的集合进行操作。若要用到代理模式,一般需要一个代理基恩类如ProxyItem,代理集合管理类如ManageItems。
5)界面显示以及消息映射类UFrame
package BridgeModel.Proxy.virtualProxy; import javax.swing.*; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.Vector; /** * Created by lenovo on 2017/4/21. */ public class UFrame extends JFrame implements MouseListener { ManageItems manage = new ManageItems(); JTable table; JTextArea t = new JTextArea(); JTextArea t2 = new JTextArea(); public void init() throws Exception{ setLayout(null); manage.firstSearch(); String title[] = {"账号","姓名","项目名称"}; String data[][] = null; Vector<ProxyItem> v = manage.v; data = new String[v.size()][title.length]; for(int i=0;i<v.size();i++){ ProxyItem proxyItem = v.get(i); data[i][0] = proxyItem.getAccount(); data[i][1] = proxyItem.getName(); data[i][2] = proxyItem.getProject(); } table = new JTable(data,title); JScrollPane pane = new JScrollPane(table); pane.setBounds(10,10,200,340); JLabel label = new JLabel("项目主要内容"); JLabel label2 = new JLabel("计划安排"); label.setBounds(230,5,100,20); t.setBounds(230,40,200,100); label2.setBounds(230,160,100,20); t2.setBounds(230,195,200,100); add(pane); add(label); add(t); add(label2); add(t2); table.addMouseListener(this); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(500,350); setVisible(true); } public void mouseClicked(MouseEvent event){ //进行二级查询 try{ int n = table.getSelectedRow(); if(n>=0){ ProxyItem item = manage.v.get(n); item.itemFill(); t.setText(item.getContent()); t2.setText(item.getPlan()); } } catch (Exception ex){ ex.printStackTrace(); } } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public static void main(String[] args) throws Exception{ new UFrame().init(); } }
6)数据库处理封装类
public class DbProc { private String strDriver = "com.mysql.jdbc.Driver"; private String strDb = "jdbc:mysql://localhost:3306/buildModel"; private String strUser = "root"; private String strPwd = "root"; //注意测试时候strPwd要加上自己本地mysql的账户密码 private Connection conn; public Connection connect() throws Exception{ Class.forName(strDriver); conn = DriverManager.getConnection(strDb,strUser,strPwd); return conn; } public int executeUpdate(String strSQL) throws Exception{ Statement stm = conn.createStatement(); int n = stm.executeUpdate(strSQL); stm.close(); return n; } public List executeQuery(String strSQL) throws Exception{ List l = new Vector(); Statement stm = conn.createStatement(); ResultSet rst = stm.executeQuery(strSQL); ResultSetMetaData rsmd = rst.getMetaData(); while(rst.next()){ Vector unit = new Vector(); for(int i=1;i<=rsmd.getColumnCount();i++){ unit.add(rst.getString(i)); } l.add(unit); } return l; } public void close() throws Exception{ conn.close(); } }
二.远程代理
远程代理的含义是:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以在另一台主机中。也就是说,远程对象驻留于服务器上,客户机请求调用远程对象调用相应方法,执行完毕后,结果由服务器返回给客户端。基本框架如下
虚拟代理一般有一个代理,而远程代理包括两个代理:客户端通信代理与服务器端通信代理,编程时需要考虑这两部分。实际上JDK已经提供了远程服务器端通信代理,如RMI(Remote Method Invocation)远程方法调用。这意味着我们可以不用考虑远程代理编码,只需要编写普通代码即可。
为了更好地理解远程代理程序作用,下面编写一个简单的RMI代理模拟系统,客户端输入字符串数学表达式,只含+,-运算。服务器计算表达式值并返回给客户端。
1)创建服务器工程
1.定义抽象主题远程接口ICalc
public interface ICalc { float calc(String s) throws Exception; }
2.定义具体远程主题实现ServerCalc
public class ServerCalc implements ICalc { public float calc(String s) throws Exception{ s += "+0"; float result = 0; float value = 0; char opcur = '+'; char opnext; int start = 0; if(s.charAt(0)=='-'){ opcur = '-'; start = 1; } for(int i=start;i<s.length();i++){ if(s.charAt(i)=='+' || s.charAt(i)=='-'){ opnext = s.charAt(i); value = Float.parseFloat(s.substring(start,i)); switch (opcur){ case '+': result += value; break; case '-': result -= value; break; } start = i+1; opcur = opnext; i = start; } } return result; } }
3.定义服务器端远程代理类ServerProxy
import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; /** * Created by lenovo on 2017/4/21. */ public class ServerProxy extends Thread { ServerCalc obj; public ServerProxy(ServerCalc obj){ this.obj = obj; } public void run(){ try{ ServerSocket s = new ServerSocket(4000); Socket socket = s.accept(); while(socket != null){ ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); String method = (String)in.readObject(); if(method.equals("calc")){ String para = (String)in.readObject(); float f = obj.calc(para); out.writeObject(new Float(f)); out.flush(); } } }catch (Exception ex){ ex.printStackTrace(); } } }
一般服务器一定是支持多线程的。所以ServerProxy从Thread派生。RMI服务器段程序是通过网络通信,所以用Socket接口。Run中封装了远程代理功能。当客户端申请远程执行calc(String expression)方法时,in首先先读取方法名method,若method为字符串“calc”,in继续读取网络获得表达式expression。然后调用ServerCalc类中的calc()方法,计算expression的value,最后传回给客户端。
2)创建客户端工程RmiClientSimu
1.定义抽象主题远程接口ICalc (这里和Server的ICalc一样,就不再列出代码了)
2.定义服务器远程代理类ClientProxy
import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; /** * Created by lenovo on 2017/4/21. */ public class ClientProxy implements ICalc { Socket socket; public ClientProxy() throws Exception{ socket = new Socket("localhost",4000); } public float calc(String expression) throws Exception{ ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); out.writeObject("calc"); out.writeObject(expression); Float value = (Float)in.readObject(); return value.floatValue(); } }
测试代码
public class ClientTest { public static void main(String[] args) throws Exception{ ICalc obj = new ClientProxy(); float value = obj.calc("23+2+3"); System.out.println("value = " + value); } }
public class ServerTest { public static void main(String[] args) { ServerCalc obj = new ServerCalc(); ServerProxy spobj = new ServerProxy(obj); spobj.start(); } }
先运行ServerTest,同时再运行ClientTest,再Client端窗口就会得到服务器端返回来的计算结果 value=28!
有人或许会迷惑,那这样与直接处理从客户端获得表达式求解再发回去有什么区别?当然,这个例子可能不太能体现远程代理的好处。但是,我们打个比方,你寄快递的时候,你只需要给包裹给快递站并签好快递单就好了,根本不需要去了解快递公司是用什么路线,通过什么方式去寄过去,只要能到达目的地就好了对吧?远程代理的作用其实好比于快递公司!这样就理解了对吧。至于JDK提供的RMI接口,这里就不再详叙了。
今天的节目就到这里,感谢大家收看!
下回预告:计数代理与动态代理