注解、类的加载、反射
封面来源:本文封面来源于网络,如有侵权,请联系删除。
参考视频:
类加载器相关代码:mofan-demo/classloader
本文基于 JDK 21
1. 注解
1.1 自定义注解
使用 @interface
自定义注解,就会自动继承 java.lang.annotation.Annotation
接口。
@interface
可以用来声明一个注解,格式:public @interface 注解名 {定义内容}
。注解中每一个方法表示声明了一个配置参数,而方法的名称就是参数的名称,返回值类型就是参数的类型(返回值只能是基本类型、Class、String 和 enum),并且可以通过 default
关键词来声明参数的默认值。如果只有一个参数成员,一般参数名为 value。
注解元素必须要有值。定义注解元素时,经常使用空字符串或 0 作为默认值。
1.2 元注解
元注解的作用就是负责注解其他注解,Java 定义了 4 个标准的元注解(meta-annotation)类型,它们被用来提供对其它注解(annotation)类型作说明。
这四个元注解位于 java.lang.annotation
包中,它们分别是:
1 | // 用于描述注解的使用范围 |
2. 类的加载
2.1 Java 程序的启动与运行
JVM 在启动时,会加载 main()
方法所在的类(这个类被叫做起始类),接着 JVM 会执行 main()
方法,在执行的过程中,可能会触发进一步的执行,继续加载其他的类并执行其他的方法,直到程序退出。
需要注意的是,在加载某个类或执行某个方法时,也可能会触发其他类的加载。
Java 的类加载是在运行时动态完成的,这种动态的特性,正是 Java 语言灵活性的根源。
2.2 类的加载
在 Java 中,所有类的加载都是通过类加载器 ClassLoader 来完成。
首先使用 Java 代码或 JVM 来触发一个加载动作,然后将类的全限定名传给类加载器,类加载器再通过类名获取到字节码的二进制流,这份二进制流可以通过以下方式获取:
- 从本地磁盘读取类文件
- 从网络读取类文件
- 运行时计算生成字节码流
最后再根据字节码二进制流创建并加载对应的 Class 对象。
类的加载
将 Class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class
对象。
类的连接
类的连接是指将 Java 类的二进制代码合并到 JVM 的运行状态之中的过程。主要分为三步,分别是验证、准备和解析。
- 验证:确保加载的类信息符合 JVM 规范,即加载的 Class 文件的格式是否正确。
- 准备:正式为类的静态变量(static)分配内存并为其设置默认初始值的阶段。
- 解析:虚拟机常量池内的符号引用(变量名)替换为直接引用(地址)的过程。
类的初始化
类的初始化就是执行类构造器 <clinit>()
方法的过程。类构造器 <clinit>()
方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的 <clinit>()
方法在多线程环境中被正确加锁和同步。
2.3 类初始化
类的主动引用一定会发生类的初始化。 比如:
- 当虚拟机启动时,初始化
main()
方法所在的类; new
一个对象;- 调用类的静态成员(final 常量除外)和静态方法;
- 使用
java.lang.reflect
包中的方法对类进行反射调用; - 初始化一个类时,如果其父类没有被初始化,则会先初始化它的父类。
类的被动引用不会发生类的初始化。 比如:
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化;
- 通过数组定义类引用,不会触发此类的初始化;
- 引用常量不会触发其所在类的初始化(常量在连接阶段就存入调用类的常量池中了)。
2.4 类加载器
类加载器
类加载器(ClassLoader)的作用:将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的 java.lang.Class
对象,作为方法区中类数据的访问入口。
简单来说,类的加载阶段有这样一个动作:通过一个类的全限定名来获取描述此类的二进制字节流。这个动作放到了 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,而实现这个动作的代码模块就叫做类加载器。
类加载器在 Java 程序中起到的作用不局限于类加载阶段。为了确定任意一个类在 JVM 中的唯一性,除了这个类本身以外,还需要加上加载这个类的类加载器作为依据。每一个类加载器,都拥有一个独立的类命名空间。
也就是说,要判断两个类是否相等,只有在这两个类都是由同一个类加载器加载的前提下才有意义。因此就算是两个来自于同一 class 文件,被同一个虚拟机加载的类,但加载它们的类加载器不同,那也不能认为这两个类相等。这里的“相等”不仅仅指使用 equals()
方法比较,还包括 isAssignableFrom()
方法和 isInstance()
方法的返回结果,以及使用 instanceof
关键字进行判断。
拓展:Class#isAssignableFrom()
方法与 instanceof
关键词的使用。
Class#isAssignableFrom()
方法用于判断某个类是否是另一个类的父类,instanceof
关键词用于判断某个实例是否是某个父类类型。
使用方式:
1 | 父类.class.isAssignableFrom(子类.class) |
类加载器的种类
- 启动类加载器(Bootstrap ClassLoader),或者说引导类加载器、根加载器。该类加载器在 JVM 中通常使用 C/C++ 语言原生实现,是 JVM 自带的类加载器(是 JVM 的一部分)。
- 其他的类加载器。这些类加载器由 Java 语言实现,不在 JVM 中,并且都继承自
java.lang.ClassLoader
。
几个系统级别的类加载器
- 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在
${JAVA_HONE}\lib
目录中,或者被-XbootstrapPath
参数所指定的 目录中,并且是虚拟机 基于一定规则(如文件名称规则,如 rt.jar)标识的 类库 加载到虚拟机内存中。该类加载器无法通过 Java 程序直接获取,如果想委派到启动类加载器直接使用null
替代即可。 - 扩展类加载器(Extension ClassLoader):该类加载器由
sun.misc.Launcher
的静态嵌套类ExtClassLoader
实现。它负责将${JAVA_HONE}\lib\ext
目录下或通过-Djava.ext.dirs
参数指定的目录下的所有类库装入工作库。开发者可以直接使用此加载器。Java 9 移除了拓展机制,ExtClassLoader 被 PlatformClassLoader 取代,PlatformClassLoader 主要用于加载 Java 平台模块中的类,包括java.sql
、java.xml
中的类。 - 应用类加载器(Application ClassLoader):该类加载器由
sun.misc.Launcher
的静态嵌套类AppClassLoader
实现,由于该类加载器的实例是ClassLoader
中静态方法getSystemClassLoader()
中的返回值,因此这个类加载器也被成为 系统类加载器。它负责加载用户类路径(ClassPath)或-Djava.class.path
所指的目录下的所有类库装入工作库,是最常用的加载器。开发者也可以直接使用此加载器。如果程序中没有自定义类加载器,一般情况下该类加载器就是程序中默认使用的类加载器。 - 线程上下文类加载器(Thread Context ClassLoader):后面再说。 😉
类加载器之间的关系
扩展类加载器(Extension ClassLoader)、应用类加载器(Application ClassLoader)以及用户自定义的类加载器都将显式继承抽象类 java.lang.ClassLoader
,它们显式拥有一个“父”类加载器,可以将类加载请求直接委派给“父”类加载器 java.lang.ClassLoader
。
启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用类加载器(Application ClassLoader)这三者之间并不是继承关系,而是 组合 关系。应用类加载器(Application ClassLoader)显式拥有一个“父”类加载器,即扩展类加载器(Extension ClassLoader),而扩展类加载器(Extension ClassLoader)的“父”类加载器则 隐式 指向启动类加载器(Bootstrap ClassLoader)。
3. 双亲委派模型
3.1 基本概念
编写的 Java 程序都是由上面四种类加载器相互配合进行类加载的,当然还可以自定义类加载器。其中,启动类加载器、拓展类加载器、系统类加载器和自定义类加载器的关系如下:
像上图这样的层次关系被称为 双亲委派模型(Parents Delegation Model),类加载器之间使用双亲委派模型来协作工作。
从 双亲委派模型 这个词上来说,可能存在以下误解:
- “双亲”被误解为存在两个“父/母”类加载器
- “Parent”容易被误解为继承关系中的父类
除了顶层的启动类加载器(Bootstrap ClassLoader),其他的类加载器有且仅有一个“父”类加载器。类加载器与其“父”类加载器之间的关系不以继承(Inheritance)来实现,而是以组合(Composition)(不是亲爹,最多算干爹🧔♂️):
1 | public abstract class ClassLoader { |
简单验证下类加载器的层次关系:
1 |
|
运行结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@5caf905d null
结果符合我们的预期,而最后打印出的 null
说明了 classLoader.getParent()
指的是启动类加载器,因为它没有父类加载器。
3.2 工作机制
对于双亲委派模型来说,有几点需要明白:
- 每个 Class 都有对应的 ClassLoader
- 除 Bootstrap ClassLoader 外(因为它是最顶层的类加载器),每个 ClassLoader 都有一个“父”类加载器(Parent ClassLoader)
- 对于一个类加载请求,总是优先委派给“父”类加载器来尝试加载(有事干爹先上!💪)
- 对于用于自定义的类加载器,默认的“父”类加载器是 Application ClassLoader
那双亲委派模型的具体工作机制是怎样的呢?
当一个类加载器收到了类加载请求时,它不会自己尝试去加载这个类,而是把这个请求委派给它的父类加载器,直到请求传递到顶层的启动类加载器。如果父类加载器无法完成当前的类加载请求(在它的搜索范围内没有找到需要加载的类),那么父类加载器又会把类加载请求委派给它的子类加载器。当然也可能直到最后这个类加载请求也没法完成,这时就会抛出 ClassNotFoundException
异常。
这里还需要引出 类缓存 的概念。所谓类缓存,就是“标准的 JavaSE 类加载器可以按要求查找类,一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过 JVM 垃圾回收机制可以回收这些 Class 对象”。
简单来说,类加载器会缓存自己已经加载过的类。当加载一个类时,首先会从缓存中加载,如果缓存中不存在,再按照前面所说的工作机制去加载类。
3.2 优点
使用双亲委派模型来协调类加载器之间的关系,可以使 Java 类随着加载它的类加载器一起具备一种带有优先级的层次关系。越顶层的类加载器,对其可见的类总是会被优先加载。
假设在 ClassPath 下自定义了一个 java.lang.String
类,而在 JDK 的 rt.jar
中也存在一个同名的类,无论是哪个类加载器加载自定义的 String
类,最终都是委派给处于最顶层的启动类加载器进行加载,也就是最终会加载位于 rt.jar
中的 String
类,而不是自定义的 String
类,这 保证了 Java 类型体系的稳定性。
也正因如此,java.lang
包下的类在程序的各个类加载器中被加载时都是相等的(来自同一个 class 文件且被同一个类加载器加载)。
如果不这样,java.lang
包下的 java.lang.Object
类被不同的类加载器加载时将不会相等,那么程序中就会出现多个 java.lang.Object
类。由于其他类都会隐式继承 java.lang.Object
类,当存在多个 Object
类时,程序将变得混乱。
3.3 ClassLoader 中的方法
loadClass()
双亲委派模型的具体实现在 java.lang.ClassLoader
中的 loadClass()
方法里,该方法能够根据类的全限定名来加载并创建一个 Class
对象。
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
如果要遵循双亲委派模型,ClassLoader
的子类 尽量不要重写 此方法。
defineClass()
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
将字节码的字节流转换为一个 Class
对象。
该方法被 final
修饰,子类无法重写。
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len, |
最终通过一个 native 原生方法,将字节流转换为 Class
对象。
findClass()
1 | protected Class<?> findClass(String name) throws ClassNotFoundException { |
根据类的全限定名,获取字节码二进制流,并创建对应的 Class
对象。
如果遵循双亲委派模型,通常不会重写 loadClass()
方法,而是选择重写 findClass()
方法。
findClass()
方法的通常实现逻辑是:
- 根据参数
name
从指定的来源获取字节码的二进制流 - 然后调用
defineClass()
方法,创建一个Class
对象
findBootstrapClassOrNull()
1 | static Class<?> findBootstrapClassOrNull(String name) |
根据类的全限定名,委派给 Bootstrap ClassLoader 进行类加载。
该方法是包私有的,这意味着如果要将某个类加载请求委派给 Bootstrap ClassLoader,那么必须间接调用类 ClassLoader
中的某个 public
或 protected
方法。
getParent()
1 | private final ClassLoader parent; |
获取当前 ClassLoader
的“父”类加载器。
parent
字段是 private final
的,它只能在构造器中被初始化。
parent != null
时,调用 parent.loadClass()
方法将加载请求委派给 parent
;parent == null
时,调用 findBootstrapClassOrNull()
方法将请求委派给 Bootstrap ClassLoader。
3.4 类加载器的特性
确定类的“唯一性”
假设使用 表示某个类的全限定名, 表示加载定义这个类的类加载器。
那么使用二元组 可以用来确定类的唯一性。
如果两个类相等,那么它们应该满足以下条件:
- 全限定名相等
- 加载这两个类的类加载器是同一个
“相等”一词指的是:
equals()
方法返回true
isAssignableFrom()
方法返回true
isInstance()
方法返回true
instanceof
关键字返回true
传递性
假设类 由类加载器 定义加载的,那么类 中所依赖的其他类也将通过 来进行加载。
通过类加载器的传递性,可以从某个入口类开始,不断使用相同的类加载器,展开加载同一个模块或应用中的其他类。
整体来说,是以某种递归的形式逐步加载所需要的类。
对于类 而言, 被称为 initial ClassLoader
,而 则是 define ClassLoader
。
可见性
如果类 对于类 可见,那么加载类 的类加载器 ,也可以直接或通过委派间接加载到类 。
比如,类 通过 Application ClassLoader 进行加载的,类 通过 Extension ClassLoader 进行加载的,那么类 对于类 是可见的,反过来,类 对于类 则是不可见的。
3.5 打破双亲委派模型
双亲委派模型并非是强制性的约束,它更多是推荐给开发者的一种类加载器的实现方式。在某些时候,由于双亲委派模型自身的局限性,此时不得不主动打破双亲委派模型。
在加载类时,如果不按照“自底向上检查是否已加载类,自顶向下尝试加载类”的方式去加载类,那么就可以叫做破坏双亲委派模型。
已经知道加载类的核心方法是抽象类 ClassLoader
中的 loadClass()
方法,可以通过继承这个抽象类并重写 loadClass()
方法,而且不按照双亲委派模型的方式去加载类,那么就可以打破双亲委派模型。
在 Java 发展史上也有双亲委派模型被破坏的情况,比如:
- 由于
java.lang.ClassLoader
在 JDK1.0 中已经存在,所以会有人继承这个抽象类并重写loadClass()
方法来实现自定义类加载器。为了在 JDK1.2 中引入双亲委派模型并向前兼容,loadClass()
方法必须要保留并且能够被重写,于是在ClassLoader
类添加了一个新的被 protected 修饰的findClass()
方法,并告知开发者不要重写loadClass()
而是重写findClass()
。由于双亲委派模型的具体实现就在loadClass()
方法内,并未禁止重写这个方法,因此委派的逻辑就被破坏了。 - 双亲委派模型存在缺陷(Java SPI 机制):双亲委派模型很好地解决了各个类加载器加载基础类的统一问题(越基础的类由越上层的类加载器加载),这些基础类大多数情况下作为用户调用的基础类库,但 这些基础类无法回调用户的代码。以 JDBC 为例,它规定了如何使用 Java 代码来连接数据库,具体的做法需要交由各个数据库厂商实现。JDBC 位于 rt.jar 中,由 Bootstrap ClassLoader 去加载,但其具体实现是在用户定义的 ClassPath 中,只能由 Application ClassLoader 去加载,因此 Bootstrap ClassLoader 只能委托子类加载器去加载数据库厂商们的具体实现,而这就破坏了双亲委派模型。具体实现方式是引入了线程上下文类加载器(Thread Context ClassLoader),可以通过
java.lang.Thread
的setContextClassLoader()
方法来设置,然后利用Thread.current.currentThread().getContextClassLoader()
获得类加载器来加载(如果直接获取,将获取到应用程序类加载器)。 - 用户对应用程序动态性的热切追求:如代码热替换(HotSwap)、热模块部署等,因此催生出
JSR-291
以及它的业界实现 OSGi,而 OSGi 定制了自己的类加载规则,利用自定义类加载器机制来完成模块化热部署,不再遵循双亲委派模型。
3.6 数组类的加载
数组类的本质
所有的数组实例都属于 Object
,每个数组实例都有对应的 Class
:
1 |
|
数组类的加载
数组类并不通过类加载器来加载创建,而是通过 JVM 直接创建的,有专门的 JVM 指令 newarray
。
如果数组类的元素类型是引用类型,那最终还是要靠类加载器去创建。
数组类的唯一性,依然需要类加载器来确定:和普通类一样,数组类的唯一性同样依靠二元组 来确定,其中 是数组类的类名, 是与数组类相关联的类加载器。
与数组类关联的类加载器
假如数组类 的组件类型是 ,那么与数组类 关联的类加载器为:
-
如果组件类型 是引用类型,那么 JVM 会将数组类 和加载组件类型 的类加载器关联起来
-
如果组件类型不是引用类型(例如
int
数组),那么 JVM 会将把数组类 标记为与 Bootstrap ClassLoader 关联
1 |
|
3.7 类加载器与 Tomcat
什么场景下使用类加载器?
在一些系统中,需要加载各种厂商提供的类。
为了防止代码被篡改,这个时候需要进行类加载器的定制,在进行类加载之前,需要先对每个类的类文件进行签名验证。
除此之外,某些关键服务是不允许随便停机的,这就需要进行程序的热更新,这就需要定制类加载器优先加载最新版本的代码,甚至需要优先通过网络下载最新的代码。
打破双亲委派模型的方式
- 主动违背类加载器的“传递依赖”原则。例如在一个 Bootstrap ClassLoader 加载的类中,又通过 Application ClassLoader 来加载所依赖的其它类,这就打破了双亲委派模型中的层次结构,逆转了类之间的可见性。典型的是 Java SPI 机制,它在类 ServiceLoader 中,会使用线程上下文类加载器来逆向加载 ClassPath 中的第三方厂商提供的 Service Provider 类。
- 第二种方式是自定义一个类加载器的类,重写抽象类
java.lang.ClassLoader
中的loadClass()
方法,不再优先委派“父”加载器进行类加载。
为什么说 Tomcat 打破了双亲委派模型
在使用 Tomcat 时,可以部署多个 war 包,每个 war 包分别代表了不同的 WebApp,可以通过不同的 context-path
来区分。
如果两个 war 包中有相同名称的两个类,比如都是 indi.mofan.User
,但它们的实现版本却不一样,比方一个有 userId
字段,另一个没有。
Tomcat 为了保证它们不会冲突,会为每个 WebApp 创建一个类加载器实例。对应的类型是 WebApp ClassLoader,它重写了 loadClass()
方法,优先加载当前 WebApp 中的类,包括目录 /WEB-INF/classes
以及 /WEB-INF/lib
中 jar 包。只有在当前 WebApp 中找不到对应的类时,才委派给上一层
的“父”类加载器,这样就做到了 WebApp 之间的类隔离。
Tomcat 支持的版本是固定的,比如 Tomcat 9.0 支持 Servlet 4.0,而 Tomcat 10.0 则支持 Servlet 5.0。如果 WebApp 的 war 包中也包含了一个 servlet-api 的 jar 包,那如何保证 WebApp 只会加载到 Tomcat 支持的 servlet-api 呢?因为先前不是说 WebApp ClassLoader 是会优先加载当前应用目录下的类吗?
Tomcat 作为 WebApp 的宿主进程,有一些内部公共类是对 Tomcat 自身以及所有的 WebApp 是可见的。针对这部分类,Tomcat 在 WebApp ClassLoader 之上,加了一个“父”类加载器 —— Common ClassLoader。
对于 JavaEE API 的核心实现类(Servlet、JSP、EL、WebSocket)是不允许 WebApp ClassLoader 直接加载的,而是先委派给“父”类加载器 Common ClassLoader 进行加载。对于 JDK 中的类,也不允许 WebAppClassLoader 直接加载,会先委派给 JDK 内置的 Extension ClassLoader 或 Bootstrap ClassLoader 先尝试加载。
Tomcat 中默认的类加载器结构:
flowchart BT
bootstrap["Bootstrap ClassLoader"]
extension["Extension ClassLoader"]
application["Application ClassLoader"]
common["Common ClassLoader"]
webapp1["WebApp1 ClassLoader"]
webapp2["WebApp2 ClassLoader"]
common --> application --> extension --> bootstrap
webapp1 --> common
webapp2 --> common
因此,从 WebApp 的角度来看,类的加载顺序应该是这样的:
- JDK 中的核心类以及扩展类,直接委派给 JDK 内置的 Extension ClassLoader 或 Bootstrap ClassLoader 进行加载
- JavaEE API 的核心实现类(Servlet、JSP、EL、WebSocket)委派给 Common ClassLoader 进行加载
- WebApp 中
/WEB-INF/classes/
目录和/WEB-INF/lib/*.jar
中的类由 WebApp ClassLoader 进行加载 - Tomcat 服务器进程的 ClassPath 中的类,委派给 JDK 内置的 Application ClassLoader 进行加载
- Tomcat 和 WebApp 共享的内部公共类,委派给 Common ClassLoader 进行加载
Tomcat 中还有哪些类加载器?
Tomcat 还提供了 Server ClassLoader 和 Shared ClassLoader。
在默认配置中,这两个类加载器是未定义的,需要在配置文件 conf/catalina.properties
中,通过配置项 server.loader
和 shared.loader
分别配置这两个类加载器的加载目录或 jar 包。
考虑到在不同的 WebApp 中也可以共享一些依赖类库,比如 MySQL 相关的类就可以在不同的 WebApp 之间共享,如果它们的版本相同,就没必要在每个 WebApp 都独自加载一份。此时可以将 MySQL 的相关 jar 包部署在 shared.loader
指定的共享目录下,当 WebApp ClassLoader 自身没有加载到某个类时,就会委派给 Shared ClassLoader 去加载。
如果还想隔绝 WebApp 与 Tomcat 本身的内部类,可以使用 Server ClassLoader 来加载 Tomcat 本身的内部类。
经过拓展,Tomcat 的类加载器结构:
flowchart BT
bootstrap["Bootstrap ClassLoader"]
extension["Extension ClassLoader"]
application["Application ClassLoader"]
common["Common ClassLoader"]
server["Server ClassLoader"]:::dash
shared["Shared ClassLoader"]:::dash
webapp1["WebApp1 ClassLoader"]
webapp2["WebApp2 ClassLoader"]
shared --> common --> application --> extension --> bootstrap
server --> common
webapp1 --> shared
webapp2 --> shared
classDef dash stroke-dasharray: 5 5
在结构图中 Server ClassLoader 对 WebApp 而言是不可见的,换言之,WebApp 是无法使用部署在 server.loader
中的类。
4. 反射
4.1 几个相关的类
java.lang.Class
,代表一个类。java.lang.reflect.Method
,代表类的方法。java.lang.reflect.Field
,代表类的成员变量。java.lang.reflect.Constructor
,代表类的构造器。
4.2 Class 类
通过类的 Class 类可以知道这个类的:
- 属性;
- 方法和构造器;
- 实现了哪些接口。
对于每个类而言,JRE 都为其保留了一个不变的 Class 属性的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
需要注意的是:
- Class 类本身也是一个类;
- Class 对象只能由系统建立对象;
- 一个加载的类在 JVM 中只会有一个 Class 实例;
- 一个 Class 对象对应的是一个加载到 JVM 中的一个
.class
文件; - 可以通过任意一个类的实例获取到这个类的 Class 类;
- 通过 Class 对象可以完整地得到某一个类中所有被加载的结构;
- Class 类是反射的根源,要想进行反射,得先获取到对应的 Class 对象。
4.3 如何获取 Class 类的实例
若已知具体的类,通过类的 class
属性获取。该方法最为可靠,程序性能最高。
1 | Class clazz = Person.class; |
已知某个类的实例,调用该实例的 getClass()
方法获取 Class
对象。
1 | Class clazz = person.getClass(); |
已知一个类的全限定名,且该类在类路径下,可通过 Class
类的静态方法获取 forName()
获取。这一方法可能抛出 ClassNotFoundException
异常。
1 | Class clazz = Class.forName("com.yang.reflect.Person"); |
内置基本数据类型可以直接用 类名.Type
:
1 | Class<Integer> type = Integer.TYPE; |
还可以利用 ClassLoader
:
1 | ClassLoader classLoader = ClassLoader.getSystemClassLoader(); |
拥有 Class
对象的类型:外部类、成员(成员内部类、静态嵌套类)、局部内部类、匿名内部类、接口、数组、枚举、注解、基本数据类型,void。
4.4 使用反射获取对象的信息
给定一个 Person 类:
1 | package com.yang.reflect; |
利用反射创建 Person 对象,并调用该对象的方法、修改该对象成员变量的值:
1 | public class Test01 { |
利用 isAccessible()
方法可以判断 是否关闭 Java 语言访问控制的检查,关闭后才能操作私有属性。当未关闭安全检测时,可以使用 setAccessible()
方法并传入 true
表示关闭检查。
在 JDK 9 之后 isAccessible()
方法被废弃,废弃原因是它的方法名称不够准确,会让人觉得此方法用于检查反射的对象是否可访问,而实际上并非如此,作为代替,引入了 canAccess()
方法。
canAccess()
方法可以接收一个 Object
类型的参数,如果判断的是一个实例对象的方法或字段,应当传入此实例对象,反之传入 null
即可。
4.5 使用反射获取注解信息
1 | package com.yang.reflect; |
运行结果如下:
1 | @com.yang.reflect.Tableyang(value=db_student) |
4.6 Class 的各种 name
参考链接:
- What is the difference between canonical name, simple name and class name in Java Class?
- Class类 getName()、getCanonicalName()、getSimpleName()、getTypeName() 方法的异同
- 本地类
在 Class
中有 getName()
、getCanonicalName()
、getSimpleName()
以及 JDK8 中新增的 getTypeName()
方法。
getName()
getName()
返回的信息可以在动态加载某个类时使用。比如使用默认的 ClassLoader
调用 Class.forName()
方法来加载某个类。在某个 ClassLoader
的范围内,所有类 getName()
返回的信息唯一。
1 | public String getName() { |
getCanonicalName()
getCanonicalName()
返回的信息可以在 import
语句中使用,也能在 toString()
方法或日志操作时使用。 注意: 在一个 ClassLoader
中,getCanonicalName()
返回的信息并不能用来唯一标识一个类。
1 | public String getCanonicalName() { |
isArray()
判断该 Class 对象是否为数组。
getComponentType()
返回数组中元素的 Class 对象,如果该对象不是数组,则返回 null
。
isHidden()
是 JDK15 新增的方法,用于判断一个类是否是隐藏类,像 Lambda 表达式、方法引用就是隐藏类。
isLocalOrAnonymousClass()
判断该 Class 对象是否为本地类(定义在一个代码块中的类,比如定义在方法中、静态代码块中)或匿名类。匿名类和本地类在 Java 中无法呈现出类结构,所在位置不能通过名称表示出来,因此 getCanonicalName()
返回 null
。
getEnclosingClass()
返回该 Class 对象的封装 Class 对象,如果该 Class 对象是顶级类,则返回 null
。
getSimpleName()
getSimpleName()
返回的信息可以 不精准地 来标识一个类,因此不能保证唯一性。
1 | public String getSimpleName() { |
getSimpleBinaryName()
返回该 Class 对象的简单二进制名称。如果该 Class 对象是顶级类,则返回 null
;否则以顶级类 getName()
信息的长度为开始截取索引,截取该 Class 对象的 getName()
信息(这是 JDK 8 里的逻辑,JDK 17 中由本地方法实现):
1 | private String getSimpleBinaryName() { |
getTypeName()
getTypeName()
返回此类型名称的信息字符串,就像 toString()
一样,表示纯粹的信息。
1 | public String getTypeName() { |
示例对比
目标测试类:
1 | public class GetNameTestClass { |
测试方法:
1 |
|
运行测试方法后,测试通过,控制台打印出:
静态代码块本地类: indi.mofan.pojo.GetNameTestClass$1LocalClassInStaticBlock null LocalClassInStaticBlock indi.mofan.pojo.GetNameTestClass$1LocalClassInStaticBlock 静态方法中的本地类: indi.mofan.pojo.GetNameTestClass$1LocalClassInMethod null LocalClassInMethod indi.mofan.pojo.GetNameTestClass$1LocalClassInMethod Lambda 表达式: indi.mofan.reflection.JavaReflectionUtilTest$$Lambda$356/0x0000000800ca8c58 null JavaReflectionUtilTest$$Lambda$356/0x0000000800ca8c58 indi.mofan.reflection.JavaReflectionUtilTest$$Lambda$356/0x0000000800ca8c58 方法引用: indi.mofan.reflection.JavaReflectionUtilTest$$Lambda$357/0x0000000800ca8e78 null JavaReflectionUtilTest$$Lambda$357/0x0000000800ca8e78 indi.mofan.reflection.JavaReflectionUtilTest$$Lambda$357/0x0000000800ca8e78
4.7 获取类的泛型信息
getGenericSuperclass()
getGenericSuperclass()
方法用于获取含有泛型信息的父类,如果父类不含泛型信息,该方法等价于 getSuperclass()
方法。
在测试类 GetGenericInfoTest
中有这样两个静态嵌套类:
1 | static class MyList extends ArrayList<String> { |
分别获取 MyList
和 MyLinkList
含有泛型信息的父类:
1 |
|
对 MyList
来说,其父类是 ArrayList
,不含泛型参数,其泛型是确切的 String
类型,因此 ParameterizedType
对象的 getActualTypeArguments()
方法返回的是含有 String.class
的数组。由于 ArrayList
不是某个类的嵌套类,因此 getOwnerType()
方法的返回结果是 null
。
对 MyLinkList
来说,其父类是 LinkedList
,含有泛型参数 T
,因此 ParameterizedType
对象的 getActualTypeArguments()
方法返回的信息中含有泛型参数 T
。LinkedList
也不是某个类的嵌套类,getOwnerType()
方法的返回结果也是 null
。
如果要获取实现的接口的泛型信息呢?还可以使用 getGenericSuperclass()
方法吗?
比如在测试类 GetGenericInfoTest
中有以下接口和嵌套类:
1 | interface MyInterface<T> { |
尝试使用 getGenericSuperclass()
方法获取 MyInterfaceImpl_1
和 MyInterfaceImpl_2
的实现的接口的泛型信息:
1 |
|
很遗憾,使用 getGenericSuperclass()
方法并不能成功获取,获取父接口的泛型信息可以使用 getGenericInterfaces()
方法。
getGenericInterfaces()
1 |
|
Java 不允许多继承,但允许实现多个接口,因此 getGenericInterfaces()
返回的是一个数组,表示实现的多个接口信息。
在返回的 Type
数组中,如果实现的某个接口带有泛型信息,可以将 Type
对象转换为 ParameterizedType
对象来获取泛型信息。
MyInterface
接口是定义在测试类中的嵌套类,因此调用 getOwnerType()
方法返回的是当前测试类的 Class
对象。
只能使用 getGenericInterfaces()
方法来获取实现的接口的泛型信息,而不能获取继承的父类的泛型信息,否则最终得到的 Type
数组是一个空数组:
1 | static class MyClass<T> { |
需要获取其他位置的泛型信息时,参考:JAVA反射 | 泛型解析
4.8 反射调用可变参数方法
可变参数可以当成对应的数组类型参数。
如果可变参数类型是引用类型:接收到参数后,会自动拆包取出参数再分配给底层方法,因此需要将传入的数组包装成 Object
对象或者将其作为 Object[]
中的一个元素;
如果可变参数类型是基本类型:不会将参数拆包,因此可以不用包装,但包装了也不会抛出异常,为了统一,可以和引用类型的可变参数一样,都包装一层。
1 | static class MyClass { |