设计模式——代理模式
在阎宏博士的《JAVA与模式》一书中开头是这样描述代理(Proxy)模式的:代理模式是对象的结构模式。代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。
在代理模式(Proxy Pattern)中,由于客户端无法直接或者不想直接引用或使用一个对象,所以通过“中间件”起到代理目标对象功能的作用,为其他对象提供一种代理以控制对这个对象的访问。
代理模式的角色:
- 抽象主题类(Subject):该类或接口负责定义具体目标类和代理类之间的共同接口方法;
- 具体主题类(ConcreteSubject):该类也称为被代理类或被委托类,定义了一个真实的对象,最终执行代理类的代理方法中的业务逻辑,客户端通过代理类间接调用真实主题类中定义的方法;
- 代理类(ProxySubject):该类也被称为委托类或代理类,该类持有一个对真实主题类的引用,在它的方法中调用与真实主题类对应的接口方法;
Java的三种代理模式
静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。
实例场景
在生活中我们打官司找律师,买房找中介这些场景中都有代理的身影,这里我们以买房子找中介来模拟代理模式。
抽象主题类和具体主题类
/**
* 定义代理类和被代理的接口
* @author Iflytek_dsw
*
*/
interface IUserSubject {
public void buyHouse();
}
class NormalUser implements IUserSubject{
@Override
public void buyHouse() {
System.out.println("普通人想买房子");
}
}
创建客户端
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
IUserSubject userSubject = new NormalUser();
NormalUserProxy proxy = new NormalUserProxy(userSubject);
proxy.buyHouse();
}
}
静态代理总结:
- 有点:可以做到在不修改目标对象的功能前提下,对目标功能扩展。
- 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多,同时,一旦接口增加方法,目标对象与代理对象都要维护.
静态代理是最简单的代理表现形式,它的UML图跟装饰者模式的比较像,但是表达的含义用途是完全不一样的:
- 装饰者模式的核心是动态的扩展功能,是在Decorator类中持有一个具体组件的引用,然后动态扩展具体组件的功能。
- 代理模式是代理具体主题的功能,即具体主题把功能实现好,代理只是负责调用,不做补充修改。
- 客户端使用都是持有一个抽象组件,然后指向对应的装饰者或代理来进行使用。
动态代理
动态代理是指在运行时动态生成代理类。即代理类的字节码将在运行时生成并载入当前代理的 ClassLoader。在JDK中通过Proxy类进行实现,Proxy类提供了静态的方法用于动态创建代理类和实例。代理实例是代理类的一个实例。 每个代理实例都有一个关联的调用处理程序对象,即实现接口 InvocationHandler的自定义实例对象。
Proxy类
供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。
- static InvocationHandler getInvocationHandler(Object proxy) :返回指定代理实例的调用处理程序。
- static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces):返回代理类的 java.lang.Class 对象,并向其提供类加载器和接口数组。
- static boolean isProxyClass(Class<?> cl):当且仅当指定的类通过 getProxyClass 方法或 newProxyInstance 方法动态生成为代理类时,返回 true。
- static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
参数说明
- ClassLoader loader:指定当前目标对象使用类加载器,获取加载器的方法是固定的
- Class<?>[] interfaces:目标对象实现的接口的类型,使用泛型方式确认类型
- InvocationHandler handler:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
InvocationHandler
每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke 方法。
动态代理是不需要定义代理角色的,通过一个处理器来处理代理角色的业务逻辑。动态代理有以下特点:
- 代理对象,不需要实现接口
- 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
- 动态代理也叫做:JDK代理,接口代理
实例场景
在数据库连接中,我们都是通过系统封装好的Driver类来进行数据库的操作。这里我以此来模拟一下。
抽象角色接口类
/**
* 定义接口
* @author Iflytek_dsw
*
*/
interface DBDriver{
public boolean connect();
public boolean insert();
public boolean delete();
}
在抽象角色接口类中,我们定义了对应的角色接口。
实现接口,具体角色类
/**
* 数据库管理
* @author Iflytek_dsw
*
*/
class DBDriverManager implements DBDriver{
@Override
public boolean connect(){
System.out.println("实际操作者:连接数据库");
return true;
}
@Override
public boolean insert(){
System.out.println("实际操作者:插入数据库");
return true;
}
@Override
public boolean delete() {
System.out.println("实际操作者:删除数据库");
return true;
}
}
代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理。动态就体现在用接口来动态指向上。
定义处理器对象
class MyinvocationHandler implements InvocationHandler{
private DBDriver dbDriver;
public MyinvocationHandler(DBDriver dbDriver){
this.dbDriver = dbDriver;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if("connect".equals(methodName)){
System.out.println("通过代理调用方法:" + methodName);
dbDriver.connect();
}else if("insert".equals(methodName)){
System.out.println("通过代理调用方法:" + methodName);
dbDriver.connect();
}else if("delete".equals(methodName)){
System.out.println("通过代理调用方法:" + methodName);
dbDriver.connect();
}
return Boolean.TRUE;
}
}
定义一个工厂方法用于创建Proxy类
这里有两种创建方式:
- 直接通过newProxyInstance方法进行创建;
- 通过getProxyClass获取对应的Class对象,然后在通过反射获取对应的实例,绑定处理器完成。
class DynamicProxyFactory{
public static Object getProxyInstance(Object object){
Class proxy = Proxy.getProxyClass(object.getClass().getClassLoader(),
object.getClass().getInterfaces());
try {
//通过反射获取构造函数,然后构建处理器
return proxy.getConstructor(new Class[] { InvocationHandler.class })
.newInstance(new MyinvocationHandler((DBDriver)object));
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
return null;
}
public static Object getProxyInstance2(Object object){
return Proxy.newProxyInstance(object.getClass().getClassLoader(),
object.getClass().getInterfaces(), new MyinvocationHandler((DBDriver)object));
}
}
客户端使用
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
DBDriver dbDriver = new DBDriverManager();
DBDriver flextProxy = (DBDriver) DynamicProxyFactory.getProxyInstance(dbDriver);
flextProxy.connect();
flextProxy.insert();
System.out.println("简化方式\n");
DBDriver simpleProxy = (DBDriver) DynamicProxyFactory.getProxyInstance2(dbDriver);
simpleProxy.connect();
simpleProxy.delete();
}
}
结果
通过代理调用方法:connect
实际操作者:连接数据库
通过代理调用方法:insert
实际操作者:连接数据库
简化方式
通过代理调用方法:connect
实际操作者:连接数据库
通过代理调用方法:delete
实际操作者:连接数据库
动态代理和静态代理对比
- 不需要为真实主题写一个形式上完全一样的代理类,假如主题接口中的方法很多,为每一个接口写一个代理方法也很麻烦。如果接口有变动,则真实主题和代理类都要修改,不利于系统维护。
- 使用一些动态代理的生成方法甚至可以在运行时制定代理类的执行逻辑,从而大大提升系统的灵活性。比如过滤数据。
动态代理类使用字节码动态生成加载技术,在运行时生成加载类。生成动态代理类的方法很多,如,JDK 自带的动态处理、CGLIB、Javassist 或者 ASM 库。JDK 的动态代理使用简单,它内置在 JDK 中,因此不需要引入第三方 Jar 包,但相对功能比较弱。CGLIB 和 Javassist 都是高级的字节码生成库,总体性能比 JDK 自带的动态代理好,而且功能十分强大。ASM 是低级的字节码生成工具,使用 ASM 已经近乎于在使用 Java bytecode 编程,对开发人员要求最高,当然,也是性能最好的一种动态代理生成工具。但 ASM 的使用很繁琐,而且性能也没有数量级的提升,与 CGLIB 等高级字节码生成工具相比,ASM 程序的维护性较差,如果不是在对性能有苛刻要求的场合,还是推荐 CGLIB 或者 Javassist。(引用自【周明耀-代理模式原理及实例讲解】)
代理模式的使用场景
远程代理
为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。使服务器端Server实现对客户端的隐藏,以便Server可以忽略服务器端。
虚拟代理
是根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象,并在需要的时候进行加载。
保护代理
通过代理模式来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;
只能引用
在访问原始对象时执行一些额外的附加操作并对指向原始数据计数。
总结
设计模式是前人的工作总结经验,灵活运用,针对某一类型的教优解决方案。