黑马程序员——java拾遗之代理类及AOP

------ Java培训、Android培训、iOS培训、.Net培训 、期待与您交流! -------


动态代理其实就是java.lang.reflect.Proxy类动态的根据您指定的所有接口生成一个class 字节码,该class会继承Proxy类,并实现所有你指定的接口(您在参数中传入的接口数组);然后再利用您指定的classloader将 class 字节码加载进系统,最后生成这样一个类的对象,并初始化该对象的一些值,如invocationHandler,以即所有的接口对应的Method成员。 初始化之后将对象返回给调用的客户端。这样客户端拿到的就是一个实现你所有的接口的Proxy对象。


下面通过对Proxy类的使用来探究java中的代理机制:
首先,我们用Proxy来创建一个Collection接口的字节码,集合框架在java中地位重要,Collection接口里面也定义了很多的方法,方便的后面对动态生成的代理类的观察。创建的代码如下:
<span style="font-size:14px;">Class clazzProxy1 
	= Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
System.out.println(clazzProxy1.getName());</span>
打印一下这个动态生成的Class对象的名字,发现名称是$Proxy0,看起来挺奇怪,不过这个名称无关紧要,也和我们使用java的代理机制没多大关系,不必理会,重要的是,这个Class对象创建成功了。

后面我们利用java的反射机制,探究一下Proxy类生成的动态对象结构是什么样子的,
首先,打印一下这个动态生成的Class对象实现了什么接口:
<span style="font-size:14px;">for(Class clazz : clazzProxy1.getInterfaces()){
    System.out.println(clazz);
}</span>
发现打印的结果是:interface java.util.Collection
说明这个动态生成的Class对象实现了我们指定的接口。


接下来再看一下这个对象的父类:
<span style="font-size:14px;">System.out.println(clazzProxy1.getSuperclass());</span>
结果是:class java.lang.reflect.Proxy
可以看出,由Proxy类动态生成的Class对象,父类都是Proxy。


看完了Proxy生成的Class对象实现接口和父类,接下来看看这个动态对象的构造方法:

先看看这个对象有几个构造方法,代码如下
<span style="font-size:14px;">Constructor[] constructors = clazzProxy1.getConstructors();
System.out.println(constructors.length);</span>
结果是1
看来生成的Proxy对象只有一个构造方法,我们来看看这个构造方法接收几个参数,代码如下
<span style="font-size:14px;">System.out.println(constructors[0].getParameterTypes().length);</span>
结果是1
说明构造方法只接受一个参数,这样我们来看看Proxy生成的动态的Class对象构造方法的结构,代码如下
<span style="font-size:14px;">Constructor[] constructors = clazzProxy1.getConstructors();
for(Constructor constructor : constructors){
    StringBuilder sb = new StringBuilder(constructor.getName());
    sb.append("(");
    for(Class clazz : constructor.getParameterTypes()){
        sb.append(clazz.getName()).append(",");
    }
    sb.deleteCharAt(sb.length()-1);
    sb.append(")");
    System.out.println(sb.toString());
}</span>
打印的结果是:$Proxy0(java.lang.reflect.InvocationHandler)
我们看到Proxy类的构造方法接收了一个接口InvocationHandler,这个接口的作用很关键,我们接下来详细的探究一下。
InvocationHandler 接口在java官方JDK的API上,解释是:是代理实例的调用处理程序 实现的接口。理解起来有点抽象,但是接下来我们用代码来解释,会清楚很多。
上面我们了解了Proxy类动态生成的Class对象的构造方法结构,接下来我们就要用这个Class对象的构造方法来实例化出实现了我们指定接口的动态代理类的对象。代码如下:
<span style="font-size:14px;">public class ProxyDemo1
{
    public static void main(String[] args) throws Exception{
        Class<?> clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
        Constructor<?> constructor = clazzProxy1.getConstructor(InvocationHandler.class);
        Collection collection = (Collection)constructor.newInstance(
            new InvocationHandler(){
                Collection target = new ArrayList();
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                    System.out.println("proxy:"+proxy.getClass());
                    System.out.println("method:"+method.getName());
                    if(args != null && args.length > 0){
                        for( Object arg: args){
                            System.out.println("arg:"+arg);
                        }
                    }
                    return method.invoke(target,args);
                }
        });
        collection.toString();
        collection.add("123");
        collection.add("abc");
    }
}</span>

执行的结果是:
proxy:class $Proxy0
method:toString
proxy:class $Proxy0
method:add
arg:123
proxy:class $Proxy0
method:add
arg:abc

到这里我们就可以看出 InvocationHandler 接口的作用了, InvocationHandler 接口的 invoke方法在 Proxy 的实例每次调用它所指定实现的接口的方法时,都会去调用它。到这里我们就能知道invoke方法里应该写一些什么了。首先,invoke方法应该保证这个 Proxy 实例每次调用接口中的方法时,得到的结果都和不用代理,直接调用接口实现类同一个方法的结果一致。否则代理没有实现原本应该实现的功能,代理就失去了意义(代理的目的首先要实现它要代理的功能,其次才是在功能的基础上添加自己个性化的功能需要)。例如上面代码中的clazzProxy1,对它调用add方法应该和对Collection的实现类 ArrayList 调用add方法作用相同。我们在最后添加一行代码:
<span style="font-size:14px;">System.out.println(collection);</span>
可以看到打印了如下结果:
proxy:class $Proxy0
method:toString
[123, abc]

可以看到,println方法其实也是调用了代理实例collection的toString方法,并且这个代理类$Proxy0的实例collection也能像ArrayList一样实现往集合中添加元素的功能,根本原因是在匿名内部类的第一行,Collection target = new ArrayList(); 赋予了一个代理实例的操作对象target,这个对象就是ArrayList的对象。因此操作代理对象的时候,就是间接的操作了这个ArrayList的对象target。
看到这里可能会疑惑,绕了一大圈,不如直接操作这个target得了,问什么还要通过代理的方式间接的实现同样的功能。这就要说代理模式的设计思想,在调用代理实例的方法的时候,这些方法都会调用invoke方法,而 invoke 方法的内容其实都是我们程序员自己写的,这就意味着,我们可以在里面写很多我们自己需要的代码,最常见的就是在方法调用的时候添加日志输出。
在不使用代理的时候,我们要添加日志,需要在接口实现类调用方法的外部进行编码,每调用一次方法,这个记录日志的代码就要重复写一遍。有了代理之后,我们把接口实现类要调用的方法和我们需要记录日志的方法一起塞进invoke方法中,这样通过代理来调用原本我们使用的方法,达到的效果一样,但是不用在重复的写记录日志的代码。因为代理的实例会自己去调用我们写在invoke方法里面的记录日志方法。
说到这里,已经引出了Spring框架的一个重要思想,面向切面编程AOP。用上面的例子来理解,记录日志的功能和我每次调用的return method.invoke(target,args);这个ArrayList类的方法其实没什么根本关系,去掉日志功能,调用 ArrayList 的方法照样正常。我把日志的功能换成其他功能(例如计算圆周长),也一样不会影响ArrayList的方法调用。用形象的比喻来说,就像在代理类调用实现接口方法的前后,加入了“切面”,这个切面在invoke方法中,对代理类的所有方法调用都起作用,但是又不影响代理的基本功能。用我上面的代码来讲,记录invoke方法三个参数的几行println语句,
<span style="font-size:14px;">System.out.println("proxy:"+proxy.getClass());
System.out.println("method:"+method.getName());
if(args != null && args.length > 0){
    for( Object arg: args){
        System.out.println("arg:"+arg);
    }
}</span>
就是一个切面,它对collection对象的所有方法起作用,都在method.invoke(target,args)方法之前统一调用,且不影响代理的根本功能。就像一个切面,切入了collection这个代理对象的所有方法中。所以上面的invoke方法代码可以看成是这样的结构:
<span style="font-size:14px;">Collection collection = (Collection)constructor.newInstance(
    new InvocationHandler(){
        Collection target = new ArrayList();
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            /*前面的切面*/ 
            System.out.println("proxy:"+proxy.getClass());
            System.out.println("method:"+method.getName());
            if(args != null && args.length > 0){
                for( Object arg: args){
                    System.out.println("arg:"+arg);
                }
            }
            Object obj = method.invoke(target,args);
            /*后面的切面*/ 
           return obj;
        }
});</span>
上面的代码演示的是由Proxy先获取Class类实例,之后通过Class类实例获取构造方法,最后通过构造方法来实例化对象的过程,Proxy类提供的便捷的方法,可以把这三个步骤合并为一步,简化代码,上面的代码也可以写成这样:
<span style="font-size:14px;">Collection collection = (Collection) Proxy.newProxyInstance(
    Collection.class.getClassLoader(),
    new Class[] { Collection.class }, 
    new InvocationHandler() {
        Collection target = new ArrayList();
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            /* 前面的切面 */
            System.out.println("proxy:" + proxy.getClass());
            System.out.println("method:" + method.getName());
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    System.out.println("arg:" + arg);
                }
            }
            Object obj = method.invoke(target, args);
            /* 后面的切面 */
            return obj;
        }
    });</span>
在框架中,生成动态代理的方法往往是通用的,不会像上面这样根据不同的接口单独去写不同功能的代理类。在实际应用中都是对同一个生成动态代理的方法,需要实现什么接口,就传进去什么接口,而方法根据不同的接口生成不同的代理类,这样就需要对上面我们的代码做一些改动。
先把上面要实现的接口子类对象target提出到InvocationHandler匿名内部类的外面,让InvocationHandler要操作的接口类型由外部传入,就像这样:
<span style="font-size:14px;">final Collection target = new ArrayList();
Collection collection = (Collection) Proxy.newProxyInstance(
    Collection.class.getClassLoader(),
    new Class[] { Collection.class }, 
    new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            /* 前面的切面 */
            System.out.println("proxy:" + proxy.getClass());
            System.out.println("method:" + method.getName());
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    System.out.println("arg:" + arg);
                }
            }
            Object obj = method.invoke(target, args);
            /* 后面的切面 */
            return obj;
        }
    });</span>
接下来把生成动态代理的对象抽象成一个方法,假如起名叫getProxy,传入的对象类型改成通用的Object类,返回的类型也改为Object类,代码如下:
<span style="font-size:14px;">public static void main(String[] args) throws Exception{
    final Collection target = new ArrayList();
    Collection collection = (Collection)getProxy(target);
}

private static Object getProxy(final Object target) {
    Object obj = Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(), 
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
                /* 前面的切面 */
                System.out.println("proxy:" + proxy.getClass());
                System.out.println("method:" + method.getName());
                if (args != null && args.length > 0) {
                    for (Object arg : args) {
                        System.out.println("arg:" + arg);
                    }
                }
                Object obj = method.invoke(target, args);
                /* 后面的切面 */
                return obj;
            }
        });
    return obj;
}</span>
这样一个能根据需要生成不同代理类的通用方法就写好了,但是上面的方法还是有不令人满意的地方,那就是对于切面的描述,上面的切面按理应该抽象成一个方法,后面要修改切面的逻辑可以直接修改切面方法,而不是去改动代理生成类的内部逻辑。我们接着来修改上面的代码,来达到我们满意的效果。首先,我们需要定义一个用来描述切面的接口,里面需要定义两个方法,beforeMethod和afterMethod,分别表示代理类调用它所实现接口(例如上面的Collection接口)的方法(比如Collection接口的add方法)前后的切面。然后再定义一个自定义的切面类,里面封装我们要实现的切面具体实现。比如我这样定义切面:
<span style="font-size:14px;">//描述切面的接口
public interface Advice
{
    /** 功能描述:前切面  */
    void beforeMethod(Object proxy, Method method, Object[] args);
    /** 功能描述:后切面  */
    void afterMethod(Object proxy, Method method, Object[] args);
}
//封装具体实现的切面类
public class MyAdvice implements Advice
{
    public void beforeMethod(Object proxy, Method method, Object[] args){
        System.out.println("我是 method "+method.getName()+" 前面的切面!");
    }
    public void afterMethod(Object proxy, Method method, Object[] args){
        System.out.println("我是 method "+method.getName()+" 后面的切面!");
    }
}</span>
我们获取动态代理类的方法现在就可以改成这样:
<span style="font-size:14px;">private static Object getProxy(final Object target, final Advice advice) {
    Object obj = Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(), 
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
                advice.beforeMethod(proxy, method, args);//前切面
                Object obj = method.invoke(target, args);
                advice.afterMethod(proxy, method, args);//后切面
                return obj;
            }
        });
    return obj;
}</span>
看起来是不是清爽许多,我们再来测试下现在这个代理获取方法看看效果:
<span style="font-size:14px;">final Collection target = new ArrayList();
Collection collection = (Collection)getProxy(target, new MyAdvice());
collection.toString();
collection.add("123");
collection.add("abc");    
System.out.println(collection);</span>
执行结果:
我是 method toString 前面的切面!
我是 method toString 后面的切面!
我是 method add 前面的切面!     
我是 method add 后面的切面!     
我是 method add 前面的切面!     
我是 method add 后面的切面! 
我是 method toString 前面的切面!    
我是 method toString 后面的切面!
[123, abc]

大功告成,一个实现了简单AOP功能的通用动态代理类现在就写好了。

我们来总结一下上面的内容,


1.java里有动态代理机制,可以动态生成Class类的实例,这个实例可以实现我们指定的接口,通常来说,这个生成的动态类目的是让我们在实现接口原有功能的基础上,添加新的功能,而不用重复编写这个新的功能的代码。


2.为了实现动态代理实例在原有接口功能上添加新功能的目的,引入了面向切面AOP的概念,基本思想就是在InvocationHandler中的invoke方法调用接口本身方法的前后,加入我们自定义的方法,这样我们自定义的方法就可以在动态代理调用接口方法的时候,自动的被调用。


3.切面我们通常抽象出对应的接口和实现类,在动态代理的invoke方法中,通过切面的实现类来调用切面的功能,而不是直接把切面的代码写进invoke方法中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值