Java设计模式:代理模式(二)

时间:2021-08-05 16:43:40

承接上文

三.计数代理

计数代理的应用场景是:当客户程序需要在调用服务提供者对象的方法之前或之后执行日志或者计数等额外功能时,就可以用到技术代理模式。计数代理模式并不是把额外操作的代码直接添加到原服务中,而是把它们封装成一个单独的对象,这就是计数代理。

考虑这样一个应用,用计数代理统计图书馆中每天借阅书籍的具体次数。

1.定义书籍基本类Book

public class Book {
private String No;
private String name; public Book(String no, String name) {
No = no;
this.name = name;
} public String getNo() {
return No;
} public void setNo(String no) {
No = no;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}

2.定义抽象主题接口IBorrow

public interface IBorrow {  //借阅过程
boolean borrow();
}

3.定义借阅实现类Borrow

public class Borrow implements IBorrow{
private Book book;
public void setBook(Book book){
this.book = book;
}
public Book getBook(){
return book;
}
public boolean borrow(){
//保存信息到数据库等功能,代码就不写了哈哈哈
return true;
}
}

4.定义借阅代理类BorrowProxy

public class BorrowProxy implements IBorrow {
private Borrow obj;
private Map<String,Integer> map;
public BorrowProxy(Borrow obj){
this.obj = obj;
map = new HashMap();
}
public boolean borrow(){
if(!obj.borrow()){ //借阅失败
return false; //返回
}
Book book = obj.getBook();
Integer i = map.get(book.getNo());
i = (i == null) ? 1 : i+1;
map.put(book.getNo(),i);//保存书号-次数 键值对
return true;
}
public void log() throws Exception{
Set<String> set = map.keySet();
String key = "";
String result = "";
Iterator<String> it = set.iterator();
while(it.hasNext()){
key = it.next();
result += key + "\t" + map.get(key) + "\r\n";
}
Calendar c = Calendar.getInstance();
RandomAccessFile fa = new RandomAccessFile("D:/log.txt","rw");
fa.seek(fa.length());
fa.writeBytes(+c.get(Calendar.YEAR) + "-" + (c.get(Calendar.MONTH) +1) +
"-" + c.get(Calendar.DAY_OF_MONTH) + "\r\n");//记录日志时间
fa.writeBytes(result);
fa.close();
}
}

该代理类定义了Map成员变量map,键是书号,每天借阅次数是值。日志文件中首先存放当前时间,然后保存“书号和借阅次数信息”,一行一条记录。

5.简单测试类

public class Test {
public static void main(String[] args) throws Exception{
Borrow br = new Borrow();
BorrowProxy bp = new BorrowProxy(br); Book book = new Book("1000","计算机应用");
br.setBook(book);
bp.borrow(); book = new Book("1001","计算机应用2");
br.setBook(book);
bp.borrow();
bp.log();
}
}

可以看出示例并没有实现一天记录日志一次,因为从设计思想角度来说,时间控件应该在主框架实现,到了约定时间,调用代理类的log()方法即可。

四.动态代理

上述所说的代理模式都是静态代理,所谓静态代理就是 一个主题类与一个代理类一一对应。而动态代理,则是多个主题对应了一个代理类。它们共享“前处理,后处理”功能,动态调用所需主题,大大减少了程序规模,这就是动态代理的特显。实现动态而代理的关键是反射。程序框架如下图所示

Java设计模式:代理模式(二)

在JAVA设计模式:动态代理(一)提到的RMI代理模式的内容,该功能完全可以由动态代理模式来实现,具体过程如下

1.创建服务器工程URmiServer

1)定义抽象主题远程接口ICalc

public interface ICalc {
float calc(String s) throws Exception;
}

2)定义具体远程主题实现ServerCalc

package BridgeModel.Proxy.dynamicProxy;

/**
* Created by lenovo on 2017/4/22.
*/
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

public class ServerProxy {
public static Map<String,Object> map = new HashMap();
public void registry(String key,Object value){ //远程对象注册功能
map.put(key,value); //注册到HashMap映射中
}
public void process(int socketNO) throws Exception{
ServerSocket s = new ServerSocket(socketNO);
while(true){
Socket socket = s.accept();
if(socket != null){
MySocket ms = new MySocket(socket);
ms.start();
}
}
}
}

该类是远程代理通信功能的核心类。通过registry()方法,把主题对象注册到成员变量map映射中,方便将来获得该对象,并利用反射机制执行该对象的方法。

Process()方法表明ServerProxy是一个多线程类,常规线程负责监听客户端的链接,若有连接,则获得连接并创建MySocket线程对象,启动该线程。

4)MySocket类

public class MySocket extends Thread{
Socket socket;
public MySocket(Socket socket){
this.socket = socket;
}
public Object invoke(String registname,String methodname,Object para[]) throws Exception{
Object obj = ServerProxy.map.get(registname); //获得注册对象
//形成函数参数列表
Class classType = Class.forName(obj.getClass().getName());
Class c[] = new Class[para.length];
for(int i=0;i<para.length;i++){
c[i] = para[i].getClass();
}
Method mt = classType.getMethod(methodname,c);
return mt.invoke(obj,para);
}
public void run(){
while(true){
try{
InputStream ins = socket.getInputStream();
if(ins == null || ins.available() == 0)
continue;
//前处理
ObjectInputStream in = new ObjectInputStream(ins);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
String registname = (String)in.readObject(); //获得远程对象注册名称
String methodname = (String)in.readObject(); //获得远程调用方法名称
Object[] para = (Object[])in.readObject(); //获得远程对象方法参数列表 //动态调用主题对象
Object result = invoke(registname,methodname,para);
//后处理
out.writeObject(result); //将结果返回给客户端
out.flush();
}
catch (Exception ex){
ex.printStackTrace();
}
}
}
}

线程运行run方法中包含了远程代理服务端的主要功能,“前处理”主要是三次读网络,第一次获取远程对象注册名称registname,第二次获得方法名methodname,第三次获得方法参数para,根据获得这三个值,利用反射机制完成“动态主题调用”,主要体现在invoke()方法中,“后处理”负责把结果写回客户端。

对于某功能来说,若使用动态代理,那么“前处理和后处理”功能的划分是最为关键的!

5)简单测试类

public class URmiServer {
public static void main(String[] args) throws Exception{
ServerCalc obj = new ServerCalc();
ServerProxy spoobj = new ServerProxy();
spoobj.registry("calc",obj);
spoobj.process(4000);
}
}

2.创建客户端工程URmiClient

1)定义抽象主题远程接口ICalc

public interface ICalc {
float calc(String s) throws Exception;
}

2)定义客户端计算代理ClientProxy

public class ClientProxy implements ICalc {
ClientComm comm;
public ClientProxy(String IP, int socketNO) throws Exception{
comm = new ClientComm(IP,socketNO);
}
public float calc(String s) throws Exception{
Float result = (Float)comm.invoke("calc","calc",new Object[]{s});
return result.floatValue();
}
}

该类从接口ICalc派生,但calc方法中并没有真正细线求表达式的具体过程。只是把参数负责向远程服务器端传送,由ClientComm类对象具体完成,也就是说发送任意远程方法所需相关参数是由该类完成,是共享的。

public class ClientComm {
Socket socket;
public ClientComm(String IP,int socketNO) throws Exception{
socket = new Socket(IP,socketNO);
}
Object invoke(String registname,String methodname,Object[] para) throws Exception{
//前处理
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
out.writeObject(registname);
out.writeObject(methodname);
out.writeObject(para);
return in.readObject(); //后处理
}
}

Invoke方法中有三个参数,第一个是远程对象注册名称,第二个是方法名称,第三个方法参数,因此向网络写三次,然后读网络,等待返回结果。

3)一个简单测试类

public class URmiClient {
public static void main(String[] args) throws Exception{
ICalc obj = new ClientProxy("localhost",4000);
System.out.println(obj.calc("1+5+10"));
System.out.println(obj.calc("1+5+20"));
}
}

先运行URmiServer,再运行URmiClient,就会得到26和36了!

希望通过上面的demo,读者可以对动态代理有一个初步的理解。当然,这个demo中还有一些缺陷例如客户端执行完没有通知服务器端断开socket通信等。

下面简单再讲讲JDK动态代理

动态代理工具是java.lang.reflect包的一部分。它允许程序创建代理对象。他能实现一个或多个已知接口,并用反射机制动态执行相应主题对象。

下面一用JDK动态代理仿真这样一个应用:有两种渠道接受信件,一种是通过Email方式,另一种是通过传统邮寄方式。对来的信件都要进行登记,现在要求利用代理模式增加功能,对不同的方式来的信件统计。

1.定义抽象主题IRegist

public interface IRegist {
void regist(String msg); //对来的信件进行登记
}

2.定义两个具体主题

public class fromEmail implements IRegist {     //Email信件登记类
public void regist(String msg){
System.out.println("from Email");
}
}
public class fromPost implements IRegist {
public void regist(String msg){
System.out.println("from post");
}
}

3.定义动态代理相关类以及接口

上面以及定义了两个主题类fromEmail,fromPost。如果是静态代理,那么一定需要两个代理类。由于功能相同,如果采用JDK动态代理,那么只需要一个代理类,但是必须严格规范编程。具体步骤如下

1)定义计数实现类CountInvoke

public class CountInvoke implements InvocationHandler {
private int count = 0;
private Object obj;
public CountInvoke(Object obj){
this.obj = obj;
} public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
count ++;
method.invoke(obj,args); //对主题对象obj应用反射技术调用相应方法
return null;
} public int getCount(){
return count;
} }

注意,该类并不是动态代理类,它是动态代理类所调用的接口实现类,因此接口不能随意,有系统指定,接口名为InvocationHandler

Invoke是接口定义的方法,必须实现,该方法完成了代理所需要的功能,包括前处理,利用反射技术调用主题方法,后处理等。一般来说,实现动态代理,主要是完成接口方法invoke的编写。

2)创建代理类GenericProxy

public class GenericProxy {
public static Object createProxy(Object obj, InvocationHandler invokeObj){
Object proxy = Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),invokeObj);
return proxy;
}
}

主要通过createProxy()产生代理对象,该方法有两个参数,第一个参数是obj表示具体代理对象,第二个参数是invokeOBj表示代理调用的接口实现类对象。简单来说,就是为了主题对象obj创建代理对象,并与调用接口invokeobj对象建立关联,以便将来代理对象能调用接口方法。

3)简单测试类Test

public class Test {
public static void main(String[] args) {
IRegist email = new fromEmail();
IRegist post = new fromPost();
CountInvoke emailinvoke = new CountInvoke(email);
CountInvoke postinvoke = new CountInvoke(post); IRegist emailproxy = (IRegist)GenericProxy.createProxy(email,emailinvoke);
IRegist postproxy = (IRegist)GenericProxy.createProxy(post,postinvoke); emailproxy.regist("email1");
postproxy.regist("post1");
System.out.println(emailinvoke.getCount());
System.out.println(postinvoke.getCount());
}
}