JAVA SE学习笔记(五)Java反射原理和代理机制

  • 1 类的加载、连接和初始化
    • 1.1 JVM和类
    • 1.2 类的加载
    • 1.3 类的连接
    • 1.4 类的初始化
    • 1.5 类初始化的时机
  • 2 类加载器
    • 2.1 简介
    • 2.2 类加载机制
  • 3 反射
    • 3.1 通过反射分析类
      • 3.1.1 获得Class对象
      • 3.1.2 从Class中获取信息
      • 3.1.3 Java 8新增的方法参数反射
    • 3.2 通过反射操作类
      • 3.2.1 创建对象
      • 3.2.2 调用方法
      • 3.2.3 访问成员变量
      • 3.2.4 操作数组
  • 4 代理机制
    • 4.1 代理的作用
    • 4.2 Java动态代理机制
      • 4.2.1 使用Proxy和InvocationHandler创建动态代理
      • 4.2.2 动态代理和AOP

1 类的加载、连接和初始化

  Java类加载器除了根类加载器之外,其他类加载器都是使用Java语言编写的

1.1 JVM和类

  • 同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区,不同的JVM并不会共享数据
  • JVM进程的终止:
    • 程序运行到最后正常的结束
    • 程序运行到使用System.exit()Runtime.getRuntime().exit()代码处结束程序
    • 程序执行过程中遇到未捕获的异常或错误而结束
    • 程序所在平台强制结束了JVM进程

1.2 类的加载

  • 类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象
  • 类的加载由类加载器完成,类加载器由JVM提供,JVM提供的这些加载器通常被称为系统类加载器,除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器

1.3 类的连接

  • 连接阶段负责把类的二进制数据合并到JRE中
  • 类连接的三阶段
    • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致
    • 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值
    • 解析:将类的二进制数据中的符号引用替换成直接引用

1.4 类的初始化

  • 虚拟机负责对类进行初始化,主要就是对类变量进行
  • JVM初始化一个类包含如下几个步骤:
    • 假如这个类还没有被加载和连接,则程序先加载和连接这个类
    • 假如这个类的直接父类还没有被初始化,则先初始化其直接父类
    • 假如类中有初始化语句,则系统依次执行这些初始化语句
  • JVM最先初始化的永远是java.lang.Object

1.5 类初始化的时机

  • 初始化的时机:
    • 创建类的实例:使用new创建、通过反射来创建、通过反序列化来创建
    • 调用某个类的类方法
    • 访问某个类或接口的类变量,或为该类变量赋值
    • 使用反射的方式来强制创建某个类或接口对应的java.lang.Class对象
    • 初始化某个类的子类
    • 直接使用java.exe命令来运行某个主类
  • 注意:final类变量,如果在编译时值就可以确定,那么在编译时就定下来了,使用它不会导致类的初始化,如果值不能确定,将会导致类的初始化
  • 当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化;使用Class类的forName()静态方法才会导致强制初始化该类

2 类加载器

  类加载器负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象

2.1 简介

  在Java中,一个类使用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类使用其全限定类名和其类加载器作为其唯一标识

  • JVM三个类加载器组成的初始化类加载器层次结构:
    • Bootstrap ClassLoader:根类加载器,负责加载Java的核心类
    • Extension ClassLoader:扩展类加载器,负责加载JRE的扩展目录中JAR包的类
    • System ClassLoader:系统类加载器,负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径

2.2 类加载机制

  • 类加载机制:
    • 全盘负责:类加载器负责加载和这个类相关的所有类
    • 父类委托:首先使用父类加载器加载,加载失败的话再采用自己的类路径中加载该类
    • 缓存机制:保证加载过的类全部被缓存下来,当程序需要使用某个类时,首先从缓存中检索,只有不存在的时候才会重新加载
  • 类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系,根类加载器 > 扩展类加载器 > 系统类加载器 > 用户类加载器
  • JVM的根类加载器不是Java实现的,程序通常无法访问根类加载器,因此访问扩展类加载器的父类加载器时返回null
    // 获取系统类加载器
    ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
    System.out.println("系统类加载器:" + systemLoader);
    /*
        获取系统类加载器的加载路径——通常由CLASSPATH环境变量指定
        如果操作系统没有指定CLASSPATH环境变量,默认以当前路径作为
        系统类加载器的加载路径
    */
    Enumeration em1 = systemLoader.getResources("");
    while(em1.hasMoreElements()){
        System.out.println(em1.nextElement());
    }
    // 获取系统类加载器的父类加载器:得到扩展类加载器
    ClassLoader extensionLader = systemLoader.getParent();
    System.out.println("扩展类加载器:" + extensionLader);
    System.out.println("扩展类加载器的加载路径:" + System.getProperty("java.ext.dirs"));
    System.out.println("扩展类加载器的parent: " + extensionLader.getParent());
  • 类加载器加载Class的步骤:
    1. 检测此Class是否载入过,如果有直接进入第8步,否则执行第2步;
    2. 如果父类加载器不存在(parent是根类加载器,或本身是根类加载器),则跳到第4步执行,如果父类加载器存在,则接着执行第3步;
    3. 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步;
    4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步;
    5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,否则跳到第7步;
    6. 从文件中载入Class,成功载入后跳到第8步;
    7. 抛出ClassNotFoundException异常;
    8. 返回对饮的java.lang.Class对象。

3 反射

  由于Java中的对象在运行时会出现两种类型:编译时类型和运行时类型,程序需要在运行时发现对象和类的真实信息,方式:

  • 假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成其运行时类型的变量即可
  • 编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真是信息,这就必须使用反射

  能够分析类能力的程序称为反射(reflective),反射机制的功能极其强大,可以用来:

  • 在运行时分析类的能力;
  • 在运行时查看对象;
  • 实现通用的数组操作代码;
  • 利用Method对象,这个对象很像C++中的函数指针。

反射是一种功能强大且复杂的机制,使用它的主要人员是工具构造者。

3.1 通过反射分析类

3.1.1 获得Class对象

  在程序运行期间,JRE始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类,JVM利用运行时类型信息选择相应的方法执行,这些信息可以通过Class类来访问。Object类(所有类的父类)中的getClass()方法将会返回一个Class类型的实例,一个Class对象将标识一个特定类的属性。虚拟机为每个类型管理一个Class对象,因此,可以用==运算符实现两个类对象比较的操作。在获取类名称的时候,可以利用forNamenewInstance配合来创建一个对应对象,但是假如该类没有默认构造器(构造器参数为0)的话,就会抛出一个异常。

  • 获得Class类对象的三种方式:

    • getClass()方法:需要对应对象的一个实例
    • forName()方法:需要对应对象的完整类名称
    • T.class方法:需要对应对象的类型T
  • 注意: Class是一个类,而Class对应的类型并不一定是类,例如:int.class

3.1.2 从Class中获取信息

  在java.lang.reflect包中有三个类FieldMethodConstructor,分别用于描述类的域、方法、构造器。这三个类都有一个叫做getName()的方法,用来返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。MethodConstructor有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述publicstatic这样的修饰符使用情况。另外,可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。
  Class类中的getFieldsgetMethodsgetConstructors方法将分别返回类提供的public域、方法和构造函数,其中包括超类公有成员。Class类的getDeclareFieldsgetDeclaredMethodsgetDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但是不包括超类的成员。

3.1.3 Java 8新增的方法参数反射

  • 新增Executable抽象类,该对象代表可执行的类成员,该类派生出了ConstructorMethod两个子类
  • 提供的方法:
    • 获取修饰该方法或构造器的注解信息
      • isVarArgs():判断该方法或构造器是否包含数量可变的形参
      • getModifiers():获取该方法或构造器的修饰符
      • getParameterCount():获取该构造器或方法的形参个数
      • getParameters():获取该构造器或方法的所有形参
  • 新增Parameter API,每个Parameter对象代表方法或构造器的一个参数,提供大量方法来获取申明该参数的泛型信息,
    • getModifiers():获取修饰该形参的修饰符
    • getName():获取形参名
    • getParameterizedType():获取带泛型的形参类型
    • getType():获取形参类型
    • isNamePresent():该方法返回该类的class文件中是否包含了方法的形参名信息

3.2 通过反射操作类

  程序可以通过Method对象来执行对应的方法,通过Constructor对象来调用对应的构造器创建实例,能通过Field对象直接访问并修改对象的成员变量值

3.2.1 创建对象

  • 通过反射来生成对象的两种方式:

    • 使用Class对象的newInstance()方法来创建该Class对象对应类的实例,这种方式要求该Class对象的对应类有默认构造器,而执行newInstance()方法时实际上是利用默认构造器来创建该类的实例。
    • 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例。
  • 利用指定的构造器来创建Java对象的步骤:

    • 获取该类的Class对象;
    • 利用Class对象的getConstructor()方法来获取指定的构造器;
    • 调用ConstructornewInstance()方法来创建Java对象。
  • 注意:通过反射创建对象时性能要稍低一些,实际上只有程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。

3.2.2 调用方法

  • 获得某个类的Class对象后,就可以通过该Class对象的getMethods()方法或getMethod()方法来获取方法了,每个Method对象对应一个方法,可以通过该Method对象来调用它对应的方法,在Method里包含一个invoke()方法,该方法签名:Object invoke(Object obj, Object... args);该方法中的obj是执行该方法的主调,后面的args是执行该方法时传入该方法的实参。
  • 调用private方法:setAccessible(boolean flag);将Method对象的accessible设置为指定的boolean值,值为true,指示该Method在使用时应该取消Java语言的访问权限审查;值为false,指示该Method在使用时要实施Java语言的访问权限检查

3.2.3 访问成员变量

  • 通过Class对象的getFields()getField()方法可以获取该类所包括的全部成员变量或指定成员变量,Field提供了如下两组方法来读取或设置成员变量值。
    • getXxx(Object obj):获取obj对象的该成员变量的值。此处的Xxx对应8种基本类型,如果该成员变量的类型是引用型,则取消get后面的Xxx
    • setXxx(Object obj, Xxx val):将obj对象的该成员变量设置成val值。此处的Xxx对应8种基本类型,如果该成员变量的类型是引用型,则取消set后面的Xxx

3.2.4 操作数组

  • java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。程序可以通过使用Array来动态地创建数组,操作数组元素等
  • Array提供了如下几类方法:
    • static Object newInstance(Class componentType, int… length):创建一个具有指定的元素类型、指定维度的新数组
    • static xxx getXxx(Object array, int index):返回Array数组中第index个元素
    • static void setXxx(Object array, int index, xxx val):将array数组的第index个元素的值设为val

4 代理机制

  使用代理可以在运行时创建一个实现了一组给定接口的新类,这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用,主要用于编写框架和底层基础代码

4.1 代理的作用

  • 代理类可以在运行时创建全新的类,这样的代理类能够实现指定的接口,并具有下列方法:
    • 指定接口所需要的全部方法;
    • Object类中的全部方法。
  • 调用处理器:由于不能在运行时定义这些方法的新代码,需要通过调用处理器(实现了InvocationHandler接口的类对象,在这个接口中只有一个方法:Object invoke(Object proxy, Method method, Object[] args);,无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数)来实现。
  • 使用代理的原因:
    • 路由对远程服务器的方法调用
    • 在程序运行期间,将用户接口事件与动作关联起来
    • 为调试、跟踪方法调用

4.2 Java动态代理机制

  在Java的·java.lang.reflect·包下提供了一个·Proxy·类和一个·InvocationHandler·接口,通过使用这个类和接口可以生成动态代理类或动态代理对象。

4.2.1 使用Proxy和InvocationHandler创建动态代理

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

  • Proxy提供了如下两个方法来创建动态代理类和动态代理实例:
    • static Class getProxyClass(ClassLoader loader, Class… interfaces):创建一个动态代理类所对应的Class对象,该代理类将实现interfaces所指定的多个接口。第一个ClassLoader参数指定生成动态代理类的类加载器。
    • static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了interfaces指定的系列接口,执行代理对象的每个方法都会被替换执行InvocationHandler对象的invoke方法。
  • InvocationHandler:每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的invoke方法来进行调用。
  • invoke方法:Object invoke(Object proxy, Method method, Object[] args);
    • proxy:被代理的对象
    • method:被代理的对象的某个方法的Method对象
    • args:调用真实对象某个方法时的参数
  • 通过Proxy类的Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h);方法创建一个代理对象,该方法有三个参数:
    • 类加载器:null(使用默认加载器);定义了由哪个ClassLoader对象来对生成的代理对象进行加载
    • Class对象数组,每个元素都是需要实现的接口;给被代理的对象提供一组接口,这个代理对象实现了该接口(多态),这样就可以调用这组接口中的方法
    • 调用处理器:表示的是当动态代理对象在调用方法的时候,关联的InvocationHandler对象
interface Person {
    void walk();
    void sayHello(String name);
}
class MyInvokationHandler 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 MyInvokationHandler();
        // 使用指定的InvocationHandler来生成一个动态代理对象
        Person p = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[]{Person.class}, handler);
        // 调用动态代理对象的walk()和sayHello()方法
        p.walk();
        p.sayHello("孙悟空");
    }
}
  • 注意:通过 Proxy.newProxyInstance创建的代理对象是在JVM运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。

参考博客:java的动态代理机制详解

4.2.2 动态代理和AOP

  • 为了解决代码的耦合和重复,又不愿在程序中以硬编码方式调用额外的代码,可以使用动态代理方法实现,采用动态代理可以非常灵活地实现解耦
interface Dog {
    // info方法声明
    void info();
    // run方法声明
    void run();
}
class GunDog implements Dog {
    // 实现info()方法,仅仅打印一个字符串
    public void info() {
        System.out.println("我是一只猎狗");
    }
    // 实现run()方法,仅仅打印一个字符串
    public void run() {
        System.out.println("我奔跑迅速");
    }
}
class DogUtil {
    // 第一个拦截器方法
    public void method1() {
        System.out.println("=====模拟第一个通用方法=====");
    }
    // 第二个拦截器方法
    public void method2() {
        System.out.println("=====模拟通用方法二=====");
    }
}
class MyInvokationHandler implements InvocationHandler
{
    // 需要被代理的对象
    private Object target;
    public void setTarget(Object target) {
        this.target = target;
    }
    // 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
        DogUtil du = new DogUtil();
        // 执行DogUtil对象中的method1。
        du.method1();
        // 以target作为主调来执行method方法
        Object result = method.invoke(target , args);
        // 执行DogUtil对象中的method2。
        du.method2();
        return result;
    }
}
class MyProxyFactory {
    // 为指定target生成动态代理对象
    public static Object getProxy(Object target) throws Exception {
        // 创建一个MyInvokationHandler对象
        MyInvokationHandler handler = new MyInvokationHandler();
        // 为MyInvokationHandler设置target对象
        handler.setTarget(target);
        // 创建、并返回一个动态代理
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces() , handler);
    }
}
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();
    }
}
  • AOP(Aspect Oriented Programming,面向切面编程):AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法,AOP代理的方法可以在执行目标方法之前、之后插入一些通用处理。

图4-1 AOP代理的方法与目标对象的方法示意图

JAVA SE学习笔记(五)Java反射原理和代理机制_第1张图片

你可能感兴趣的:(JAVA,SE)