"黑马程序员"代理的概念、作用

来源:互联网 发布:尚雯婕的法语水平 知乎 编辑:程序博客网 时间:2024/04/29 18:30

------ <a href="http://www.itheima.com" target="blank">android培训</a> <a href="http://www.itheima.com" target="blank">java培训</a>、期待与您交流! ----------

代理类原理

程序中的代理:要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能。例如:异常处理、日志、计算方法的运行时间、事务处理、等。就该编写一个与目标具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上些功能代码。

代理架构图如下,分析:

客户端原理调用目标类,现在出现代理类后,客户端不用调用目标类,而是直接调用代理类。代理类和目标类有相同的接口,客户端在程序中不止调用目标类或者代理类 的引用,而是调用接口的引用。在代理类中一定要调用目标类对应的方法,在调用目标类方法的前面和后面会调用系统功能的代码:


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

AOP面向方面的编程(Aspent Oriented Program)

系统中存在着交叉业务,一个交叉业务就是要切入到系统中的一个方面,如下所示:StudentServer类负责对学生进行管理,还有一个类CourseServer负责对课程进行管理,还有其他的类MisoServer管理其他的信息,例如管理老师信息、教师信息。不论哪个类,都有安全管理、事物关联和日志管理。安全、事物和日志等功能要贯穿到了好多个模块中,所有它们就是交叉业务。

StudentService------|----|----|------

CourseService------|-----|---|------

MiscService------|----|----|------

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

 method1    method2    method3

  {                   {                   {

----------------------------------------------切面

......

----------------------------------------------切面

   }                     }                   }

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

-----------------------------------------------切面

func1    func2    func3

{             {             {

... ... ...

}}}

-----------------------------------------------切面

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

动态代理技术

1. 要为系统中的各个接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理的方式,将是一件非常麻烦的事情。写成千上百个代理类,很累。

2. JVM可以在运行期间动态生成出类的字节码这种动态生成的类往往被用作代理类,即动态代理类。

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

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

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

 

    ①在调用目标方法之前;

    ②在调用目标方法之后;

    ③在调用目标方法前后;

    ④在处理目标方法的catch块中。

 创建动态类及查看其方法列表信息

使用JVM动态生成类一定要实现一个接口,有一个java.lang.reflect.Proxy类,该类中有一个方法getProxyClass,返回值类型是Class(即字节码)。方法参数Class<?>…interface表示实现那个类的接口,参数ClassLoader表示需要指定一个类加载器。

创建动态类的实例对象及调用其方法

创建动态类的实例对象,步骤如下:

    1. 用反射获取实例对象。

    2. 编写一个简单的InovationHandler类

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

    4. 打印创建的对象和调用对象的没有返回值的方法和getClass方法,而调用提前有返回值的方法报告了异常。

    5. 将创建动态类的实例对象的代理类,改写为匿名内部类。

 

结思考:定义一个类实现InvocationHandler接口,很麻烦,其实可以使用匿名内部类的方式来实现,让jvm创建动态类及实例对象,需要给它提供以下信息。

创建动态类及实例对象有三个方面的内容,以上面的代码进行说明:

1. 生成的类实现InvocationHandler接口,这里使用内部类方式:

new InvocationHandler() {

           public Object invoke(Object arg0, Method arg1, Object[] arg2)

                  throws Throwable {

              return null;

           }

       }

 

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

Class clazzProxy1 =

Proxy.getProxyClass(Collection.class.getClassLoader(),Collection.class);

 

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

用Proxy.newInstance方法直接一步就创建出代理对象。

 创建动态类和创建实例对象合二为一

在上面例子中,创建动态类和创建实例对象是分开来实现的,其实java.lang.reflect.Proxy类提供了一个静态方法(如下),该方法即可以获得字节码,还可以创建实例对象。三个参数分别为:类加载器、接口(可能有多个,所以是数组)、handler实现对象:

Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),

        new Class[] { Foo.class },

        handler);

 

下面使用该方法,改写上一节代码:

//loader:类加载器使用Collection类加载器

//创建代理类对象

Collection proxy3 = (Collection)Proxy.newProxyInstance(

       Collection.class.getClassLoader(),

       new Class[]{Collection.class},

       //匿名内部类实现InvocationHandler接口

       new InvocationHandler() {

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

                  throws Throwable {

              ArrayList target = new ArrayList();//指定目标

              long begTime = System.currentTimeMillis();

              Object reVal = method.invoke(target, args);//调用目标方法

              long endTime = System.currentTimeMillis();

              System.out.println(method.getName()+" running time of "+(begTime - endTime));

              return reVal;

           }

       }

);

proxy3.add("Larry");

proxy3.add("Steve");

proxy3.add("Girl");

System.out.println(proxy3.size());

 

运行结果如下,分析:说明没调用一个方法,就执行InvocationHandler类的invoke方法,这里没有出现异常。我们向proxy3中添加了3个元素,为什么输出的size为零呢?

因为每调用一个方法就执行一次invoke方法,在invoke方法中创建了目标,所以调用四次方法,创建了四个目标,不是同一个目标。解决方法是将目标提出来,作为InvocationHandler类的成员变量,修改后的代码在下面:

add running time of 0

add running time of 0

add running time of 0

size running time of 0

0


修改上面代码(只显示修改部分),指定同一个目标: 

//匿名内部类实现InvocationHandler接口

new InvocationHandler() {

    ArrayList target = new ArrayList();//指定目标

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

           throws Throwable {

       long begTime = System.currentTimeMillis();

       Object reVal = method.invoke(target, args);//调用目标方法

       long endTime = System.currentTimeMillis();

       System.out.println(method.getName()+" running time of "+(begTime - endTime));

       return reVal;

    }

}

这样运行的size方法输出的结果就是3了,这说明添加的了3个元素。

让动态生成的类成为目标类的代理

分析动态代理的工作原理


直接在InvocationHander实现类中创建目标类的实例对象,可以看运行效果和加入日志代码,但没有实际意义。

为InvactionHandler实现类访问外面方法中的目标类实例对象的final类型的引用变量。

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

将系统功能代码模块化,即将切面代码也改为通过参数形式提供,怎样把要执行的系统功能代码以参数形式提供?把要执行的代码装到一个对象的某个方法里,然后把这个对象作为参数传递,接受者只要调用这个对象的方法,即为bind方法增加一个Advice参数。

动态代理的工作原理图

实现AOP功能的封装配置

工厂类BeanFactory负责创建目标类或代理类的实例对象,并通过配置文件实现切换,如果参数字符串在配置文件中对应的类名不是ProxyFactoryBean,则直接返回该类的实例对象,否则,返回该类实例对象的getFactory方法返回的对象。

BeanFactory的构造方法接受代表配置文件的输入流对象,配置文件格式如下:

#xxx = java.util.ArrayList

xxx = cn.itcast.ProxyFsctoryBean

xxx.target=java.util.ArrayList

xxx.advice=cn.itcast.MyAdvice

ProxyFactoryBean充当封装生成动态类代理的工厂,需要为工厂类提供哪些配置参数信息?

目标

通知

编写客户端应用:

编写实现Advice接口的类和在配置文件中进行配置

调用BeanFactory获取对象。

0 0