15. JAVA 反射机制 Part 2(动态代理、类的生命周期、工厂设计模式) ----- 学习笔记

15.5 动态代理

     在第6章(面对对象高级篇--抽象类与接口的应用)中讲过代理机制的操作,但是所讲解的代理设计属于静态代理。因为每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理类。为了避免这一问题,最好的做法就是通过一个代理类完成全部的代理功能,那么,此时就必须用到动态代理来完成了。

     在Java中要想实现动态代理机制,需要用到java.lang.reflect.Proxy类、java.lang.reflect.InvocationHandler接口的支持!!

//InvocationHandler接口的定义
public interface InvocationHandler{
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
}

        在InvocationHandler接口中只定义了一个invoke()方法,此方法中的3个参数含义:

  • Object proxy : 被代理的对象。代表动态代理对象
  • Method method : 要调用的方法。 代表正在执行的方法
  • Object[] args : 方法调用时所需要的参数 。代表调用目标方法时传入的实参

   Proxy类提供了用于创建动态代理类和代理对象的静态方法, 它也是所有动态代理类的父类。如果在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy类来创建动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。

(一)通过Proxy类创建动态代理对象  (当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法

//Proxy类创建动态代理对象的方法,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

参数含义:

  • ClassLoader loader:  类加载器
  • Class<?>[] interfaces:  得到全部的接口
  • InvocationHandler h:  得到InvocationHandler接口的子类实例

在Java中主要有以下3种类加载器:

  1. Bootstrap ClassLoader: 此加载器采用C++编写,一般开发中是看不到的
  2. Extension ClassLoader: 用来进行扩展类的加载;一般对应的是jre\lib\ext目录中的类
  3. AppClassLoader: 加载classpath指定的类,是最常使用的一种加载器

范例: 取得类加载器

class Person{
    
}
public class ClassLoaderDemo{
    public static void main(String args[]){
        Person per = new Person();
        System.out.println("类加载器:" + per.getClass().getClassLoader().getClass().getName());
    }
}

运行结果:

类加载器:sun.misc.Launcher$AppClassLoader
从运行结果可以看出,默认的ClassLoader是AppClassLoader。在开发中,可以通过继承ClassLoader类来实现自己的类加载器。但是这样做的意义不大。

(二)创建一个动态代理类所对应的Class对象

//通过Proxy类创建一个动态代理类所对应的Class对象
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException

参数含义:

  • ClassLoader loader: 定义代理类的类加载器
  • Class<?>... interfaces:  代理类实现的接口列表


完成动态代理操作实例:

1, 定义一个InvocationHandler接口的子类,主要作用是完成代理的具体操作~~~

          接口InvocationHandler中只有一个抽象方法invoke()。。。所有实现InvocationHandler接口的类都必须实现该方法

package org.forfan06.dynaproxydemo;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler{
    private Object obj;  //真实的主体
    public Object bind(Object obj){  //绑定真实操作主体
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }
    //动态调用方法invoke()
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        Object temp = method.invoke(this.obj, args); //调用方法,传入真实主体和参数
        return temp; //返回方法的返回信息
    }
}

         在MyInvocationHandler类的bind()方法中接受被代理对象的真实主体实现,之后覆写InvocationHandler接口中的invoke()方法,完成具体的方法调用。

2, 定义接口

package org.forfan06.dynaproxydemo;
public interface Subject{
    public String say(String name, int age);
}

3,定义真实主体实现类

package org.forfan06.dynaproxydemo;
public class RealSubject implements Subject{
    public String say(String name, int age){
        return "姓名:" + name + ";年龄:" + age;
    }
}

第1,2,3步定义了接口及真实主体类,这样在操作时直接将真实主体类的对象传入到MyInvocationHandler类的bind()方法中即可。

4,测试动态代理:

package org.forfan06.dynaproxydemo;
public class DynaProxyDemo{
    public static void main(String args[]){
        MyInvocationHandler handler = new MyInvocationHandler(); //实例化代理操作类
        Subject sub = (Subject) handler.bind(new RealSubject()); //绑定对象
        String info = sub.say("forfan06", 27); //通过动态代理调用方法
        System.out.println(info);
    }
}

总结:从上面的程序看来,动态代理感觉与静态代理操作没什么不同,操作也比较复杂。 实际上,在普通编程过程中,确实无须用到动态代理,但在编写框架或底层基础代码时,动态代理的作用就非常大!!!! 


=====================================动态代理和AOP==============================================

       当程序通过反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口,动态代理对象就需要实现一个或多个接口里定义的所有方法。但问题是: 系统怎么知道如何实现这些方法??这个时候就轮到InvocationHandler对象登场了 --- 当执行动态代理对象里的方法时,实际上会替换成调用InvocationHandler对象的invoke方法。


只要我们开发一个实际使用的软件系统,就总会存在相同代码段重复出现的情况,在这种情况下,(为了方便讲解,我们准备代码段1、代码段2、代码段3、重复代码段)

  • 对于刚从事软件开发的人而言,他们的做法是: 选择那些代码,一路“复制”、“粘贴”,立即实现了系统功能。但是,如果有一天需要修改重复代码段,则意味着每个源代码(代码段1、代码段2、代码段3)都需要进行修改,那么此段代码的修改、维护工作量将非常大!!!
  • 大部分有经验的开发着会将重复代码段定义成一个方法,然后让代码段1、代码段2、代码段3都直接调用该方法即可。那么如果需要修改公用的代码段,只需要修改一个地方就可以了。而不需要去修改调用该方法的主体。 这种方式大大降低了软件后期维护的复杂度。。。。。但是这种方式依然产生了一个重要的问题:代码段1、代码段2、代码段3和重复代码段是分离开了, 但是代码段1、代码段2、代码段3又与一个特定的方法耦合了!!
  • 最理想的效果是:代码段1、代码段2、代码段3既可以执行“重复代码段”,又无须在程序中以硬编码方式直接调用“重复代码段”方法,这时就可以通过动态代理来达到这种效果

          由于JDK动态代理只能为接口创建动态代理,所以下面先提供一个Dog接口,在该接口中只定义了两个抽象方法。

public interface Dog{
    public void info(); //info方法声明
    public void run();  //run方法声明
}

           上面接口里只是简单地定义了两个方法,并没有提供方法的实现,如果此时我们直接使用Proxy类为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。 在这种情况,我们先为该接口提供一个简单的实现类: GunDog

public class GunDog implements Dog{
     //info方法的实现,仅仅打印一个字符串
     public void info(){
          System.out.println("我是一个猎狗");
     }
     //run方法的实现
     public void run(){
          System.out.println("我奔跑迅速");
     }
}

Dog接口的实现类仅仅是为每个方法提供了一个简单实现。

              我们需要实现的功能:让代码段1、代码段2和代码段3既可以执行重复代码段,又无须在程序中以硬编码方式直接调用重复代码段的方法。此时我们假设info()、run()两个方法代表代码段1、代码段2,那么要求:程序执行info()、run()方法时能调用某个通用方法,但是又不想以硬编码方式调用该方法。

下面提供一个DogUtil类,该类中包含两个通用方法:

public class DogUtil{
     //第一个拦截器方法
     public void method1{
          System.out.println("=====模拟第一个通用方法=====");
     }
     //第二个拦截器方法
     public void method2{
          System.out.println("=====模拟通用方法二=====");
     }
}

           借助于Proxy和InvocationHandler就可以实现 ---- 当程序调用info()、run()方法时,系统可以 “自动”  将method1()和method2()两个通用方法插入info()、run()方法中执行。

此时,程序的关键在于下面的InvocationHandler接口的实现类:MyInvocationHandler类,该实现类的invoke()方法将会作为代理对象的方法实现

public class MyInvocationHandler implements InvocationHandler{
    //需要被代理的对象
    private Object target;
    public void setTarget(Object target){
        this.target = target;
    }
    //执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        DogUtil du = new DogUtil();
        //执行DogUtil对象中的method1方法
        du.method1();
        //以target作为主调来执行method方法
        Object result = method.invoke(target, args);
        //执行DogUtil对象中的method2方法
        du.method2();
        return result;
    }
}

        上面程序实现invoke()方法时包含了一行关键代码(Object result = method.invoke(target, args);) ,这行代码通过反射以target作为主调来执行method方法,这就回调了target对象的原有方法。 在这行代码之前调用DogUtil对象的method1()方法,在其后调用了DogUtil对象的method2()方法。

        下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例。

public class MyProxyFactory{
    //为指定的target生成动态代理对象
    public static Object getProxy(Object target) throws Exception{
        //创建一个MyInvocationHandler对象
        MyInvocationHandler handler = new MyInvocationHandler();
        //为MyInvocationHandler设置target对象
        handler.setTarget(target);
        //创建并返回一个动态代理
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
    }
}

       上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法 --- 从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvocationHandler对象的invoke()方法。 例如,调用动态代理对象的info()方法,程序将开始执行invoke()方法,其执行步骤如下:

  1. 创建DogUtil实例
  2. 执行DogUtil实例的method1()方法
  3. 使用反射以target作为调用者执行info()方法
  4. 执行DogUtil实例的method2()方法

从以上执行过程,可以发现:当使用动态代理对象来替换target对象时,代理对象的方法就实现了前面的要求 --- 程序执行info()、run()方法时既能 “插入” method1()、method2()通用方法,但GunDog()方法中又没有以硬编码方式调用method1()和method2()方法。

  下面提供一个主程序来测试这种动态代理的效果

public class Test{
    public static void main(String args[]) throws Exception{
        //创建一个原始的GunDog对象,作为target
        Dog target = new GunDog();
        //以指定的target来创建动态代理对象
        Dog dog = (Dog)MyProxyFactory.getProxy(target);
        dog.info();
        dog.run();
    }
}

  上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行dog的info()、run()方法时,实际上会先执行DogUtil的method1()方法,再执行target对象的info()、run()方法,最后执行DogUtil的method2()方法。

**********采用动态代理可以非常灵活地实现解耦。通常而言,当我们使用Proxy生成一个动态代理时,往往不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象生成动态代理**********
     这种动态代理在AOP(Aspect Orient Programming,面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理!!!!!!!!!!!!


补充一个实例: 使用Proxy和InvocationHandler来生成动态代理对象。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Person{
    void walk();
    void sayHello(String name);
}
class MyInvocationHandler implements InvocationHandler{
    /*
    执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
    其中:
    proxy:  代表执行代理对象
    method: 代表正在执行的方法
    args: 代表调用目标方法时传入的实参
    */
    public Object invoke(Object proxy, Method method, Object[] args){
        System.out.println("------正在执行的方法:" + method);
        if(args != null){
            System.out.println("下面是执行该方法时传入的实参为: ");
            for(Object val:args){
                System.out.println(val);
            }
        }else{
            System.out.println("调用该方法没有实参!");
        }
        return null;
    }
}
public class ProxyTest{
    public static void main(String args[]) throws Exception{
        //创建一个InvocationHandler对象
        InvocationHandler handler = new MyInvocationHandler();
        //使用指定的InvocationHandler来生成一个动态代理对象
        Person p = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, handler);
        //调用动态代理对象的walk()、sayHello()方法
        p.walk();
        p.sayHello("forfan06");
    }
}

运行结果:

------正在执行的方法:public abstract void Person.walk()
调用该方法没有实参!
------正在执行的方法:public abstract void Person.sayHello(java.lang.String)
下面是执行该方法时传入的实参为: 
forfan06

     以上程序首先提供了一个Person接口,该接口中包含了walk()和sayHello()两个抽象方法,接着定义了一个简单的InvocationHandler实现类MyInvocationHandler,定义该实现类时需要覆写invoke()方法  -- 调用代理对象的所有方法时都会被替换成调用此invoke()方法。该方法的三个参数解释如下:

proxy: 代表动态代理对象

method: 代表正在执行的方法。 例如 上面的程序就是 walk()方法。一般会在invoke()里面增加一些通用方法(譬如验证用户合法性什么的),也要包括调用目标方法的语句(用method.invoke(proxy,args)

args: 代表调用目标方法时传入的实参
 

从程序结果来看,不管程序是执行代理对象的walk()方法,还是执行代理对象的sayHello()方法,实际上都是执行了InvocationHandler对象的invoke()方法!!!!!!!!!

=====================================动态代理和AOP============================================== 

15.6 类的生命周期

     在一个类编译完成之后,下一步就要开始使用类。如果要使用一个类,肯定离不开JVM。在程序执行中JVM通过加载、链接、初始化3个步骤完成。类的加载就是通过类加载其把 .class 二进制文件装入JVM的方法区,并在堆区创建描述该类的java.lang.Class对象,用来封装数据。 需要注意的是,同一个类只会被JVM加载一次。链接就是把二进制数据组装成可以运行的状态。

    链接分为校验、准备和解析3个步骤。校验用来确认此二进制文件是否适合当前的JVM(版本);准备就是为静态成员分配内存空间,并设置默认值;解析指的是转换常量池的代码引用为直接引用的过程,直到所有的符号引用都可以被运行程序使用(建立完整的对应关系)。 完成之后,类型即可初始化,初始化之后类的对象就可以正常地使用,直到一个对象不再被使用之后,将被垃圾回收,释放空间。当没有任何引用指向Class对象时将会被卸载,结束类的生命周期。


15.7 工厂设计模式

        15.7.1 将反射应用在工厂模式上

       工程设计模式在实际的开发中使用非常多。之前也讲过简单的工厂模式,通过简单的工厂设计模式可以达到类的解耦合目的,但是,也存在一个重要的问题:在增加一个子类时都需要修改工厂类,这样代码的修改和维护会很麻烦。学习了反射机制后,就可以通过发射机制来改善工厂类,让其在增加子类时可以不用做任何的修改,就可以达到功能的扩充。

(一)使用反射完成工厂设计

package org.forfan06.factorydemo;
interface Fruit{
    public void eat();
}
class Apple implements Fruit{
    public void eat(){
        System.out.println("**吃苹果**");
    }
}
class Orange implements Fruit{
    public void eat(){
        System.out.println("**吃橘子**");
    }
}
class Factory{
    public static Fruit getInstance(String className){
        Fruit fruit = null;
        try{
            fruit = (Fruit) Class.forName(className).newInstance();
        }catch(ClassNotFoundException e){
            e.printStackTrace();
        }
        return fruit;
    }
}
public class FactoryDemo01{
    public static void main(String args[]){
        //通过工厂类取得接口实例,传入完整的包.类名称
        Fruit f = Factory.getInstance("org.forfan06.factorydemo.Apple");
        if(f != null){
            f.eat();
        }
    }
}
此时,上面的工厂操作类中使用了反射操作来取得Fruit实例,这样无论增加多少个子类,工厂类都不用去做任何修改。

        15.7.2 结合属性文件的工厂模式

     上面操作代码虽然通过反射取得了接口的实例,但是在操作时还是需要传入完整的 包.类名称,而且用户也无法知道一个接口有多少个可以使用的子类。

     所以此时可以通过属性文件的形式配置所要的子类信息

(一)范例:属性文件fruit.properties

apple = org.forfan06.factorydemo.Apple
orange = org.forfan06.factorydemo.Orange

在属性文件中使用apple和orange表示完整的包.类名称,这样在使用时可以直接通过属性名称即可。

(二)范例: 属性操作类

class Init{
    public static Properties getPro(){
        Properties pro = new Properties();
        File file = new File("D:" + File.separator + "fruit.properties");
        try{
            if(file.exists()){
                pro.load(new FileInputStream(file));
            }else{
                pro.setProperty("apple", "org.forfan06.factorydemo.Apple");
                pro.setProperty("orange", "org.forfan06.factorydemo.Orange");
                pro.store(new FileOutputStream(f), "FRUIT CLASS");
            }
        }catch(){
            e.printStackTrace();
        }
        return pro;
    }
}

此类的主要功能是取得属性文件中的配置信息。如果属性文件不存在,则创建一个新的,并设置默认值

(三)测试程序

public class FactoryDemo02{
    public static void main(String args[]){
        Properties pro = Init.getPro();
        Fruit f = Factory.getInstance(pro.getProperty("apple"));
        if(f != null){
            f.eat();
        }
    }
}

       通过工厂类取得接口实例时,直接输入属性的key就可以找到其完整的包.类名称,以达到对象的实例话功能。

(四)总结

在本程序中可以发现,程序很好地实现了代码与配置文件的分离。通过配置文件配置要使用的类,之后通过程序读取配置文件,完成具体的功能。

当然,这些程序完成的前提是基于接口,所以接口在实际的开发中用处是最大的!!!

15.8 本章要点

  1. Class类是反射机制操作的源头
  2. Class类的对象有3种实例化方式:(1)通过Object类中的getClass()方法;(2)通过 “ 类.class ” 的形式获取Class类的实例化对象;(3)通过Class.forName()方法,此种方法最为常用。
  3. 可以通过Class类中的newInstance()方法进行对象的实例化操作,但是要求类中必须要存在无参构造方法。如果类中没有无参构造方法,则必须使用Constructor类来完成对象的实例化操作(Constructor类中的newInstance()方法)
  4. 可以通过反射取得一个类所继承的父类、实现的接口、类中的全部构造方法、全部普通方法、全部属性
  5. 使用反射机制可以通过Method调用类中的方法;也可以通过Field类直接操作类中的属性
  6. 动态代理可以解决开发中代理类过多的问题,提供统一的代理功能实现
  7. 在程序开发中使用反射机制并结合属性文件,可以达到代码与配置文件分离的目的。

15.9 习题

     定义一个学生类,其中包含姓名、年龄、成绩的属性。之后由键盘输入学生的内容,并将内容保存在文件中。所有的操作要求全部使用反射机制完成,即不能使用通过关键字new创建学生类对象的操作。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值