带你掌握框架的灵魂——反射技术

·  阅读 552

「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战

反射概述

Java反射机制指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制。 Java的反射机制允许编程人员在对类未知的情况下,获取类相关信息的方式变得更加多样灵活,调用类中相应方法,是Java增加其灵活性与动态性的一种机制。 反射能动态编译和创建对象,极大的激发了编程语言的灵活性,强化了多态的特性,进一步提升了面向对象编程的抽象能力,在很多框架中被大量使用,所以可以说框架的灵魂即是:反射技术。

这些都是很官方的一些解释,通过概述能够知道反射技术的强大,所以接下来,我们细细品味一下反射的用法。

类的加载过程

当程序要使用某个类的时候,如果该类还没有被加载到内存,则系统会通过加载、连接、初始化三个步骤来实现对这个类的初始化。

  • 加载:指将class文件读入内存,并为之创建一个Class对象,任何类被使用时系统都会为其创建Class对象
  • 连接:连接又分为三个步骤(验证、准备、解析)            验证:验证是否有正确的内部结构,并和其它类协调一致            准备:负责为类的静态成员分配内存,并设置默认初始化值            解析:将类的二进制数据中的符号引用替换为直接引用
  • 初始化:初始化会为所有的静态变量赋予正确的值。注意初始化操作和准备操作的区别,准备操作为静态成员分配内存,设置默认初始化值,而初始化操作是设置正确的值。举个例子:static int num = 1024,这样的一个成员变量在准备阶段会为其赋值为0,只有到初始化阶段才会赋值为1024。

类加载器

了解完类的加载过程之后,我们来看看到底是谁完成了类的加载操作,它就是:类加载器。 类加载器负责将.class文件加载到内存中,并为之生成对应的Class对象,类加载器由以下三大加载器组成:

  1. 根类加载器(Bootstrap ClassLoader):根类加载器也被称为引导类加载器,负责Java核心类的加载,例如System、String类等
  2. 扩展类加载器(Extension ClassLoader):扩展类加载器负责JRE的扩展目录中jar包的加载
  3. 系统类加载器(System ClassLoader):系统类加载器负责在Java虚拟机启动时加载来自Java命令的class文件以及classpath变量所指定的jar包和类路径

获取Class对象

有了理论的知识之后,我们就可以开始实践了,先来看看如何获取类的Class对象(有三种方式)。 首先创建一个基本类用于测试:

package com.wwj.reflect;

public class Programmer {
    private String name;
    public int age;
    private String address;

    public Programmer() {
    }

    private Programmer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Programmer(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public void test() {
        System.out.println("test---无参无返回值方法");
    }

    public void test2(String str) {
        System.out.println("test2---带参无返回值方法");
    }

    public String test3(String str, int num) {
        System.out.println("test3--带参带返回值方法");
        return str + "--" + num;
    }

    private void test4() {
        System.out.println("test4---私有方法");
    }
    
	@Override
    public String toString() {
        return "Programmer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}
复制代码

那么第一种获取Class对象的方式就是通过Object类的getClass()方法:

Programmer programmer = new Programmer();
Class pClass = programmer.getClass();
复制代码

第二种方式就是通过静态属性class:

Class pClass = Programmer.class;
复制代码

第三种方式通过Class类中的静态方法forName():

Class pClass = Class.forName("com.wwj.reflect.Programmer");
复制代码

获取构造方法

拿到了Class对象后,我们就可以通过该对象获取类的成员并使用,先来看看如何获取类的构造方法。

	public static void main(String[] args) throws ClassNotFoundException {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor[] constructors = pClass.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
    }
复制代码

运行结果:

public com.wwj.reflect.Programmer(java.lang.String,int,java.lang.String)
public com.wwj.reflect.Programmer()
复制代码

控制台只打印了两个构造方法,但很显然,Programmer类中有三个构造方法,其中的私有构造方法获取不到。所以Class类中的getConstructors()只能获取到公共的构造方法,要想获取到所有的构造方法,可以使用getDeclaredConstructors()方法:

	public static void main(String[] args) throws ClassNotFoundException {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor[] constructors = pClass.getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
    }
复制代码

运行结果:

public com.wwj.reflect.Programmer(java.lang.String,int,java.lang.String)
private com.wwj.reflect.Programmer(java.lang.String,int)
public com.wwj.reflect.Programmer()
复制代码

通常情况下,我们并不需要这么多的构造方法,往往我们只需要一个构造方法就行了。

1.获取无参构造方法

先说说如何获取类的无参构造方法。

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        System.out.println(object);
    }
复制代码

我们可以通过Class类的getConstructor()方法获得单个的构造方法,不传参则代表获取无参构造方法,然后通过返回的构造方法对象调用newInstance()方法即可创建Programmer对象,所以运行结果应为Programmer类的信息。

Programmer{name='null', age=0, address='null'}
复制代码
2.获取带参构造方法

获取带参构造方法的方式同样是通过getConstructor()方法,只不过需要传入参数,返回的是对应参数的构造方法对象,直接看例子:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor(String.class,int.class,String.class);
        Object object = constructor.newInstance("张三",18,"杭州");
        System.out.println(object);
    }
复制代码

运行结果:

Programmer{name='张三', age=18, address='杭州'}
复制代码
3.获取私有构造方法

前面已经说到,getConstructor()方法无法获取到私有构造方法,所以我们改用getDeclaredConstructors()方法即可,用法和getConstructor()相同。

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor declaredConstructor = pClass.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);//取消访问检查
        Object object = declaredConstructor.newInstance("李四", 20);
        System.out.println(object);
    }
复制代码

运行结果:

Programmer{name='李四', age=20, address='null'}
复制代码

需要注意的是,虽然getDeclaredConstructor()方法能够获取到私有构造方法,但由于Java语言的访问检查机制,在创建对象的时候会抛出非法访问异常,所以我们需通过setAccessible()方法取消访问检查,参数为true则为取消,取消了访问检查后才能正常创建对象。

获取成员变量

我们再来看看如何通过Class对象获得类的成员变量。

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Field[] fields = pClass.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }
    }
复制代码

运行结果:

public int com.wwj.reflect.Programmer.age
复制代码

输出结果不难理解,和获取构造方法一样,getFields()方法无法获取类的私有成员变量,可以通过getDeclaredFields()方法获取:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Field[] fields = pClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field);
        }
    }
复制代码

运行结果:

private java.lang.String com.wwj.reflect.Programmer.name
public int com.wwj.reflect.Programmer.age
private java.lang.String com.wwj.reflect.Programmer.address
复制代码
1.获取公共成员变量
	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Field ageField = pClass.getField("age");
        ageField.set(object, 20);
        System.out.println(object);
    }
复制代码

运行结果:

Programmer{name='null', age=20, address='null'}
复制代码

通过Class对象的getField()方法能够获取指定属性名的成员变量,但若想对属性进行赋值,则首先需要创建出Programmer对象,然后调用成员变量对象的set()方法,传入要赋值的对象和属性值。这个逻辑其实和正常创建对象赋值是刚好相反的,反射是通过成员变量对象调用方法并将类对象和参数值传入。

2.获取私有成员变量

获取私有成员变量的方式和获取私有构造方法相同,通过getDeclaredField()方法获得成员变量对象,并且在赋值之前需要先取消访问检查,直接看示例:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Field nameField = pClass.getDeclaredField("name");
        nameField.setAccessible(true);//取消访问检查
        nameField.set(object,"李四");
        System.out.println(object);
    }
复制代码

运行结果:

Programmer{name='李四', age=0, address='null'}
复制代码

获取成员方法

获取成员方法的方式和前面相同,通过getMethods()方法可以获取到公共的成员方法,通过getDeclaredMethods()方法可以获取到包括私有的所有成员方法,在此不做重复讲解,接下来说一说如何获取单个成员方法。

1.获取无参无返回值成员方法
	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getMethod("test");
        method.invoke(object);
    }
复制代码

运行结果:

test---无参无返回值方法
复制代码

同样地,通过getMethod()方法可以获取到对应参数名的成员方法,该方法需要传入两个参数:第一个参数为方法名;第二个参数为方法的参数类型。 这里因为是无参方法,所以无需传入第二个参数,获取到成员方法的对象之后,同样是调用该对象的invoke()方法并将需要执行方法的对象传入才能成功执行方法。

2.获取带参无返回值成员方法

获取带参成员方法就很简单了,在getMethod()方法中传入参数类型即可,直接看示例:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getMethod("test2", String.class);
        method.invoke(object, "王五");
    }
复制代码

运行结果:

test2---带参无返回值方法
复制代码
3.获取带参带返回值成员方法

获取带参带返回值成员方法同样十分简单,只不过多了一个返回值处理罢了:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getMethod("test3", String.class, int.class);
        Object obj = method.invoke(object, "赵六", 20);
        System.out.println(obj);
    }
复制代码

运行结果:

test3--带参带返回值方法
赵六--20
复制代码
4.获取私有成员方法

获取私有成员方法,即通过getDeclaredMethod()方法获取成员方法对象,并取消访问检查,然后执行方法即可:

	public static void main(String[] args) throws Exception {
        Class pClass = Class.forName("com.wwj.reflect.Programmer");
        Constructor constructor = pClass.getConstructor();
        Object object = constructor.newInstance();
        Method method = pClass.getDeclaredMethod("test4");
        method.setAccessible(true);
        method.invoke(object);
    }
复制代码

运行结果:

test4---私有方法
复制代码

利用反射无视泛型检查

到这里关于反射的基本知识就介绍完了,接下来我们用泛型来解决一个问题:无视掉Java的泛型检查。 我们看这样的一段代码:

	public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("world");
        list.add(1024);
    }
复制代码

因为list集合的泛型被指定为String类型,所以该集合将只能存储字符串,所以我们在放入1024的时候编译器会报错,那有没有可能通过一些手段将其它类型也能够放入该集合呢?办法是有的,那就是通过反射。 因为Java泛型机制其实只在编译阶段有效,在真正运行的时候是不带泛型的,这种现象叫泛型擦除。这是因为这一特点,我们就能通过反射越过编译期的泛型检查,实现将其它类型的数据存放到指定类型的集合中。

	public static void main(String[] args) throws Exception {
        List<String> list = new ArrayList<>();
        list.add("hello");
        list.add("world");
        Class listClass = list.getClass();
        Method addMethod = listClass.getMethod("add", Object.class);
        addMethod.invoke(list, 1024);
        System.out.println(list);
    }
复制代码

运行结果:

[hello, world, 1024]
复制代码

这样,int类型数据就成功被存放到了集合中。

动态代理

动态代理是反射技术的高级应用,其目的就是为其它对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。 在Java中,JDK为我们提供了Proxy类和InvocationHandler接口,通过这个类和接口就可以生成动态代理对象,但需要注意,JDK提供的代理只能针对接口做代理,如果需要对普通类做代理,我们可以使用cglib。 由于篇幅有限,这里不对动态代理做详细介绍,就通过一个案例让大家先了解一下动态代理。 新建一个接口ICat:

interface ICat{
	public void run();
}
复制代码

新建一个类继承该接口:

public class Cat implements ICat{

	@Override
	public void run(){
		System.out.println("喵喵~一只猫在奔跑");
	}
}
复制代码

这是一只会奔跑的猫,通常我们会使用动态代理来增强某个类的方法,例如该类中,我们可以增强run()方法,使其还会抓老鼠:

	public static void main(String[] args) throws Exception {
        final ICat cat = new Cat();//原对象
        ICat catProxy = (ICat) Proxy.newProxyInstance(cat.getClass().getClassLoader(), cat.getClass().getInterfaces(), new InvocationHandler() {

            public Object invoke(Object proxy, Method method, Object[] objs)
                    throws Throwable {
                //增强run方法
                if (method.getName().equals("run")) {
                    method.invoke(cat, objs);//调用原对象的方法,保留原方法的功能
                    //新增功能
                    System.out.println("抓住一只老鼠");
                }
                return null;
            }
        });
        catProxy.run();
    }
复制代码

运行结果:

喵喵~一只猫在奔跑
抓住一只老鼠
复制代码

主要说一说invoke()方法,该方法有三个参数:

  • proxy:在其调用方法的代理实例
  • method:对应于在代理实例上调用的接口方法的Method实例。Method对象的声明类将是在其中声明方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口
  • objs:包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为null。基本类型的参数被包装在适当基本包装器类的实例中

返回值即是代理方法的返回值,因为这里run()方法没有返回值,所以返回null即可,然后调用method对象的invoke()方法,并将需要执行方法的对象和参数值objs传入即可执行原方法的逻辑,这在如何获取成员方法中已经说过,然后我们就可以在下面写上需要添加的功能,这样该方法就比原先的方法功能更加丰富了。

最后

本篇文章总体是偏简单的,适合刚入门的学习者,虽然简单,但也写了挺久,从8点多一直写到11点,目的也是希望大家能够快速掌握反射技术,反射技术在后期的框架学习中是至关重要的,理解反射,对于框架的底层实现你就能够更加了解。

最后祝大家节日快乐!

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改