文章目录
分享一篇 【Java注解+Java反射+Java类加载机制+javaweb 技术搭建MVC框架并用于项目】,更加深入的学习和配置使用所学知识点。
一、前言
参看:
https://juejin.cn/post/6917050648563777544
https://juejin.cn/post/6909692344291819533
https://blog.csdn.net/a745233700/article/details/82893076
学习反射之前,先回顾一下Java中创建对象的方式:
- new关键字:最常用最简单的创建对象的方式。
- clone()方法:调用对象的clone()方法,JVM会给我们创建一个新的对象,而clone()也分为深克隆和浅克隆。
- Java反射创建对象:通过
Class.forName("path")
获取到类对象Class<T>,通过class能获取到获取到实体类对象,这当中会涉及的 类型擦除。 - 反序列化:通过序列化技术,从本地文件中可加载一个之前序列化存储的类对象(通过二进制文本转化为具体的Java类)。
反射是框架设计的灵魂,使用反射总能实现出一些骚操作。先看一个例子解一下。
反射实现示例
Hello类
public class Hello {
// 私有数据
private String data1;
// 公有数据
public String data2;
// 公有方法
public String hello(String name, Date date) {
System.out.println("hello " + name + ",现在是" + date);
return "hello被调用";
}
}
使用反射技术调取Hello类的hello方法
public class Reflect {
public static void main(String[] args) throws Exception {
// 1.得到Hello的字节码文件
Class<Hello> cla = Hello.class;
// 2.获取Hello的构造方法
Constructor<Hello> helloCon = cla.getConstructor();
// 3.获取到Hello的实体类
Hello h = helloCon.newInstance();
// 4.获取 通过Hello字节码绑定指定的Method
Method m = cla.getMethod("hello", String.class, Date.class);
// 5.执行 Hello 的hello()方法
m.invoke(h,"张三",new Date());
}
}
结果:
hello 张三,现在是Fri Sep 17 17:15:03 CST 2021
成功调用到Hello类的hello()方法。
二、反射的原理
在上面的示例中,已经感受到了反射带来的作用。那么要给他具体的定义:
Java反射机制的核心是 在程序运行时动态加载类并获取类的详细信息(事先是不知道需要动态加载的类指向的是谁)从而操作类或对象的属性或方法。本质是Java把代码编译成class文件后,JVM获取class对象后,再通过class对象进行反编译,从而获取到某一类对象的各种信息。
反射可实现反编译,将.class文件 --> .java文件。通过反射机制访问java对象的属性,方法,构造方法等。
以为三个点来分析反射的原理:
- 类正常加载过程
- class对象
- 反射
Class对象的由来是将.class文件读入JVM内存中,并为之创建一个Class对象。
而针对于前面提到的 反射实现示例 中,并没有使用new,而是直接请求获取Hello类的字节码文件,那么此时JVM也会去内存中寻找Hello.class,若在内存中没有找到的话,就会去加载Hello.class到JVM内存。
下面使用一个小小的操作来帮助理解反射这个点:
通过类型擦除实现,看代码(类型擦除分析点击我跳转)
List<Integer> list = new ArrayList<>();
list.add(1);
Class clazz = list.getClass();
Method method = clazz.getMethod("add", Object.class);
// List<Integer> 里存储String字符串
method.invoke(list, "hello");
method.invoke(list, "你好");
for (Object o : list) {
System.out.println(o);
}
结果:
1
hello
你好
这就是反射+类型擦除的作用。
三、反射的用法
学习反射的用法之前,先了解一下反射机制常用的类。
1.常用类
- Java.lang.Class:Class对象
- Java.lang.reflect.Constructor:构造器对象
- Java.lang.reflect.Field:类的属性对象
- Java.lang.reflect.Method:类的方法对象
- Java.lang.reflect.Modifier:类的… …
2.基本的用法
使用
public native boolean isInstance(Object obj);
判断是否为某个类的实例。类似于instanceof的功能。
2.1 获取Class对象
本质就是获取到类对象的class字节码文件,JVM只能识别字节码文件,Java代码编译之后成为了字节码文件。Class获取的过程,就是由 .class
-> .java
的过程。
- Object.getClass()
- Object.class:任何数据类型都有一个静态的class属性
- Class.forName(String className):常用
还是以最上面的Hello类为例:
// 1.Object.getClass()
Hello h = new Hello();
Class clazz1 = h.getClass();
// 2.Object.class
Class clazz2 = Hello.class;
// 3.Class.forName(String className) 常用
Class clazz3 = Class.forName("com.pdh.bean.Hello");
由于Java的JVM不会重复加载类,所以以上三者获取到的Class对象是同一个。
下面的操作都是属于Class类的内置的方法,学会如何调用即可,具体的业务以具体的场景来定。
2.2 获取构造方法Constructor
获取构造方法就是为获取实体类的方法、属性等数据。
获取单个构造方法
Class类提供的方法
- public Constructor getConstructor(Class… parameterTypes):获取单个的"公有" 的构造方法。
- public Constructor getDeclaredConstructor(Class… parameterTypes):获取单个的 “私有、受保护、默认、公有” 的构造方法。
Class clazz = Class.forName("com.pdh.bean.Hello");
// 1.获取无参构造方法
Constructor constructor11 = clazz.getConstructor(); //公有
Constructor constructor12 = clazz.getDeclaredConstructor(); //私有、受保护、默认、公有
// 2.获取有参构造方法
Constructor constructor21 = clazz.getConstructor(String.class,Integer.class);//公有
Constructor constructor22 =
clazz.getDeclaredConstructor(String.class,Integer.class);//私有、受保护、默认、公有
获取所有构造方法
Class类提供的方法
- public Constructor[] getConstructors():所有"公有的"构造方法。
- public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)。
Class clazz = Class.forName("com.pdh.bean.Hello");
// 1.获取所有 公有的 构造方法
Constructor[] constructors1 = clazz.getConstructors();
// 2.获取所有 私有、受保护、默认、公有 的构造方法
Constructor[] constructors2 = clazz.getDeclaredConstructors();
2.3 反射创建实例
Class类提供的方法
- Class对象调用newInstance()创建:默认调用类的无参构造方法,已经过时。
- Class对象获取构造器Constructor对象,在由Constructor对象调用newInstance()创建。
Class clazz = Class.forName("com.pdh.bean.Hello");
// 1.Class对象调用newInstance()创建
Object o1 = clazz.newInstance();
// 2.Class -> Constructor -> newInstance()
Object o2 = clazz.getConstructor().newInstance();
2.4 获取成员变量
Class类提供的方法
- public Field getField(String name):获取指定的 共有的 单个成员变量。
- public Field getDeclaredField(String name):获取指定的单个成员变量;
- public Field[] getFields():获取 共有的 所有成员变量。
- public Field[] getDeclaredFields():获取所有成员变量。
Class clazz = Class.forName("com.pdh.bean.Hello");
// 1.获取指定的单个成员变量
Field f11 = clazz.getField("data1"); //公有
Field f12 = clazz.getDeclaredField("data2"); //私有、受保护、默认、公有
// 2.获取所有成员变量
Field[] f21 = clazz.getFields(); //公有
Field[] f22 = clazz.getDeclaredFields();//私有、受保护、默认、公有
Field类的setAccessible(boolean flag)方法传递一个boolean值,true表示暴力访问,无论修饰符是什么。
Field类提供有大量的操作成员变量的方法,在Field类的加持下,可实现对成员变量的各种 操作。
2.5 获取成员方法
Class类提供的方法
- public Method getMethod(String name, Class<?>… parameterTypes):获取指定的 共有的 内置方法,传递 方法名+方法形参字节码对象。
- public Method getDeclaredMethod(String name, Class<?>… parameterTypes):获取指定的内置方法,传递 方法名+方法形参字节码对象。。
- public Method[] getMethods():获取所有的 共有的 内置方法。
- public Method[] getDeclaredMethods():获取所有的内置方法。
Class clazz = Class.forName("com.pdh.bean.Hello");
// 1.获取指定的单个方法 传递 方法名+方法形参字节码对象
Method m11 = clazz.getMethod("hello",String.class,Date.class); //公有
Method m12 = clazz.getDeclaredMethod("hello",String.class,Date.class); //私有、受保护、默认、公有
// 2.获取类的所有方法
Method[] m21 = clazz.getMethods(); //公有
Method[] m22 = clazz.getDeclaredMethods(); //私有、受保护、默认、公有
对于Method类而言,它也提供非常多的API给我们操作类方法,最常用的就是 invoke(Object obj, Object... args)
方法(传递类对象+参数):
Class clazz = Class.forName("com.pdh.bean.Hello");
Method m11 = clazz.getMethod("hello",String.class,Date.class);
// invoke() 第一个参数为类对象,之后就是可变参数,传递的是对于方法的参数
m11.invoke(clazz.newInstance(),"张三",new Date());
如果调用的是类中的 static静态方法 ,invoke可以这样写:
Method.invoke(null, ... ...);
需要注意的一点:
invoke中的只传递两个参数的时候(即使类的方法只传递一个参数,比如main方法),对于第二个参数(即传递到类方法的参数),在jdk1.4时是使用一个数组传递多个参数(数组被拆分为多个参数),jdk1.5之后是可变参数,由于JDK向下兼容的原则,所以这里只是传递两个参数,且第二个参数为数组时,会以拆分数组作为多个参数传递到类的方法中。此时,就需要注意一下:
假设Hello类有方法show(需要传递数组参数):
public void show(String[] data){
Arrays.stream(data).forEach(System.out::println);
}
那么我们调用的时候:
Class<Hello> cla = Hello.class;
Method m = cla.getMethod("show", String[].class);
m.invoke(cla.newInstance(),new String[]{"张三","李四"});
会报出Exception,意思是:参数个数错误。
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments
解决方法是把这个参数变为Object类型,或者在外再套一个数组:
// 1.把数组变为Object类型数据
m.invoke(cla.newInstance(),(Object) new String[]{"张三","李四"});
// 2.再套一个数组,拆分后还是会准确传递到方法
m.invoke(cla.newInstance(),new Object[]{new String[]{"张三","李四"}});
这里我是自行编写了一个方法只接收一个数组参数,出现了这个问题,同样的如果是main方法也是会出现这个问题,也需要进行处理,但是针对多参数的方法,就不会出现这个问题,因为已经指定了参数个数,JVM就不会再进行拆分。
四、反射实例
1.运行配置文件
看一个Spring中XML配置文件中的bean配置:
<bean id="User" class="com.pdh.entity.User"></bean>
这看起来是不是可以用反射很轻易的就可以实现:解析xml把xml里的内容作为参数,利用反射创建对象。
2.Spring
Spring中的很多功能都是运用反射技术实现的。比如耳熟能详的:
IoC(DI)、单元测试等。
3.序列化
序列化与反序列化技术也是基于反射才得以强大。