当前位置: 首页 > 工具软件 > advanced-java > 使用案例 >

[Advanced Java] 2 反射

呼延俊风
2023-12-01

2 反射

反射是Java利用JVM底层的一种机制,目的是提高耦合和扩展性。

2.1 内存的作用和Java的运行机制

内存的作用:内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。

Java的运行机制:在Java运行时,会分为编译、加载两个阶段:

  1. 编译:将Java文件,在磁盘中转化为.class文件(二进制码)。
  2. 加载:将需要的.class文件从磁盘加载到内存中。

2.2 .class文件

2.2.1 Java文件转化为.class文件的特性

  • 若Java文件中只存在一个类或者接口,编译后产生一个.class文件。命名格式为“类名.class”。
  • 若Java文件中存在内部类。编译后产生多个.class文件,其中内部类命名为“外部类 + $ + 内部类.class”。
  • 若Java文件中存在多个并行类(或接口),编译时产生多个.class文件。命名格式为“类名.class”。

2.2.2 .class文件结构

名称长度功能
Magic4个字节判定是否为Java文件
Version: Major_version/Minor_version分别2个字节主次版本号,判断jvm是否有能力处理该文件
Constantpool: Constantpool_count/Constantpool不固定常量池,包含类和接口的相关常量
Access_flags2个字节指明类型,类或者接口、抽象或者具体。公共、final等修饰符
This_class2个字节索引,指向常量池中属于类的常量
Super_class2个字节指向父类全限定名
Interface: Interface_count/Interfaces不固定该类实现接口的数量/常量池引用
Field: FieldsCount/Fields不固定字段数量和信息表。描述字段的类型、描述符等
Method: Methods_count/Methods不固定方法数量和方法本身。每一个方法都有一个Method_info表,记录Method的属性
Attribute: Attributes_count/Attributes不固定属性数量和属性本身

2.3 信息加载

注意:加载——将.class文件从磁盘移动到内存;装载——包含加载、校验、准备,将.class转化为Class类

2.3.1 Java基础类的加载

1. 预先装载

  1. 在JDK目录找到并载入jvm.dll,启动虚拟机,初始化数据。
  2. 创建一个BootstrapLoader对象——启动类装载器,由C++编写。此类负责一次性加载JVM的基础类。
  3. 其中JVM的基础类包含sum.misc命下空间的Launcher类:ExtClassLoader——加载AppClassLoader的加载器,由Bootstrap Loader加载;AppClassLoader——加载类的加载器,由ExtClassLoader加载。

2. 按需装载

当需要某个类时,JVM才会去动态装载它。

  1. 装载条件:当使用该类的a. 静态方法;b. 静态属性;c. 构造方法。

    注意:类中的静态常量属性处于常量池,所以没必要初始化该类;构造方法是一种特殊的静态方法,所以当创建该类的实例时就会装载该类。

  2. 按需装载流程:

    1. 先查看是否加载。
    2. 若加载则直接调用结束,若未加载则继续。
    3. 查找对应的.class文件,载入.class文件,生成并链接(校验、准备、解析)该类的Class对象。
    4. 初始化静态变量并执行该类的静态域代码。

2.3.2 类加载器

当创建一个Java类的实例时,必须先将该类加载到内存中。JVM使用类加载器来加载类。Java加载器在Java核心和CLASSPATH环境中的所有类查找类。如果需要的类找不到,则会抛出ClassNotFoundException异常。

Java类加载器分为:bootstrap类加载器、extension类加载器和system类加载器。顺序为结构的顶端到底层。

  1. bootstrap类加载器用于引导JVM,一旦调用java.exe程序,bootstrap类加载器就开始工作。因此,它必须使用本地代码实现,然后加载JVM需要的类到函数中。另外,还负责加载所有的Java核心类,例如java.lang和java.io包。另外,bootstrap类加载器还会查找核心类库如rt.jar、il8n.jar等。这些类库根据JVM和操作系统来查找。
  2. extension类加载器负责加载标准扩展目录下面的类。这样就可以使得编写程序变得简单,只需把JAR文件复制到扩展目录下面即可,类加载器会自动地在下面查找。不同的供应商提供的扩展类库是不同的,Sun公司的JVM的标准扩展目录是/jdk/jre/lib/ext。
  3. system加载器是默认的加载器,它在环境变量CLASSPATH目录下面查找相应的类。

JVM使用类加载器决定于委派模型(delegation model),出于安全原因,每次类都需要加载,system类加载器首先调用。但是,它不会马上加载类。相反,它委派该任务给它的父类extension类加载器,然后extension类加载器也罢任务委派给它的父类bootstrap类加载器。因此,bootstrap类加载器总是先加载类。如果bootstrap类加载器不能找到所需要的类,则extension类加载器会尝试加载,如果扩展类加载器也失效,system类加载器将执行任务。如果最后系统类加载器找不到类,则会抛出一个ClassNotFoundException异常。

Java类加载器机制的优势在于可以通过扩展java.lang.ClassLoarder抽象类来扩展自己的.class文件。自定义自己的类加载器的好处在于:

  1. 要制定类加载器的某些特定规定,例如加载指定目录下的类文件、加载经过加密的.class类文件。
  2. 缓存以前加载的类。
  3. 实现加载类以预备使用。
  4. 当.class文件修改后自动加载新的类文件。

2.3.3 类的加载顺序

  • 父类先加载,子类后加载。
  • 引用类若未初始化,则不加载引用类;若引用类已初始化,则在该类之后初始化引用类。

2.4 Class类

Class类的对象用来表示运行时类或者接口的信息。Java中枚举是一种类、注解是一种接口,数据也被看做一个类。这些类的信息,在运行时都由Class类来描述。对应数组而言,具有相同元素类型和维数的数组共享一个Class对象。可以通过Class对象获取类名、父类等信息,并可通过Class类来获取该类的属性、方法、构造方法、包等。

通过实例获取的Class类可以通过反射得到该实例对应的属性值,以及注解等信息。

2.4.1 获取Class对象

  1. .class

    Class<Person> clazz1 = Person.class;
    
  2. getClass()方法

    Person person1 = new Person();
    Class<Person> clazz2 = person1.getClass();
    
  3. forName(className)方法

    String className = "xxx.xxx.reflect.packageName.Person";
    Class<?> clazz3 = Class.forName(className);
    

2.4.2 获取Construct对象

方法体返回类型说明
getConstructor(Class…parameterTypes)Constructor其中参数为指定构造参数中类型的class数组,该方法只能获取public构造方法
getConstructors()Constructor[]获取指定类的public构造函数,若没有则返回长度为0的Construct数组
getDeclaredConstructor(Class…parameterTypes)Constructor可获取所有的构造方法的对象
getDeclaredConstructors()Constructor[]同理

2.4.3 获取Method对象

这里补充两个知识点:

final方法
使用final的方法不需要再扩展,不能被子类重写。同时允许编译器将素有对此方法的调用转化为inline(行内,来源于C++)调用机制。当调用final方法时,直接将方法主体插入到调用处,并非进行例行的方法调用,例如保存断点、压栈等。这样可能会使得程序效率有所提高,然后方法主体庞大时,或在多处调用此方法时,那么调用主体代码便会迅速膨胀,反而会降低效率。

native方法
Native Method就是一个Java调用非Java代码的接口。该类方法的实现由非Java语言实现,例如C。这个特性其他的编程语言都有这一机制,例如在C++中,可以用extern ”C”告知C++编译器去调用一个C的函数。在定义一个Native Method时,并不提供实现体(有些定义一个Java Interface),因为其实现体是由非Java语言在外面实现的。

获取方法:

方法体返回值说明
getMethod(String name, Class…parameterTypes)Method同理
getMethods()Method[]同理
getDeclaredMethod(String name, Class…parameterTypes)Method同理
getDeclaredMethods()Method[]同理

2.4.4 获取Field对象

方法体返回值说明
getField(String name)Field同理
getFields()Field[]同理
getDeclaredField(String name)Field同理
getDeclaredFields()Field[]同理

2.5 运行时类型识别

运行时类型识别:在程序运行时,动态地识别对象和类的信息。

识别方法:

  1. 关键字instanceof

    二元运算符,前者是对象,后者是类。用于判断一个对象是否是该类或者该类的子类的实例。

  2. Class.isInstanceof()

    效果同上,但是该方法更适用于遍历。

2.6 反射

2.6.1 概念

Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,修改它的任意属性,这种动态获取的信息以及动态调用对象成员的功能称为Java语言的反射机制。

2.6.2 作用

  1. 提高了程序的灵活性和扩展性。
  2. 让程序可以创建和控制任何类的对象,无需提前硬编码目标类。
  3. 让程序可以在运行时构造一个类的对象,调用一个对象的方法。
  4. 反射机制是构建框架技术的基础所在。

2.6.3 应用

注意:以下前4点仅适用于共有方法或共有属性。

1. 显式加载指定类

使用Class类中forName(String name)方法

2. 通过反射实例化类

  1. Class类的newInstance()方法

    该方法会调用该类的无参构造进行实例化。(警告:该方式在Java1.9版本中已被弃用)

  2. Constructor类的newInstance()方法

    该方法需要在Class中通过指定参数类型获取指定的构造方法,相比较第一个方法更明确化,明确调用的是哪一个构造器,而不是直接采用默认的无参构造器。

3. 通过反射执行方法

使用Method类的invoke(Object obj, Object… args)方法。

下面是invoke()方法的源码:

public Object invoke(Object obj, Object... args)
              throws IllegalAccessException,
                     IllegalArgumentException,
                     InvocationTargetException

obj——从中调用底层方法的对象(简单的说就是调用谁的方法用谁的对象)。

args——用于方法调用的参数。

  • 如果底层方法是静态的,那么可以忽略指定的obj参数。该参数可以为null。
  • 如果底层方法所需的形参数为0,则所提供的args数组长度可以为0或null。
  • 如果底层方法是静态的,并且尚未初始化声明此方法的类,则会将其初始化。
  • 如果方法正常完成,则将该方法返回的值返回给调用者;如果该值为基本类型,则首先适当地将其包装在对象中。但是,如果该值的类型为一组基本类型,则数组元素不被包装在对象中;换句话说,将返回基本类型的数组。如果底层方法返回类型为void,则该调用返回null。

4. 通过反射修改属性

使用Field的set(Object obj, Object value)方法。

obj——表示要修改的属性对象。

value——表示修改后的值。

该方法用于动态地给属性赋值。

5. 修改访问权限

使用目标对象的setAccessible(boolean flag)方法。

该方法来源于AccessibleObject类,Field类、Method类和Constructor类都继承了AccessibleObject,都允许被setAccessible()方法从私有设置为共有。

6. 反射中的各种异常分析

  1. ClassNotFoundException

    抛出该异常的原因是未在命名空间内找到指定的类,有可能是因为类名错误,或者是类文件不存在。抛出该异常时请检查指定的类是否存在,或检查类名是否正确是否完整(类名英文全类名即简单类名加上完整包名)

  2. SecurityException

    该异常是由安全管理器抛出的异常,指示存在安全侵犯。例如修改不允许修改的accessible标志时,会该异常。

  3. NoSuchMethodException

    无法找到某一特定方法时,抛出该异常。抛出异常时,可打印指定类的所有字段名,进行比较检查。

  4. NoSuchFieldException

    无法找到指定字段时,抛出该异常。抛出异常时,可打印指定类的所有字段名,进行比较检查。

  5. IllegalArgumentException

    抛出的异常表明向方法传递了一个不合法或者不正确的函数。可获取Method对象的参数,进行比较检查。

  6. InstantiationException

    当应用程序试图使用Class类中的newInstance方法创建一个类的实例,而指定的类对象无法被实例化,抛出该异常。

  7. IllegalAccessException

    当应用程序试图反射性地创建一个实例(而不是一个数组)、设置或获取一个字段,或者一个方法,但当前正在指定的方法无法访问指定类、字段、方法或构造方法的定义时,抛出该异常。例如该方法本身为私有属性,试图通过反射得到该方法被视为非法。

2.7 代理

2.7.1 代理模式

1. 概念

代理模式是指为目标对象提供一个代理对象,外部对目标对象的访问,通过代理委托进行,以达到控制访问的目的。为保持行为的一致性,代理类通常与委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。

2. 作用

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

3. 使用方法

代理模式涉及三个角色:

  • 抽象角色:声明真是对象和代理对象的共同接口。
  • 代理角色:代理对象角色内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象想同的接口,以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象的操作时,附加其他的操作,相当于对真实对象进行封装。
  • 真实角色:代理角色代表的真实对象,是我们最终要引用的对象。

使用时创建对象后,再创建其代理对象,然后调用代理对象的方法即可。

4. 使用示例

public interface Speakable() {
    public void speak(String message);
}
public class Person implements Speakable {
    @Override
    public void speak(String message) {
        System.out.println("Speak: " + message);
    }
}
public class PersonProxy implements Speakable {
    private Person person;
    public PersonProxy(Person person) {
        this.person = person;
    }
    @Override
    public void speak(String message) {
        this.person.speak(message);
        System.out.println("Running time: " + System.currentTimeMillis());
    }
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        PersonProxy proxy = new PersonProxy(person);
        proxy.speak("Lesson one!");
    }
}

运行效果:

Speak: Lesson one!
Running time: 1234567

5. 评价

代理模式的确解决了很多问题,但同时也给我们增加了一些负担,因为必须为委托类维护一个代理,不易管理而且增加了代码量。Java的动态代理机制的思想则更加先进一步。

2.7.2 Java动态代理

1. 概念

利用Java的反射机制,在程序运行时,创建目标对象的代理对象,并对目标对像中的方法进行功能性增强的技术。代理对象会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能,这时一套非常灵活有弹性的代理框架。

2. 使用示例

注意,在此基础上了解使用方法即可,底层其实更为复杂。

public class DynamicProxy{
    public static Speakable createSpeakableProxy(Speakable speakable){
        Speakable speakableProxy = (Speakable) Proxy.newProxyInstance(
                DynamicProxy.class.getClassLoader(),//Using this class loader to load the proxy.
                new Class[]{Speakable.class},//Interface class files which the proxy need to implements.
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        /*functions*/
                        Object result = method.invoke(speakable, args);
                        /*functions*/
                        return result;
                    }
                });
        return speakableProxy;
    }
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        Speakable personProxy = DynamicProxy.createSpeakableProxy(person);
        personProxy.speak("Test message");
    }
}

2.7.3 动态代理机制的特点与不足

动态生成的代理类特点:

  1. 包:如果所带来的接口都是public的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非public的接口(因为接口不能被定义为protect或private,所以除了public之外就是默认的package访问级别),那么它将被定义在该接口所在包(假设代理了org.ddd.reflect包中的某些非public接口A,那么新生成的代理类所在的包就是org.ddd.reflect),这样设计的目的是为了最大程度地保证动态代理类不会因为包管理的问题而无法被成功定义并访问。
  2. 类修饰符:该代理类具有final和public修饰符,意味着它可以被所有的类访问,但是不能被再度继承。
  3. 类名:通过调用其Class类的TypeName,得到其格式是“$ProxyN”,其中N是一个逐一递增的阿拉伯数字,代表Proxy类第N次生成的动态代理类。值得注意的是,并不是每次调用Proxy的静态方法创建动态代理类都会使得N值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。
  4. 类继承关系:动态生成的代理类继承了类Proxy,并实现了所代理的所有接口。

实际上,每个动态代理实例都会关联一个调用处理器对象,可以通过Proxy提供的静态方法getInvocationHandler去获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终会由调用处理器的invoke方法执行。当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

至于被代理的接口,首先,不能有重复的接口,以避免动态代理类代码生成时的编译错误。其次,这些接口对于类加载器必须可见,否则类加载器将无法链接他们,将会导致类定义失败。再次,需被代理的所有非public的接口必须在同一个包中,否则代理类生成也会失败。最后,接口的数目不能超过65535,这时JVM设定的限制。

2.7.4 扩展阅读之AOP

AOP为Aspect Oriented Programming的缩写,意为“面向方面编程”,是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。 其主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等。

如果说面向对象编程是关注将需求功能划分为不同的并且相对独立、封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话;那么面向方面编程则是希望能够将通用需求功能从不相关的类中分离出来,能够使得很多类共享一个行为,一且发生变化,不必修改很多类,而只需要修改这个行为即可。

面向方面编程是一个令人兴奋不已的新模式。就开发软件系统而言,它的影响力必将会和有着数十年应用历史的面向对象编程一样巨大。 面向方面编程和面向对象编程不但不是互相竞争的技术而且彼此还是很好的互补。面向对象编程主要用于为同一对象层次的公用行为建模。它的弱点是将公共行为应用于多个无关对象模型之间。而这恰恰是面向方面编程适合的地方。有了AOP,我们可以定义交叉的关系,并将这些关系应用于跨模块的、彼此不同的对象模型。AOP同时还可以让我们层次化功能性而不是嵌人功能性,从而使得代码有更好的可读性和易于维护。

在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的一完成业 务逻辑仅此而已。 它们并不负责(甚至是意识)其他的系统级关注点,例如日志或事务支持。

2.8 依赖注入实例

待定加入。若该文本发布后仍未被修改,则表示未安排加入。请提示笔者加入内容或者删除这段文字。

 类似资料: