代理模式——史上最强分析
代理模式
-
- 代理模式
-
- 一、代理模式的简述
- 二、代理模式的结构
- 三、代理模式的实现
- 四、JDK代理方式
- 五、小结
代理模式
引入:
代理模式是常用的结构性模式之一,当无法直接访问某个对象或访问某个对象困难时,我们可以通过代理对象间接访问,为了保证客户端使用的透明性,我们所被代理对象与代理对象将会实现一个共同的接口。
根据代理模式的使用目的不同,代理模式又可以分为保护代理、远程代理、虚拟代理、缓冲代理等,需求不一样应用场景不同。
在本章节可以学习到代理模式的定义和结构,并且理解代理模式的多种类型的作用以及实现原理。
一、代理模式的简述
为什么需要"代理"呢,因为在面向对象编程时,被代理对象可能任务太多了,此时就需要一个助手去帮助他完成一系列事情。
例如:在电子商务的这个背景下,就衍生出一个代购的概念,也就是找人来帮忙购买用品,肯定需要向代购人支付一定的费用。
可能"本人(被代理对象)"事情比较繁忙,这种事让"代理"对象去实现,就减少了"本人"的工作量。
代理模式的定义:
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
Proxy Pattern : Provide a surrogate or placeholder for another object to control access to it
简单来说:代理对象起到的是一个中介作用,去掉客户不能看到的内容和服务,或者添加一些新的功能。
二、代理模式的结构
代理模式的结构比较简单,代理模式包含三个角色:
- Subject(抽象主题角色) 声明真实对象和代理对象的共同接口,保证在任意地方使用真实对象的地方都可以使用代理对象。
- Proxy(代理主题角色) 包含对真实对象的引用,从而可以在任意情况下操作真实对象,在真实对象的基础上还可以增加一些新的功能,不仅仅当真实对象的调用者。
- RealSubject(真实主题角色) 它定义了代理对象所代表的真实对象,在真实对象中实现了真实的业务操作,客户端可以通过代理对象间接调用真实对象中定义的操作。
三、代理模式的实现
提前了解相关概念:
- 透明性:代理对象和真实对象都实现了共同的接口,因此在测试中完全不必关注调用的究竟是代理对象还是真实对象。无论是直接调用真实对象还是间接对象都可以。在这里代理对象具有"透明性",就像一幅画之间放置了一块玻璃,仍然可以看见画的内容,即使在Main类和Printer之间加入一个PrinterProxy类,也不会有问题
- 代理与委托 : 代理对象只能代理他能解决的问题。当遇到他不能解决的问题时,还是会"转交"给真实对象去解决。这里的转交就是委托。从PrinterProxy类的print方法中调用real.print方法正好体现出这种委托
现在我们看一段实例:
使用Proxy模式(静态代理)
案例:带名字的打印机,将文字信息显示在控制台上即可
名字 | 说明 |
---|---|
Printer | 带名字的打印机类 |
Printable | Printer 和 PrinterProxy共同接口 |
PrinterProxy | 带名字的打印机的类(代理类) |
Main | 测试程序的类 |
public interface Printable { void setPrinterName(String name); //设置名字 String getPrinterName(); //获取名字 void print(String name); //打印信息}
/* 被代理类 */public class Printer implements Printable{ private String name; public Printer() { System.out.println("正在生成实例..."); } public Printer(String name) { this.name = name; heavyJob("生成Printer的实例对象 (" + name + " ) "); } @Override public void setPrinterName(String name) { this.name = name; } @Override public String getPrinterName() { return name; } @Override public void print(String name) { System.out.println(name); } private void heavyJob(String msg){ System.out.println(msg); for (int i = 0; i < 5; i++){ try { //每一次工作 休息一秒 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("结束..."); }}
/* 代理类 */public class PrinterProxy implements Printable{ private String name ; //名字 private Printer real; //被代理对象 真实对象 public PrinterProxy() { } public PrinterProxy(String name) { this.name = name; } @Override public synchronized void setPrinterName(String name) { //设置名字 this.name = name; if (real != null){ this.setPrinterName(name); //同时设置真实对象名字 } } @Override public String getPrinterName() { return name; } @Override public void print(String name) { realize(); real.print(name); } private synchronized void realize(){ if (real == null){ real = new Printer(name); } }}
public class Main { public static void main(String[] args) { Printable p = new PrinterProxy("惠普打印机"); System.out.println("现在的名字是 : " + p.getPrinterName()); p.setPrinterName("不知名的打印机"); System.out.println("现在的名字是 : " + p.getPrinterName()); p.print("Hello World"); //调用该方法时 才生成Printer类实例 并调用带参构造器初始化对象 }
详解PrinterProxy类
是一个代理类,实现Printable接口。
name字段中保存了打印机的名字,而real字段保存的是 “被代理人”
不论setPrinterName和getPrinterName方法被调用多少次,都不会生成Printer对象。只有当真正需要真实对象,才会生成Printer实例。
realize方法很简单 当real字段为空时 会使用new Printer来生成Printer类实例。反之
最重要的是,PrinterProxy知道Printer类的存在,Printer并不知晓PrinterProxy的存在。也就是说Printer类不知道自己是被直接调用还是间接调用(PrinterProxy调用)。
四、JDK代理方式
在上述,传统的代理模式中通过客户端通过代理类调用封装好的方法print()的这种方法。如果按这种方法使用代理模式,很显然,代理类和真实主题类都应该是事先存在,代理类的接口和所代理的方法都已明确指定。每一个代理类在经过编译生成一个字节码文件,代理类所实现的接口和方法都是固定的,这样的代理方式称为静态代理
现在我们将介绍动态代理——JDK方式
动态代理:
根据实际需求来动态的创建代理类,同一个代理类能够代理多个真实主题类,并且可以代理不同的方法。尤其注意的是在SpringAOP中就采用了这种方式来实现。
介绍JDK当中相关的类和接口:
1、Proxy类
这个类提供了创建动态代理类和实例对象的方法,它是创建动态代理类的父类,常用的方法有:
a、public static Class getProxyClass(ClassLoader loader,Class… interfaces):该方法用于返回一个Class类型的代理类
在参数中需要指定代理的接口数组
b、public static Object newProxyInstance(ClassLoader loader,Class[]… interfaces,InvocationHandler h):该方法用于返回一个动态创建的代理类实例,方法中第一个参数表示代理类的类加载器,第二个参数interfaces表示代理类所实现的接口列表,第三个参数表示调用处理的程序类
2、InvocationHandler接口
这个接口表示的是处理程序类的实现接口,该接口作为代理对象的调用处理的共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(该接口的子类)。该接口的声明了如下方法:
public Object invoke(Object proxy,Method moethod,Object[] args)
该方法用于处理对代理类实例的方法调用并返回相应的结果,当一个代理实例中的业务方法被调用时 自动调用该方法。invoke方法包含三个参数,其中第一个参数proxy表示代理类的实例,第二个参数mehtod表示需要代理的方法,第三个参数args表示代理方法的参数数组
动态代理需要在运行时指定所代理真实主题类的接口,客户端调用动态代理对象的方法时调用请求会将请求转发给InvocationHandler对象invoke()方法,由invoke()方法来实现对请求的统一处理。
实例:
类(接口) | 描述 |
---|---|
UserDao | 抽象主题角色 |
DocumentDao | 抽象主题角色 |
UserDaoImpl | 具体主题角色 |
DocumentDaoImpl | 具体主题角色 |
DaoLogHandler | 自定义请求处理程序类 |
Client | 客户端测试类 |
public interface UserDao { Boolean findUserById(String id);}
/* 具体主题角色 */public class UserDaoImpl implements UserDao{ @Override public Boolean findUserById(String id) { if (id.equalsIgnoreCase("张三")){ System.out.println("查询ID为:" + id); return true; }else { System.out.println("查询失败"); return false; } }}
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Calendar;import java.util.GregorianCalendar;/* 自定义请求处理程序类 */public class DaoLogHandler implements InvocationHandler { private Calendar calendar; private Object object; public DaoLogHandler() { } //有参构造 注入一个需要提供代理的真实主题对象 public DaoLogHandler(Object object){ this.object = object; } //实现invoke方法 调用在真实主题类中定义的方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { beforeInvoke(); Object obj = method.invoke(object, args); //转发调用 afterInvoke(); return obj; } //记录方法调用事件 public void beforeInvoke(){ calendar = new GregorianCalendar(); int hour = calendar.get(Calendar.HOUR_OF_DAY); int minute = calendar.get(Calendar.MINUTE); int second = calendar.get(Calendar.SECOND); String time = hour + ":" + minute + ":"+second; System.out.println("调用时间:" + time); } public void afterInvoke(){ System.out.println("调用结束"); }}
/* 文档接口 抽象主题类 */public interface DocumentDao { Boolean deleteDocumentById(String id);}
/* 具体文档类 具体主题角色 */public class DocumentDaoImpl implements DocumentDao{ @Override public Boolean deleteDocumentById(String id) { if (id.equalsIgnoreCase("001")){ System.out.println("删除ID是: " + id); return true; }else { System.out.println("删除失败"); return false; } }}
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;public class Client { public static void main(String[] args) { InvocationHandler handler = null; UserDao dao = new UserDaoImpl(); handler = new DaoLogHandler(dao); UserDao proxy = null; //动态创建对象 代理UserDao类型的真实对象 proxy = (UserDao) Proxy.newProxyInstance (UserDao.class.getClassLoader(),new Class[] {UserDao.class},handler); proxy.findUserById("张三"); System.out.println("------------------------"); DocumentDao documentDao = new DocumentDaoImpl(); handler = new DaoLogHandler(documentDao); DocumentDao proxy_new = null; proxy_new = (DocumentDao) Proxy.newProxyInstance( DocumentDao.class.getClassLoader(),new Class[] {DocumentDao.class},handler); proxy_new.deleteDocumentById("1"); //调用代理对象的业务方法 }}
五、小结
通过动态代理可以实现对多个真实主题类的统一代理和控制。
JDK中提供的动态代理只能代理一个或多个接口,如果需要动态代理具体类或抽象类,我们可以使用CGLib(Code Generation Library)工具。这是一个功能强大、性能和质量比较好的代码生成包。
面向接口的时候我们选择JDK的方式实现动态代理
面向类选择CGLib的方式。