引言:
Java 反射和注解学习笔记
概述
反射 (Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通过 Class 获取 class 信息称之为反射(Reflection)
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。
类加载器
当程序有使用某个类时,如果该类还没有被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化
加载
就是指将class
文件读入内存,并为之创建一个Class
对象,任何类被使用时系统都会建立一个Class
对象连接
验证 是否有正确的内部结构,并和其他类协调一致
准备 负责为类的静态成员分配内存,并设置默认初始化值
解析 将类的二进制数据中的符号引用替换为直接引用初始化
对类的静态变量,静态代码块执行初始化操作
类初始化时机
创建类的实例
类的静态变量,或者为静态变量赋值
类的静态方法
使用反射方式来强制创建某个类或接口对应的
java.lang.Class
对象初始化某个类的子类
直接使用
java.exe
命令来运行某个主类
类加载器
负责将
.class
文件加载到内在中,并为之生成对应的Class
对象虽然我们不需要关心类加载机制,但是了解这个机制我们就能更好的理解程序的运行
类加载器的组成
Bootstrap ClassLoader
根类加载器
也被称为引导类加载器,负责Java核心类的加载
比如System
,String
等。在 JDK 中 JRE 的 lib 目录下 rt.jar 文件中Extension ClassLoader
扩展类加载器
负责 JRE 的扩展目录中 jar 包的加载。
在 JDK 中 JRE 的 lib 目录下 ext 目录System ClassLoader
系统类加载器
负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径
通过这些描述就可以知道我们常用的类,都是由谁来加载完成的。
到目前为止我们已经知道把class文件加载到内存了,那么,如果我们仅仅站在这些class文件的角度,我们如何来使用这些class文件中的内容呢?
这就是我们反射要研究的内容
反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。
Class类
阅读API的Class
类得知,Class
没有公共构造方法。Class
对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的 defineClass
方法自动构造的
获取Class对象的三种方式
类对象概念: 所有的类,都存在一个类对象,这个类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法。
获取类对象有3种方式:
方式一:通过Object
类中的getObject()
方法
1 | Person p = new Person(); |
方式二:通过 类名.class
获取到字节码文件对象(任意数据类型都具备一个class
静态属性,看上去要比第一种方式简单)
1 | Class c2 = Person.class; |
方式三:通过Class
类中的方法(将类名作为字符串传递给Class
类中的静态方法forName
即可)
1 | Class c3 = Class.forName("cn.cuzz.Person"); |
注意:第三种和前两种的区别
前两种你必须明确Person类型
后面是指定这种类型的字符串就行(要包含包名),这种扩展更强,我不需要知道你的类,我只提供字符串,按照配置文件加载就可以了
Person类
1 | public class Person { |
反射获取构造方法
在反射机制中,把类中的成员(构造方法、成员方法、成员变量)都封装成了对应的类进行表示。其中,构造方法使用类Constructor
表示。可通过Class
类中提供的方法获取构造方法:
返回一个构造方法
public Constructor getConstructor(Class... parameterTypes)
获取public
修饰, 指定参数类型所对应的构造方法public Constructor getDeclaredConstructor(Class... parameterTypes)
获取指定参数类型所对应的构造方法(包含私有的)
返回多个构造方法
public Constructor[] getConstructors()
获取所有的public
修饰的构造方法public Constructor<?>[] getDeclaredConstructors()
获取所有的构造方法(包含私有的)
1 | package cn.cuzz; |
反射获取构造方法,创建对象
获取构造方法,步骤如下:
获取到Class对象
获取指定的构造方法
通过构造方法类
Constructor
中的方法,创建对象public T newInstance(Object... initargs)
1 | package cn.cuzz; |
反射获取成员变量
在反射机制中,把类中的成员变量使用类Field表示。可通过Class类中提供的方法获取成员变量:
返回一个成员变量
public Field getField(String name)
获取指定的public
修饰的变量public Field getDeclaredField(String name)
获取指定的任意变量
返回多个成员变量
public Field[] getFields()
获取所有public
修饰的变量public Field[] getDeclaredFields()
获取所有的 变量 (包含私有)
1 | package cn.cuzz; |
反射创建对象进行赋值
获取成员变量,步骤如下:
获取Class对象
获取构造方法
通过构造方法,创建对象
获取指定的成员变量(私有成员变量,通过setAccessible(boolean flag)方法暴力访问)
通过方法,给指定对象的指定成员变量赋值或者获取值
public void set(Object obj, Object value)
在指定对象obj中,将此 Field 对象表示的成员变量设置为指定的新值public Object get(Object obj)
返回指定对象obj中,此 Field 对象表示的成员变量的值
1 | package cn.cuzz; |
反射获取成员方法并使用
在反射机制中,把类中的成员方法使用类Method表示。可通过Class类中提供的方法获取成员方法:
返回获取一个方法:
public Method getMethod(String name, Class... parameterTypes)
获取 public 修饰的方法public Method getDeclaredMethod(String name, Class... parameterTypes)
获取任意的方法,包含私有的
参数1: name 要查找的方法名称; 参数2: parameterTypes 该方法的参数类型
返回获取多个方法:
public Method[] getMethods()
获取本类与父类中所有public 修饰的方法public Method[] getDeclaredMethods()
获取本类中所有的方法(包含私有的)
1 | package cn.cuzz; |
反射,创建对象,调用指定的方法
获取成员方法,步骤如下:
获取Class对象
获取构造方法
通过构造方法,创建对象
获取指定的方法
执行找到的方法(如果获取的是私有方法则要开启暴力访问
m5.setAccessible(true)
),public Object invoke(Object obj, Object... args)
执行指定对象obj中,当前Method对象所代表的方法,方法要传入的参数通过args
指定
1 | package cn.cuzz; |
反射练习
下面展示一下反射的利用场景。
泛型擦除
思考,将已存在的ArrayList
集合中添加一个字符串数据,如何实现呢?
程序编译后产生的.class
文件中是没有泛型约束的,这种现象我们称为泛型的擦除。那么,我们可以通过反射技术,来完成向有泛型约束的集合中,添加任意类型的元素
1 | package cn.cuzz; |
反射配置文件
示例一
通过配置文件得到类名和要运行的方法名,用反射的操作类名得到对象和调用方法
实现步骤:
准备配置文件,键值对
IO流读取配置文件 Reader
文件中的键值对存储到集合中 Properties
集合保存的键值对,就是类名和方法名反射获取指定类的class文件对象
class文件对象,获取指定的方法
运行方法
1 | public class Test8 { |
配置文件
1 | # className=cn.cuzz.Student |
示例二
- 首先准备两个业务类,这两个业务类很简单,就是各自都有一个业务方法,分别打印不同的字符串
1 | package reflection; |
- 当需要从第一个业务方法切换到第二个业务方法的时候,使用非反射方式,必须修改代码,并且重新编译运行,才可以达到效果
1 | package reflection; |
- 使用反射
使用反射方式,首先准备一个配置文件,叫做spring.txt,放在src目录下。 里面存放的是类的名称,和要调用的方法名。
在测试类Test中,首先取出类名称和方法名,然后通过反射去调用这个方法。
当需要从调用第一个业务方法,切换到调用第二个业务方法的时候,不需要修改一行代码,也不需要重新编译,只需要修改配置文件spring.txt,再运行即可。
这也是Spring框架的最基本的原理,只是它做的更丰富,安全,健壮。
1 | class=reflection.Service1 |
1 | import java.io.File; |
注解
内置注解
@Override 用在方法上,表示这个方法重写了父类的方法,如toString()。
@Deprecated 表示这个方法已经过期,不建议开发者使用。(暗示在将来某个不确定的版本,就有可能会取消掉)
@SuppressWarnings Suppress英文的意思是抑制的意思,这个注解的用处是忽略警告信息。
比如使用集合的时候,有时候为了偷懒,会不写泛型,像这样: List heros = new ArrayList();
那么就会导致编译器出现警告,而加上@SuppressWarnings({ "rawtypes", "unused" })
就对这些警告进行了抑制,即忽略掉这些警告信息。
@SuppressWarnings 有常见的值,分别对应如下意思
1.deprecation:使用了不赞成使用的类或方法时的警告(使用@Deprecated使得编译器产生的警告);
2.unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
3.fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
4.path:在类路径、源文件路径等中有不存在的路径时的警告;
5.serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
6.finally:任何 finally 子句不能正常完成时的警告;
7.rawtypes 泛型类型未指明
8.unused 引用定义了,但是没有被使用
9.all:关于以上所有情况的警告。
@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题。
@FunctionalInterface这是Java1.8 新增的注解,用于约定函数式接口。
函数式接口概念: 如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口。函数式接口其存在的意义,主要是配合Lambda 表达式 来使用。
自定义注解
元注解
元注解 meta annotation用于注解 自定义注解 的注解。元注解有这么几种:
@Target
@Retention
@Inherited
@Documented
@Repeatable (java1.8 新增)
@Target
可以选择的位置列表如下:
ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包
@Retention
表示生命周期,可选的值有3个:
RetentionPolicy.SOURCE: 注解只在源代码中存在,编译成class之后,就没了。@Override就是这种注解。
RetentionPolicy.CLASS: 注解在java文件编程成.class文件后,依然存在,但是运行起来后就没了。@Retention的默认值,即当没有显式指定@Retention的时候,就会是这种类型。
RetentionPolicy.RUNTIME: 注解在运行起来之后依然存在,程序可以通过反射获取这些信息。
@Inherited
表示该注解具有继承性。
@Documented
文档注解
@Repeatable
表示注解在同一个位置可以使用多次。示例:
1 | package annotation; |
为了紧凑起见,把注解作为内部类的形式放在一个文件里。
- 注解FileTypes,其value()返回一个FileType数组
- 注解FileType,其@Repeatable的值采用FileTypes
- 运用注解:在work方法上重复使用多次@FileType注解
- 解析注解: 在work方法内,通过反射获取到本方法上的FileType类型的注解数组,然后遍历本数组
自定义注解
示例一
将DBUtil这个类改造成为支持自定义注解的方式。 首先创建一个注解JDBCConfig
1 | package anno; |
注解方式DBUtil
1 | package util; |
原DBUtil
1 | package util; |
解析注解,接下来就通过反射,获取这个DBUtil这个类上的注解对象。拿到注解对象之后,通过其方法,获取各个注解元素的值,根据这些配置信息得到一个数据库连接Connection实例。
1 | package util; |
示例二
参考hibernate的注解配置方式,自定义5个注解,分别对应hibernate中用到的注解:
hibernate_annotation.MyEntity 对应 javax.persistence.Entity
hibernate_annotation.MyTable 对应 javax.persistence.Table
hibernate_annotation.MyId 对应 javax.persistence.Id
hibernate_annotation.MyGeneratedValue 对应 javax.persistence.GeneratedValue
hibernate_annotation.MyColumn 对应 javax.persistence.Column
1 | package hibernate_annotation; |
应用
1 | package pojo; |
解析注解
1 | package test; |
思路如下:
- 首先获取Hero.class类对象
- 判断本类是否进行了MyEntity 注解
- 获取注解 MyTable
- 遍历所有的方法,如果某个方法有MyId注解,那么就记录为主键方法primaryKeyMethod
- 把主键方法的自增长策略注解MyGeneratedValue和对应的字段注解MyColumn 取出来,并打印
- 遍历所有非主键方法,并且有MyColumn注解的方法,打印属性名称和字段名称的对应关系。