反射机制

125 阅读9分钟

反射:框架设计的灵魂

一:反射

1.1 框架 ————————————————————————————————————————

半成品软件,可以在框架的基础上进行软件开发,简化编码。学习框架并不需要了解反射,但是要是自己写一个框架,那么就要对反射机制有很深入的了解。

1.2 反射 ————————————————————————————————————————

反射机制:将类的各个组成部分封装为其他对象,这就是反射机制。

反射的好处:

    1. 可以在程序运行过程中,操作这些对象。
    1. 可以解耦,提高程序的可扩展性。

Java代码在计算机中经历的三个阶段

  • Source源代码阶段: .java被编译成.class字节码文件。
  • Class类对象阶段: *.class字节码文件被类加载器加载进内存,并将其封装成Class对象(它用于在内存中描述字节码文件),Class对象将原字节码文件中的成员变量抽取出来封装成数组Field[],将源字节码文件中的构造函数抽取出来封装成数组Construction[],将成员方法封装成Method[]。当然Class类内不止这三个,还封装了很多,我们常用的就这三个。
  • RunTime运行时的阶段:创建对象的过程new。

二:获取Class对象的方式 ————————————————————————————————————————

2.1 获取Class对象的三种方式对应着java代码在计算机中的三个阶段

  1. 【Source源代码阶段】Class.forName("全类名"):将字节码文件加载进内存,返回Class对象。多用于配置文件,将类名定义在配置文件中。读取文件,加载类。
  2. 【Class类对象阶段】类名.class:通过类名的属性class获取。多用于参数的传递
  3. 【Runntime运行时的阶段】对象.getClass():getClass()方法事定义在Object类中的方法。多用于对象的获取字节码的方式。

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

public static void main(String[] args) throws ClassNotFoundException {
        reflect1();
    }

    public static void reflect1() throws ClassNotFoundException {
        //方式1:Class.forName("全类名")
        Class cls1 = Class.forName("ddw.test.reflect.ReflectDemo");
        System.out.println("cls1=" + cls1);


        //方式2:类名.class
        Class cls2 = ReflectDemo.class;
        System.out.println("cls2=" + cls2);

        //方式3:对象.getClass();
        ReflectDemo reflectDemo = new ReflectDemo();
        Class cls3 = reflectDemo.getClass();
        System.out.println("cls3=" + cls3);


        // ==比较三个对象
        //结论:同一个字节码文件(*.class)在一次程序运行的过程中,只会被加载一次,无论哪一种方式获取的Class对象都是同一个

        System.out.println("cls1==cls2:" + (cls1 == cls2));
        System.out.println("cls1==cls3:" + (cls1 == cls3));


    }

Class对象的功能 ————————————————————————————————————————

3.1 获取功能

(1)获取成员变量们

Field[] getFields[]:获取所有public修饰的成员变量
Field[] getField(String name):获取指定名称的public修饰的成员变量

Field[] getDeclaredFields():获得所有的成员变量,不考虑修饰符
Field[] getDeclaredFields(String name):获取指定名称的所有成员变量,不考虑修饰符

(2)获取构造方法们

Constructor<?>[] getConstructors()
Constructor<T> getConstructor(类<?>...parameterTypes)

Constructor<?>[] getDeclaredConstructors()
Constructor<T> getDeclaredConstructor(类<?>...parameterTypes)

(3)获取成员方法们

Method[] getMethods()
Method getMethod(String name,类<?>...parameterTypes)

Method[] getDeclareMethods()
Method getDeclaredMethod(String name,类<?>...parameterTypes)

(4)获取全类名

String getName()

3.2 Field:成员变量

  1. 设置值 void set(Object obj,Object value)
  2. 获取值 get(Object obj)
  3. 忽略访问权限修饰符的安全检查setAccessible(true):暴力反射
private static void reflect2() throws NoSuchFieldException, IllegalAccessException {

        //获取ReflectDemo的Class对象
        Class cls = ReflectDemo.class;

        //1. Field[] getFields()获取所有的public修饰的成员变量
        Field[] fields = cls.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        System.out.println("========================================");

        Field a = cls.getField("a");

        //获取成员变量a的值(也只能获取公有的,获取私有的或者不存在的字符会抛出异常)
        ReflectDemo re = new ReflectDemo();
        Object value = a.get(re);
        System.out.println("value=" + value);

        //设置属性a的值
        a.set(re, "张三");
        System.out.println(re);

    }

getDeclareedFields和getDeclareedField(String name)方法

private static void reflect3() throws NoSuchFieldException, IllegalAccessException {
        Class cls = ReflectDemo.class;

        Field[] declaredFields = cls.getDeclaredFields();
        for (Field field : declaredFields) {
            System.out.println(field);
        }


        System.out.println("========================================");

        Field d = cls.getDeclaredField("d");
        ReflectDemo re = new ReflectDemo();

        //忽略访问权限修饰符的安全检查
        d.setAccessible(true);
        Object value2 = d.get(re);
        System.out.println("value2=" + value2);

        d.set(re, "李四");
        System.out.println(re);
    }

3.3 Constructor:构造方法

创建对象: T newInstance(Object... initargs) 注意:如果使用空参构造方法创建对象,操作可以简化:Class对象的newInstance方法

public class Person {

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        reflect4();
    }

    private static void reflect4() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class personClass = Person.class;

        // 此处获得的是public修饰的构造方法
        Constructor[] constructors = personClass.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }


        System.out.println("========================================");

        //获取无参构造函数 注意:Person类中必须要有无参的构造函数,不然会抛出异常
        Constructor constructor1 = personClass.getConstructor();
        System.out.println("constructor1=" + constructor1);

        //获取到构造函数之后可以用于创建对象
        Object person1 = constructor1.newInstance();
        System.out.println("person1=" + person1);


        System.out.println("========================================");

        //获取有参的构造函数  参数类型顺序必须要与构造函数内一致,且参数类型为字节码类型
        Constructor constructor2 = personClass.getConstructor(String.class, Integer.class);
        System.out.println("constructor2=" + constructor2);
        //创建对象
        Object person2 = constructor2.newInstance("张三", 23);
        System.out.println(person2);


        System.out.println("========================================");

        //对于一般的无参构造函数,我们都不会先获取无参构造器之后再进行初始化,而是
        //直接调用CLass类内的newInstance()方法
        Object person3 = personClass.newInstance();
        System.out.println("person3=" + person3);


        //我们之前使用的Class.forName("").newInstance:其本质就是调用了类内的无参构造函数来完成实例化的
        //故可以得出结论,我们以后使用Class.forName("").newInstance反射创建对象的时候,一定要保证类内有无参构造函数


    }


    private String name;
    private Integer age;

    //无参构造函数
    public Person() {

    }

    //单个参数的构造函数吗,且为私有构造方法
    private Person(String name) {

    }

    //有参构造函数
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }



}

**关于getDeclaredConstructor方法和getDeclaredConstructors方法 **

对于多出Declared关键字的两个方法,与不带这两个词的方法的对比,与前面成员变量的叙述一致。 getDeclaredConstructor方法可以获取到任何权限的构造器,而getConstructor方法只能获取public修饰的构造器。此外在构造器的对象内也有setAccessible(true)方法,并设置成true就可以操作了。

关于为什么使用private访问权限的构造器,使用这个构造器不就不能外部访问了吗,不也就无法进行实例化对象了吗?无法在类的外部实例化对象正是私有构造器的意义所在,在单例模式下经常使用,整个项目就只有一个对象,外部无法实例化对象,可以在类内进行实例化并通过静态方法返回(由于实例化对象是静态的,故只有一个对象,也就是单例的)。网上说这就是单例模式中的饿汉模式,不管是否调用,都只创建一个对象。

package ddw.test.reflect;

public class Singleton {
    public static void main(String[] args) {
        SingletonDemo s1 = SingletonDemo.getInstance();
        //输出对象的地址,如果地址存在,则说明对象创建成功并获取到
        System.out.println(s1);


        SingletonDemo s2 = SingletonDemo.getInstance();
        //如果为true,则说明是同一个对象
        System.out.println(s1 == s2);
    }
}


class SingletonDemo {
    //私有化构造方法
    private SingletonDemo() {

    }


    //创建一个对象,类内实例化(静态的对象)
    private static SingletonDemo singleton = new SingletonDemo();


    //提供public方法外部访问,返回这个创建的对象
    public static SingletonDemo getInstance() {
        return singleton;
    }
}

3.4 Method:方法对象

  • 执行方法:Object invoke(Object obj,Object...args)
  • 获取方法名称:String getName();
  private static void reflect4() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        System.out.println("===========================================");
        Class person1Class = Person1.class;

        //获取指定名称的方法
        Method eat_method1 = person1Class.getMethod("eat");

        //执行方法
        Person1 person1 = new Person1();
        Object rtValue = eat_method1.invoke(person1);

        //输出返回值 eat方法没有返回值,故输出null
        System.out.println("rtValue=" + rtValue);


        System.out.println("=========================================");

        //获取有参的构造函数,有两个参数 第一个方法名 第二个参数列表,不同的参数是不同的方法(重载)
        Method eat_method2 = person1Class.getMethod("eat", String.class);
        //执行方法
        eat_method2.invoke(person1, "饭");
        System.out.println("=========================================");


        //获取方法列表
        //注意:获取到的方法名称不仅仅是我们在Person1类内看到的方法,继承下来的方法也会被获取到(前提是public修饰的)
        Method[] methods = person1Class.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }
    }

测试getName方法

getName()方法获取的方法名仅仅就是方法名(不带全类名),且不带有参数列表

  private static void reflect5() {
        System.out.println("===========================================");
        Class personClass = Person.class;
        Method[] methods = personClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
            //获取方法名
            String name = method.getName();
            System.out.println(name);
        }

    }

获取关于成员方法们的另外两个方法

Method[] getDeclaredMethods()
Method getDeclaredMethod(String name,类<?>...parameterTypes)


method.setAccessible(true);//暴力反射

同之前叙述的一样,带有Declared关键字的方法,可以获取到任意的修饰符的方法。同样提供了setAccessible(true);方法进行暴力反射

综上所述:对于反射机制来说。在反射面前没有公有私有,都可以通过暴力反射解决。

4 案例 ————————————————————————————————————————

4.1 案例分析

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

4.1.2 实现

  1. 配置文件
  2. 反射

4.1.3步骤

  1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中
  2. 程序中加载读取配置文件
  3. 使用反射技术来加载文件进入内存
  4. 创建对象
  5. 执行方法

4.2 代码实现

1 Person类

package ddw.test.reflect;

public class Person2 {
    public void eat() {
        System.out.println("eat...");

    }
}

学生类

package ddw.test.reflect;

public class Student {
    public void study() {
        System.out.println("sudent's job is to learn");

    }
}

配置文件

className=ddw.test.reflect.Student
methodName=study

编写测试方法

public class ReflectTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        /*
        * 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
        * 即:拒绝硬编码
        *
        *
        * */

        //1.加载配置文件
        //1.1创建Properties对象
        Properties pro = new Properties();

        //加载配置文件,转换为一个集合

        //获取class目录下的配置文件 使用类加载器
        ClassLoader classLoader = ReflectTest.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("reflect.properties");
        pro.load(is);

        is.close();


        //获取配置文件中定义的数据
        String className = pro.getProperty("className");
        String methodName = pro.getProperty("methodName");


        //加载该类进内存
        Class cls = Class.forName(className);

        //创建对象
        Object obj = cls.newInstance();

        //获取方法对象
        Method method = cls.getMethod(methodName);

        //执行方法
        method.invoke(obj);
    }
}

4.3 好处

我们这样做有什么好处呢,对于框架来说,是人家封装好的,我们可以直接用就可以了,而不能去修改框架内的代码。但如果我们使用传统的new形式来实例化,那么当类名改变的时候我们就需要修改java代码,这是很繁琐的。修改java代码以我们还要进行测试,重新编译,发布等一系列的操作。而如果我们仅仅只是修改配置文件,就来的简单的多,配置文件就是一个实实在在的物理文件。

此外使用反射还能达到解耦的效果。假设我们使用的是new这种形式进行对象的实例化,此时如果在项目的某一个小模块中我们的一个实例类丢失了,那么我们在编译期就会报错,就会导致整个项目无法启动。而对于反射创建对象:Class.forName("全类名")这种形式,我们在编译期需要的仅仅是一个字符串(全类名),在编译期间不会报错,这样其他的模块就可以正常的运行,而不会因为一个模块的问题导致整个项目崩溃。这就是Spring框架中IOC控制反转的本质。