【Java基础】反射详述简单模拟SpringMVC
🚩 本文已收录至专栏:JAVA基础
👍希望能对你有所帮助
一.概述
反射是指对于任何一个Class类,在运行的时候都可以直接得到这个类全部成分,使得我们可以动态操作Java代码,同时反射也破坏了Java的封装性。
例如:在运行时,可以直接得到这个类的构造器对象(Constructor
)、成员变量对象(Field
)、成员方法对象(Method
),不管是否为私有,这种运行时动态获取类信息以及动态调用类中成分的能力称为Java语言的反射机制。
反射的关键和核心思想:必须先获取编译后的Class类对象,然后就可以分析类对象中的全部成分并进行一些操作。
HelloWorld.java -> javac -> HelloWorld.class
// 获取类对象
Class c = HelloWorld.class;
// 获取其他类中的各个成分....
反射的用途:反射为绝大部分Java框架的底层实现原理。它常常被用于开发各种具有通用性的框架或者工具。
二.获取类对象
(1) 引入
反射的第一步就是先获取Class类对象,然后我们便可以基于获取的类对象解析类中的全部成分。我们有三种方式可以获取类对象:
三种方式对比显然直接通过类名.class
方式获取最简单,但我们依旧需要看使用场景选择不同的方式。例如:假如我们需要获取方法参数传递对象的类对象,只能使用对象.getClass()
。
我们先创建一个学生类,用于获取类对象。
// 反射学习
package relate;public class Student {
}
(2) 第一种方式
我们可以通过Class类的一个静态方法forName("全限名")
获取类对象
package relate;public class Test {public static void main(String[] args) throws ClassNotFoundException {// 获取类对象Class c = Class.forName("relate.Student");// 打印类对象,也可以直接通过getSimpleName获取类名System.out.println(c+"====>"+c.getSimpleName());}
}// 打印结果
// class relate.Student====>Student
全限名:可以看作是包名+类名。
(3) 第二种方式
我们也可以通过一种更为简单的方式获取:类名.class
。
package relate;public class Test {public static void main(String[] args) throws ClassNotFoundException {// 获取类对象Class c = Student.class;// 打印类对象,,也可以直接通过getSimpleName获取类名System.out.println(c+"====>"+c.getSimpleName());}
}
// 打印结果相同
// class relate.Student====>Student
(4) 第三种方式
此外我们还可以通过Object类中的getClass方法获取类对象:对象.getClass()
package relate;public class Test {public static void main(String[] args) throws ClassNotFoundException {// 获取类对象Student student = new Student();Class c = student.getClass();// 打印类对象,,也可以直接通过getSimpleName获取类名System.out.println(c+"====>"+c.getSimpleName());}
}
// 打印结果也相同
// class relate.Student====>Student
我们已经获取了类对象,接下来便可以逐步解析类对象中的各个成分了,尽管类中存在一些私有的成分,但我们仍然可以获取到,也就是说可以破坏封装性
三.获取构造器对象
(1) 引入
我们可以通过反射获取类对象的任意构造器,然后再创建对象,尽管有些类的构造器是私有的,但并不妨碍我们基于此创建对象。
Class类提供了一些方法供我们获取构造器对象:
方法 | 说明 |
---|---|
Constructor<?>[] getConstructors() | 返回所有构造器对象的数组(只能拿public的) |
Constructor<?>[] getDeclaredConstructors() | 返回所有构造器对象的数组,存在(包括private)就能拿到 |
Constructor getConstructor(Class<?>… parameterTypes) | 返回单个构造器对象,只能拿public的 |
Constructor getDeclaredConstructor(Class<?>… parameterTypes) | 返回单个构造器对象,存在包括private都能拿到 |
(2) 获取多个构造器
(2.1) 公有构造器
通过getConstructors
方法我们只能获取所有的公有构造器:
// public Student() {
// }// public Student(String name, int age) {
// this.name = name;
// this.age = age;// }public class Test {public static void main(String[] args) {// 1.获取类对象Class c = Student.class;// 2.获取公有public构造器对象Constructor[] constructors = c.getConstructors();// 3.打印结果for (Constructor constructor : constructors) {System.out.println(constructor);}}
}// 可以看到我们声明的两个构造器
// public relate.Student()
// public relate.Student(java.lang.String,int)
假如我们将带参的构造器改为私有的:
private Student(String name, int age) {this.name = name;this.age = age;
}
再次运行发现我们只能获取到公有的无参构造器,而私有的带参构造器则无法获取到
// 打印结果
// public relate.Student()
(2.2) 任意构造器
我们只需要使用getDeclaredConstructors
即可获取所有构造器,包括私有构造器,这也是我们常用的。用法与上述类似:
// public Student() {
// }// private Student(String name, int age) {
// this.name = name;
// this.age = age;// }public class Test {public static void main(String[] args) {// 1.获取类对象Class c = Student.class;// 2.获取所有存在的构造器对象Constructor[] constructors = c.getDeclaredConstructors();// 3.打印结果for (Constructor constructor : constructors) {System.out.println(constructor);}}
}
// 打印结果
// public relate.Student()
// private relate.Student(java.lang.String,int)
(3) 获取单个构造器
我们也可以根据构造器参数的类型与数量获取单个构造器。
(3.1) 公有构造器
我们可以通过getConstructor
获取单个构造器,与上述类似,只能获取公有构造器
// public Student() {
// }// public Student(String name, int age) {
// this.name = name;
// this.age = age;
// }package relate;import java.lang.reflect.Constructor;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取单个公有构造器对象Constructor constructor1 = c.getConstructor(); // 无参直接方法名获取Constructor constructor2 = c.getConstructor(String.class, int.class); // 带参数需要指定每个参数类型// 3.打印结果System.out.println("无参构造器===》" + constructor1);System.out.println("带参构造器===》" + constructor2);}
}
// 打印结果
// 无参构造器===》public relate.Student()
// 带参构造器===》public relate.Student(java.lang.String,int)
(3.2) 任意构造器
我们还可以通过getDeclaredConstructor
获取单个任意构造器
// public Student() {
// }// public Student(String name, int age) {
// this.name = name;
// this.age = age;
// }package relate;import java.lang.reflect.Constructor;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取任意构造器对象Constructor constructor1 = c.getDeclaredConstructor(); // 无参直接方法名获取Constructor constructor2 = c.getDeclaredConstructor(String.class, int.class); // 带参数需要指定每个参数类型// 3.打印结果System.out.println("无参构造器===》" + constructor1);System.out.println("带参构造器===》" + constructor2);}
}
// 打印结果
// 无参构造器===》public relate.Student()
// 带参构造器===》private relate.Student(java.lang.String,int)
(4) 创建对象
我们可以通过如下方法利用构造器对象创建对象。
方法 | 说明 |
---|---|
T newInstance(Object… initargs) | 根据指定的构造器创建对象(若构造器为private需要先取消检查) |
public void setAccessible(boolean flag) | 设置为true,表示取消访问检查,进行暴力反射 ,无视private |
(4.1) 基于公有构造器
我们可以基于获取到的构造器对象来创建对象。
// public Student() {
// }package relate;import java.lang.reflect.Constructor;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取构造器对象Constructor constructor = c.getDeclaredConstructor();// 3.根据无参构造器创建对象,0个参数Student student = (Student) constructor.newInstance();System.out.println(student);}
}
// 打印结果
// Student{name='null', age=0}
(4.2) 基于私有构造器
尽管我们可以获取到私有构造器,但是通过其创建对象时会发现程序会抛出异常,这时因为我们还要通过setAccessible
打开权限。这也暴露了我们使用单例模式并不能一定保证只有一个对象。
// private Student(String name, int age) {
// this.name = name;
// this.age = age;
// }package relate;import java.lang.reflect.Constructor;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取构造器对象Constructor constructor = c.getDeclaredConstructor(String.class, int.class);// 3.打开权限constructor.setAccessible(true);// 4.创建对象Student student = (Student) constructor.newInstance("观止", 19);System.out.println(student);}
}
// 打印结果
// Student{name='观止', age=19}
四.获取成员变量对象
(1) 引入
我们同样可以通过反射获取类对象的任意成员变量并进行赋值或获取值。
Class类也提供了一些方法供我们获取成员变量对象:
方法 | 说明 |
---|---|
Field[] getFields() | 返回所有成员变量对象的数组(只能拿public的) |
Field[] getDeclaredFields() | 返回所有成员变量对象的数组,存在就能拿到 |
Field getField(String name) | 返回单个成员变量对象(只能拿public的) |
Field getDeclaredField(String name) | 返回单个成员变量对象,存在就能拿到 |
需要注意的是:
getFields
将返回所有的公共字段,包括从父类中继承来的公共字段。getDeclaredFields
则只返回自身包含的全部字段。
(2) 获取多个成员变量
(2.1) 公有成员变量
可以通过getFields
获取所有公有成员变量
// public String name;// private int age;import java.lang.reflect.Field;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取公有成员变量Field[] fields = c.getFields();// 3. 遍历打印结果for (Field field : fields) {System.out.println(field + "===>" + field.getName());}}
}
// 打印结果
// public java.lang.String relate.Student.name ===> name ===> class java.lang.String
(2.2) 任意成员变量
可以通过getDeclaredFields
获取所有任意成员变量
// public String name;// private int age;package relate;import java.lang.reflect.Field;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取任意成员变量Field[] fields = c.getDeclaredFields();// 3. 遍历打印结果for (Field field : fields) {System.out.println(field + " ===> " + field.getName());}}
}
// 打印结果
// public java.lang.String relate.Student.name ===> name ===> class java.lang.String
// private int relate.Student.age ===> age ===> int
(3) 获取单个成员变量
(3.1) 公有成员变量
可以通过getField
获取单个公有成员变量
package relate;import java.lang.reflect.Field;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.根据名称获取公有成员变量Field name = c.getField("name");// 3.打印结果System.out.println(name + " ===> " + name.getName()+ " ===> " + name.getType());}
}
// 打印结果
// public java.lang.String relate.Student.name ===> name ===> class java.lang.String
(3.2) 任意成员变量
可以通过getDeclaredField
获取单个任意成员变量
package relate;import java.lang.reflect.Field;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.根据名称获取任意成员变量Field age = c.getDeclaredField("age");// 3.打印结果System.out.println(age + " ===> " + age.getName()+ " ===> " + age.getType());}
}
// 打印结果
// private int relate.Student.age ===> age ===> int
(4) 赋值&获取值
我们可以获取或修改对象的字段值,尽管有限字段是私有不对外暴露的,但依旧可以通过反射进行操作。
方法 | 说明 |
---|---|
void set(Object obj, Object value): | 赋值 |
Object get(Object obj) | 获取值。 |
我们必须先创建对象,再进行操作,因为不基于对象,对象值是毫无意义的。
package relate;import java.lang.reflect.Field;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取成员变量Field name = c.getDeclaredField("name");Field age = c.getDeclaredField("age");// 3. 赋予访问私有字段权限age.setAccessible(true);// 3.给字段赋值Student student = new Student();name.set(student, "观止");age.set(student, 20);// 打印对象并获取字段值System.out.println(student + "===>" + name.get(student) + "===>" + age.get(student));}
}// 打印结果
// Student{name='观止', age=20}===>观止===>20
我们以前都是通过对象.getXX
获取字段值和对象.setXX()
设置值,而通过反射则是以XX.get(对象)
获取字段值和xx.set(对象,X)
设置值,刚好相反。
五.获取方法对象
(1) 引入
我们同样可以通过反射获取类对象的任意方法对象并且手动进行触发操作。
Class类也提供了一些方法供我们获取方法对象:
方法 | 说明 |
---|---|
Method[] getMethods() | 返回所有成员方法对象的数组(只能拿public的) |
Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,存在就能拿到 |
Method getMethod(String name, Class<?>… parameterTypes) | 返回单个成员方法对象(只能拿public的) |
Method getDeclaredMethod(String name, Class<?>… parameterTypes) | 返回单个成员方法对象,存在就能拿到 |
需要注意的是:
getMethods
将返回所有的公共方法,包括从父类中继承来的公共方法。getDeclaredMethods
则只返回自身包含的全部方法。
我们为学生类添加三个方法用于测试:
public void run() {System.out.println("invoke public run...");}public void sing(String name) {System.out.println("sing " + name);}private void dance() {System.out.println("invoke private dance...");}private void eat(String name) {System.out.println("eat " + name);}
(2) 获取多个方法
(2.1) 公有方法
我们可以通过getMethods
获取类中所有的方法包括其父类中的方法。
package relate;import java.lang.reflect.Method;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取多个成员方法Method[] methods = c.getMethods();// 3.遍历for (Method method : methods) {System.out.println("方法名称:"+method.getName()+",返回值类型:"+method.getReturnType()+",参数个数:"+method.getParameterCount());}}
}
// 除了Student类中的方法,其继承自Object的方法也会打印
// 方法名称:run,返回值类型:void,参数个数:0
// 方法名称:toString,返回值类型:class java.lang.String,参数个数:0
// 方法名称:wait,返回值类型:void,参数个数:2
// 方法名称:wait,返回值类型:void,参数个数:1
// 方法名称:wait,返回值类型:void,参数个数:0
// 继承自Object.....
(2.2) 任意方法
我们可以看到上述虽然获取到了很多方法,但并没有获取到私有的两个方法,我们可以通过getDeclaredMethods
获取任意方法。
package relate;import java.lang.reflect.Method;public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取多个成员方法Method[] methods = c.getDeclaredMethods();// 3.遍历for (Method method : methods) {System.out.println("方法名称:"+method.getName()+",返回值类型:"+method.getReturnType()+",参数个数:"+method.getParameterCount());}}
}
// Student类中声明的任意方法
// 方法名称:run,返回值类型:void,参数个数:0
// 方法名称:toString,返回值类型:class java.lang.String,参数个数:0
// 方法名称:dance,返回值类型:void,参数个数:0
// 方法名称:eat,返回值类型:void,参数个数:1
(3) 获取单个方法
(3.1) 公有方法
我们可以通过getMethod
获取单个公有方法对象。
public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取公有成员方法Method run = c.getMethod("run"); // 无参直接方法名获取Method sing = c.getMethod("sing", String.class); // 带参数需要指定每个参数类型// 3.打印结果System.out.println("方法名称:"+run.getName()+",返回值类型:"+run.getReturnType()+",参数个数:"+run.getParameterCount());System.out.println("方法名称:"+sing.getName()+",返回值类型:"+sing.getReturnType()+",参数个数:"+sing.getParameterCount());}
}// 打印结果
// 方法名称:run,返回值类型:void,参数个数:0
// 方法名称:sing,返回值类型:void,参数个数:1
(3.2) 私有方法
我们可以通过getMethod
获取任意方法对象。
public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取私有成员方法Method dance = c.getDeclaredMethod("dance"); // 无参直接方法名获取Method eat = c.getDeclaredMethod("eat", String.class); // 带参数需要指定每个参数类型// 3.打印结果System.out.println("方法名称:"+dance.getName()+",返回值类型:"+dance.getReturnType()+",参数个数:"+dance.getParameterCount());System.out.println("方法名称:"+eat.getName()+",返回值类型:"+eat.getReturnType()+",参数个数:"+eat.getParameterCount());}
}
// 打印结果
// 方法名称:dance,返回值类型:void,参数个数:0
// 方法名称:eat,返回值类型:void,参数个数:1
(4) 触发方法
我们可以触发获取到的方法,不管它是否为私有方法。
方法 | 说明 |
---|---|
Object invoke(Object obj, Object… args) | 参数一:通过对象调用该方法 参数二:调用方法的传递的参数(如果没有可以不写) 返回值:方法的返回值(如果没有可以不写) |
public class Test {public static void main(String[] args) throws Exception {// 1.获取类对象Class c = Student.class;// 2.获取私有成员方法Method run = c.getDeclaredMethod("run");Method eat = c.getDeclaredMethod("eat", String.class);// 2.1 获取执行私有方法权限eat.setAccessible(true);// 3.创建对象用于触发方法Student student = new Student();// 4. 触发方法Object runRes = run.invoke(student); // 没返回值runRes为nullObject eatRes = eat.invoke(student, "水果"); // 没返回值eatRes为null// 3.打印结果System.out.println("runRes===>" + runRes + ",eatRes===>" + eatRes);}
}
// 打印结果
// invoke public run...
// eat 水果
// runRes===>null,eatRes===>null
六.作用
(1) 泛型擦除
由于反射是作用在运行时的技术,而泛型只在编译阶段可以约束只能操作某种数据类型,使用反射泛型将不能产生对我们产生约束,此时就相当于泛型被擦除了。
例如:我们编写如下代码时,往集合添加Integer类型数据不会产生报错,而添加其他类型数据将无法通过编译。
ArrayList<Integer> list = new ArrayList<>();
list.add(100); // 合法
// list.add(“观止"); // 产生编译错误
list.add(99); // 合法
而使用反射操作,此时集合的泛型将不能产生约束,可以为集合存入其他任意类型的元素的。泛型只是在编译阶段可以约束集合只能操作某种数据类型,在编译成Class文件进入运行阶段的时候,其真实类型都是ArrayList了。
public class Test {public static void main(String[] args) throws Exception {// 1.创建对象ArrayList<Integer> list = new ArrayList<>();// 尝试添加list.add(666);
// list.add("观止"); // 报错// 2.获取类对象Class listClass = list.getClass();// 3.获取add方法Method add = listClass.getDeclaredMethod("add", Object.class);// 4.添加其他类型元素add.invoke(list,"观止");add.invoke(list,999);// 5.打印查看数据是否添加System.out.println(list);}
}
// 打印结果
// [666, 观止, 999]
可以看到外面不单单存在Integer类型数据,还存在它所不允许的String类型数据,跳过了泛型对我们的约束作用。
(2) 通用框架的底层原理
几乎所有的框架实现都用到了反射技术,使用反射可以帮助我们很好的处理多变的情况。
例如:给你任意一个对象,在不清楚对象字段的情况下,可以把对象的字段名称和对应值存储到文件中去。
不使用反射显然我们无法对可变的情况进行固定的操作,更别说获取一些不对外暴露的私有字段。使用反射却能很好的帮助我们解决这种情况。
public class Test {public static void main(String[] args) throws Exception {Student s = new Student("彭于晏", 40, '男', 177.5, "男星");printAllFiled(s);Teacher t = new Teacher("观止", 6000);printAllFiled(t);}public static void printAllFiled(Object obj) throws IllegalAccessException {// 1.获取类对象Class objClass = obj.getClass();// 2. 获取类名打印横幅System.out.println("========" + objClass.getSimpleName() + "========");// 3.获取所有字段Field[] fields = objClass.getDeclaredFields();// 4.打印所有字段以及字段值for (Field field : fields) {// 5. 允许访问操作私有字段field.setAccessible(true);System.out.println(field.getName() + " = " + field.get(obj));}}
}
可以看到即使我们的类结构存在差异它也能正常的获取并打印数值。
(3) 简单模拟SpringMVC
我们知道使用SpringMVC大幅度的简化了我们使用Servlet开发需要重复写大量类和重复代码的烦恼。我们使用反射加上上文所学的自定义注解可以简单模拟一下SpringMVC中@RequestMapping
的功能。
- 导入servlet所需要的坐标并进行一些配置
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope></dependency><!-- 确保能获取方法参数列表真实名称--><build><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>utf8</encoding><compilerArgs><arg>-parameters</arg></compilerArgs></configuration></plugin></plugins></build>
- 自定义
@RequestMapping
注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {String value();
}
- 创建一个Servlet简单模拟在SpringMVC中配置请求路径的写法
// 接收以AppServlet开头的任意路径
@WebServlet("/AppServlet/*")
public class AppServlet extends HttpServlet {// mvc请求路径 -> 方法对象Map<String, Method> hashmap = new HashMap<>();// 获取并加载所有存在注解的映射路径以及方法对象@Overridepublic void init() {// 1. 获取类对象Class<AppServlet> appServletClass = AppServlet.class;// 2. 获取所有方法Method[] methods = appServletClass.getDeclaredMethods();// 3. 保存所有路径for (Method method : methods) {// 判断是否存在注解boolean present = method.isAnnotationPresent(RequestMapping.class);if (present) {// 将路径+方法对象存入mapRequestMapping annotation = method.getAnnotation(RequestMapping.class);String path = annotation.value();hashmap.put(path, method);}}}// 作为中转站,根据请求路径转发到对应注解匹配的路径@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {response.setCharacterEncoding("utf-8"); //设置 HttpServletResponse使用utf-8编码response.setHeader("Content-Type", "text/html;charset=utf-8"); //通知浏览器使用utf-8解码// 0.获取请求路径String requestURI = request.getRequestURI();String targetPath = requestURI.substring("/MyMVC_war/AppServlet".length());// 获取请求参数集合ArrayList list = new ArrayList();Map<String, String[]> parameterMap = request.getParameterMap();// 3. 创建对象用于触发方法AppServlet appServlet = new AppServlet();// 判断请求路径是否存在if (hashmap.containsKey(targetPath)) {// 存在调用方法try {Method method = hashmap.get(targetPath);Object res = "";// 判断方法是否带参数if (method.getParameterCount() == 0) {// 无参res = method.invoke(appServlet);} else {// 带参for (Parameter parameter : method.getParameters()) {// 类型转换适配String value = parameterMap.get(parameter.getName())[0];if (value == null) {throw new RuntimeException("参数名称不匹配");}list.add(convert(parameter.getType(), value));}// 参数传递res = method.invoke(appServlet, list.toArray());}response.getWriter().write(res.toString());} catch (Exception e) {throw new RuntimeException("server error 500!", e);}} else {response.getWriter().write("路径不存在");}}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {doGet(request, response);}// 将获取到的参数转为指定类型public static Object convert(Class<?> type, String str) {// 整型if (type.isAssignableFrom(int.class) || type.isAssignableFrom(Integer.class)) {return Integer.valueOf(str);} else if (type.isAssignableFrom(double.class) || type.isAssignableFrom(Double.class)) {// 浮点型return Double.valueOf(str);} else {// 字符串return str;}}// 模拟SpringMVC写法 @RequestMapping("/add")private Object add(String name, int age) {return "add invoke ==> " + name + " ==> " + age;}@RequestMapping("/delete")private Object delete(int id) {return "delete invoke:" + id;}@RequestMapping("/update")private Object update() {return "update invoke";}@RequestMapping("/select")private Object select() {return "select invoke";}}
- 通过postman进行测试
可以看到我们成功通过反射将请求路径映射到注解所对应的方法中