posts - 13,  comments - 3,  trackbacks - 0
最近在学习Spring。某大人跟我说,Spring的AOP其实就是Java反射中的动态代理。OK,那我就从动态代理开始看起。

一、基本概念
所谓动态代理,基本上是如下场景:假设我有个接口IHelloWorld

public interface IHelloWorld{
void sayHello();
}


我再有一个实现类HelloWorldImpl实现了IHelloWorld接口
public class HelloWorldImpl implements IHelloWorld{
public void sayHello(){
System.out.println(
"Hello, World");
}
}


这样,我就可以创建一个HelloWorldImpl对象,来实现IHelloWorld中定义的服务。

 问题是,现在,我打算为HelloWorldImpl增强功能,需要在调用sayHello方法前后各执行一些操作。在有些情况下,你无法修改HelloWorldImpl的源代码,那怎么办呢?
从道理上来说,我们可以拦截对HelloWorldImpl对象里sayHello()函数的调用。也就是说,每当有代码调用sayHello函数时,我们都把这种调用请求拦截下来之后,做自己想做的事情。

 那怎么拦截呢?

 首先,需要开发一个InvocationHandler。这个东东表示的是,你拦截下函数调用之后,究竟想干什么。InvocationHandler是一个接口,里面的声明的函数只有一个:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
这个函数表示一次被拦截的函数调用。因此,proxy表示这个被拦截的调用,原本是对哪个对象调用的;method表示这个被拦截的调用,究竟是调用什么方法;args表示这个被拦截的调用里,参数分别是什么。

 我们下面写一个拦截器,让他在函数调用之前和之后分别输出一句话。

import java.lang.reflect.*;

public class HelloHandler implements InvocationHandler{
Object oriObj;

public HelloProxy(Object obj){
oriObj 
= obj;
}

public Object invoke(Object proxy, Method m, Object[] args) throws Throwable{
Object result 
= null;

//在函数调用前输出一些信息
    System.out.println("################################");
String methodName 
= m.getName();
System.out.println(
"method name : " + methodName);
doBefore();

//利用反射,进行真正的调用
    result = m.invoke(oriObj, args);

//在函数调用后执行
    doAfter();
System.out.println(
"################################");
return result;
}

public void doBefore(){
System.out.println(
"Do Before");
}
public void doAfter(){
System.out.println(
"Do After");
}
}

 有了这个Handler之后,下面要做的,就是把这个Handler和一个IHelloWorld类型的对象装配起来。重点的函数只有一个,那就是java.lang.reflect.Proxy类中的一个静态工厂方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
这个方法返回一个对象,我们称返回的对象为代理对象(proxy)。
而后,我们就不把真正的原对象暴露给外接,而使用这个代理对象。这个代理对象接受对源对象的一切函数调用(也就是把所有调用都拦截了),然后根据我们写的InvocationHandler,来对函数进行处理。

 产生代理对象的过程,我把它理解成一个装配的过程:由源对象、源对象实现的接口、InvocationHandler装配产生一个代理对象。

 相应的测试代码如下:

public class TestHello{
public static void main(String args[])throws Exception{
HelloWorldImpl h 
= new HelloWorldImpl();

Object proxy 
= Proxy.newProxyInstance(
h.getClass().getClassLoader(),
new Class[]{IHelloWorld.class},
new HelloProxy(h)
);
((IHelloWorld)proxy).sayHello();
}
}

 利用ant编译运行的结果:
[java] ################################
[java] method name : sayHello
[java] Do Before
[java] Hello, World
[java] Do After
[java] ################################


二、更多理解
我们看产生代理对象的newProxyInstance函数的声明:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

 这个函数的第一个参数是ClassLoader,第三个参数是InvocationHandler,基本都没什么问题。
第二个参数是一个Class类型的数组,名字叫interfaces,表示的是产生的动态代理对象实现的接口。

 仔细想想,有两个问题。第一,产生一个代理对象,需要源对象么?第二,我能不能产生一个动态代理对象,来实现源对象没有实现的接口?

 第一个问题和第二个问题其实是一致的。我们完全可以脱离源对象,而直接产生一个代理对象,也可以利用动态代理,让源对象实现更多的接口,为源对象增强功能。

 例如,假设我们希望让源对象实现java.io.Closeable接口,则首先修改一下我们的Handler的invoke方法,让他在获取colse方法时,不要传递给源对象(因为源对象没有实现该方法):

public Object invoke(Object proxy, Method m, Object[] args) throws Throwable{
Object result 
= null;

//在函数调用前输出一些信息
  System.out.println("################################");
String methodName 
= m.getName();
System.out.println(
"method name : " + methodName);
doBefore();

//判断是否是Closeabled的方法
  if (m.getDeclaringClass().isAssignableFrom(java.io.Closeable.class)){
System.out.println(
"I got the close() method!");
}
else{
//传递给源对象
//利用反射,进行真正的调用
    result = m.invoke(oriObj, args);
}

//在函数调用后执行
  doAfter();
System.out.println(
"################################");
return result;
}

 然后,我们在装配的过程中,改变一下参数,并强转之后调用一下close方法:

Object proxy = Proxy.newProxyInstance(
h.getClass().getClassLoader(),
new Class[]{IHelloWorld.class,java.io.Closeable.class},
new HelloProxy(h)
);
((Closeable)proxy).close();

 ant运行结果:
[java] ################################
[java] method name : close
[java] Do Before
[java] I got the close() method!
[java] Do After
[java] ################################

三、更多的代理~
我们现在能够让sayHello()函数执行之前和之后,输出一些内容了。那如果我还想在装配一个Handler呢?
最简单的方法:

Object proxy = Proxy.newProxyInstance(
h.getClass().getClassLoader(),
new Class[]{IHelloWorld.class, java.io.Closeable.class},
new HelloProxy(h)
);
Object proxy2 
= Proxy.newProxyInstance(
h.getClass().getClassLoader(),
new Class[]{IHelloWorld.class, java.io.Closeable.class},
new HelloProxy(proxy)
);
((IHelloWorld)proxy2).sayHello();

ant运行结果:
[java] ################################
[java] method name : sayHello
[java] Do Before
[java] ################################
[java] method name : sayHello
[java] Do Before
[java] Hello, World
[java] Do After
[java] ################################
[java] Do After
[java] ################################

不用我解释了吧!

posted @ 2009-03-02 22:28 Antony Lee 阅读(505) | 评论 (0)编辑 收藏

和人讨论设计模式的时候,看到这样一句话:

         
大阿亮<yighter@qq.com> 22:24:40
java扩展功能就是继承和组合。肯定结构都很相似。模式思想都是从解决问题背景和目的来区分的。

恍然大悟。原来,很多情况下,所谓设计模式,是对同一种技术、实现的不同角度的理解。

所以,设计无所谓好坏,只要能解决问题的,就是好设计。至于所谓“强耦合”,“Bad Smell”,本质上是因为采用这些设计无法解决问题(就是无法快速应对需求变化)。

“不管黑猫白猫,只要能抓住耗子,就是好猫”,这句话蕴含着深刻的设计思想。

 

posted @ 2009-02-28 23:31 Antony Lee 阅读(118) | 评论 (0)编辑 收藏

在啃《The Java Programming Language 4th Edition》时看到的一个小知识点。先描述一下问题。

一个类中,静态初始代码块中的代码会在类加载时自动运行。考虑下面这种情况:

ClassA定义了静态初始代码块,其中调用了ClassB的一个方法m(静态非静态均可)。而在ClassB的m方法中,又使用了ClassA类的信息。则,当虚拟机在没有ClassB类的情况下,加载ClassA类时,会遇到这样一条线索:

加载ClassA --> 调用ClassA的静态初始化代码块 --> 调用ClassB的m方法 --> 加载ClassB --> 使用ClassA的信息

注意这条线索的一头一尾,我们要在对ClassA还没完成加载时,使用ClassA的信息!

示例代码:

 1public class TestStaticInit{
 2 public static void main(String args[]){
 3  ClassA a= new ClassA();
 4 }

 5}

 6
 7class ClassA{
 8 static int a1;
 9 static int a2;
10 static{
11  a1 = 10;
12  ClassB.print();
13  a2 = 30;
14 }

15}

16
17class ClassB{
18 public static void print(){
19  System.out.println(ClassA.a1);
20  System.out.println(ClassA.a2);
21 }

22}

23
24

 

首先,编译器无法解决这个问题,因为在编译ClassA类时,无法找到ClassB的代码,也就无法检查是否存在静态初始化代码块循环问题。事实上,上述程序在java中是能够编译通过的。

其次,运行时的结果。当程序运行到第3行时,JVM加载ClassA类,此时,会执行ClassA类中的静态初始化代码块。当程序执行到第12行时,调用ClassB的print方法,此时,程序跳转到18行。

关键在这儿:此时的print方法需要调用ClassA的信息,并打印其静态属性。而ClassA的信息正在加载过程中。此时,JVM采用的策略是:在print方法中使用ClassA不完整的信息。在print方法中ClassA的信息,是在第12行对ClassB.print方法之前的信息。此时ClassA.a1已经被赋值为10,而ClassA.a2还未被赋值,它的值为默认值。因此,最后打印出的是10、0。

posted @ 2009-01-05 23:48 Antony Lee 阅读(411) | 评论 (0)编辑 收藏
终于要在Blogjava安家了。写下第一篇日志,纪念一下
posted @ 2008-12-28 00:27 Antony Lee| 编辑 收藏
仅列出标题
共2页: 上一页 1 2 

<2024年5月>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

常用链接

留言簿(1)

随笔分类

随笔档案

文章分类

搜索

  •  

最新评论

阅读排行榜

评论排行榜