2.3.2 使用案例

下面用一个完整的案例来说明动态代理在实际开发中的作用。在实际开发中,我们会遇到的一个具体问题就是程序的版本更新。在开发新版本的时候,一方面要考虑与旧版本的兼容,另一方面又希望能够修复旧版本中存在的一些设计上的问题。动态代理可以帮助平衡这两方面的需求。

比如在程序中有一个接口用来生成显示给用户的问候语。最开始设计的时候,这个接口GreetV1就一个方法greet,它接收2个参数,分别是用户的姓名和性别,如代码清单2-27所示。

代码清单2-27 早期版本的GreetV1接口的定义


public interface GreetV1{

String greet(String name, String gender)throws GreetException;

}


在开发新版本的时候,发现这个接口的设计不太合理,希望把方法的参数改为一个,表示用户名即可,姓名和性别可以通过进一步的查找来完成。另外,也不希望方法抛出受检异常,希望使用更为方便的非受检异常。基于上面的考虑,就有了新的接口定义GreetV2,如代码清单2-28所示。

代码清单2-28 新版本的GreetV2接口的定义


public interface GreetV2{

String greet(String username);

}


这个新接口比旧接口更加简单和实用,同时新定义了相关的非受检异常GreetRuntimeException。以后的代码中都应该使用这个新的接口,同时使用旧接口的代码也要能够继续使用。这就是动态代理可以发挥作用的地方。通过动态代理,可以把实现旧接口GreetV1的对象实例转换成可以通过新接口GreetV2来调用。这样既保证了对新接口的使用,又使旧接口的实现可以继续存在。这实际上是通过动态代理来实现适配器设计模式的。实现这样的动态代理的关键就在于适配两个接口的InvocationHandler的实现,完整的实现如代码清单2-29所示。

代码清单2-29 完成接口适配的InvocationHandler接口的实现


public class GreetAdapter implements InvocationHandler{

private GreetV1 greetV1;

public GreetAdapter(GreetV1 greetV1){

this.greetV1=greetV1;

}

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

String methodName=method.getName();

if("greet".equals(methodName)){

String username=(String)args[0];

String name=findName(username);

String gender=findGender(username);

try{

Method greetMethodV1=GreetV1.class.getMethod(methodName, new Class<?>[]{String.class, String.class});

return greetMethodV1.invoke(greetV1,new Object[]{name, gender});

}catch(InvocationTargetException e){

Throwable cause=e.getCause();

if(cause!=null&&cause instanceof GreetException){

throw new GreetRuntimeException(cause);

}

throw e;

}

}else{

return method.invoke(greetV1,args);

}

}

private String findGender(String username){

return Math.random()>0.5?username:null;

}

private String findName(String username){

return username;

}

}


由于动态代理把GreetV1接口的实现对象适配到GreetV2接口上,因此需要有一个已有的GreetV1接口的对象作为调用的接收者,通过构造方法传递即可实现。在invoke方法中,首先通过检查调用的方法的名称来判断是否为GreetV1中已有的greet方法。这是因为其他方法的调用也会被传入到invoke方法中,而这些方法是不需要被处理的。如果调用的是greet方法,则说明这次调用需要代理给GreetV1接口的实现对象来完成。因为GreetV1和GreetV2接口中的greet方法的参数不匹配,需要先进行转换。在GreetAdapter中使用了两个方法来通过传入的用户名查找对应的姓名和性别。接着通过反射API获取GreetV1接口中的greet方法,再把转换之后的参数传入以进行方法调用,最后返回调用的结果。在调用的时候,GreetV1接口中的greet方法可能会抛出受检异常GreetException,因此需要捕获这个异常,并重新包装成非受检异常RuntimeGreetException之后再次抛出。因为GreetV1的接口实现在参数gender的值为null时会抛出GreetException。这里就通过生成随机数的方式来模拟出错的情况。

对于每一个GreetV1接口的实现,都可以通过一个工厂方法转换成可以通过GreetV2接口来使用的新对象。工厂方法如代码清单2-30所示。

代码清单2-30 进行对象转换的工厂方法


public class GreetFactory{

public static GreetV2 adaptGreet(GreetV1 greet){

GreetAdapter adapter=new GreetAdapter(greet);

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

return(GreetV2)Proxy.newProxyInstance(cl, new Class<?>[]{GreetV2.class},adapter);

}

}


在实际的使用中,如果遇到GreetV1接口的实现,只需要将调用GreetFactory的adaptGreet方法转换成GreetV2接口,再按照GreetV2接口的方式来使用即可。GreetV1接口可以继续在遗留代码中使用。