反射 – 框架设计的灵魂

前言

在Java中,反射机制的作用非常强大,虽然在平时我们使用的场景好像并不多,但是现在市面上绝大部分框架比如Spring家族、MyBatis、RocketMQ等等,在他们的底层实现中都有反射的影子,反射机制在框架设计中占有举足轻重的作用,可以说是框架设计的灵魂。

那么作为Java的进阶知识,如果我们想要更高层次的提升我们的技术能力,Java反射机制是我们必须要掌握的。

一、初识反射机制

1.1 出现背景

Java 程序中,所有的对象都有两种类型:编译时类型和运行时类型,而很多时候,对象的编译时类型和运行时类型不一致(面向对象编程三大特点之一:多态)。 比如:Object obj = new String("hello");我们都记得在学习多态时,有这样一句话:编译看左边,运行看右边。

也就是说某些变量或形参的声明类型是 Object 类型,但是程序却需要调用该对象运行时类型的方法,该方法不是 Object 中的方法,我们应该如何获取运行时类型的全部属性和方法呢?如何解决呢?

解决这个问题,有两种方案:

  • 方案 1:在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用 instanceof 运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。

  • 方案 2:编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这时就必须使用反射技术。

说到这里,我们还是似懂非懂,那么到底什么是反射呢?

1.2 概念理解

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

简单说,Java 的反射技术就是允许程序在运行过程中,对于任意一个类,都能够获取到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。

1.3 提供功能

  • 在运行时获取任意类的名称、package信息、注解、类型、类加载器等

  • 在运行时实例化任意一个类的对象

  • 在运行时判断任意一个类所具有的成员变量和方法

  • 在运行时判断任意一个对象所属的类

  • 在运行时获取任意对象的属性,并且能改变对象的属性

  • 在运行时调用任意对象的方法

  • 在运行时获取泛型信息

  • 在运行时处理注解

  • 通过反射我们可以实现动态装配,降低代码的耦合度,动态代理等。

1.4 实现原理

在知道了反射的理论概念和它所能提供的一些功能后,那么Java 反射机制为何这么强大,它是如何做到的呢?

既然反射技术是被视为动态语言的关键,允许程序在运行期间取得任何类的内部信息,那么它实现这些功能必须依赖于在当class文件被Java 虚拟机加载之后,由类加载器加载完类之后,也就是通过操作字节码文件才能够去获取类的全部信息。

Java程序从编写源代码到执行的过程在计算机中所需要经历三个阶段,首先第一阶段是编写源代码.Java的文件,然后通过Javac命令启动Java编译器对源代码文件进行编译得到.class的字节码文件,此时Java程序并没有开始被运行,第二阶段是准备运行,此时需要由JVM虚拟机进行加载字节码文件,通过类加载器去加载分析每一个类的字节码文件并生成一个Class类,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整类的结构信息。最后是运行时阶段。那么Java反射技术的实现就在于第二阶段通过分析字节码获取到每一个类的Class类对象,我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构, 所以,我们形象的称之为:反射。

追根溯源,我们说 Java 是一个纯面向对象的编程语言,我们把任何能够反映现实生活中的事物都可以映射为一个类,包括他的所有属性和操作,那么此时,我们可以想到Java中的所有类是不是也可以抽象为一个类,因为每一个类都包含固定的结构内容,比如字段属性 Filed、方法 Method、构造器 Constructor等,所以Class类就是描述类的类

二、使用反射的前置知识

2.1 反射相关的主要API

含义
java.lang.Class 代表整个字节码。代表一个类型,代表整个类。
java.lang.reflect.Method 代表字节码中的方法字节码。代表类中的方法。
java.lang.reflect.Constructor 代表字节码中的构造方法字节码。代表类中的构造方法。
java.lang.reflect.Field 代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。

2.2 反射机制的相关类

反射机制的相关类都在java.lang.reflect.*;包下。

2.2.1 Class类

要想解剖一个类,必须先要获取到该类的 Class 对象。而剖析一个类或用反射解决具体的问题就是使用相关 API。所以,Class 对象是反射的根源

Class类表示正在运行的Java应用程序中的类和接口。 枚举是一种类, 每个数组也属于一个反映为Class对象的类,该对象由具有相同元素类型和维数的所有数组共享。 原始Java类型( booleanbytecharshortintlongfloatdouble ),和关键字void也表示为Class对象。

Class没有公共构造函数。 相反, Class对象由Java虚拟机自动构建,因为加载了类,并且通过调用类加载器中的defineClass方法。

在 Object 类中定义了以下的方法,此方法将被所有子类继承:

public final Class getClass()

 

 

以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

通过一个对象获取到Class后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。一个 Class 对象包含了特定某个类的有关信息。

获取Class类对象

通过上面的学习,我们知道Class 对象是反射的根源。要想使用反射技术那么首先毋庸置疑的就是获取到Class对象,那么如何获取呢?主要有如下的几种方法:

  1. 前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高。要求编译期间已知类型。

    Class clazz = String.class;

  2. 前提:已知某个类的实例,调用该实例的getClass()方法获取

    Class clazz = people.getClass();

  3. 前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛ClassNotFoundException

    Class clazz = Class.forName(“java.lang.String”);

    将字节码文件加载进内存,返回Class对象。多用于配置文件,将类名定义在配置文件中。读取文件,加载类。
  4. 前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型

    同一个字节码文件(.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

    ClassLoader cl = this.getClass().getClassLoader();
    Class clazz4 = cl.loadClass(“类的全类名”);
Class类的特点

对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。有关Class的对象的特点如下:

  • Class本身也是一个类

  • Class 对象只能由系统建立对象

  • 一个类在JVM中只会有一个Class实例

  • 一个Class对象对应的是一个加载到JVM中的一个.class文件

  • 每个类的实例都会记得自己是由哪个Class实例所生成

  • 通过Class可以完整地得到一个类中的完整结构

  • Class 类是 Reflection 的根源,针对任何你想动态加载、运行的类,唯有先获得相应的 Class 对象。

常用方法

既然Class类也是一个类,那么也就应该有它自己的属性和方法,下面列举一些常用方法:

方法名 功能说明
static Class forName(String name) 返回指定类名 name 的 Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getFields() 获得类的public类型的属性。
Field[] getDeclaredFields() 返回Field对象的一个数组,包括private 声明的和继承类
Method[] getMethods() 获得类的public类型的方法。
Method[] getDeclaredMethods() 获得类的所有方法。包括private 声明的和继承类
Method getMethod(String name,Class … paramTypes) 返回一个Method对象,获得类的特定方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型。
Constructor[] getConstructors() 获得类的public类型的构造方法。
Constructor getConstructor(Class[] parameterTypes) 获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型。

Class类全部属性和方法见参考手册:Class (Java Platform SE 8 )

补充:哪些类型可以有 Class 对象

简言之,所有 Java 类型!

  1. class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类

  2. interface:接口

  3. []:数组

  4. enum:枚举

  5. annotation: 注解@interface

  6. primitive type:基本数据类型

  7. void

public class Demo1 {
    public static void main(String[] args) {
        // 所有的类
        Class c1 = Object.class;  
        // 接口
        Class c2 = Comparable.class;
        // 一维数组
        Class c3 = String[].class;
        // 二维数组
        Class c4 = int[][].class;
        // 枚举
        Class c5 = ElementType.class;
        // 注解
        Class c6 = Override.class;
        // 基本数据类型
        Class c7 = char.class;
        // void
        Class c8 = void.class;
        // Class
        Class c9 = Class.class;
    }
}

 

 

 

2.2.2 Method类

Method类位于java.lang.reflect包下,在Java反射中Method类描述的是类的方法信息(包括:方法修饰符、方法名称、参数列表)等。

Method提供有关类或接口上的单个方法的信息和访问权限。 反射的方法可以是类方法或实例方法(包括抽象方法)。

Method允许在将实际参数与基础方法的形式参数进行匹配时进行扩展转换,但如果发生缩小转换则会抛出IllegalArgumentException

获取Method类对象

一共有4种方法,全部都在Class类中:

  1. getMethods(): 获得类的public类型的方法

  2. getMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型

  3. getDeclaredMethods(): 获取类中所有的方法(public、protected、default、private)

  4. getDeclaredMethod(String name, Class[] params): 获得类的特定方法,name参数指定方法的名字,params参数指定方法的参数类型

获取的方法包含继承自父类的方法。

方法权限总结
  1. public static方法:没有任何权限问题,getMethod()就可以满足,根本不用getDeclaredMethod出马,更不用setAccessiable(true);

  2. public 非静态方法:没有任何权限问题,getMethod()就可以满足,根本不用getDeclaredMethod出马,更不用setAccessiable(true),但是,在invoke时,第一个参数必须是具体的某一个对象,static的可要可不要

  3. protected 非静态方法:必须使用getDeclaredMethod,不能使用getMethod,不用设置setAccessiable(true);

  4. friendly 非静态方法:必须使用getDeclaredMethod,不能使用getMethod,不用设置setAccessiable(true);

  5. private 非静态方法:必须使用getDeclaredMethod,不能使用getMethod,必须设置setAccessiable(true)

常用方法

类型 方法 说明
Class getDeclaringClass() 得到一个 Class 对象
String getName() 获得 Method 类对象所表示的方法的名字
Class[ ] getParameterTypes() 获得 Method 类对象表示的方法中的参数类型
Object invoke(Object o, Object… args) 调用 Method 类对象表示的方法,相当于对象 o 用参数 args 调用该方法

2.2.3 Field类

Field是一个类,位于Java.lang.reflect包下,在Java反射中Field用于获取某个类的属性或该属性的属性值。

Field提供有关类或接口的单个字段的信息和动态访问。 反射的字段可以是类(静态)字段或实例字段。

Field允许在获取或设置访问操作期间扩展转换,但如果发生缩小转换,则抛出IllegalArgumentException

获取Field类对象
  1. Class.getDeclaredField(String name); 返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段(包括私有成员)。

  2. Class.getDeclaredFields(); 返回 Field 对象的一个数组,该数组包含此 Class 对象所表示的类或接口所声明的所有字段(包括私有成员)。

  3. Class.getField(String name); 返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段。

  4. Class.getFields(); 返回一个包含某些 Field 对象的数组,该数组包含此 Class 对象所表示的类或接口的所有可访问公共字段。

常用方法

返回值 名字和参数 作用
Object get(Object obj) 返回这个object对应field字段的Object
xxx getXXX(Object obj) 同上,不过XXX可以是Int,Char,Boolean等
void set(Object obj, Object value) 设置obj对象的调用方法的这个field的值为value
void setXXX(Object obj, XXX value) 设置特定类型值,例setInt(Object obj,int value)
Class getDeclaringClass() 返回定义中的Class对象
String getName() 获取属性声明时名字
int getModifier() 返回一个修饰符的值[0:默认 1:public 2:private 4:protected ]
Class getType() 返回这个field的对象Class
Annotation[] getAnnotations() getAnnotations()
void setAccessible(boolean flag) 设置是否允许set get
boolean isEnumConstant() 判断这个属性是否是枚举类
boolean isAccessible() 查看field是否允许set和get。

2.3.4 Constructor类

Constructor是一个类,位于java.lang.reflect包下,它代表某个类的构造方法,用来管理所有的构造函数的类。

  • 提供关于类的单个构造方法的信息以及对它的访问权限。

  • 允许在将实参与带有底层构造方法的形参的 newInstance() 匹配时进行扩展转换,但是如果发生收缩转换,则抛出 IllegalArgumentException

获取Constructor类对象

一共有4种方法,全部都在Class类中:

  1. getConstructors():获取类中的公共构造方法

  2. getConstructor(Class[] params): 获取类的特定构造方法,params参数指定构造方法的参数类型

  3. getDeclaredConstructors(): 获取类中所有的构造方法(public、protected、default、private)

  4. getDeclaredConstructor(Class[] params): 获取类的特定构造方法,params参数指定构造方法的参数类型

常用方法

类型 方法 说明
Class getDeclaringClass() 得到一个 Class 类的对象
Class[ ] getParameterTypes() 获得 Constructor 类对象表示的构造方法中的参数的类型
String getName() 获得 Method 类对象所表示的构造方法的名字
T newInstance() 通过调用当前 Constructor 类对象所表示的类的构造方法创建一个新对象

三、基础使用反射技术

3.0 定义Person类

public class Person {
    public String name;
    private byte age;
    private char gender;
    String phone;
    protected String[] hobby;
    
    // 省略 getter setter toString
    
    public void eat(){
        System.out.println("吃饭....");
    }

    public static void speak(){
        System.out.println("speak....");
    }

    private void sleep(){
        System.out.println("sleep....");
    }

    protected void play(){
        System.out.println("play....");
    }


    public String study(String course){
        System.out.println("在学习" + course);
        return "已经学习" + course;
    }
}

 

 

3.1 获取Class对象

public class GetClass {
    public static void main(String[] args) throws ClassNotFoundException {
        // 1.若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高
        Class<Person> clazz1 = Person.class;

        // 2. 实例化对象以获取Class对象:已知某个类的实例,调用该实例的getClass()方法获取Class对象
        Class<? extends Person> clazz2 = new Person().getClass();

        // 3.该类在类路径下,通过Class类的静态方法forName()获取
        // throws ClassNotFoundException
        Class<?> clazz3 = Class.forName("cn.imyjs.entity.Person");

        // 4. 其他方式
        ClassLoader classLoader = GetClass.class.getClassLoader();
        Class<?> clazz4 = classLoader.loadClass("cn.imyjs.entity.Person");

        /*
        同一个字节码文件(*.class)在一次程序运行过程中,
        只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
         */
        System.out.println(clazz1 == clazz2);  // true
        System.out.println(clazz2 == clazz3);  // true
        System.out.println(clazz3 == clazz4);  // true
    }
}

 

 

3.2 操作Field类

public class GetField {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException {
        // 定义Person类并初始化字段属性,注意:年龄字段定义为byte类型 需要强制转换
        Person person =
                new Person("admin", (byte)18, '男', "15936788888", new String[] {"吃", "喝","玩","乐"});

        // 根据已知某个类的实例,调用该实例的getClass()方法获取Class对象
        Class<? extends Person> clazz = person.getClass();

        // 获取Person类的全部公开属性 仅是public修饰的字段 不包括默认、私有、保护
        System.out.println("获取Person类的全部公开属性");
        Field[] fields = clazz.getFields();
        for (Field field: fields) {
            System.out.println(field.getName());
        }

        // 获取所有的成员变量,不考虑修饰符,全部字段
        System.out.println("获取所有的成员变量,不考虑修饰符");
        Field[] allFields = clazz.getDeclaredFields();
        for (Field field: allFields) {
            System.out.println(field.getName());
        }

        // 获取Person类的公开属性 public String name; 使用getField()
        Field name = clazz.getField("name");
        String origenName = (String) name.get(person);
        System.out.println("初始name: " + origenName);

        // 设置name值为 理想
        name.set(person, "理想");

        // 获取属性声明时的名字
        String name1 = name.getName();
        System.out.println("属性声明时的名字:" + name1);

        // 判断这个属性是否是枚举类
        boolean isEnumConstant = name.isEnumConstant();
        System.out.println("判断这个属性是否是枚举类"+ isEnumConstant);

        // 判断是否允许set和get
        boolean isAccessible = name.isAccessible();
        System.out.println("判断是否允许set和get:" + isAccessible);

        // 设置是否允许set和get
        name.setAccessible(false);

        // 获取属性的修饰符
        int modifiers = name.getModifiers();
        // 0:默认 1:public 2:private 4:protected
        System.out.println("属性的修饰符" + modifiers);

        // 获取Person类的私有属性 private char gender gender为private不可以直接使用getField
        Field gender = clazz.getDeclaredField("gender");

        /*
        注:在类中属性都设置为private的前提下,在使用set()和get()方法时,
        首先要使用Field类中的setAccessible(true)方法将需要操作的属性设置为可以被外部访问。
        setAccessible(true);访问私有属性时,让这个属性可见。 暴力反射!!
         */
        gender.setAccessible(true);

        // 设置gender值为 男
        gender.set(person, '男');

        Field hobby = clazz.getDeclaredField("hobby");
        // 取得指定对象obj上此Field的属性内容
        hobby.setAccessible(true);
        String[] hobbys = (String[]) hobby.get(person);
        for (int i = 0; i < hobbys.length; i++) {
            System.out.println(hobbys[i]);
        }
        // 得到Field的属性类型
        Class<?> type = hobby.getType();
        System.out.println(type);

        System.out.println("********修改后的Person*********");
        System.out.println(person);
    }
}

 

 

3.3 操作Method类

public class GetMethod {
    public static void main(String[] args) throws Exception {

        // 该类在类路径下,通过Class类的静态方法forName()获取Class
        Class<?> clazz = Class.forName("cn.imyjs.entity.Person");

        // 获取该类的一个实例 newInstance()已经被废弃
        // Person o = (Person) clazz.newInstance();

        Constructor<?> constructor = clazz.getConstructor();
        Person person = (Person) constructor.newInstance();

        // 获取此Class对象所表示的类或接口的public的方法
        Method[] methods = clazz.getMethods();
        for (Method m: methods) {
            System.out.println(m.getName());
        }

        System.out.println("*********全部方法*********");
        // 获取此Class对象所表示的类或接口的全部方法
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method m: declaredMethods) {
            System.out.println(m.getName());
        }

        // 获取单一的方法
        Method eatMethod = clazz.getMethod("eat");
        // 获取方法参数的个数
        int parameterCount = eatMethod.getParameterCount();
        System.out.println("方法参数的个数" + parameterCount);

        // 执行方法
        eatMethod.invoke(person);
        Method studyMethod = clazz.getMethod("study", String.class);
        String returnValue = (String) studyMethod.invoke(person, "Java");
        System.out.println(returnValue);
    }
}

 

 

使用Object invoke(Object obj, Object[] args)进行方法调用,需要向方法中传递要设置的obj对象的参数信息。

说明:

1.Object 对应原方法的返回值,若原方法无返回值,此时返回null

2.若原方法若为静态方法,此时形参Object obj可为null

3.若原方法形参列表为空,则Object[] args为null

4.若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方法

3.4 操作Constructor类

public class GetConstructor {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<Person> clazz = Person.class;
        // 获取类中的公共构造方法
        Constructor<?>[] constructors = clazz.getConstructors();
        for (Constructor c: constructors) {
            String constructorName = c.getName();
            // cn.imyjs.entity.Person
            System.out.println(constructorName);
        }

        System.out.println("*****获取类中所有的构造方法*****");
        // 获取类中所有的构造方法(public、protected、default、private)
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        for (Constructor c: declaredConstructors) {
            String constructorName = c.getName();
            // cn.imyjs.entity.Person
            System.out.println(constructorName);
        }

        // 获取指定构造器
        Constructor<Person> constructor = clazz.getConstructor(String.class, byte.class);
        Person person = constructor.newInstance("admin", (byte) 22);
        System.out.println(person);

        // 获取指定私有构造器
        Constructor<Person> declaredConstructor = clazz.getDeclaredConstructor(String.class);
        declaredConstructor.setAccessible(true);
        Person zs = declaredConstructor.newInstance("张三");
        System.out.println(zs);
    }
}

 

 

反射机制相关类的API有很多,不需要刻意去记忆,也没有必要全部去写Demo。主要是要理解反射技术的原理,在需要用到时记得去查找相关的API即可。

3.5 反射案例

需求:写一个"框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法

public class Demo2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中
		// className=cn.imyjs.entity.Teacher
		// methodName=teach
        // 2. 在程序中加载读取配置文件
        Properties properties = new Properties();
        properties.load(Demo2.class.getClassLoader().getResourceAsStream("config.properties"));

        // 3.使用反射技术来加载类文件进内存
        String className = properties.getProperty("className");
        String methodName = properties.getProperty("methodName");

        // 4.创建对象
        Class<?> clazz = Class.forName(className);
        Constructor<?> constructor = clazz.getConstructor();
        Object o = constructor.newInstance();
        // 5.执行方法
        Method method = clazz.getMethod(methodName);
        method.invoke(o);
    }
}

 

四、补充-类的加载过程

4.1 类的生命周期

类在内存中完整的生命周期:加载-->使用-->卸载。其中加载过程又分为:装载、链接、初始化三个阶段。

4.2 类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过装载、链接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。

类的加载又分为三个阶段:

  • 装载(Loading)

    将类的class字节码文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

  • 链接(Linking)

    • ①验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。

    • ②准备Prepare:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。

    • ③解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

  • 初始化(Initialization)

    执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。

    当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

    虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

 

4.3 类加载器(classloader)

4.3.1 类加载器的作用

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

4.3.2 类加载器的分类(JDK8为例)

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:

(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。获取它的对象时往往返回null

  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。

  • 并不继承自java.lang.ClassLoader,没有父加载器。

  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

(2)扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。

  • 继承于ClassLoader类

  • 父类加载器为启动类加载器

  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

 

(3)应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现

  • 继承于ClassLoader类

  • 父类加载器为扩展类加载器

  • 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库

  • 应用程序中的类加载器默认是系统类加载器。

  • 它是用户自定义类加载器的默认父加载器

  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

(4)用户自定义类加载器(了解)

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。

  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。

  • 同时,自定义加载器能够实现应用隔离,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。

  • 自定义类加载器通常需要继承于ClassLoader。

4.3.3 查看某个类的类加载器对象

(1)获取默认的系统类加载器

 ClassLoader classloader = ClassLoader.getSystemClassLoader();

(2)查看某个类是哪个类加载器加载的

 ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();
 
 //如果是根加载器加载的类,则会得到null
 ClassLoader classloader1 = Class.forName("java.lang.Object").getClassLoader();

(3)获取某个类加载器的父加载器

 ClassLoader parentClassloader = classloader.getParent();

示例代码:

package cn.imyjs.loader;

import org.junit.Test;

public class TestClassLoader {
    @Test
    public void test01(){
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("systemClassLoader = " + systemClassLoader);
    }

    @Test
    public void test02()throws Exception{
        ClassLoader c1 = String.class.getClassLoader();
        System.out.println("加载String类的类加载器:" + c1);

        ClassLoader c2 = Class.forName("sun.util.resources.cldr.zh.TimeZoneNames_zh").getClassLoader();
        System.out.println("加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器:" + c2);

        ClassLoader c3 = TestClassLoader.class.getClassLoader();
        System.out.println("加载当前类的类加载器:" + c3);
    }

    @Test
    public void test03(){
        ClassLoader c1 = TestClassLoader.class.getClassLoader();
        System.out.println("加载当前类的类加载器c1=" + c1);

        ClassLoader c2 = c1.getParent();
        System.out.println("c1.parent = " + c2);

        ClassLoader c3 = c2.getParent();
        System.out.println("c2.parent = " + c3);

    }
};

4.3.4 使用ClassLoader获取流

关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流

InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);

举例:

//需要掌握如下的代码
    @Test
    public void test5() throws IOException {
        Properties pros = new Properties();
        //方式1:此时默认的相对路径是当前的module
//        FileInputStream is = new FileInputStream("info.properties");
//        FileInputStream is = new FileInputStream("src//info1.properties");

        //方式2:使用类的加载器
        //此时默认的相对路径是当前module的src目录
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");


        pros.load(is);

        //获取配置文件中的信息
        String name = pros.getProperty("name");
        String password = pros.getProperty("password");
        System.out.println("name = " + name + ", password = " + password);
    }

五、总结

5.1 反射机制优缺点

  • 优点 : 运行期类型的判断,class.forName() 动态加载类,提高代码的灵活度;可以使代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

  • 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

    • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射

    • 安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。

    • 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如:访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。

5.2 反射的应用场景

像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。

但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。

工厂模式,使用反射机制,根据全限定类名获得某个类的 Class 实例。

这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。

另外,像 Java 中的 注解 的实现也用到了反射。

微信关注

编程那点事儿

                    编程那点事儿

阅读剩余
THE END