壹 介绍
1.1 为什么使用反射?
学任何东西的动力的来源就是知道其使用的价值,为什么使用反射?举一个例子,当我们在写代码并且编译后,需要动态加载一个jar包文件,这时候我们按照正常逻辑编写代码一般都会将这个文件在编写代码的时候就进行列举和写入,这时候就会出现一个问题,当后面我们需要更新迭代更多类似的jar包文件时,那么我们就需要去重新编译程序运行,当然对于小程序来说,这个重启操作是轻而易举的,但是对于大型服务或者是企业级服务的时候,重启服务是一项需要慎重的事情,这时候就需要通过动态加载,我们可以将程序模型写出来,然后通过接口去获取加载这些jar包,包括获取这个程序内部的结构,例如:类、方法、属性,并通过这些来运行必要的程序步骤,这时候Reflection(反射)便应运而生。
个人理解在开发角度使用反射,更多是为了方便去动态加载获取程序的结构内容来进行开发,而从安全角度使用反射,更多是为了获取程序内部的恶意类来动态加载恶意payload实现恶意操作目的,说白了,一个是为了创造,一个是为了破坏。
1.2 静态语言和动态语言
动态语言是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。主要动态语言:
Object-C、C#、JavaScript、PHP、Python等。静态语言与动态语言相对应的,运行时结构不可变的语言就是静态语言,如:
Java、C、C++,Java不是动态语言,但Java可以称之为准动态语言,即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性,Java的动态性让编程的时候更加灵活!
举个例子:
function f(){
var x = "var a = 3;var b = 5; alert(a+b);"
eval(x);
}
在这段代码中x内的值可以是字符串,也可以是执行的代码,我们可以通过eval将其字符串转为代码执行,但是在Java中就不能这样,该类型是字符串,那么就是字符串类型,并不会改变为其他类型,所以动态语言与静态语言的区别就是在运行时代码可以根据某些条件改变了自身结构,例如这里变量x通过eval将其数据改变为代码,而不是字符串,而Java、C/C++这种强类型语言,在编译前就已经将变量的类型进行了定义,往往运行时是改变不了其类型的,当然可以用一些特殊技术,例如:Java反射。
1.3 介绍
Reflection(反射)是Java被视为动态语言的关键,Java的反射机制允许程序在执行期借助于Reflection API获得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
Student s1 = new Student();
上面是一个平时我们创建类的格式,当程序运行时,程序会生成一个Student.class文件,然后通过JVM的类加载器将Student.class文件进行加载,接着在堆内存的方法区中产生一个Student类对应的Class对象(一个类只有一个Class对象),这个Class对象就包含了Student类的所有结构信息。这个Class对象就像镜子一样,通过这个镜子我们可以获取到到Student类的结构,所以我们形象称自为:反射。
Class类就是反射的核心,是一个特殊的类,用来描述所有类的结构信息,Class类只能由系统创建对象,一个加载的类在JVM中只会有一个Class实例,不同的加载类在JVM中的Class实例是不同的,一个Class对象对应的是一个加载在JVM中的一个.class文件,所以要使用反射,首先要获取相应类的Class对象。
正常方式:引入需要的包名 => 通过new实例化 => 取得实例化对象
Student s1 = new Student();
反射方式:实例化对象 => Class.forName()方法 => 得到完整的包名
Class c = Class.forName("com.demo.student");
可以看到两种方式就行镜子的对照一样,正常方式是通过包获取对象,反射方式是通过对象获取包。
1.4 优缺点
- 使用反射优点:可以实现动态创建对象和编译,体现初很大的灵活性
- 使用反射缺点:对性能有一定影响,使用反射基本上是一种解释性操作,相对于我们告诉
JVM我们需要做什么,然后它满足我们要求,这类操作显然比JVM自动去操作要慢。
贰 获取Class类的方式
在Java编程中,首先我们需要获取到一个类,然后才能使用这个类内部的一些方法和属性,那么要怎么获取呢?这里有三种方法:
- 通过
.class静态属性 - 通过
getClass()方法 - 通过
Class.forName("完整的类名")方法
这三种方式各有千秋。后面的文章会用Student类,这里将代码提供出来,方便查看:
package com.demo.annotation;
import java.util.Arrays;
public class Student implements Interface1 {
// 名字
public String Name;
// 年龄
private int Age;
// 爱好
private String[] Like;
// 班级
private String Classname;
// 无参构造方法
public Student(){}
// 有参构造方法
public Student(String name){
this.Name = name;
}
// 多个有参
public Student(String name,int age){
this.Name = name;
this.Age = age;
}
// 私有的构造方法
private Student(int age){
this.Age = age;
}
public void Run1(){
System.out.println("上课1");
}
public String Run2(String name){ return "上课2"+name; }
private void Run3(){
System.out.println("上课3");
}
private void Run4(String name){
System.out.println("上课4"+name);
}
@Override
public String toString() { return "Student{" + "Name='" + Name + '\'' + ", Age=" + Age + ", Like=" + Arrays.toString(Like) + ", Classname='" + Classname + '\'' + '}'; }
}
2.1 通过.class静态属性
若已知具体的类,通过类的class属性获取,该方法最为安全可靠,但是不方便,需要知道具体的类。
package com.demo.reflection;
import com.demo.Student;
public class GetClassTest {
public static void main(String[] args) {
// 2.第一种获取Class对象方式
// 通过类名.class获取到
// 需要引入包名
Class<Student> studentClass3 = Student.class;
}
2.2 通过getClass()方法
若已知某个类的实例,调用该实例的getClass()方法获取Class对象。
package com.demo.reflection;
import com.demo.Student;
public class GetClassTest {
public static void main(String[] args) {
// 1.第二种获取Class对象的方式
// 通过对象.getClass()获取
// 需要引入包名
Student student1 = new Student();
Class<? extends Student> studentClass1 = student1.getClass();
}
2.3 通过Class.forName(“完整的类名”)方法
若已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException,这种方式就是我们平时常用的反射方式,这种方式虽然方便获取任意类,但是程序性能是最差的。
package com.demo.reflection;
public class GetClassTest {
public static void main(String[] args) throws ClassNotFoundException {
// 3.第三种获取Class对象的方式
// 通过Class.forName("完整的类名");
// 可以发现该步骤不需要引入包名,但是由于获取的包不确定存在所以需要抛出异常
Class<?> studentClass4 = Class.forName("com.demo.reflection.Student");
}
需要简单说明一下Class.forName方法,该方法存在两个重载方法:
Class<?> forName(String className)
Class<?> forName(String className, boolean initialize, ClassLoader loader)
首先说明参数className表示类名,就是我们传递进去的完整类名,initialize参数表示是否进行类的初始化,loader参数是一个加载器,告诉Java虚拟机如何加载这个类。
其实通过源码可以发现,第一个forName方法与第二个forName方法的关系如下:
# 第一个forName
forName(className)
# 等价于
# 第二个forName
forName(className, true, currentLoader)
需要注意的是通过Class.forName方法并不会将类进行初始化和构造方法也不会执行,即使initialize=true,这里我们就不具体展开可以看下面的参考中的狂神说的bilibili或者是phith0n的Java安全漫谈。
叁 通过反射获取Class类内部结构信息
当我们获取了Class类后,我们就要去通过Constructor(构造方法)实例化对象,然后获取这个对象里面的Field(属性)、Method(方法)、Superclass(父类)、Interface(接口)、Annotation(注解)等。
package com.demo.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class GetClassInfo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
Class<?> studentClass1 = Class.forName("com.demo.annotation.Student");
// =======获取类的其他信息=======
// 获取包名+类名
System.out.println(studentClass1.getName());
// 获取类名
System.out.println(studentClass1.getSimpleName());
// 获取包名
System.out.println(studentClass.getPackage());
// 获取父类
System.out.println(studentClass.getSuperclass());
// 获取接口
System.out.println(studentClass.getInterfaces());
// =======获取类内部属性=======
// 1.只获取 public 修饰的属性
Field[] fields1 = studentClass1.getFields();
for (Field field : fields1){
System.out.println(field);
}
// 2.获取指定的 public 修饰的属性,这里的参数是类的属性名字
Field fields2 = studentClass1.getField("Name");
System.out.println(fields2);
// 3.获取全部属性
Field[] declaredFields1 = studentClass1.getDeclaredFields();
for (Field declaredField : declaredFields1){
System.out.println(declaredField);
}
// 4.获取指定的属性,这里的参数是类的属性名字
Field declaredFields2 = studentClass1.getDeclaredField("Age");
System.out.println(declaredFields2);
// =======获取构造方法=======
// 1.获取所有 public 修饰的构造方法对象,是通过Class.getConstructors()方法获取
Constructor<?>[] constructors1 = studentClass1.getConstructors();
for (Constructor<?> constructor : constructors1){
System.out.println(constructor);
}
// 2.获取某个指定的 public 修饰的构造方法对象,是通过Class.getConstructor("参数的类型")方法获取
// 这个方法的参数需要与构造方法的参数类型一致,例如:String类型就是String.class
Constructor<?> constructor2 = studentClass1.getConstructor(String.class);
System.out.println(constructor2);
// 3.获取所有的构造方法对象,是通过Class.getDeclaredConstructors()方法获取
Constructor<?>[] constructors3 = studentClass1.getDeclaredConstructors();
for (Constructor<?> constructor : constructors3){
System.out.println(constructor);
}
// 4.获取某个指定的构造方法对象,是通过Class.getDeclaredConstructor("参数的类型")方法获取
Constructor<?> constructors4 = studentClass1.getDeclaredConstructor(int.class);
System.out.println(constructors4);
// =======获取方法=======
// 1.获取本类及其父类的所有 public 修饰的方法对象
Method[] methods1 = studentClass1.getMethods();
for (Method method: methods1){
System.out.println(method);
}
// 2.获取本类及其父类的指定的 public 修饰的方法对象
Method run1 = studentClass1.getMethod("Run1", null);
System.out.println(run1);
Method run2 = studentClass1.getMethod("Run2", String.class);
System.out.println(run2);
// 3.获取本类的所有方法对象
Method[] declaredMethods1 = studentClass1.getDeclaredMethods();
for (Method method: declaredMethods1){
System.out.println(method);
}
// 4.获取本类的指定方法对象
Method run3 = studentClass1.getDeclaredMethod("Run3");
System.out.println(run3);
Method run4 = studentClass1.getDeclaredMethod("Run4", String.class);
System.out.println(run4);
}
}
通过上面的方法获取Class对象的内部结构信息,我们可以知道主要有两大类方法分别是获取public修饰的结构信息和获取全部的结构信息,方法的区别就是有没有Declared。
肆 通过反射技术动态创建对象
前面我们已经获取到了Class类对象中的结构信息,这时候我们就需要通过这些信息去创建实例化一个对象,我们可以通过newInstance()方法进行创建对象,通过invoke()方法来激活使用对象,通过setAccessible()方法来设置开放访问权限。
package com.demo.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class newClass {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
// 1.获取Class对象
Class<?> studentClass = Class.forName("com.demo.annotation.Student");
// ==========创建构造方法==========
// 2.获取构造方法,这里我们可以理解为我们要去写一个构造方法的模型
Constructor<?> constructor = studentClass.getConstructor(String.class);
// 3.通过构造器创建对象,这里的参数与构造方法对应,这里可以理解为我们写了模型后在程序中执行该构造方法
Object o = constructor.newInstance("Demo");
// 输出toString默认方法的值
System.out.println(o);
// ==========创建普通方法==========
// 4.通过反射获取其内部普通方法,这里我们可以理解为我们要去写一个普通方法的模型
Method run2 = studentClass.getDeclaredMethod("Run2", String.class);
// 5.如果是私有的方法或者属性,那么就需要开放访问权限,如果不是可以不写
// run2.setAccessible(true);
// 6.通过invoke方法将Student类中的Run2方法激活并使用,其中第一个参数是被激活的方法对象,后面是该被激活方法需要的方法参数值,这里可以理解为我们写了模型后在程序中执行该普通方法
// 注意这里需要使用到前面构造器创建的对象,只有这样我们才能找到是哪个对象使用的方法
Object ok = run2.invoke(o,"ok1");
System.out.println(ok);
// ==========创建获取属性==========
// 7.通过Class对象来获取类内部定义的某个特定成员变量
Field name = studentClass.getField("Name");
// 8.如果是私有的方法或者属性,那么就需要开放访问权限,如果不是可以不写
// name.setAccessible(true);
// 9.通过set设置属性,get获取属性值
name.set(o,"名字内容");
System.out.println(name.get(o));
}
}
伍 通过反射弹出计算器
通过Runtime类中的exec执行命令,弹出计算器,Runtime类的构造器是私有方法,需要通过getRuntime来构造对象。
package com.demo.reflection;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test01 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, IllegalAccessException {
// ===========正常写法,通过 Runtime 弹计算器===========
Runtime.getRuntime().exec("calc");
// ===========通过反射方式弹计算器===========
// 1.获取Class对象
Class<?> aClass = Class.forName("java.lang.Runtime");
// 由于使用Runtime的构造方法是私有,所以我们直接获取getRuntime这个public方法构造一个对象
Method getRuntime = aClass.getMethod("getRuntime");
// 激活方法,构造Runtime对象
Object invoke1 = getRuntime.invoke(aClass);
// 获取exec方法
Method exec = aClass.getMethod("exec",String.class);
// 激活方法
exec.invoke(invoke1,"calc");
// ===========缩减为一句话===========
// 这里可以使用多个Class.forName("java.lang.Runtime")是因为每个加载类都有一个共同的Class类,所以无论用多少次,该类的Class类都一样
Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")),"calc");
}
}
陆 小结
我们来理解一下这句话:Java可以利用反射机制获得类似动态语言的特性。首先Java是一个静态语言,例如:
public void printok(String str) {
for (int i = 0; i < 10; i++) {
System.out.println(str);
}
}
这段代码我们是可以分析出其确定的功能和会输出的结果的,并且传入的参数也是可以确定的,而下面这段代码,就显得很模糊:
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
传入的参数methodName会由于传入的className不同输出的结果也不同,及时传入的参数都是字符串,但是通过反射进行调用,我们并不知道其表现的功能是什么,这就体现了通过反射使得Java具有动态特性。