2.3 动态代理

这一节要介绍Java语言支持动态性的另外一个方面,即动态代理(dynamic proxy)机制。这个名称中的“代理”会让人很容易联想到设计模式中的代理模式。实际上,使用动态代理机制,不但可以实现代理模式,还可以实现装饰器和适配器模式。通过使用动态代理,可以在运行时动态创建出同时实现多个Java接口的代理类及其对象实例。当客户代码通过这些被代理的接口来访问其中的方法时,相关的调用信息会被传递给代理中的一个特殊对象进行处理,处理的结果作为方法调用的结果返回。动态代理的这种实现机制,属于代理模式的基本用法。客户代码看到的只是接口,具体的逻辑被封装在代理的实现中。

动态代理机制的强大之处在于可以在运行时动态实现多个接口,而不需要在源代码中通过implements关键词来声明。同时,动态代理把对接口中方法调用的处理逻辑交给开发人员,让开发人员可以灵活处理。通过动态代理可以实现面向方面编程(AOP)中常见的方法拦截功能。

2.3.1 基本使用方式

使用动态代理时只需要理解两个要素即可:第一个是要代理的接口,另外一个是处理接口方法调用的java.lang.reflect.InvocationHandler接口。动态代理只支持对接口提供代理,一般的Java类是不行的。如果要代理的接口不是公开的,那么被代理的接口和创建动态代理的代码必须在同一个包中。在创建动态代理的时候,需要提供InvocationHandler接口的实现,以处理实际的调用。在进行处理的时候可以得到表示实际调用方法的Method对象和调用的实际参数列表。代码清单2-21给出了一个简单的InvocationHandler接口的实现类。InvocationHandler接口只有一个需要实现的方法invoke。当客户代码调用被代理的接口中的方法时,invoke方法就会被调用,而代理对象、所调用方法的Method对象和实际参数列表都会作为invoke方法的参数。在下面invoke方法的实现代码中,只是简单地通过Java的日志API记录下方法调用的相关信息,再调用原始的方法,并返回结果。

代码清单2-21 InvocationHandler接口的实现类的示例


public class LoggingInvocationHandler implements InvocationHandler{

private static final Logger LOGGER=Logger.getLogger(LoggingInvocationHandl er.class);

private Object receiverObject;

public LoggingInvocationHandler(Object object){

this.receiverObject=object;

}

public Object invoke(Object proxy, Method method, Object[]args)throws

Throwable{

LOGGER.log(Level.INFO,"调用方法"+method.getName()+";参数为"+Arrays.deepToString(args));

return method.invoke(receiverObject, args);

}

}


在有了InvocationHandler接口的实现之后,就可以创建和使用动态代理,代码清单2-22给出了一个示例。创建动态代理时需要一个InvocationHandler接口的实现,这里用到的是上面的LoggingInvocationHandler类的实例。动态代理的创建是由java.lang.reflect.Proxy类的静态方法newProxyInstance来完成的。创建时需要提供类加载器实例、被代理的接口列表以及InvocationHandler接口的实现。在创建完成之后,需要通过类型转换把代理对象转换成被代理的某个接口来使用。

代码清单2-22 创建和使用动态代理的示例


public static void useProxy(){

String str="Hello World";

LoggingInvocationHandler handler=new LoggingInvocationHandler(str);

ClassLoader cl=SimpleProxy.class.getClassLoader();

Comparable obj=(Comparable)Proxy.newProxyInstance(cl, new Class[]{Comparable.class},handler);

obj.compareTo("Good");

}


在上面的代码中,当通过代理对象的Comparable接口来调用其中的方法时,这个调用会被传递给LoggingInvocationHandler中的invoke方法。代理对象本身obj、所调用的方法compareTo对应的Method对象,以及实际参数字符串“Good”都会作为参数传递过去。在输出相关的日志信息之后,原始的compareTo方法会被执行。而invoke方法的执行结果则被作为方法调用“obj.compareTo("Good")”的返回结果。

虽然LoggingInvocationHandler类只是简单地记录了日志,并没有改变方法的实际执行,但是实际上,在InvocationHandler接口的invoke方法中可以实现各种各样复杂的逻辑。比如对实际调用的参数进行变换,或是改变实际调用的方法,还可以对调用的返回结果进行修改。开发人员可以根据自己的需要,添加感兴趣的业务逻辑。这实际上就是AOP中常用的方法拦截,即拦截一个方法调用,以在其上附加所需的业务逻辑。InvocationHandler很适合于封装一些横切(cross-cutting)的代码逻辑,包括日志、参数检查与校验、异常处理和返回值归一化等。

一般来说,在创建一个动态代理的InvocationHandler实例的时候,需要把原始的方法调用的接收者对象也传入进去,以方便执行原始的方法调用。这可以在创建InvocationHandler的时候,通过构造方法来传递。在大多数情况下,代理对象只会实现一个Java接口。对于这种情况,可以结合泛型来开发一个通用的工厂方法,以创建代理对象。在代码清单2-23中,工厂方法makeProxy为任何接口及其实现类创建代理。

代码清单2-23 为任何接口及其实现类创建代理的工厂方法


public static<T>T makeProxy(Class<T>intf, final T object){

LoggingInvocationHandler handler=new LoggingInvocationHandler(object);

ClassLoader cl=object.getClass().getClassLoader();

return(T)Proxy.newProxyInstance(cl, new Class<?>[]{intf},handler);

}


上面的通用工厂方法的使用方式如代码清单2-24所示。

代码清单2-24 创建代理对象的工厂方法的使用示例


public static void useGenericProxy(){

String str="Hello World";

Comparable proxy=makeProxy(Comparable.class, str);

proxy.compareTo("Good");

List<String>list=new ArrayList<String>();

list=makeProxy(List.class, list);

list.add("Hello");

}


在这里需要注意的是,通过Proxy.newProxyInstance创建出来的代理对象只能转换成它所实现的接口类型,而不能转换成接口的具体实现类。这是因为动态代理只对接口起作用。

上面的示例代码都只代理了一个接口,如果希望代理多个接口,只需要传入多个接口类即可。所得到的代理对象可以被类型转换成这些接口中的任何一个。如果希望直接代理某个类所实现的所有接口,可以参考代码清单2-25中的做法。代码清单2-25中的proxyAll方法并没有对创建的代理对象进行类型转换,而是直接返回给调用者。这是为了让调用者可以灵活操作,允许它们根据需要转换成不同的接口。比如,如果传入的是String类的对象实例,则调用者可以将其转换成String类所实现的Comparable或是CharSequence接口。

代码清单2-25 代理某个类所实现的所有接口


public static Object proxyAll(final Object object){

LoggingInvocationHandler handler=new LoggingInvocationHandler(object);

ClassLoader cl=object.getClass().getClassLoader();

Class<?>[]interfaces=object.getClass().getInterfaces();

return Proxy.newProxyInstance(cl, interfaces, handler);

}


上面介绍的是通过Proxy.newProxyInstance方法来直接创建动态代理对象,实际上这是一个快速创建代理对象的捷径。还可以通过Proxy.getProxyClass方法来首先获取到代理类。得到的代理类实现了被代理的接口。通过Proxy.newProxyInstance方法得到的代理对象实际上是通过反射API调用代理类的构造方法来得到的。代理类的构造方法只有一个参数,即前面提到的InvocationHandler接口的实现。对于一个Java类,可以通过Proxy.isProxy方法来判断是否为代理类。

当同时代理多个接口时,这些接口在代理类创建时的排列顺序就显得尤为重要。即便是同样的接口,不同的排列顺序所产生的代理类也是不同的。实际上,对于相同排列的接口类型,其对应的代理类只会被创建一次。创建完成之后就会被缓存起来。之后的创建请求得到的是缓存的代理类。强调接口的排列顺序的一个重要原因是,这个顺序会对接口中声明类型相同的方法的选择产生影响。如果多个接口中都存在声明类型相同的方法,那么在调用方法时,排列顺序中最先出现的接口中的方法会被选择。代码清单2-26中给出了被代理的接口中包含声明类型相同的方法的情况。在这里并没有使用Proxy.newProxyInstance方法来直接创建代理对象,而是先通过Proxy.getProxyClass来创建代理类,再使用反射API来创建代理类的对象。

代码清单2-26 被代理的接口中包含声明类型相同的方法的示例


public void proxyMultipleInterfaces()throws Throwable{

List<String>receiverObj=new ArrayList<String>();

ClassLoader cl=MultipleInterfacesProxy.class.getClassLoader();

LoggingInvocationHandler handler=new LoggingInvocationHandler(receiverObj);

Class<?>proxyClass=Proxy.getProxyClass(cl, new Class<?>[]{List.class, Set.class});

Object proxy=proxyClass.getConstructor(new Class[]{InvocationHandler.class});

newInstance(new Object[]{handler});

List list=(List)proxy;

list.add("Hello");

Set set=(Set)proxy;

set.add("World");

}


在上面代码中,代理类代理了java.util.List和java.util.Set两个接口,所以下面两个对代理对象进行类型转换的操作都会成功。而LoggingInvocationHandler中实际调用的接收者receiverObj其实是一个java.util.ArrayList对象,并没有实现Set接口。但是上面代码中的set.add("World")语句并不会出现错误。这是因为创建代理类时,List接口出现在Set接口的前面。当调用add方法的时候,实际上调用的是List接口中的方法,而与转换之后的接口类型Set无关。如果把List和Set接口在创建代理类时的顺序调换一下,再运行代码就会出现错误。因为调换之后实际调用的是Set接口中的add方法,而实际的调用接收者并没有实现Set接口,所以会出现类型错误。

注意 在通过动态代理对象来调用Object类中声明的equals、hashCode和toString等方法的时候,这个调用也会被传递给InvocationHandler中的invoke方法。

动态代理的关键就是上面提到的InvocationHandler接口,通过它可以添加动态的方法调用逻辑。从InvocationHandler的invoke方法可以看出,动态代理在方法调用上额外添加一个新的抽象层次,使开发人员有机会在方法调用发生时和实际的调用执行之间,添加自己的代码逻辑。如果希望根据调用方法的名称和参数的不同,实现不同的逻辑,可以考虑使用动态代理,以减少代码重复。