长篇图解java反射机制及其应用场景

在java的面向对象编程过程中,通常我们需要先知道一个Class类,然后new 类名()方式来获取该类的对象。也就是说我们需要在写代码的时候(编译期或者编译期之前)就知道我们要实例化哪一个类,运行哪一个方法,这种通常被称为「静态的类加载」

但是在有些场景下,我们事先是不知道我们的代码的具体行为的。比如,我们定义一个服务任务工作流,每一个服务任务都是对应的一个类的一个方法。

  • 服务任务B执行哪一个类的哪一个方法,是由服务任务A的执行结果决定的
  • 服务任务C执行哪一个类的哪一个方法,是由服务任务A和B的执行结果决定的
  • 并且用户不希望服务任务的功能在代码中写死,希望通过配置的方式执行不同的程序

面对这个情况,我们就不能用代码new 类名()来实现了,因为你不知道用户具体要怎么做配置,这一秒他希望服务任务A执行Xxxx类的x方法,下一秒他可能希望执行Yyyy类的y方法。当然你也可以说提需求嘛,用户改一次需求,我改一次代码。这种方式也能需求,但对于用户和程序员个人而言都是痛苦,那么有没有一种方法「在运行期动态的改变程序的调用行为的方法」呢?这就是要为大家介绍的“「java反射机制」”。

那么java的反射机制能够做那些事呢?大概是这样几种:

  • 在程序运行期动态的根据package名.类名实例化类对象
  • 在程序运行期动态获取类对象的信息,包括对象的成本变量和方法
  • 在程序运行期动态使用对象的成员变量属性
  • 在程序运行期动态调用对象的方法(私有方法也可以调用)

我们定义一个类叫做Student

如果不用反射的方式,我相信只要学过java的朋友肯定会调用dinner方法

如果是反射的方式我们该怎么调用呢?

通过上面的代码我们看到,com.zimug.java.reflection.Student类名和dinner方法名是字符串。既然是字符串我们就可以通过配置文件,或数据库、或什么其他的灵活配置方法来执行这段程序了。这就是反射最基础的使用方式。

java的类加载机制还是挺复杂的,我们这里为了不混淆重点,只为大家介绍和“反射”有关系的一部分内容。

java执行编译的时候将java文件编译成字节码class文件,类加载器在类加载阶段将class文件加载到内存,并实例化一个java.lang.Class的对象。比如:对于Student类在加载阶段

  • 在内存(方法区或叫代码区)中实例化一个Class对象,注意是Class对象不是Student对象
  • 一个Class类(字节码文件)对应一个Class对象
  • 该Class对象保存了Student类的基础信息,比如这个Student类有几个字段(Filed)?有几个构造方法(Constructor)?有几个方法(Method)?有哪些注解(Annotation)?等信息。

有了上面的关于Student类的基本信息对象(java.lang.Class对象),在运行期就可以根据这些信息来实例化Student类的对象。

  • 在运行期你可以直接new一个Student对象
  • 也可以使用反射的方法构造一个Student对象

但是无论你new多少个Student对象,不论你反射构建多少个Student对象,保存Student类信息的java.lang.Class对象都只有一个。下面的代码可以证明。

了解了上面的这些基础信息,我们就可以更深入学习反射类相关的类和方法了:

  • java.lang.Class: 代表一个类
  • java.lang.reflect.Constructor: 代表类的构造方法
  • java.lang.reflect.Method: 代表类的普通方法
  • java.lang.reflect.Field: 代表类的成员变量
  • Java.lang.reflect.Modifier: 修饰符,方法的修饰符,成员变量的修饰符。
  • java.lang.annotation.Annotation:在类、成员变量、构造方法、普通方法上都可以加注解

虽然有三种方法可以获取某个类的Class对象,但是只有第一种可以被称为“反射”。

Class类对象信息中几乎包括了所有的你想知道的关于这个类型定义的信息,更多的方法就不一一列举了。还可以通过下面的方法

  • 获取Class类对象代表的类实现了哪些接口:getInterfaces()
  • 获取Class类对象代表的类使用了哪些注解:getAnnotations()

结合上文中的Student类的定义理解下面的代码

  • getFields()方法获取类的非私有的成员变量,数组,包含从父类继承的成员变量
  • getDeclaredFields方法获取所有的成员变量,数组,但是不包含从父类继承而来的成员变量
  • getMethods() : 获取Class对象代表的类的所有的非私有方法,数组,「包含从父类继承而来的方法」
  • getDeclaredMethods() : 获取Class对象代表的类定义的所有的方法,数组,「但是不包含从父类继承而来的方法」
  • getMethod(methodName): 获取Class对象代表的类的指定方法名的非私有方法
  • getDeclaredMethod(methodName): 获取Class对象代表的类的指定方法名的方法

上面代码的执行结果如下:

可以看到getMethods获取的方法中包含Object父类中定义的方法,但是不包含本类中定义的私有方法sleep。另外我们还可以获取方法的参数及返回值信息:

  • 获取参数相关的属性:
    • 获取方法参数个数:getParameterCount()
    • 获取方法参数数组对象:getParameters() ,返回值是java.lang.reflect.Parameter数组
  • 获取返回值相关的属性
    • 获取方法返回值的数据类型:getReturnType()

实际在上文中已经演示了方法的调用,如下invoke调用dinner方法

dinner方法是无参的那么有参数的方法怎么调用?看看invoke方法定义,第一个参数是Method对象,无论后面 Object… args有多少参数就按照方法定义依次传参就可以了。

  • 通过配置信息调用类的方法
  • 结合注解实现特殊功能
  • 按需加载jar包或class

将上文的hello world中的代码封装一下,你知道类名className和方法名methodName是不是就可以调用方法了?至于你将className和 methodName配置到文件,还是nacos,还是数据库,自己决定吧!

大家如果学习过mybatis plus都应该学习过这样的一个注解TableName,这个注解表示当前的实例类Student对应的数据库中的哪一张表。如下问代码所示,Student所示该类对应的是t_student这张表。

下面我们自定义TableName这个注解

有了这个注解,我们就可以扫描某个路径下的java文件,至于类注解的扫描我们就不用自己开发了,引入下面的maven坐标就可以

看下面代码:先扫描包,从包中获取标注了TableName注解的类,再对该类打印注解value信息

输出结果是:

有的朋友会问这有什么用?这有大用处了。有了类定义与数据库表的对应关系,你还能通过反射获取类的成员变量,之后你是不是就可以根据表明t_student和字段名nickName,age构建增删改查的SQL了?全都构建完毕,是不是就是一个基础得Mybatis plus了?

反射和注解结合使用,可以演化出许许多多的应用场景,特别是在架构优化方面,等待你去发觉啊!

在某些场景下,我们可能不希望JVM的加载器一次性的把所有的jar包装载到JVM虚拟机中,因为这样会影响项目的启动和初始化效率,并且占用较多的内存。我们希望按需加载,需要用到哪些jar,按照程序动态运行的需求取加载这些jar。

同样的把.class文件放在一个路径下,我们也是可以动态加载到的

类的动态加载能不能让你想到些什么?是不是可以实现代码修改,不需要重新启动容器?对的,就是这个原理,因为一个类的Class对象只有一个,所以不管你重新加载多少次,都是使用最后一次加载的class对象(上文讲过哦)。

  • 优点:自由,使用灵活,不受类的访问权限限制。可以根据指定类名、方法名来实现方法调用,非常适合实现业务的灵活配置。
  • 缺点:
    • 也正因为反射不受类的访问权限限制,其安全性低,很大部分的java安全问题都是反射导致的。
    • 相对于正常的对象的访问调用,反射因为存在类和方法的实例化过程,性能也相对较低
    • 破坏java类封装性,类的信息隐藏性和边界被破坏

「码文不易,如果您觉得有帮助,请帮忙点击在看或者分享,没有您的支持我可能无法坚持下去!」

JAVA反射机制详解,一学就会

目录

反射(Reflection),是指Java程序具有 在运行期 分析类以及修改其本身状态或行为的能力

通俗点说 就是 通过反射我们可以 动态地 获取一个类的所有属性和方法,还可以操作这些方法和属性。

一般我们创建一个对象实例 Person zhang = new Person();虽然是简简单单一句,但JVM内部的实现过程是复杂的:

  1. 将硬盘上指定位置的Person.class文件加载进内存
  2. 执行main方法时,在栈内存中开辟了main方法的空间(压栈-进栈),然后在main方法的栈区分配了一个变量zhang。
  3. 执行new,在堆内存中开辟一个 实体类的 空间,分配了一个内存首地址值
  4. 调用该实体类对应的构造函数,进行初始化(如果没有构造函数,Java会补上一个默认构造函数)。
  5. 将实体类的 首地址赋值给zhang,变量zhang就引用了该实体。(指向了该对象)

其中上图步骤1 Classloader(类加载器) 将class文件加载到内存中具体分为3个阶段:加载、连接、初始化

而又在 加载阶段,类加载器将类对应的.class文件中的二进制字节流读入到内存中,将这个字节流转化为方法区的运行时数据结构,然后在堆区创建一个**java.lang.Class 对象**(类相关的信息),作为对方法区中这些数据的访问入口

然后再通过 类的实例 来执操作 类的方法和属性 ,比如 zhang.eat(), zhang.getHeight() 等等

如果我们使用反射的话,我们需要拿到该类Person的Class对象,再通过Class对象来操作 类的方法和属性或者创建类的实例

我们可以发现 通过new创建类的实例和反射创建类的实例,都绕不开.class文件 和 Class类的。

首先我们得先了解一下 什么是.class文件

举个简单的例子,创建一个Person类:

我们执行javac命令,编译生成Person.class文件

然后我们通过vim 16进制 打开它

不同的操作系统,不同的 CPU 具有不同的指令集,JAVA能做到平台无关性,依靠的就是 Java 虚拟机。

.java源码是给人类读的,而 .class字节码是给JVM虚拟机读的 ,计算机智能识别 0 和 1组成的二进制文件,所以虚拟机就是我们编写的代码和计算机之间的桥梁。

虚拟机将我们编写的 .java 源程序文件编译为 字节码 格式的 .class 文件,字节码是各种虚拟机与所有平台统一使用的程序存储格式,class文件主要用于解决平台无关性的中间文件

Person.class文件 包含Person类的所有信息

我们来看下jdk的官方api文档对其的定义:

Class类的类表示正在运行的Java应用程序中的类和接口。 枚举是一种类,一个注释是一种界面。 每个数组也属于一个反映为类对象的类,该对象由具有相同元素类型和维数的所有数组共享。

原始Java类型( boolean , byte , char , short , int , long , float和double ),和关键字void也表示为类对象。

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

**java 万物皆是Class类 **

【图片】

我们来看下Class类的源码,源码太多了,挑了几个重点:

我们可以发现Class也是类,是一种特殊的类,将我们定义普通类的共同的部分进行抽象,保存类的属性,方法,构造方法,类名、包名、父类,注解等和类相关的信息。

Class类的构造方法是private, 只有JVM能创建Class实例 ,我们开发人员 是无法创建Class实例的,JVM在构造Class对象时,需要传入一个 类加载器

类也是可以用来存储数据的,Class类就像 普通类的模板 一样,用来保存“类所有相关信息”的类。

我们来继续看这个利用反射的例子: Class personClass = Person.class;

由于JVM为加载的 Person.class创建了对应的Class实例,并在该实例中保存了该 Person.class的所有信息,因此,如果获取了Class实例(personClass ),我们就可以通过这个Class实例获取到该实例对应的 Person类 的所有信息。

  1. 通过对象调用 getClass() 方法来获取

像这种已经创建了对象的,再去进行反射的话,有点多此一举。

一般是用于传过来的是Object类型的对象,不知道具体是什么类,再用这种方式比较靠谱

  1. 类名.class

这种需要提前知道导入类的包,程序性能更高,比较常用,通过此方式获取 Class 对象 ,Person类不会进行初始化

  1. 通过 Class 对象的 forName() 静态方法来获取,最常用的一种方式

这种只需传入类的全路径 Class.forName会进行初始化initialization步骤 ,即静态初始化(会初始化类变量,静态代码块)。

  1. 通过类加载器对象的 loadClass() 方法

loadClass的源码:

loadClass 传入的第二个参数是\”false\”,因此它不会对类进行连接这一步骤,根据 类的生命周期 我们知道,如果一个类没有进行验证和准备的话,是无法进行初始化过程的,即 不会进行类初始化,静态代码块和静态对象也不会得到执行

我们将c1,c2,c3,c4进行 equals 比较

因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例, 一个类在 JVM 中只会有一个 Class 实例

日常开发的时候,我们一般使用反射是为了 创建类实例(对象)、反射获取类的属性和调用类的方法

我们这边就不全部展开讲了,挑几个重点讲解一下

  1. 调用class对象的 newInstance() 方法

结果:

吃饭

注意:Person类必须有一个 无参的构造器类的构造器的访问权限不能是private

  1. 使用指定构造方法 Constructor 来创建对象

如果我们非得让Person类的无参构造器设为private呢,我们可以获取对应的Constructor来创建对象

结果:

吃饭

注意:setAccessible()方法能在运行时 压制 Java语言访问控制检查(Java language access control checks),从而能任意调用 被私有化 保护的方法、域和构造方法。

由此我们可以发现** 单例模式不再安全,反射可破之!**

我们来看一个例子:

结果:

li hua

我们可以发现 反射可以破坏类的封装

我们来看一个例子:

结果:

我们发现获取方法getMethod()时,需要传参 方法名和参数

这是因为.class文件中通常有 不止一个方法 ,获取方法getMethod()时,会去调用searchMethods方法循环遍历所有Method,然后根据 方法名和参数类型 找到唯一符合的Method返回。

我们知道类的方法是在JVM的方法区中 ,当我们new 多个对象时,属性会另外开辟堆空间存放,而方法只有一份,不会额外消耗内存,方法就像一套指令模板,谁都可以传入数据交给它执行,然后得到对应执行结果。

method.invoke(obj, args) 时传入目标对象,即可调用对应对象的方法

如果获取到的Method表示一个静态方法,调用静态方法时, 无需指定实例对象 ,所以invoke方法传入的第一个参数永远为null, method.invoke(null, args)

那如果 方法重写了呢, 反射依旧遵循 多态 的原则

如果平时我们只是写业务代码,很少会接触到直接使用反射机制的场景,毕竟我们可以直接new一个对象,性能比还反射要高。

但如果我们是工具框架的开发者,那一定非常熟悉,像 Spring/Spring Boot、MyBatis 等等框架中都大量使用反射机制, 反射被称为框架的灵魂

比如:

  1. Mybatis Plus可以让我们只写接口,不写实现类,就可以执行SQL
  2. 开发项目时,切换不同的数据库只需更改配置文件即可
  3. 类上加上@Component注解,Spring就帮我们创建对象
  4. 在Spring我们只需 @Value注解就读取到配置文件中的值
  5. 等等

我们来模拟一个配置高于编码的例子

新建my.properties,将其放在resources的目录下

Person类 还是本文 一直用的,在文章的开头有

最后我们来编写一个测试类

结果:

配置文件中的内容:className=com.zj.demotest.domain.Person

配置文件中的内容:methodName=eat

吃饭

紧接着,我们修改配置文件:

结果变为:

配置文件中的内容:className=com.zj.demotest.domain.Person

配置文件中的内容:methodName=Dance

跳舞

是不是很方便?

反射机制是一种功能强大的机制,让Java程序具有在 运行期 分析类以及修改其本身状态或行为的能力

对于特定的复杂系统编程任务,它是非常必要的,为各种框架提供开箱即用的功能提供了便利,为解耦合提供了保障机制。

但是世事无绝对,反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接访问对象要差点(JIT优化后,对于框架来说实际是影响不大的),还会增加程序的复杂性等(明明直接new一下就能解决的事情,非要写一大段代码)。

Java反射机制最全详解(图文全面总结)

Java反射是各种框架以及中间件实现的基石,也是大厂面试重点考察内容,下面全面来详解Java反射机制@mikechen

本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》里面。

Java反射就是指:在运行状态中,对于任意一个类,都能够知道这个类的所有属性、方法等。

这种动态获取的信息,以及动态调用对象的方法的功能,我们称为Java反射机制。

Java反射功能非常强大,如下图所示:

  • 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等;
  • 获取任意对象的属性,用任意对象的方法,并且能改变对象的属性;
  • 通过反射我们可以实现动态装配,降低代码的耦合度,动态代理等。

Java反射的应用场景,如下图所示:

1)数据库驱动:项目底层数据库有时是用mysql,有时用oracle,需要动态地根据实际情况加载驱动类,这个时候反射就有用了。

2)Spring IOC:Spring IOC 通过配置文件配置各种各样的bean,你需要用到哪些bean就配哪些,这里会涉及到Java反射。

3)Spring AOP:Spring AOP的Java动态代理功能,都和Java反射有关系。

4)主流框架:除此之外还有很多Java开发框架:Mybatis、Dubbo、Rocketmq等等都会用到Java反射机制。

一般情况下我们通过反射创建类对象主要有两种方式:

第一种:通过 Class 对象的 newInstance() 方法

如下所示:

第二种:通过 Constructor 对象的 newInstance() 方法

如下所示:

如下所示:

更加详细成员变量获取参考如下:

Java反射方法获取,如下所示:

更加详细方法获取参考如下:

Java反射获得构造函数,如下所示:

更加详细构造函数获取参考如下:

调用反射的总体流程如下:

1.源文件转Class文件

当我们编写完一个Java项目之后,每个java文件都会被编译成一个.class文件,这是第一步。

如下图所示:

2.加载到JVM

编译好的class文件,在程序运行时会被ClassLoader加载到JVM中。

如下图所示:

当一个类被加载以后,JVM就会在内存中自动产生一个Class对象。

3.获取Class对象

被JVM加载进来后,就可以通过Class对象获取:属性、方法、构造函数。

如下图所示:

我们一般平时是通过new的形式,创建对象实际上就是通过这些Class来创建的,只不过这个class文件是编译的时候就生成的,程序相当于写死了给jvm去跑。

原来使用new的时候,需要明确的指定类名,这个时候属于硬编码实现,而在使用反射的时候,可以只传入类名参数,就可以生成对象。

通过Java反射机制,这样就可以极大的降低耦合性,使得程序更具灵活性,所以才会有很多Java开发框架都采用了Java反射,就是这个原因。

本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》里面。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。