Loading

设计模式——代理模式


平时我们使用的框架中非常频繁地使用了动态代理。可以说如果一个框架不使用动态代理的话,这个框架不太可能做成一个通用的框架。因此动态代理的知识是我们必须要掌握的知识。

本博客是网络转载的,原文请点击这里


代理模式简介

Proxy代理模式是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题。(某些对象只想提供部分方法让外部对象访问)

代理模式是一种常用的结构型设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息(beforeAdvice),过滤消息并转发消息,以及进行消息被委托类执行后的后续处理(afterAdvice

为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性

更通俗的说,代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可,而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但是切记,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法。

使用场景

  • 方法增强(添加注解实现一些额外的功能)
  • AOP

代理模式分类

按照代理的创建时期,代理类可以分为两种:

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。(代理类在程序编译前就已经存在)
  • 动态代理:代理类在程序运行时运用反射机制动态创建而成。(代理类运行时动态生成)

下面分别用静态代理与动态代理演示一个示例:添加打印日志的功能,即每个方法调用之前和调用之后写入日志。

静态代理

具体用户管理实现类——UserManager接口相当于Subject角色,UserManagerImpl相当于RealSubject角色

public class UserManagerImpl implements UserManager {
 
	@Override
	public void addUser(String userId, String userName) {
		System.out.println("UserManagerImpl.addUser");
	}
 
	@Override
	public void delUser(String userId) {
		System.out.println("UserManagerImpl.delUser");
	}
 
	@Override
	public String findUser(String userId) {
		System.out.println("UserManagerImpl.findUser");
		return "张三";
	}
 
	@Override
	public void modifyUser(String userId, String userName) {
		System.out.println("UserManagerImpl.modifyUser");
 
	}
}

代理用户管理实现类——UserManagerImplProxy相当于Proxy角色

public class UserManagerImplProxy implements UserManager {
 
	// 目标对象
	private UserManager userManager;
	// 通过构造方法传入目标对象
	public UserManagerImplProxy(UserManager userManager){
		this.userManager=userManager;
	}
	@Override
	public void addUser(String userId, String userName) {
		try{
				//添加打印日志的功能
				//开始添加用户
				System.out.println("start-->addUser()");
				userManager.addUser(userId, userName);
				//添加用户成功
				System.out.println("success-->addUser()");
			}catch(Exception e){
				//添加用户失败
				System.out.println("error-->addUser()");
			}
	}
 
	@Override
	public void delUser(String userId) {
		userManager.delUser(userId);
	}
 
	@Override
	public String findUser(String userId) {
		userManager.findUser(userId);
		return "张三";
	}
 
	@Override
	public void modifyUser(String userId, String userName) {
		userManager.modifyUser(userId,userName);
	}
 
}

客户端调用

public class Client {
 
	public static void main(String[] args){
		//UserManager userManager=new UserManagerImpl();
		UserManager userManager=new UserManagerImplProxy(new UserManagerImpl());
		userManager.addUser("1111", "张三");
	}
}

静态代理类优缺点

优点

代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合),对于如上的客户端代码,newUserManagerImpl()可以应用工厂将它隐藏,如上只是举个例子而已。

缺点

  • 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
  • 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。如上的代码是只为UserManager类的访问提供了代理,但是如果还要为其他类如Department类提供代理的话,就需要我们再次添加代理Department的代理类。

举例说明:代理可以对实现类进行统一的管理,如在调用具体实现类之前,需要打印日志等信息,这样我们只需要添加一个代理类,在代理类中添加打印日志的功能,然后调用实现类,这样就避免了修改具体实现类。满足我们所说的开闭原则。但是如果想让每个实现类都添加打印日志的功能的话,就需要添加多个代理类,以及代理类中各个方法都需要添加打印日志功能(如上的代理方法中删除,修改,以及查询都需要添加上打印日志的功能)

即静态代理类只能为特定的接口(Service)服务。如想要为多个接口服务则需要建立很多个代理类。

动态代理

根据如上的介绍,你会发现在静态代理中每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类。所以我们就会想办法可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理。(一个代理类服务于多个委托对象)

在上面的示例中,一个代理只能代理一种类型,而且是在编译期就已经确定被代理的对象。而动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象

在Java中要想实现动态代理机制,需要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy 类的支持。

java.lang.reflect.InvocationHandler接口的定义如下:

//Object proxy:被代理的对象
//Method method:要调用的方法
//Object[] args:方法调用时所需要参数
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

java.lang.reflect.Proxy类的定义如下:

//CLassLoader loader:类的加载器
//Class<?> interfaces:得到全部的接口
//InvocationHandler h:得到InvocationHandler接口的子类的实例
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

具体实现——相当于Subject角色

public class UserManagerImpl implements UserManager {
 
	@Override
	public void addUser(String userId, String userName) {
		System.out.println("UserManagerImpl.addUser");
	}
 
	@Override
	public void delUser(String userId) {
		System.out.println("UserManagerImpl.delUser");
	}
 
	@Override
	public String findUser(String userId) {
		System.out.println("UserManagerImpl.findUser");
		return "张三";
	}
 
	@Override
	public void modifyUser(String userId, String userName) {
		System.out.println("UserManagerImpl.modifyUser");
 
	}
 
}

动态创建代理对象的类——相当于Proxy角色

public class LogHandler implements InvocationHandler {
 
	// 目标对象
	private Object targetObject;
	//绑定关系,也就是关联到哪个接口(与具体的实现类绑定)的哪些方法将被调用时,执行invoke方法。            
	public Object newProxyInstance(Object targetObject){
		this.targetObject=targetObject;
		//该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例  
		//第一个参数指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
		//第二个参数要实现和目标对象一样的接口,所以只需要拿到目标对象的实现接口
		//第三个参数表明这些被拦截的方法在被拦截时需要执行哪个InvocationHandler的invoke方法
		//根据传入的目标返回一个代理对象
		return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
				targetObject.getClass().getInterfaces(),this);
	}
	@Override
	//关联的这个实现类的方法被调用时将被执行
	/*InvocationHandler接口的方法,proxy表示代理,method表示原对象被调用的方法,args表示方法的参数*/
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		System.out.println("start-->>");
		for(int i=0;i<args.length;i++){
			System.out.println(args[i]);
		}
		Object ret=null;
		try{
			/*原对象方法调用前处理日志信息*/
			System.out.println("satrt-->>");
			
			//调用目标方法
			ret=method.invoke(targetObject, args);
			/*原对象方法调用后处理日志信息*/
			System.out.println("success-->>");
		}catch(Exception e){
			e.printStackTrace();
			System.out.println("error-->>");
			throw e;
		}
		return ret;
	}
 
}

被代理对象targetObject通过参数传递进来,我们通过targetObject.getClass().getClassLoader()获取ClassLoader对象,然后通过targetObject.getClass().getInterfaces()获取它实现的所有接口,然后将targetObject包装到实现了InvocationHandler接口的LogHandler对象中。通过newProxyInstance函数我们就获得了一个动态代理对象。

客户端代码

public class Client {
 
	public static void main(String[] args){
		LogHandler logHandler=new LogHandler();
		UserManager userManager=(UserManager)logHandler.newProxyInstance(new UserManagerImpl());
		//UserManager userManager=new UserManagerImpl();
		userManager.addUser("1111", "张三");
	}
}

可以看到,我们可以通过LogHandler代理不同类型的对象,如果我们把对外的接口都通过动态代理来实现,那么所有的函数调用最终都会经过invoke函数的转发,因此我们就可以在这里做一些自己想做的操作,比如日志系统、事务、拦截器、权限控制等。这也就是AOP(面向切面编程)的基本原理。

AOP(Aspect Oriented Programming):将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码——解耦。

针对如上的示例解释:

我们来看上面的UserManagerImplProxy类,它的两个方法System.out.println("start-->addUser()")和System.out.println("success-->addUser()"),这是做核心动作之前和之后的两个截取段,正是这两个截取段,却是我们AOP的基础,在OOP里,System.out.println("start-->addUser()")、核心动作、System.out.println("success-->addUser()")这个三个动作在多个类里始终在一起,但他们所要完成的逻辑却是不同的,如System.out.println("start-->addUser()")里做的可能是权限的判断,在所有类中它都是做权限判断,而在每个类里核心动作却各不相同,System.out.println("success-->addUser()")可能做的是日志,在所有类里它都做日志。正是因为在所有的类里,核心代码之前的操作和核心代码之后的操作都做的是同样的逻辑,因此我们需要将它们提取出来,单独分析,设计和编码,这就是我们的AOP思想。一句话说,AOP只是在对OOP的基础上进行进一步抽象,使我们的类的职责更加单一。

动态代理优点:

动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强

JDK动态代理类的一些说明

下面是一个动态代理类的反编译代码:

package com.sun.proxy;
import com.mikan.proxy.HelloWorld;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements HelloWorld{
  privatestatic Method m1;
  privatestatic Method m3;
  privatestatic Method m0;
  privatestatic Method m2;
  public $Proxy0(InvocationHandler paramInvocationHandler) {
    super(paramInvocationHandler);
  }
  public final boolean equals(Object paramObject){
    try {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    }
    catch (Error|RuntimeException localError) {
      throw localError;
    }
    catch (Throwable localThrowable) {
      thrownew UndeclaredThrowableException(localThrowable);
    }
  }
  public final void sayHello(String paramString){
    try {
      this.h.invoke(this, m3, new Object[] { paramString });
      return;
    }
    catch (Error|RuntimeException localError) {
      throw localError;
    }
    catch (Throwable localThrowable) {
      thrownew UndeclaredThrowableException(localThrowable);
    }
  }
  public final int hashCode(){
    try {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    }
    catch (Error|RuntimeException localError) {
      throw localError;
    }
    catch (Throwable localThrowable) {
      thrownew UndeclaredThrowableException(localThrowable);
    }
  }
  public final String toString(){
    try {
      return (String)this.h.invoke(this, m2, null);
    }
    catch (Error|RuntimeException localError) {
      throw localError;
    }
    catch (Throwable localThrowable) {
      thrownew UndeclaredThrowableException(localThrowable);
    }
  }
  static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("com.mikan.proxy.HelloWorld").getMethod("sayHello", new Class[] { Class.forName("java.lang.String") });
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException) {
      thrownew NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException) {
      thrownew NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
}

可以看到,动态生成的代理类有如下特性:

  • 继承了Proxy类,实现了代理的接口,由于java不能多继承,这里已经继承了Proxy类了,不能再继承其他的类,所以JDK的动态代理不支持对类的代理,只支持接口的代理。

  • 提供了一个使用InvocationHandler作为参数的构造方法。

  • 生成静态代码块来初始化接口中方法的Method对象,以及Object类的equals、hashCode、toString方法。

  • 重写了Object类的equals、hashCode、toString,它们都只是简单的调用了InvocationHandler的invoke方法,即可以对其进行特殊的操作,也就是说JDK的动态代理还可以代理上述三个方法。

  • 代理类实现代理接口的sayHello方法中,只是简单的调用了InvocationHandler的invoke方法,我们可以在invoke方法中进行一些特殊操作,甚至不调用实现的方法,直接返回。

    至此JDK动态代理的实现原理就分析的差不多了。同时我们可以想像一下Spring AOP提供的各种拦截该如何实现,就已经很明了了,如下所示:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // BeforeAdvice
        Object retVal = null;
        try {
            // AroundAdvice
            retVal = method.invoke(target, args);
            // AroundAdvice
            // AfterReturningAdvice
        }
        catch (Throwable e) {
            // AfterThrowingAdvice
        }
        finally {
            // AfterAdvice
        }
        return retVal;
    }

上面是对于Spring AOP使用JDK动态代理实现的基本框架代码,当然具体的实现肯定比这个复杂得多,但是基本原理不外乎如是。所以理解基本原理对于理解其他的代码也是很有好处的。

动态代理实现的几种方式

  • JDK动态代理(对接口进行代理)
  • cglib动态代理(可以对类进行代理)

代理模式和其他模式的对比

如果你还学过其他的设计模式,你肯定会发现代理模式和适配器模式、装饰器模式很像。下面来讲下这几种模式的区别。

1. 装饰模式和代理模式的区别
装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话 说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

2. 外观模式和代理模式的区别
代理与外观的主要区别在于,代理对象代表一个单一对象而外观对象代表一个子系统,代理的客户对象无法直接访问对象,由代理提供单独的目标对象的访问,而通常外观对象提供对子系统各元件功能的简化的共同层次的调用接口。代理是一种原来对象的代表,其他需要与这个对象打交道的操作都是和这个代表交涉的。

3. 适配器模式和代理模式的区别
适配器模式改变所考虑的对象的接口,代理模式不能改变所代理对象的接口。

适配器模式偏重的是适配,即实现要适配的接口功能。装饰模式偏重的是对已有对象的功能扩展。而代理模式则偏重的是访问逻辑的控制,因此通常这个代理的构建过程是不直接由client控制的。

简单总结

代理模式最大的有点就是可以有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性

缺点就是需要设计代理类,一定程度上增加了系统设计的复杂性。

代理模式可以分为静态代理和动态代理。静态代理的通用型较差,一般在开发实践中使用动态代理比较多。

动态代理的实现方式有JDK动态代理和cglib动态代理。JDK动态代理的缺陷是只能代理接口(因为Java只能单继承,JDK动态代理生成的代理类已经继承了Proxy类,所以不能再继承其他类)

代理模式和适配器模式、装饰器比较像。适配器模式更加注重于接口的适配转换、装饰器模式更加注重于对象功能的增强扩展,而代理模式更加注重于对象的访问控制。

以上。

参考

posted @ 2020-03-16 14:13  程序员自由之路  阅读(991)  评论(0编辑  收藏  举报