实际上通过java的反射机制和内建的代理模式,完全可以做到这一点。下面就一步一步的说一下这是如何实现的。
1,准备知识
- 代理模式
- 反射机制与动态编译
- Java的内建代理
在我后边讲到的具体实现中就可以看到,我们正是在实现InvocationHandler的MyInvocationHandler的invoke()方法中来判断java文件的改变,对于改变动态的编译和装载和调用来达到我们预期的目标的,java内建的代理模式可谓居功至尾。
为了演示像jsp一样的java的效果,让我们来定义的一种服务。
public interface Postman {
void deliverMessage(String msg);
}
具体的实现中,候选的有两种方案:
- 第一种是将输出字符串到控制台:
private PrintStream output;
public PostmanImpl() {
output = System.out;
}
public void deliverMessage(String msg) {
output.println("[Postman] " + msg);
output.flush();
}
}
- 第二种是输出字符串到文本:
public class PostmanImpl implements Postman {
private PrintStream output;
public PostmanImpl() throws IOException {
output = new PrintStream(new FileOutputStream("msg.txt"));
}
public void deliverMessage(String msg) {
output.println("[Postman] " + msg);
output.flush();
}
}
在程序运行中,我们就是要通过动态修改PostmanImpl 来观察这个JSP一样的现象。
3,访问服务
这里要介绍一些访问服务的main程序,以便看到这种方式的优越性。
public class PostmanApp {
public static void main(String[] args) throws Exception {
BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));
Postman postman = getPostman();
while (true) {
System.out.print("Enter a message: ");
String msg = sysin.readLine();
postman.deliverMessage(msg);
}
}
private static Postman getPostman() {
DynaCode dynacode = new DynaCode();
dynacode.addSourceDir(new File("dynacode"));
return (Postman) dynacode.newProxyInstance(Postman.class,
"sample.PostmanImpl");
}
}
我们可以看到获取PostMan对象,只是在初始化的过程中做过一次,后边只是访问其deliverMessage()方法,而 sample.PostmanImpl这一实现的动态改变完全被掩藏在这个小小框架的后边。Java文件改变后的编译、重新载入、对象实例化和方法的调用过程完全不可见,是不是很神奇的实现。
4,DynaCode
看了这样的主程序,你可能会首先看一下DynaCode的实现,然而我不准备详细讲述DynaCode的实现,尽管它的实现最为复杂。因为 DynaCode只是简单的包装了java.lang.reflect.Proxy,通过添加几个处理class路径和java路径的方法辅助来完成工作的,我不想重点介绍java反射机制的,因此我们只来看DynaCode一个重要的方法。
public Object newProxyInstance(Class interfaceClass, String implClassName)
throws RuntimeException {
MyInvocationHandler handler = new MyInvocationHandler(
implClassName);
return Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class[] { interfaceClass }, handler);
}
好了,看到了吧,DynaCode只是在内部做了一个InvocationHandler实现,并简单的使用了Proxy的newProxyInstance()方法。
5,InvocationHandler实现
根据上面提到的java的内建代理模式,要实现一个InvocationHandler,下面这段代码展示了MyInvocationHandler 的主要部分:
private class MyInvocationHandler implements InvocationHandler {
String backendClassName;//实际的类名
Object backend;//实际的类对象
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 在第一载入的时候通过loadClass来载入所需要的服务实现对象,后边每次检查java文件是否被修改过,
* 如果被修改就unload当前的对象,编译并重新载入该类。
*/
Class clz = loadClass(backendClassName);
if (backend.getClass() != clz) {
backend = newDynaCodeInstance(clz);
}
// 调用有效的对象的方法
return method.invoke(backend, args);
}
}
在MyInvocationHandler中保存了实现接口的类名和该类的一个对象,该对象也就是通过newDynaCodeInstance()方法得到的对象。在每一次调用该对象的方法的时候,java的Proxy机制保证了系统会自动调用MyInvocationHandler的invoke方法。
这里采用反射进行动态载入的程序,调用的实际工作是在InvocationHandler的invoke方法中做的,因此InvocationHandler要保存实际的对象。
代理模式的好处是从使用者看来如同调用实际的对象是一样的,而实际上可以通过代理对象,程序可以动态采用不同的接口实现来完成工作,这一过程只需要在Proxy.newProxyInstance()中给定不同的实现类即可。
本文主要参照lostfire