学习目的
- 了解java反射概念及反射机制
- 了解反射在实际开发中的运用
- 了解java注解的概念
- 掌握常用注解的使用
- 了解自定义注解
一、反射
- 概念
反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。 - 实质
允许运行中的 Java程序对自身进行检查,或者说"自审" 或 "自省",并能直接操作程序的内部属性。 - 作用
- 反射得到字节码文件,创建字节码对象;
- 反射获取字节码对象得属性;
- 反射获取字节码对象得方法(构造器或普通方法);
- 反射可以操作class文件的代码片段;
- 反射机制允许反过来对java字节码文件进行操作(类似于黑客可以读和修改字节码文件);
- 能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接;
- 反射允许我们在编写与执行代码时,使程序代码能够接入 装载到 JVM中的类的内部信息,而不是源代码中选定的类协作的代码。
- java反射相关类
- 包:java.lang.reflect包;
- 类:java.lang.Class,代表整个字节码 = 一个类型 = 整个类;
- 属性:java.lang.reflect.Field,代表字节码中的属性字节码 = 类中的成员变量(静态变量+实例变量);
- 构造器:java.lang.reflect.Constructor,代表字节码中的构造方法字节码 = 类中的构造方法;
- 方法:java.lang.reflect.Method,代表字节码中的方法字节码 = 类中的方法。
1.1 反射获取字节码
- 概念
在平常开发中,一般是程序员编写好.java源文件,然后通过javac命令进行编译得到.class字节码文件。而反射则是通过编写好的字节码文件来获取文件中的类、对象、属性、方法等内容。
1.1.1 Class.forName("完整类名")
- 实现
该方法是Class这个类的静态方法,通过类的完整类名 即类的全路径来获取字节码对象,返回的是一个"类型" 或者 "类"。 - 特点
采用这种方式获取字节码,类加载器就会启动,并将该字节码文件加载到内存中(该方式还可以只加载静态代码块)
// Class--类获取
Class c = Class.forName("java.lang.String");
1.1.2 对象.getClass()
- 实现
该方法是通过具体的java对象获得该对象类型的"类",返回的是该类的字节码对象。
// 对象获取
Class cs = "String".getClass();
1.1.3 类.class属性
- 实现
每一个类(包括基本数据类型和引用数据类型)都拥有一个.class属性,该属性是一个静态属性,直接通过类名调用,返回的是该类的字节码对象。
// 属性获取--任何类都有静态属性.class
Class css = String.class;//引用类
Class u = User.class;//自定义类
Class i = int.class;//基本类
1.1.4 类加载
类的加载就是字节码文件的加载,字节码文件仅在方法区内存中加载一次,因此无论使用哪一种方式获取字节码文件的类对象,得到的都是同一个"类"。
// Class--类获取
Class c = Class.forName("java.lang.String");
// 对象获取
Class cs = "String".getClass();
// 属性获取--任何类都有静态属性.class
Class css = String.class;
// 同一个字节码文件只在内存加载一次
System.out.println(c == cs);//true
System.out.println(cs == css);//true
1.2 反射实例化对象
- 概念
通过反射来创建实例化对象,即是通过Class字节码文件对象来创建获取其文件内的实例对象。 - 字节码实例化对象
通过获取得到字节码文件,反向通过字节码文件创建实例对象,具体是字节码.newInstance()。
1.2.1 字节码.newInstance()
- 概念
通过类加载器加载进来的字节码文件,反过来使用反射创建字节码文件内的实例化对象。 - 底层实质
newInstance()底层实际也是通过调用想要实例化的那个对象的无参构造方法来创建,因此要求每一个类定义时都必须手动提供无参构造。
//获取字节码对象
Class ca = Class.forName("文件路径");
//通过字节码对象 来实例化对象
Object obj = ca.newInstance();
1.2.2 读取.properties属性文件
- 概念
可以通过加载properties属性配置文件,读取配置文件中的属性内容来反射获得实例对象。 - 特点
- 写法灵活且可扩展性强:反射实例化对象时只需要修改配置文件,不需要修改java源代码;
- Spring全套底层原理:高级框架底层源代码的实现原理都采用了反射机制,如Spring、SpringMVC、MyBatisSpring、Struts、Hibernate等等,其创建实例化对象都是通过配置.properties文件后通过反射创建。
- 写法实例
// 通过IO流读取classinfo.properties文件
FileReader reader = new FileReader("配置文件.properties");
// 创建属性类对象Map--Properties底层采用map,且key-value都是String
Properties pro = new Properties();
// 加载配置文件
pro.load(reader);
// 关闭流
reader.close();
// 通过配置文件的key获取value -- value就是字节码路径
String className = pro.getProperty("className");
// 根据key对应的value获取字节码对象
Class c = Class.forName(className);
// 通过反射机制实例化对象
Object obj = c.newInstance();
System.out.println(obj);
1.2.3 获取配置文件路径
- 原由
每一种集成开发环境、每一种操作系统平台,其存储一个java项目的配置文件路径时不一样的。当需要将一个项目从一个平台移植到另外一个平台时,项目的文件路径和编译路径都会改变。因此需要一个相对统一的文件路径存储编写文件路径,以方便移植。(采用绝对路径直接复制会出错) - 前提
配置文件需要在"类"路径下编写和存储,即在src目录下存储, src目录是类的根路径。 - getPath()获取资源文件绝对路径方式
- Thread.currentThread():当前线程对象;
- getContextClassLoader():获取线程对象的方法,可以获取到当前线程的类加载器对象;
- getResource():【获取资源】,"类加载器对象"的方法,当前线程的类加载器默认从类的根路径(src目录)下加载配置文件资源;
- getPath():获取类加载器中加载的配置文件的绝对路径。
//Thread.currentThread().getContextClassLoader().getResource("配置文件.properties").getPath();
// 获取文件资源的绝对路径 -- 通用方式(通过当前线程对象的执行路径)
String path = Thread.currentThread().getContextClassLoader().getResource("classinfo2.properties").getPath();
- getResourceAsStream()获取文件资源路径
- 以流形式返回配置文件
// 直接以流的形式返回配置文件
InputStream reader = Thread.currentThread().getContextClassLoader().getResourceAsStream("classinfo2.properties");
// 创建文件对象
Properties pro = new Properties();
/ /加载文件流资源
pro.load(reader);
// 获取流资源后即刻关闭流
reader.close();
// 通过key获取value--Properties文件只能是String类型数据
String className = pro.getProperty("className");
System.out.println(className);
- java.util.ResourceBundle资源邦定器
- 前提条件:xxx.properties文件必须在类路径下(src路径),且文件扩展名必须只能是.properties
// 编写路径参数时,路径后面的扩展名不能写
//ResourceBundle bundle = ResourceBundle.getBundle("classinfo2");//不能带扩展名
ResourceBundle bundle = ResourceBundle.getBundle("com/java/bean/db");//src路径的子包下,不能带扩展名
// 获取properties配置文件的key--value
String className = bundle.getString("className");
System.out.println(className);
// 根据key对应的value获取字节码对象
Class c = Class.forName(className);
1.3 反射获取类/对象属性
- 概念
通过反射获取得到字节码文件中的类属性或对象属性。 - 特点
除了public修饰的属性可以通过字节码对象获取,其他修饰符(protected、private、默认不写)等修饰的都不能被获取。
1.3.1 获取属性Field对象
- 概念
每一个Field属性对象都是一条完整的java属性定义语句。 - 组成
Field对象 = 修饰符 + 数据类型 + 属性名/变量名
private String name; // 整条定义语句是一个Field对象
protected int age; // Field对象
boolean sex;// Field对象
public int no;// Field对象
- 获取方式
- Field[] getFields():只能获取public修饰的公开的Field属性对象,返回一个"Field属性对象"数组;
- Field[] getDeclaredFields():获取所有修饰符修饰的Field属性对象,返回一个"Field属性对象"数组。
- 示例
// 创建字节码对象 -- 先获取类
Class studentClass = Class.forName("com.ava.bean.Student");
// 获取字节码对象全类名(完整类名--带路径)
String className = studentClass.getName();
// 获取字节码对象的类名(简类名--仅类名)
String simpleName = studentClass.getSimpleName();
// 获取类所有的Field属性对象 -- 仅能获取public修饰的属性
Field[] fields = studentClass.getFields();
System.out.println(fields.length); // 属性对象数组中只有1个元素
// 获取属性数组中的单个Field对象
Field f = fields[0];
// 获取Field对象的名字 --getName()获取
String fieldName = f.getName();
// 获取类所有的Field属性对象 -- 可以获取所有修饰符修饰的属性
Field[] fs = studentClass.getDeclaredFields();
System.out.println(fs.length); // 属性对象数组中有 4个元素
1.3.2 获取属性的名称
- field属性对象.getName():获取Field属性对象的名称(变量名称)。
// field为属性对象
String sname = field.getName();
System.out.println(field.getName());
1.3.3 获取属性的类型
- field属性对象.getType():获取Field属性对象的类型(类型的名称)。
// field为属性对象
// 获取属性的类型
Class fieldType = field.getType();
// 获取属性的类型(全路径类型)
//String fName = fieldType.getName();
// 获取属性的类型(简类型)
String fName = fieldType.getSimpleName();
1.3.4 获取属性的修饰符
- field属性对象.getModifiers():获取Field属性对象的修饰符列表。
// 获取类所有的Field属性对象 -- 可以获取所有修饰符修饰的属性
// studentClass为字节码对象
Field[] fs = studentClass.getDeclaredFields();
// 遍历属性数组对象
for(Field field : fs){
// 获取属性的修饰符 列表
int i = field.getModifiers(); // 返回的修饰符是一个数字(修饰符代号)
// 将修饰符的"代号"数字转换成"字符串"
String modifierString = Modifier.toString(i);
}
1.3.5 反射给属性赋值/修改值
- 常用方式
在编写程序的过程中,常用的给一个对象的属性赋值或修改值,是先创建出对象,再通过对象给各个属性赋值。 - 常用赋值/修改方法
- 构造器:通过调用构造器创建对象时,同时初始化对象给对象赋值;
- 对象.属性:直接给对象的属性使用赋值号 =赋值;
- set():通过set()方法对一个对象的属性进行修改或赋值。
// 创建对象
Student s = new Student();
// 创建对象,且初始化赋值
Student s = new Student("name",age,sex);
// 直接给属性赋值
s.age = 111;
// set()方法赋值
s.setAge(111);
// 直接获取属性值
System.out.println(s.age);
// get()方法获取属性值
System.out.println(s.getAge());
- 反射赋值
反射给属性赋值说通过反射得到字节码对象,再反射获取得到字节码对象的属性,然后给字节码对象属性赋值。 - 反射获取/赋值属性的方法
- 字节码对象.getDeclaredField("属性名称"):反射获取对应属性名称的"属性"对象,每一个字节码对象有多个属性对象,但是属性名称唯一;
- 属性.set(实例对象, 属性值):利用反射获得具体的属性,通过该属性给具体的实例对象赋"xxx属性值",一个字节码对象的属性不会改变,但是具体的实例对象可以创建多个。
// 使用反射机制访问一个对象的属性
// 创建字节码对象
Class studentClass = Class.forName("com.java.bean.Student");
// 字节码反射创建实例化对象,newInstance()底层调用无参构造
Object obj = studentClass.newInstance(); // obj就是Student对象
// 获取obj对象的name属性,根据唯一性属性的名称来 获取Field对象
Field nameFiled = studentClass.getDeclaredField("name");
Field ageFiled = studentClass.getDeclaredField("age");
// 通过属性对象,给obj对象(Student对象)的name属性赋值
nameFiled.set(obj, "lisi"); // 给obj对象的name属性赋值lisi
ageFiled.set(obj, 22); // 给obj对象的age属性赋值22
// 通过属性对象,获取属性的值:获取obj对象的name属性的值
System.out.println(nameFiled.get(obj));//lisi
// 反射可以访问私有的属性?(假设name为private)
Field nameField = studentClass.getDeclaredField("name");
// setAccessible() 方法可以打破封装
nameField.setAccessible(true);
// 给name属性赋值
nameField.set(obj, "jackson");
// 获取name属性的值
System.out.println(nameField.get(obj));
- 区别点
常用赋值:编写代码前明确了赋值,私有属性不能在外部访问并赋值;
反射赋值:更加体现了反射机制的灵活性,可以打破封装给私有属性赋值(反射的缺点)。
1.4 反射获取方法
- 概念
通过字节码文件对象反射来获取对象的方法。日常开发方法的调用:new创建对象 --> 调用相关方法。
反射调用方法:获取字节码文件对象 --> 通过字节码文件创建出实例对象 --> 通过字节码文件获取方法 --> 反射的Method方法对象调用invoke()方法。 - 相关类
1.4.1 可变参数
- 概念
可变参数,即一个方法内 传入不确定个数的参数,可以为0个或n个。 - 写法格式
可变参数的固定写法为: 方法名(参数类型... 参数名称) - 特点
- 参数个数可以为0个或n个;
- 编写位置只能是参数列表的最后一个位置,且只能有一个;
- 可变参数实际是一个数组,拥有length属性,且方法定义为可变参数时,也可以传进数组对象。
- 示例
public static void main(String[] args) {
// 基本类型
m();
m(10);
m(10, 20);
// 基本类型 + String类型
m2(100);
m2(200, "abc");
m2(200, "abc", "def");
m3("ab", "de", "kk", "ff");
String[] strs = {"a","b","c"};
// 传入1个数组对象
m3(strs);
// 直接传入1个数组
m3("我","是","中","国", "人");
}
public static void m(int... args){
System.out.println("m方法执行了!");
}
//public static void m2(int... args2, String... args1){ }//不能编写两个可变参数
// 必须编写在最后且只能有1个
public static void m2(int a, String... args1){
}
public static void m3(String... args){
//args有length属性,说明args是一个数组(对象)
for(int i = 0; i < args.length; i++){
System.out.println(args[i]);
}
}
1.4.2 获取静态方法
1.4.3 获取构造器
//创建字节码对象
Class c = Class.forName("com.yry.pratice.reflection.bean.User");
//获取该字节码对象的构造器
Constructor[] constructors = c.getConstructors();
for (Constructor con:constructors) {
con.getModifiers();//构造器的修饰符
con.getDeclaringClass();//声明该构造器的类
con.getName();//构造器的名称
//获取构造器的参数类型数组
Class[] parameterTypes = con.getParameterTypes();
for (Class cas: parameterTypes) {
cas.getClass();//构造器参数的类型
cas.getSimpleName();//构造器参数的名称
}
}
1.4.4 获取普通方法
//创建字节码对象
Class c = Class.forName("com.yry.pratice.reflection.bean.User");
//获取该字节码对象的"方法对象"
Method[] methods = c.getMethods();
for (Method me:methods) {
me.getDeclaringClass().getSimpleName();//声明该方法的类
me.getModifiers();//方法的修饰符
me.getReturnType();//方法的返回值类型
me.getName();//方法的名称
Class<?>[] parameterTypes = me.getParameterTypes();//方法的参数列表
for (Class cs:parameterTypes) {
cs.getClass().getSimpleName();//参数类型的名称
cs.getName();//参数的名称
//System.out.println(cs.getSimpleName());//参数的名称
}
}
1.4.5 反射调用方法
- 步骤
反射调用方法:获取字节码文件对象 --> 通过字节码文件创建出实例对象 --> 通过字节码文件获取方法 --> 反射的Method方法对象调用invoke()方法。 - 反射调用普通方法
Method对象.invoke(实例对象,参数1,参数2...);
// 使用反射机制来调用一个对象的方法
// 创建字节码对象
Class userClass = Class.forName("com.java.service.User");
// 通过字节码反射创建实例对象
Object obj = userServiceClass.newInstance();
// 获取字节码对象中的Method对象(实则为一个方法),通过"方法名"和参数的类型(重载根据参数列表区分)
Method loginMethod = userClass.getDeclaredMethod("login", String.class, String.class);
//Method loginMethod = userClass.getDeclaredMethod("login", int.class);
// 调用方法4要素:Method方法对象,实例对象Obj,参数,返回值
// 通过Method对象.invoke()执行obj对象的Method方法,参数为...
Object retValue = loginMethod.invoke(obj, "admin","123123");
System.out.println(retValue);
- 反射调用构造器
// 使用反射机制创建对象
Class c = Class.forName("com.java.bean.Vip");
// 调用无参数构造方法
Object obj = c.newInstance();
// 反射调用有参构造方法
// 第一步:先获取到这个有参数的构造方法
Constructor con = c.getDeclaredConstructor(int.class, String.class, String.class,boolean.class);
// 第二步:调用构造方法new对象,有参构造需要多少个参数传进多少个参数
Object newObj = con.newInstance(110, "jackson", "1990-10-11", true);
// 获取无参数构造方法
Constructor con2 = c.getDeclaredConstructor();
// 直接调用构造方法new对象,无参构造不需要初始化参数
Object newObj2 = con2.newInstance();
1.5 反射反编译
- 概念
通过反射机制,将.class字节码文件反编译成源.java文件。 - 实现
通过反射获取字节码对象 + 反射获取类/对象 + 反射获取属性 + 反射获取方法。 - 代码示例
//通过反射机制,反编译一个类的属性Field
import java.lang.reflect.Field;//"属性"类
import java.lang.reflect.Modifier;//"属性修饰符"类
public class ReflectTest06 {
public static void main(String[] args) throws Exception{
// 创建拼接字符串对象
StringBuilder s = new StringBuilder();
// 获取自定义类的字节码对象(字节码文件)
//Class studentClass = Class.forName("com.java.bean.Student");
// 获取JDK提供字节码对象(字节码文件)
Class studentClass = Class.forName("java.lang.Thread");
// 拼接字符串:动态获取拼接,属性 + class + 类名 + { + 换行符
// Modifier.toString(studentClass.getModifiers():将字节码的属性修饰符转换成字符串表示
s.append(Modifier.toString(studentClass.getModifiers()) + " class " + studentClass.getSimpleName() + " {\n");
// 获取字节码对象的所有属性对象
Field[] fields = studentClass.getDeclaredFields();
// 遍历 "属性对象"集合
for(Field field : fields){
//拼接"tab空格"
s.append("\t");
// 拼接"属性"的修饰符
s.append(Modifier.toString(field.getModifiers()));
s.append(" ");
// 拼接 "属性"类型(简类名)
s.append(field.getType().getSimpleName());
s.append(" ");
// 拼接 "属性"名称
s.append(field.getName());
s.append(";\n");
}
// 拼接构造方法--先获取所有的构造器
Constructor[] constructors = studentClass.getDeclaredConstructors();
// 遍历构造器
for(Constructor constructor : constructors){
//拼接"tab空格"
s.append("\t");
// 拼接"构造器"的修饰符
s.append(Modifier.toString(constructor.getModifiers()));
s.append(" ");
// 拼接 "构造器"名称(简类名)
s.append(vipClass.getSimpleName());
s.append("(");
// 拼接 "构造器"参数--先获取所有的参数类型(方法重载也是根据参数区分)
Class[] parameterTypes = constructor.getParameterTypes();
for(Class parameterType : parameterTypes){
//参数的名称
s.append(parameterType.getSimpleName());
s.append(",");
}
// 删除最后下标位置上的字符
if(parameterTypes.length > 0){
s.deleteCharAt(s.length() - 1);
}
s.append("){}\n");
}
s.append("}");
System.out.println(s);
}
}
二.类加载器
概念
类加载器是jvm提供的,专门负责加载类的命令/工具,全称ClassLoader。在日常开发中,类的加载是通过JDK自带的加载器识别加载的jvm内存当中的。分类
- 启动类加载器:专门加载jre\lib\rt.jar下的文件,rt.jar中的class文件是JDK最核心的类库。
- 扩展类加载器:专门加载jre\lib\ext*.jar下的文件,当通过"启动类加载器"加载不到想要的class文件时,则会通过"扩展类加载器"加载启动类之外的class文件。
- 应用类加载器:专门加载系统环境变量classpath下的文件,当通过"启动类加载器"和"扩展类加载器"都无法加载到想要的class文件时,就会通过应用类加载器加载外部的class文件(一般是程序员自己编写得文件)。
-
双亲委派机制
java中为了保证类加载的安全,使用了双亲委派机制。
- 优先从"启动类加载器"中加载,这个称为"父";
- "父加载器"无法加载得到,再从"扩展类加载器"中加载,这个称为"母";
- 如果"父"和"母"都加载不到,才会考虑从"应用类加载器"中加载,直到加载到为止,这个称为"子"。
2.1 启动类加载器
- 概念
2.2 扩展类加载器
- 概念
2.3 应用类加载器
- 概念