黑马程序员java学习<基础加强>—动态代理

代理

1、程序中的代理:要为已经存在的多个具有相同接口的目标类的各个方法增加一些系统功能,如异常处理、日志、计算方法的运行时间、事物管理等等。

2、简单示例:编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码,如:

目标类:                              代理类:

class X{                               Xproxy{

   void sayHello(){                       void sayHello(){

   syso:Hello;                               startTime

   }                                                  X. sayHello();

}                                                    endTime;}

                                             }

一般用接口来引用其子类,如:Collection col = new ArrayList();

3、代理类的优点:

如果采用工厂模式和配置文件的方式进行管理,则不需要修改客户端程序,在配置文件中配置是使用目标类还是代理类。这样以后很容易切换,如果想要日志功能时,就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想换掉系统功能也很容易。

 

AOP(面向方面的编程)

1、系统中存在着交叉业务,一个交叉业务就是要切入到系统中的一个方面,如图:



安全、事务、日志等功能要贯穿于好多个模块中,所以他们就是交叉业务。

 

2、用具体的程序代码描述交叉业务:

1)代码实现

2)交叉业务的编程问题即面向方面的编程(AOP),AOP的目标就是使交叉业务模块化,可以采用将切面代理移动到原始方法的周围,这与直接在方法中编写切面代理的过程效果是一样的,如图:


因此使用代理技术正好可以解决这种问题,代理是实现AOP功能的核心和关键技术。

 

动态代理技术

1、要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,这时就不能采用静态代理方式,需用动态代理技术。

2、动态代理类:JVM可在运行时,动态生成类的字节码,这种动态(不是代理,只是拿出来作为代理类)生成的类往往被用作代理类,即动态代理类。

注:JVM生成的动态类必须实现一或多个接口,所以JVM生成的动态代理类只能用作具有相同接口的目标类代理。

3、CGLIB库可以动态生成一个类的子类,一个类的子类也可以作为该类的代理,所以,如果要为一个没有实现接口的类生成动态代理,那么可以使用CGLIB库。

4、代理类各个方法通常除了调用目标相应方法和对外返回目标返回的结果外,还可以再代理方法中的如下位置上加上系统功能代码:

1)在调用目标方法之前

2)在调用目标方法之后

3)在调用目标方法前后

4)在处理目标方法异常的catch块中。

 

分析JVM动态生成的类

一、创建动态类的实例对象:

1、用反射获得构造方法

2、编写一个最简单的InvocationHandler的类

3、调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去。

示例:

(1)打印创建的对象和调用对象的无返回值的方法和getClass方法,演示调用其他没有返回值的方法报告的异常

(2)将创建的动态类的实例对象的代理改写成为匿名内部类的形式编写。

import java.lang.reflect.*;  
import java.util.*;  
public class ProxyTest {  
public static void main(String[] args) throws Exception{  
        //创建动态代理类的三种方式  
        //方式一:通过接口的子类创建对象  
        Collection proxy1 = (Collection)  
                constructor.newInstance(new MyInvocationHandler());  
        System.out.println(proxy1);//null  
        System.out.println(proxy1.toString());//null  
        proxy1.clear();//无异常  
        //proxy1.size();//异常          
        //方式二:匿名内部类  
        Collection proxy2 = (Collection)  
                constructor.newInstance(new InvocationHandler(){  
                    public Object invoke(Object proxy, Method method,  
                            Object[] args) throws Throwable {  
                        // TODO Auto-generated method stub  
                        return null;  
                    }  
                });  
          
        //方式三:  
        //通过代理类的newProxyInstance方法直接创建对象  
        Collection proxy3 = (Collection)Proxy.newProxyInstance(  
            //定义代理类的类加载器  
            Collection.class.getClassLoader(), 
            //代理类要实现的接口列表  
            new Class[]{Collection.class}, 
            //指派方法调用的调用处理程序  
            new InvocationHandler() {  
                //创建集合,制定一个目标  
                ArrayList target = new ArrayList();  
                public Object invoke(Object proxy, Method method, Object[] args)  
                        throws Throwable {  
                    //测试程序运行时间  
                    long beginTime = System.currentTimeMillis();  
                    //调用目标方法,将其从return抽出来,加入代理所需的代码  
                    Object retVal = method.invoke(target, args);  
                    long endTime = System.currentTimeMillis();  
                    //测试  
                    System.out.println(method.getName() +   
                            " run time of " +   
                            (endTime - beginTime));  
                    return retVal;  
                }  
            }  
            );  
        //通过代理类调用目标方法,每调用一个目标的方法就会执行代理类的方法  
        //当调用一次add方法时,就会找一次InvocationHandler这个参数的invoke方法  
        proxy3.add("sdfd");  
        proxy3.add("shrt");  
        proxy3.add("rtbv");  
        System.out.println(proxy3.size());  
        System.out.println(proxy3.getClass().getName());  
    }  
}  


 

二、让JVM创建动态类需要提供的信息:

1、生成类中的哪些方法,通过让其实现哪些接口的方式进行告知。

2、产生的类字节码必须有一个关联的类加载器对象

3、生成的类中的方法的代码是怎么样的,也得由我们自己提供,把我们的代码写在一个约定好的子接口对象的方法中,把对象传给它,它调用我们的方法,即相当于插入了我们自己的代码。提供执行代码的对象就是InvocationHandler对象,它是在创建动态类的实例对象的构造方法时传递进去的,在上面的InvocationHandler对象的invoke方法中,加一点代码就可以看到这些代码被调用运行了。

 

三、分析动态生成的类的内部代码

1、构造方法接受一个InvocationHandler对象,接受此对象的用处:

接受一个handler参数是为了记录它,以便在之后的程序中运用它。

2、实现Collection接口的动态类中的各个方法的代码的解析:

InvocationHandler接口中定义的invoke方法接受三个参数的含义:

(1)Client(客户端)程序调用objProxy.add(“avc”)方法时,涉及到了三个参数,分别为:objProxy对象,add方法,”avc”参数。代码如下:

class Proxy${

  add(Object obj){

      return handler.invoke(Object proxy, Method method, Object[] args);

   }

}

(2)其中的Objectproxy 即为objProxy对象,Method method对应add方法,Object[] args就是”avc”参数。在使用newProxyInstance的方式创建代理对象实现时,当前正在调用代理对象(Object proxy),调用当前对象的哪个方法(Method method),调用此对象方法时传入的参数(Object[] args)。

3、调用代理涉及到三个因素:代理对象,代理对象的哪个方法,以及此方法接受的参数。要执行目标对象,只需要将代理对象作为目标对象即可。

4、对于上面代码中的Object retVal = method.invoke(target,args)的分析:

目标对象target执行完返回一个值为retVal,接着将值作为结果return回去,则代理方法就会收到一个返回值。其中还可以定义一个过滤器,对参数args(即当前对象的方法传入的参数)进行过滤(修改)。

5、对于上面代码中的proxy3.add(“sdfd”)的分析:

(1)代理对象调用add方法,传递了sdfd参数。

(2)add方法内部会找到InvocationHandler中的invoke方法,将代理对象proxy传进去,把add方法传入,将“sdfd”参数传入代理对象中的handler参数,返回了一个结果,就是给了add方法,add方法继续向外返回给调用的对象proxy3,即最终结果。

其中的handler的invoke方法返回又来自于目标target返回值,从而将此返回值返给Object invoke()方法,即作为handler参数位置上的值返回给add方法。add方法作为最后的结果返回。

 

四、问题:

1、在上面的方式一的代码中,调用无返回值的方法返回的是null,而调用有返回值方法的时候会报NullPointException异常的原因:

在proxy1.size()方法中,size()会调用Object invoke()方法,而对其覆写时的返回值是null,而size()本身会返回int类型,两者返回的类型不相等,所以就会报错。

而对于proxy3.size()不报错,是因为size()返回值与代理对象中handler参数返回值是一致的,当前目标返回什么,代理就返回什么,所以不会报错。

注意:目标返回值和代理返回值必须是同一类型。

2、为何动态类的实例对象的getClass()方法返回了正确结果,而没调用invoke方法:

因为代理类从Object上继承了许多方法,其中只对三个方法(hashCode、equals和toString)进行开发,委托给handler去自行处理,对于它身上其他方法不会交给代理类去实现,所以对于getClass()方法,还是由Object本身实现的。即proxy3.getClass(),该是什么结果还是什么结果,并不会交给invoke方法处理。

 

五、总结分析动态代理类的统计原理和结构:

1、怎样将目标传进去:

(1)直接在InvocationHandler实现类中创建目标类的实例对象,可看运行效果和加入日志代码,但是毫无意义。

(2)为InvocationHandler实现类注入目标的实例对象,不能采用匿名内部类的形式了。

(3)让匿名内部类的InvocationHandler实现类访问外面的方法中的目标类实例对象的final类型的引用变量。

2、动态代理的工作原理:

(1)Client(客户端)调用代理,代理的构造方法接受一个InvocationHandler,client调用代理的各个方法,代理的各个方法请求转发给刚才通过构造方法传入的handler对象,又把各请求分发给目标的相应的方法。就是将handler封装起来,其中this引用了当前的放(发来什么请求就接受哪个方法)。

(2)将创建代理的过程改为一种更优雅的方式,eclipse重构出一个getProxy方法绑定接受目标,同时返回代理对象,让调用者更懒惰,更方便,调用者甚至不用接触任何代理的API。

在这里将InvocationHandler加入到Proxy的构造方法中,因此,在创建出来的对象,就会存有构造方法中InvocationHandler的一些功能和信息,因为我们把想要运行的代码封装在InvocationHandler对象,把它传入到构造函数中,那么就实现了代理对象每次调用目标方法(因为实现了同一接口)时,都会调用我们加入到InvocationHandler对象中的代码。这就保证了每次调用代理时,可以在目标上加入我们自己加入的功能。

3、把系统功能代理模块化,即切面代码也改为通过参数形式提供,怎么把要执行的系统功能代码以参数的形式提供:

(1)把要执行的代码装到一个对象的某个方法中,然后把此对象作为参数传递,接收者只要调用这个对象的方法,即等于执行了外接提供的代码。

(2)为bind方法增加一个Advice参数。

package cn.itcast.test3;  
import java.lang.reflect.*;  
import java.util.*;  
public class MyProxy {  
    public static void main(String[] args)throws Exception {  
        //创建目标对象,并进行操作测试  
        final ArrayList target = new ArrayList();  
        Collection proxy = (Collection)getProxy(target,new MyAdvice());  
        proxy.add("sdf");  
        proxy.add("wgcd");  
        proxy.add("hgwe");  
        System.out.println(proxy.size());  
          
    }  
    //作为一个通用的方法,就使用Object  
    //传入一个目标,并传入一个接口,此接口作为通信的契约,才能调用额外的方法  
    private static Object getProxy(final Object target,final Advice advice) {  
        Object proxy = Proxy.newProxyInstance(  
                target.getClass().getClassLoader(),  
                //这里的接口要和target实现相同的接口  
                target.getClass().getInterfaces(),  
                new InvocationHandler() {  
                    public Object invoke(Object proxy, Method method, Object[] args)  
                            throws Throwable {  
                        //通过契约,使用其方法--before和after方法  
                        advice.beforeMethod(method);  
                        Object value = method.invoke(target, args);  
                        advice.afterMethod(method);  
                        return value;  
                    }  
                }  
                );  
        return proxy;  
    }  
}  
//创建实现Advice接口的子类  
package cn.itcast.test3;  
import java.lang.reflect.Method;  
//实现Advice接口中方法的具体内容  
public class MyAdvice implements Advice {  
  
    long beginTime = 0;  
    public void beforeMethod(Method method) {  
        // TODO Auto-generated method stub  
        System.out.println("从这里开始");  
        beginTime = System.currentTimeMillis();   
    }  
    public void afterMethod(Method method) {  
        // TODO Auto-generated method stub  
        long endTime = System.currentTimeMillis();  
        System.out.println("从这里结束");  
        System.out.println(method.getName() + " run time of " + (endTime-beginTime));  
    }  
}  
//创建接口Advice  
import java.lang.reflect.Method;  
/*接口中需要实现四个方法 
 * 调用目标方法之前 
 * 调用目标方法之后 
 * 调用目标方法前后 
 * 在处理目标方法异常的catch块中 
 */  
//这里只列出两个作为示例  
public interface Advice {  
    void beforeMethod(Method method);  
    void afterMethod(Method method);  
}  



 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值