在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。
在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。
代理模式定义如下:
代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。 Proxy Pattern: Provide a surrogate or placeholder for another object to control access to it. |
代理模式是一种对象结构型模式。在代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到中介的作用,它去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。
代理模式结构与实现
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层,代理模式结构如图15-2所示
由图15-2可知,代理模式包含如下三个角色:
(1) Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
(2) Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
(3) RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。
模式实现
代理模式的结构图比较简单,但是在真实的使用和实现过程中要复杂很多,特别是代理类的设计和实现。
抽象主题类声明了真实主题类和代理类的公共方法,它可以是接口、抽象类或具体类,客户端针对抽象主题类编程,一致性地对待真实主题和代理主题,典型的抽象主题类代码如下:
package com.wry.patterns.proxy.test;
/**
* 抽象主题
*/
public interface Subject {
void display();
}
真实主题类继承了抽象主题类,提供了业务方法的具体实现,其典型代码如下:
package com.wry.patterns.proxy.test;
/**
* 真实主题
*/
public class RealSubject implements Subject {
@Override
public void display() {
System.out.println("访问真实主题方法...");
}
}
代理类也是抽象主题类的子类,它维持一个对真实主题对象的引用,调用在真实主题中实现的业务方法,在调用时可以在原有业务方法的基础上附加一些新的方法来对功能进行扩充或约束,最简单的代理类实现代码如下:
package com.wry.patterns.proxy.test;
/**
* 代理类
*/
public class Proxy implements Subject {
private RealSubject realSubject;
public void before() {
System.out.println("访问真实主题之前的预处理。");
}
public void after() {
System.out.println("访问真实主题之后的后续处理。");
}
@Override
public void display() {
if (realSubject == null) {
realSubject = new RealSubject();
}
before();
realSubject.display();
after();
}
}
代理模式应用实例
实例说明
某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下: (1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统; (2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。 该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。 |
实例分析及类图
通过分析,可以采用一种间接访问的方式来实现该商务信息查询系统的设计,在客户端对象和信息查询对象之间增加一个代理对象,让代理对象来实现身份验证和日志记录等功能,而无须直接对原有的商务信息查询对象进行修改,如图15-3所示:
在图15-3中,客户端对象通过代理对象间接访问具有商务信息查询功能的真实对象,在代理对象中除了调用真实对象的商务信息查询功能外,还增加了身份验证和日志记录等功能。使用代理模式设计该商务信息查询系统,结构图如图15-4所示。
在图15-4中,业务类AccessValidator用于验证用户身份,业务类Logger用于记录用户查询日志,Searcher充当抽象主题角色,RealSearcher充当真实主题角色,ProxySearcher充当代理主题角色。
实例代码
(1) AccessValidator:身份验证类,业务类,它提供方法Validate()来实现身份验证。
package com.wry.patterns.proxy.staticproxy.type3;
/**
* 身份验证类
*/
public class AccessValidator {
//模拟实现登录验证
public boolean Validate(String userId) {
System.out.println("在数据库中验证用户'" + userId + "'是否是合法用户?");
if (userId.equals("杨过")) {
System.out.println(userId+"登录成功!");
return true;
} else {
System.out.println(userId+"登录失败!");
return false;
}
}
}
(2)Logger:日志记录类,业务类,它提供方法Log()来保存日志。
package com.wry.patterns.proxy.staticproxy.type3;
public class Log {
//模拟实现日志记录
public void Log(String userId) {
System.out.println("更新数据库,用户"+userId+"查询次数加1!" );
}
}
(3) Searcher:抽象查询类,充当抽象主题角色,它声明了DoSearch()方法。
package com.wry.patterns.proxy.staticproxy.type3;
public interface Searcher {
String DoSearch(String userId, String keyword);
}
(4) RealSearcher:具体查询类,充当真实主题角色,它实现查询功能,提供方法DoSearch()来查询信息。
package com.wry.patterns.proxy.staticproxy.type3;
public class RealSearcher implements Searcher {
//模拟查询商务信息
@Override
public String DoSearch(String userId, String keyword) {
System.out.println("用户" + userId + "使用关键词" + keyword + "查询信息!");
return "返回具体内容";
}
}
(5) ProxySearcher:代理查询类,充当代理主题角色,它是查询代理,维持了对RealSearcher对象、AccessValidator对象和Logger对象的引用。
package com.wry.patterns.proxy.staticproxy.type3;
public class ProxySearcher implements Searcher {
private RealSearcher searcher = new RealSearcher(); //维持一个对真实主题的引用
private AccessValidator validator;
private Log logger;
@Override
public String DoSearch(String userId, String keyword) {
//如果身份验证成功,则执行查询
if (this.Validate(userId)) {
String result = searcher.DoSearch(userId, keyword); //调用真实主题对象的查询方法
this.Log(userId); //记录查询日志
return result; //返回查询结果
} else {
return null;
}
}
//创建访问验证对象并调用其Validate()方法实现身份验证
public boolean Validate(String userId) {
validator = new AccessValidator();
return validator.Validate(userId);
}
//创建日志记录对象并调用其Log()方法实现日志记录
public void Log(String userId) {
logger = new Log();
logger.Log(userId);
}
}
(6) Client:客户端测试类
package com.wry.patterns.proxy.staticproxy.type3;
public class Client {
public static void main(String[] args) {
Searcher searcher=new ProxySearcher();
searcher.DoSearch("杨过","玉女心经");
}
}
结果及分析
编译并运行程序,输出结果如下:
在数据库中验证用户'杨过'是否是合法用户? 杨过登录成功! 用户杨过使用关键词玉女心经查询信息! 更新数据库,用户杨过查询次数加1! |
代理模式常见类型
1.静态代理
上边的例子就是采用的静态代理方法
2.动态代理(jdk代理/接口代理)
代理对象不需要实现接口,但是目标对象要实现接口,代理对象的生成是利用JDK的API ,动态的在内存中构建代理对象,所以动态代理模式也叫JDK代理或者接口代理
代理类所在的包:
java.lang.reflect.Proxy
实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
package com.wry.patterns.proxy.dynamic.type1;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 代理工厂创建代理对象
*/
public class ProxyFactory {
//目标对象
private Object tearget;
public ProxyFactory(Object tearget) {
this.tearget = tearget;
}
/**
* 获取代理对象
*
* @return
*/
public Object getProxyInstance() {
/**
* Proxy.newProxyInstance(
* 目标(被代理)对象使用的类加载器,
* 目标(被代理)对象实现的接口集合
* 调用处理器 ,
* )
*/
return Proxy.newProxyInstance(
tearget.getClass().getClassLoader(),
tearget.getClass().getInterfaces(),
new InvocationHandler() {
/**
*
* @param proxy 代理类代理对象(被代理对象)tearget 的真实代理对象
* @param method 代理类代理对象(被代理对象)调用的方法
* @param args 代理类代理对象(被代理对象)调用的方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("jdkAPI代理开始~~~~");
Object invokeObj = method.invoke(tearget, args);
System.out.println("jdkAPI代理结束~~~~");
return invokeObj;
}
}
);
}
}
新建接口类测试动态代理
package com.wry.patterns.proxy.dynamic.type1;
import java.util.List;
/**
* 老师接口
*/
public interface ITearcherDao {
/**
* 抽象的教书发放
*/
void tearch();
/**
* 计算学生数量
*
* @return
*/
int studentNumber();
/**
* 获取学生
*
* @return
*/
List<String> getStudent();
}
创建被代理的真实对象:
package com.wry.patterns.proxy.dynamic.type1;
import java.util.ArrayList;
import java.util.List;
/**
* 被代理的对象的类,实现ITearcherDao接口
*/
public class TearcherDaoImpl implements ITearcherDao {
@Override
public void tearch() {
System.out.println("正在教书··········");
}
@Override
public int studentNumber() {
System.out.println("正在计算学生数量··········");
return 1000;
}
@Override
public List<String> getStudent() {
System.out.println("正在获取学生··········");
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add("学生-" + Math.random() + "_" + (i + 1));
}
return list;
}
}
编写如下客户端测试代码:
package com.wry.patterns.proxy.dynamic.type1;
import java.util.List;
public class Client {
public static void main(String[] args) {
ITearcherDao tearcherDao = new TearcherDaoImpl();
ITearcherDao proxyInstance = (ITearcherDao) new ProxyFactory(tearcherDao).getProxyInstance();
proxyInstance.tearch();
System.out.println("----------------华丽的分割线------------------");
int number = proxyInstance.studentNumber();
System.out.println(number);
System.out.println("----------------华丽的分割线------------------");
List<String> studentList = proxyInstance.getStudent();
for (String s : studentList) {
System.out.println(s);
}
}
}
编译并运行程序,输出结果如下:
jdkAPI代理开始~~~~ 正在教书·········· jdkAPI代理结束~~~~ ----------------华丽的分割线------------------ jdkAPI代理开始~~~~ 正在计算学生数量·········· jdkAPI代理结束~~~~ 1000 ----------------华丽的分割线------------------ jdkAPI代理开始~~~~ 正在获取学生·········· jdkAPI代理结束~~~~ 学生-0.5426372547430357_1 学生-0.15256930488083953_2 学生-0.5521116373597125_3 学生-0.5962266977008467_4 学生-0.5729491648299113_5 学生-0.07191181639169808_6 学生-0.7292621517269671_7 学生-0.48888648384523703_8 学生-0.7545109499592815_9 学生-0.24936641525034553_10 |
3.cglib代理 (子类代理):可以在内存中动态的创建代理对象,而不需要实现借口,属于动态代理
CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate支持它来实现PO(Persistent Object 持久化对象)字节码的动态生成。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
创建真实代理对象:
package com.wry.patterns.proxy.cglib.type1;
import java.util.ArrayList;
import java.util.List;
/**
* 被代理的对象的类 Cglib代理不需要实现接口
*/
public class TearcherDao {
public void tearch() {
System.out.println("正在教书··········");
}
public int studentNumber() {
System.out.println("正在计算学生数量··········");
return 1000;
}
public List<String> getStudent() {
System.out.println("正在获取学生··········");
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add("学生-" + Math.random() + "_" + (i + 1));
}
return list;
}
}
CGLIB实现的代理工厂:
package com.wry.patterns.proxy.cglib.type1;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 代理工厂创建代理对象
*/
public class ProxyFactory implements MethodInterceptor {
//目标对象
private Object tearget;
public ProxyFactory(Object tearget) {
this.tearget = tearget;
}
/**
* 获取代理对象
*
* @return
*/
public Object getProxyInstance() {
//1.创建一个工具类
Enhancer enhancer = new Enhancer();
//2.设置父类
enhancer.setSuperclass(tearget.getClass());
//3.设置回调函数
enhancer.setCallback(this);
//4.创建子类对象,即代理对象
return enhancer.create();
}
/**
* 重写这个方法,会调用目标对象的方法
* 所有生成的代理方法调用此方法,而不是原始方法。
* 原始方法可以通过使用方法对象的法向反射调用,
* 或者使用MethodProxy(更快)。
*
* @param o “this”,代理类代理对象(被代理对象)
* @param method 代理类代理对象(被代理对象)调用的方法
* @param objects 代理类代理对象(被代理对象)参数数组;原语类型被包装
* @param methodProxy 用于调用super(非截获方法);可以调用 根据需要多次
* @throws Throwable 可以抛出任何异常;如果是,则不会调用super方法
* @返回与代理方法的签名兼容的任何值。返回void的方法将忽略此值。
**/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("Cglib代理模式-----开始");
Object invoke = method.invoke(tearget, objects);
System.out.println("Cglib代理模式-----结束");
return invoke;
}
}
编写如下客户端测试代码:
package com.wry.patterns.proxy.cglib.type1;
public class Client {
public static void main(String[] args) {
//创建目标对象
TearcherDao tearcherDao=new TearcherDao();
//获取代理对象,并且将目标对象传递给代理对象
TearcherDao instance = (TearcherDao)new ProxyFactory(tearcherDao).getProxyInstance();
instance.tearch();
}
}
编译并运行程序,输出结果如下:
Cglib代理模式-----开始 正在教书·········· Cglib代理模式-----结束 |
代理模式的应用场景
前面分析了代理模式的结构与特点,现在来分析以下的应用场景。
- 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
- 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
- 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
- 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
- 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
代理模式的优缺点:
优点:
1、代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;
2、代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了保护目标对象的作用
3、还有一个我认为的优点、使用代理类我们就可以不用管委托类方法中的变动,或者我们只需要做少量的调整,比如如果委托类中的某个方法突然改成私有了,那个不使用代理类我们需要在每个此方法调用的地方都要把反射处理才能调用,使用代理之后 我们只需要在代理类中处理就可以了,当然这些操作可以使用包装类做,但是问题就在于我们不知道哪个方法会变成私有,不可能每个方法都写个包装类进行反射处理。
缺点:
1、由于在客户端和真实对象之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢;
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
3、静态代理在委托类变多的情况时会显的非常臃肿,不方便阅读与使用