菜鸟笔记
提升您的技术认知

JAVA设计模式--代理模式(静态)

阅读 : 79

一、什么是代理模式

代理(Proxy)模式为其他对象提供一种代理以控制对这个对象的访问。(原文:The Proxy Pattern Provide a surrogate or placeholder for another object to control access to it.)

代理模式的作用:在客户端和被代理对象之间起到中介作用,通过代理可以有效地控制客户端对被代理对象的直接访问,进而可以很好地隐藏和保护被代理对象。

代理模式按照其代理类生成方式的不同又可以分为静态代理和动态代理两种,本章先跟大家来聊聊静态代理。

二、静态代理模式的结构

代理模式是一种结构型设计模式,在代理模式中,代理与被代理的对象通常要实现相同的接口,以便在需要的时候可以使用代理来替代被代理对象,客户端必须通过代理与被代理对象进行交互,并且可以在交互的过程中(交互前后)添加一些额外的功能。

静态代理模式的UML结构示意图如下。

静态代理模式涉及的角色及其职责:

抽象主题(Subject)角色:这是一个抽象角色,通常被定义为接口,真实主题角色和代理主题角色都需实现此接口,以便将来可以使用代理主题对象来替代真实主题对象。

真实主题(RealSubject)角色:也叫被代理角色,是代理主题角色所代表的真实对象,是业务逻辑的实际执行者。

代理主题(Proxy)角色:也叫代理角色,该角色内部含有对真实主题对象的引用,从而可以操作真实主题对象,同时代理对象提供与真实主题对象相同的接口以便在任何时刻都能代替真实主题对象。通常代理角色会在将客户端调用传递给真实主题对象之前或者之后执行某些操作,而不是单纯地将调用传递给真实主题对象。

静态代理模式结构示意源代码如下:

抽象主题(Subject)角色

public interface Subject {

	// 声明真实主题角色和代理主题角色都需实现的方法
	public void operation();

}

真实主题(RealSubject)角色

public class RealSubject implements Subject {

	//实现了抽象主题角色定义的接口
	@Override
	public void operation() {
		System.out.println("调用真实主题对象执行操作进行中...");
	}

}

代理主题(Proxy)角色

public class Proxy implements Subject {

	//包含一个真实主题对象的引用
	private Subject realSubject;

	public Proxy(Subject realSubject) {
		this.realSubject = realSubject;
	}

	public void before() {
		System.out.println("调用真实主题对象之前进行的相关操作...");
	}

	public void after() {
		System.out.println("调用真实主题对象之后进行的相关操作...");
	}

	//通过调用内部真实主题对象的引用实现了抽象主题角色定义的接口,并添加了一些额外处理功能
	@Override
	public void operation() {
		before();
		realSubject.operation();
		after();
	}

}

在MainClass中测一下:

public class MainClass {

	public static void main(String[] args) {
		Subject realSubject=new RealSubject(); 
		Proxy proxy= new Proxy(realSubject);
		proxy.operation();
	}

}

运行程序打印结果如下:

调用真实主题对象之前进行的相关操作...
调用真实主题对象执行操作进行中...
调用真实主题对象之后进行的相关操作...

从上面的例子可以看出代理对象将客户端的调用委派给被代理对象,并在调用被代理对象的方法之前和之后执行了一些特殊操作,此种模式与装饰模式有点相似,两者的区别在于:代理模式的目的在于控制访问,装饰模式的目的在于添加功能。

PS:不理解代理模式与装饰模式之间区别?没关系,待我们讲完接下来数据库连接池的例子,相信你一定会有所领悟。

三、静态代理模式应用举例

在软件设计中,使用代理模式的意图很多,比如因为安全原因需要屏蔽客户端直接访问真实对象,或者在远程调用中需要使用代理类处理远程方法调用的技术细节 (如 RMI),也可能为了提升系统性能,对真实对象进行封装,从而达到延迟加载的目的。

应用一

我们都知道,对于一个简单的数据库应用,由于对于数据库的访问不是很频繁。这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销。但是对于一个复杂的数据库应用,情况就完全不同了,频繁的建立、关闭数据库连接,会极大的降低系统的性能,此时,对于连接的使用就成了系统性能的瓶颈。

由于存在以上弊病,我们一般都是通过使用数据库连接池来解决连接问题,即创造一堆等待被使用的连接,等到用的时候就从连接池里取一个,不用了再放回去,数据库连接在整个应用启动期间,几乎是不关闭的。

但是在程序员编写程序的时候,会经常使用connection.close()这样的方法,去关闭数据库连接,而且这样做是对的,所以你并不能告诉程序员们说,你们使用连接都不要关了,去调用一个其他的类似方法将连接重新归还给连接池吧。这是不符合程序员的编程思维的,也很勉强,而且具有风险性,因为程序员会忘的。

解决这一问题的办法就是使用代理模式,因为通过代理可以有效控制客户端对被代理对象的直接访问,客户端只能通过代理与被代理对象进行交互,这样代理可以很轻易对被代理对象的行为进行扩展或者修改,而我们在这里要做的就是替换掉connection的close()方法。

为清晰起见,我在此重新创建了一个连接接口名为IConnection,它包含query()和close()两个方法。 

public interface IConnection {

	// 执行一条查询语句
	void query();

	// 关闭连接
	void close();
}

接下来写一个实现的IConnection接口的ImpConnection类,它扮演了代理模式中的真实主题角色。

public class ImpConnection implements IConnection {

	@Override
	public void query() {
		System.out.println("执行一条查询语句...");
	}

	@Override
	public void close() {
		System.out.println("关闭连接...");
	}
}

接下来我们要写代理类ConnectionProxy,代理类也要实现IConnection这个接口,由于通过继承方式实现代理模式不够灵活,推荐使用组合方式。

public class ConnectionProxy implements IConnection {

	private IConnection iConnection;

	public ConnectionProxy(IConnection iConnection) {
		super();
		this.iConnection = iConnection;
	}

	@Override
	public void query() {
            //对于我们不关心的方法,直接调用真实对象去处理
		iConnection.query();
	}

	@Override
	public void close() {
		// 不真正关闭连接,而是将连接归还给连接池
		ConnectionPool.getInstance().returnConnection(iConnection);
	}

}

接下来是连接池的简单代码

import java.util.LinkedList;

public class ConnectionPool {

	private static LinkedList<IConnection> connectionList = new LinkedList<IConnection>();
	private static IConnection createNewConnection() {
		return new ImpConnection();
	}

	private ConnectionPool() {
		if (connectionList == null || connectionList.size() == 0) {
			for (int i = 0; i < 10;) {
				System.out.println("创建第 " + (++i) + " 个连接...");
				connectionList.add(createNewConnection());
			}
			System.out.println("连接池初始化完成...");
			showConnectionCount();
		}
	}

	public static void showConnectionCount() {
		System.out.println("当前连接池可用连接数为: " + connectionList.size());
	}

	public IConnection getConnection() {
		if (connectionList.size() > 0) {
			// return connectionList.remove();
			// 这是原有的方式,直接返回连接,这样可能会出现程序员把连接给关闭掉的情况
			// 下面是使用代理的方式,程序员再调用close时,就会调用代理的close()方法把连接归还到连接池
			IConnection iConnection = null;
			iConnection = connectionList.remove();
			System.out.println("从连接池获取一条连接...");
			showConnectionCount();
			return new ConnectionProxy(iConnection);
		}
		return null;
	}

	public void returnConnection(IConnection connection) {
		System.out.println("将连接归还给连接池。。。");
		connectionList.add(connection);
		showConnectionCount();
	}

	public static ConnectionPool getInstance() {
		return ConnectionPoolInstance.connectionPool;
	}

	private static class ConnectionPoolInstance {
		private static ConnectionPool connectionPool = new ConnectionPool();
	}

}

在MainClass里面测试一下。

public class MainClass {

	public static void main(String[] args) {
		// 此条语句用来测试单例ConnectionPool是否实现延迟加载
		// 由于未使用连接,此条语句不会导致数据库连接池内连接初始化
		ConnectionPool.showConnectionCount();
		System.out.println("************接下来要从连接池取连接了****************");
		IConnection connection = ConnectionPool.getInstance().getConnection();
		System.out.println("************可以使用连接进行数据库操作了****************");
		// 使用连接执行查询语句
		connection.query();
		connection.query();
		System.out.println("************连接用完了之后,该归还连接了****************");
		connection.close();
	}
}

运行程序结果打印: 

当前连接池可用连接数为: 0
************接下来要从连接池取连接了****************
创建第 1 个连接...
创建第 2 个连接...
创建第 3 个连接...
创建第 4 个连接...
创建第 5 个连接...
创建第 6 个连接...
创建第 7 个连接...
创建第 8 个连接...
创建第 9 个连接...
创建第 10 个连接...

连接池初始化完成...
当前连接池可用连接数为: 10

从连接池获取一条连接...
当前连接池可用连接数为: 9

************可以使用连接进行数据库操作了****************
执行一条查询语句...
执行一条查询语句...

************连接用完了之后,该归还连接了****************
将连接归还给连接池。。。

当前连接池可用连接数为: 10

 好了,这下我们的连接池返回的连接全是代理,就算程序员调用了close方法也只会把连接归还给连接池了。如果我们使用连接池getConnection()方法中被注释掉的语句 " return connectionList.remove(); " ,直接返回连接对象,那么以上MainClass中*连接用完了之后*连接池中的可用连接数将变为:9,即连接被真正关闭了。

我们使用代理模式解决了上述问题,从静态代理的使用上来看,我们一般是这么做的。

1,代理类一般要持有一个被代理的对象的引用。

2,对于我们不关心的方法,全部委托给被代理的对象处理。

3,我们只处理我们关心的方法。

应用二

以上示例使用单例模式实现了ConnectionPool对象的延迟加载,示例稍微有点复杂,接下来我们再以一个简单的示例来感受一下使用代理模式实现延迟加载的方法及其意义。

延迟加载(lazy load)(也称为懒加载)是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才去执行数据加载操作。延迟加载的实际应用很多:比如使用浏览器打开网站,浏览器一般总是先加载文本,待文本显示出来以后再加载图片、视频等耗时较多的资源,这样做的好处是用户不必非要等到所有东西都下载完成以后才能看到页面,可以极大地减少用户的等待时间;又比如Windows操作系统的启动过程,若我们将系统的所有服务项都设置为开机启动,那么我们每次启动计算机都需要等待非常长的时间,所以我们一般都是将系统必须的服务和一些常用服务设置为开机启动,对于那些不常用的服务我们只在用到的时候再去开启,这样做可以减少每次开机时系统启动所需时间,提高系统的启动速度。

接下来该引出我们的第二个例子了:

假设某客户端软件有根据用户请求去数据库查询数据的功能,在查询数据前,需要获得数据库连接,如果我们在软件开启时就去初始化并获得数据库连接,会存在以下弊端:软件开启时需初始化系统的所有类,如果系统还有大量类似资源耗费较多的操作(比如 XML 解析,本地或远程文件资源加载)也需在此时进行初始化,所有这些初始化操作的叠加会使得系统的启动速度变得非常缓慢。为此,我们使用代理模式的代理类对数据库查询类的初始化操作进行封装,当系统启动时,初始化这个代理类,而非真实的数据库查询类,由于此时代理类什么都没有做,因此,它的构造是相当迅速的。在用户真正做查询操作时再由代理类单独去加载真实的数据库查询类,完成用户的请求,这样就使用代理模式实现了延迟加载。

源代码如下:

定义一个接口 IConnection 作为抽象主题角色,该接口用来定义代理类和真实主题类需要对外提供的服务,本例只在其中定义了一个用来进行数据库查询的 query()方法。

public interface IConnection {
	void query();
}

ImpConnection 是真实主题角色,负责实际的业务操作。

public class ImpConnection implements IConnection {

	ImpConnection() {
		try {
			for (int i = 0; i < 3; i++) {
				System.out.println("ImpConnection对象创建中...");
				Thread.sleep(1000);// 假设数据库连接等耗时操作
			}
			System.out.println("ImpConnection对象创建完成...");
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
	}

	@Override
	public void query() {
		System.out.println("执行一次数据库查询操作...");
	}

}

ConnectionProxy 是 ImpConnection 的代理类。

public class ConnectionProxy implements IConnection {

	private ImpConnection connection = null;

	@Override
	public void query() {
	    // 在真正需要的时候才能创建真实对象,创建过程可能很慢
	    if (connection == null) {
			connection = new ImpConnection();
		}
	    connection.query();
	}
}

在MainClass中进行数据库查询测试。

public class MainClass {

	public static void main(String[] args) {
		IConnection connection = new ConnectionProxy(); // 使用代理
		connection.query(); // 在真正使用时才创建真实主题对象
	}
}

运行程序结果打印: 

ImpConnection对象创建中...
ImpConnection对象创建中...
ImpConnection对象创建中...
ImpConnection对象创建完成...
执行一次数据库查询操作...

 将MainClass中的 "connection.query();" 一行删除之后,将再没有任何结果打印到控制台,由此证明:若没有发生请求,将不会创建ImpConnection对象,使用代理模式实现了被代理对象延迟加载的功能。

使用代理模式实现延迟加载的核心思想是:如果当前并没有使用这个组件,则不需要真正地初始化它,使用一个代理对象替代它的原有的位置,只在真正需要的时候才对它进行加载。使用代理模式的延迟加载是非常有意义的,首先,它可以在时间轴上分散系统压力,尤其在系统启动时,不必完成所有的初始化工作,从而加快启动速度;其次,对很多真实主题而言,在软件启动直到被关闭的整个过程中,可能根本不会被调用,初始化这些数据无疑是一种资源浪费。例如上面这个例子,若系统不使用代理模式,则在启动时就要初始化 ImpConnection 对象,而使用代理模式后,启动时只需要初始化一个轻量级的 ConnectionProxy对象。 

四、代理模式的应用场景

代理模式主要应用于以下几个场景:

-远程代理,也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。比如说 WebService,当我们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理作用的,这样可以让那个客户端程序调用代理解决远程访问的问题;

-虚拟代理,是根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如打开一个网页,这个网页里面包含了大量的文字和图片,但我们可以很快看到文字,但是图片却是一张一张地下载后才能看到,那些未打开的图片框,就是通过虚拟代理来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;

-安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;

-指针引用,是指当调用真实对象时,代理处理另外一些事。比如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问一个对象时附加一些事务处理;

-延迟加载,用代理模式实现延迟加载的一个经典应用就在 Hibernate 框架里面。当 Hibernate 加载实体 bean 时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使用代理拦截原有的 getter 方法,在真正使用对象数据时才去数据库或者其他第三方组件加载实际的数据,从而提升系统性能。

PS:此外还有防火墙代理,智能引用代理,缓存代理,同步代理,复杂隐藏代理,写入时复制代理等等,都有各自特殊的用途。

五、静态代理模式的特点

静态代理的优点:

被代理对象只要和代理类实现了同一接口即可,代理类无须知道被代理对象具体是什么类、怎么做的,而客户端只需知道代理即可,实现了类之间的解耦合。

静态代理的缺点:

代理类和被代理类实现了相同的接口,代理类通过被代理类实现了相同的方法,这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度。

每个代理类只能为一个接口服务,如果程序开发中要使用代理的接口很多的话,必然会产生许多的代理类,造成类膨胀。