JAVA中的反射

发布时间:2024年01月14日
反射-reflect

? 反射指的是:对于处于运行状态中的任意一个类,都可以通过一定的方式获取这个类中的属性,方法(包括构造方法);对于处于运行状态中的任意一个对象,都可以通过一定的方式获取这个对象的属性和方法(不包括构造方法),这种动态获取信息,动态调用对象方法的功能,称为java的反射机制。

为什么不能通过对象获取构造方法?

? 构造方法不属于类,也不属于类的对象,所以不能人为的显式的去调用它,而是通过new产生一个对象时调用,其做作用有二:一是分配内存空间,二是对象属性初始化。更加准确的描述可以认为构造方法类似一个工厂,而类的对象可以是由这个工厂获得的产品,而类的作用就是规定了这个产品的属性。

反射的普遍解释

? 在java中,有一个Class类,而在Java中处于运行状态中的类或接口都可以看做是Class类的一个实例。其中枚举看做是一种类,注释是一种接口,相同数据类型和维数的数组看做是一个Class类实例(也就是说数组可以有很多个Class实例,区分它们就是数据类型和维数的不同);同时java 中的8种基本数据类型和void类型也对应一个Class类实例(具体表现为基本数据类型的封装类以及void类型,都有其对应的Class类实例,也就是说你能想到的类,一旦处于运行状态中,就都会有对应得Class类实例)。

? 但是Class类却没有公共构造方法,这也就是说它不能人为的创建这个类的对象。而是当加载一个类时,由java虚拟机和类加载器中的defineClass()方法自动构造。最典型的就是在AOP的cglib代理生效的方式3:使用Instrumentation.redefineClass()方法替换掉原来类的字节码文件。

类加载器- ClassLoader

? ClassLoader类是一个抽象类,负责加载类的对象。每个Class类的实例都定义了一个它的类加载器的引用。

? 通常来说,如果给定一个类的二进制文件名,那么类加载器就会试图查找或生成类的定义数据,一般策略是将给定的名称转换为文件名,然后再从文件系统中查找该文件名对应的类文件。 但是需要注意的是,数组类的Class实例并不是由类加载器负责加载的,而是在需要时由java虚拟机自动创建,可以通过Class.getClassLoader()返回类加载器。并且该类加载器和数组的数据类型对应的类加载器相一致,但是如果该数组的数据类型是基本数据类型,则这个Class类的实例没有类加载器。

? defineClass(String name,byte[] b,int off,int len):将一个byte数组转换Class类的实例

JDK提供的三种默认的类加载器

? 1、BootstarpLoader(系统类加载器):该类加载器由C++语言编写,负责加载系统类库

? 2、extClassLoader(扩展类加载器):由java语言编写,负责加载扩展的jar

? 3、AppClassLoader(应用类加载器):由java语言编写,负责加载用户自定义类

运行流程如下:在java虚拟机初始化完成后,就会有第一个类加载器,即BootstarpLoader,等BootstarpLoader类加载器完成基本的初始化动作之后,就会由这个类加载器负责加载extClassLoader,并且设置其父类加载器为null(因为BootstarpLoader类加载是由c++语言编写的,并且对外不可见),等扩展类加载器加载初始化完成之后,BootstarpLoader类加载器就负责加载APPClassLoader,并且设置其父类加载器为extClassLoader,需要注意的是扩展类加载器和应用类加载器在java中是以静态的方式存在的。

线程上下文类加载:
引入线程类加载器的原因:

? 假如在创建Class实例时,给定的一个二进制名称,这时就由ClassLoader类加载器去查找或生成类的定义数据,一般的策略就是将文件名转换成文件系统中的文件名,然后再根据此文件名去文件系统中查找与之对应的文件并加载。但是由JDK提供的类加载器却存在一定的弊端,如:java提供了很多的服务提供者接口(service provider interface),并且允许用户去实现这些接口,但存在的问题是,实现这些接口的类和接口属于同一体系,这时java的父类委托机制只会加载SPI接口,而不会加载实现类(原因是:由APPClassLoader委托给extClassLoader,再由extClassLoader委托给BootStarpLoader,但是BootstarpLoader只负责加载java的核心库,因此在BootstarpLoader加载相应的SPI接口后,子类加载器就认为已经加载完毕,不会去加载接口的实现类),为了解决这种弊端,就引入了线程上下文类加载器。

解决方式

? 线程上下文类加载器(context class loader)是从jdk1.2开始引入的,而在java.lang.Thread中的方法getContextLoader()和setContextLoader(ClassLoader cl) 用来设置和获取线程的上下文加载器,但是如果没有用setContextLoader(ClassLoader cl)设置的话,线程上下文类加载器将会继承父类的上下文类加载器。也就是系统类加载器(BootstarpLoader)。这样,前面说的SPI接口实现类就可以通过这种方式进行加载,但这样做也会带来新的问题,就是全盘委托机制受到破坏,可能会引入恶意的攻击代码,如用户引入自定义的系统类,进而破坏了java中封装的系统类。

类加载器加载class文件的方式

? 类加载器的作用:根据给定的名称在文件系统中搜索字节码文件进行解析并构造JVM内部对象

过程:

? 1、装载:根据文件名查找和导入字节码文件

? 2、检查:检查导入的字节码文件的语法的正确性

? 3、解析:将文件中的符号引用变为直接引用-----该步骤可以不存在

? 4、准备:给类的静态变量分配内存空间

? 5、初始化:对静态变量、静态代码块进行初始化操作

Java装载器原理

? 全盘委托机制:就是当一个ClassLoader装载一个类时,会先委托给它的父类装载器查找目标类,如果父类装载器没有找到,最后再由当前类加载器根据路径查找并装载目标类;但如果其父类装载器找到并且撞在了目标类,那么当前年类加载器就不会再去装载目标类。

? 全盘:即全局装载,并只进行一次

? 委托:当前类装载器委托父类查找,一致递归到系统类装载器BootstarpLoader,然后找到了就装载,子类不装载,找不到就层层往下,直到当前的类装载器。

Class类中的方法以及方法参数的说明:

? 注意:泛型的概念,可变参数的概念

加载Class实例的方法说明:

? static forName(String name ) --返回名称为name的类或接口的Class实例

? static forName(String name,Boolean initialize, ClassLoader loader)–返回名称为name的Class类实例,并通过loader加载器加载字节码文件,当initialize为true时,代表未初始化时才初始化该类

获取CLass类实例的构造方法的说明:

? 类型:Constructor—封装某个类的单个构造方法的信息以及访问权限

? getConstructor(Class<?>… parameterType)–获取当前Class实例中,参数类型想匹配的Constructor对象,这个对象包含该构造方法的信息,但该方法只能获取【public】修饰的构造方法

? getConstructor()–获取当前CLass类实例中的【public】修饰的构造方法(因为一个类中的公共的构造方法有不确定的个数),,返回一个类型为Constructor的数组

? getDeclaredConstructor(Class<?>… parameterType ) – 获取当前Class实例中,与parameterType参数类型相一致的Constructor对象,该方法可以获取类中参数匹配的构造方法,包括私有的

? getDeclaredConstructor()–获取当前Class实例中所有的构造方法,包括私有,返回值类型是一个数组

Constructor中的方法说明:

? getName()–调用该方法的是Constructor对象,将会返回调用该方法的Constructor对象所描述的CLass类实例中的构造方法的名称

? newInstance(Object… init)–由Constructor对象调用,返回Constructor对象描述的构造方法的对象,并根据参数进行对象的初始化

获取Class类实例的普通成员方法的说明:

? 类型:Method–该类型中封装了类中某个方法的逻辑信息和访问权限信息

? getMethod(Class<?>… parameterType )–在调用该方法的Class实例中,获取参数类型相匹配的【public】的成员方法

? getMethod()–在调用该方法的Class类实例中,获取所有的【public】的成员方法

? getDeclaredMethod(Class<?>… parameterType )–在调用该方法的实例中,获取参数类型相匹配的成员方法,包括【private】的成员方法

? getDeclaredMethod()–在调用该方法的实例中,获取所有的成员方法,该方法会返回一个数组

Method中的常见方法说明:

? getName()–返回调用该方法的Method实例所表示的方法的名称

? invoke(Object ObjectName,Object… args)–执行调用该方法的Method实例所代表的方法,并且由ObjectName指明对象,args代表该执行方法的参数

获取Class类实例中普通成员变量方法的说明:

? 类型:Field–该类型包含的是CLass类实例中的成员变量的权限信息

? getField(String name )–在调用该方法的Class类实例中,获取名称为namepublic】成员变量

? getField()–在调用该方法的Class实例中,获取所有的【public】成员变量

? getDeclaredField(Stirng name) --在调用该方法的实例中,获取名称与name一致的成员变量

? getDeclaredField()-- 在调用该方法的实例中,获取所有的成员变量,该方法返回一个数组

Field中成员变量的常见调用方法说明:

? getName()–返回调用该方法的Field实例所代表的属性名称

? get(Object obj)–返回obj对象的Field代表的属性值

? set(Object obj,Object value)–该方法由Field实例调用,用来设置obj对象的Filed代表的属性值为value

AccessibleObject简单介绍

? AccessibleObject是Constructor、Method、Field的基类。它提供了将反射的对象标记暴力打破的能力。原因是:在java中,有一个访问控制检查机制,主要表现为public,protected、缺省的、私有的作用范围,当获取一个Constructor、Method、Field实例时,这个实例就包含所要表达的构造方法、成员方法、属性的逻辑以及修饰符,没有加入暴力打破机制,那么即便获取了也无法使用,所以,非法的获取要使用,就要使用这个类的一个方法,如下介绍:

? setAccessible(Boolean flag)–设置为true时,就可使用Constructor、Method、Field的实例所代表的的内容,而不受访问修饰符的限制

反射使用的示例:

? 加入存在下述类:

public class person{

	private String name;

	private int age;

	public person(){

	}
	public person(String name){

		this.name=name;

	}
	protected person(int age){

		this.age=age;

	}
	private person(String name,int age){

		this.name=name;

		this.age=age;		

	}
	public void setName(String name){

		this.name=name;

	}
	public String getName(){

		return this.name;

	}
	public void setAge ( int age){

		this.age=age;

	}
	public int getAge(){

		return this.age;

	}
}

那么,不通过上述类获取该类的定义数据的方式如下:

? 首先,由给定包含该类的文件的二进制文件名,然后就会根据该二进制文件名去文件系统中加载该文件,一般的方式是:首先由于是自定义类,就由APPClassLoader类加载器负责加载,然后AppClassLoader就把该加载任务委托给父类类加载器(extClassLoader),接着extClassLoader类加载器就把加载任务又委托给BootstarpLoader,这样就由系统类加载器负责加载,找不到就又让其子类加载,最后又返回到APPClassLoader加载,等加载完成后,那么就会产生一个Class类实例,也就是这个person类,

【注意下述类似方法的区别:】

Class classObject = Class.forName("person");	//获取名称为person的Class类实例

Constructor construct=classObject.getConstructor(int.class);//获取classObject类实例中参数为int的构造方法

Object object=construct.newInstance(45);//由于获取的构造方法有参数,所以必须带参数创建实例对象\

		//上述语句将会产生一个Person类的对象,并且name=null,age=45

Constructor construct2=classObject.getDeclaredConstructor(Stirng.class,int.class);

		//获取Person类中参数类型为String和int的构造方法,但该方法不可直接使用,因为在类中是私有的,所以这是就要用到Constructor类的基类,AccessibleObject中的setAccessible()方法,并设置参数为true

 construct2.setAccessible(true);//暴力打破机制

 construct2.newInstance("cultrue",21);

		//这样就存在了一个Person对象,并且name=cultrue,age=21

Method myMethod=classObject.getDeclaredMethod(String.class);//获取Class实例中参数类型为String的方法

myMethod.setAccessible(true);

myMethod.invoke(construct,"this is friday");

		//给constructor对象的由MyMethod代表的方法设置参数为“this is friday”并执行

Field myField=classObject.getdeclaredField("age");//该方法获取类中的属性为age的字段

myField.setAccessible(true);

myField.set(construct,44); 	//设置Person对象construct中由myField代表的属性的值为44 

myFiled.get(construct);	//获取Person对象construct中由myField代表的属性的值

【注意事项】:上述Constructor、Method、Field中相识方法的不同点,尤其是返回值类型的不同,单个返回值和返回值是数组的情况

重点

? 上面说的,类加载器加载一个类时,采用全盘委托机制,这样就存在一个问题,那就是自己写了一个跟系统类名称一致类,系统会不会加载?如果加载会产生什么后果?java是怎么避免这种冲突的?

系统会不会加载?

? 首先肯定的回答是:系统会加载用户定义的与系统类重名的类,而且并不会对系统的类进行破坏,这是因为在java中有一个包的概念,也就是路径问题,当用户加载一个类时,就会根据路径和名称搜索加载,加载完成后就存放在这个包的区域内,这样,当用户访问时,根据包的路径和名称使用。

加载会产生什么后果?

? 如果单从JDK的介绍文档和某些博客的粗浅解释,这种加载会破坏系统类的完整性;但是,需要注意的是,如果你打算破坏一个系统提供的类,那么你就要保证所加载的自定义类的包路径和名称与系统类完全一致;这时在加载时,根据全盘委托机制,它并不会加载你定义的类,所以这种做法没有效果;当然,你可以通过自定义线程类加载器,重写findClass()方法进行加载,并且该线程还不能继承APPClassLoader,否则也没有效果,因为这样全盘委托机制也会起作用;等通过这种极端的方式加载后,很完美,系统类你就覆盖了,吃力不讨好。

java是怎么避免在这种冲突的?

? 如果你非要冲突,那么这种冲突就避免不了,所以按照你的想法进行加载。通常来说,类名称一致的话,就根据包路径进行区分。

文章来源:https://blog.csdn.net/weixin_44487537/article/details/135505679
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。