反射:将类的各个组成部分封装为其他对象。
Java代码在计算机中经历了三个阶段:源代码阶段->Class对象阶段->Runtime运行时阶段。
源代码阶段:在编辑器中写的便是源代码。以.java做后缀。
使用javac命令可以将.java文件编译为字节码文件,以.class结尾。此时认为仍处于源代码阶段。如下为class文件,具体含义暂不做分析。
Class类对象阶段:使用javac编译成的字节码文件经过jvm中ClassLoader的加载,在内存中生成Class类对象。Class类对象由成员变量Field、构造方法Constructor、成员方法Method组成。以上面Person对象为例,经过ClassLoader加载,name和age两个成员变量封装为Field[]对象,Person(){}方法封装为Constructor[]对象,eat(){}方法封装为Methods[]对象。而这个封装的过程便是反射机制。
Runtime阶段:实例化类对象,在内存中生成实例化对象。
获取Class类对象的四种方法 反射的作用:获取Class类对象。
针对Java代码在计算机的不同阶段,存在不同的方法来获取Class类对象。
源代码阶段:Class.forName(“全类名”):将字节码文件加载进内存,返回Class类对象。
Class类对象阶段:类名.class:通过类名的属性class获取。
实例化对象阶段:对象.getClass():getClass()方法在Object类中定义着。
类加载器ClassLoader.getSystemClassLoader().loadClass(“全类名”)
编写Person类
获取Class类对象
同一个字节码文件(*.class),在一次程序运行过程中,只会被加载一次。因此获取的四个Class类对象是相等的。
这里有一个容易混淆的地方,我们介绍了四种方法来获取Class类对象,观察其输出结果,均为org.domain.Person
。而Java内置的java.lang.Class
也叫Class类对象,两者有什么区别和联系?
java.lang.Class
不只是类对象,还是一个模板,org.domain.Person
便是根据该模板生成的,因此org.domain.Person
本质上也是java.lang.Class
类对象。org.domain.Person
下存在一个getClass()
方法来获取java.lang.Class
对象。
获取Class类成员变量 获取Class类成员变量主要是为了对其数值进行更改。
Field[] getFields()
、Field getField(String name)
获取public修饰的成员变量
Field[] getDeclaredFields()
、Field getDeclaredField(String name)
获取全部成员变量
void set(Object obj,Object value)
设置field值
Object get(Object obj)
获取值
针对非public修饰符的成员变量,在单独反射时,需要将其setAccessible(true)
;
实例:
首先编写Person类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package org.domain;public class Person { private String name; private int age; public String a; protected String b; String c; private String d; public Person () { } public Person (String name,String a) { this .name=name; this .a=a; } public Person ( String name,int age) { this .name=name; this .age=age; } public Person (String a,String... name) { this .a=a; for (String n:name){ System.out.println(n); } } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", a='" + a + '\'' + ", b='" + b + '\'' + ", c='" + c + '\'' + ", d='" + d + '\'' + '}' ; } public void eat () { System.out.println("eat..." ); } public void eat (String food) { System.out.println("eat..." +food); } }
使用反射机制获取Class类成员变量并修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package org.ReflectDemo;import org.domain.Person;import java.lang.reflect.Field;public class ReflectDemo2 { public static void main (String[] args) throws Exception{ Class cls = Person.class; Field[] fields=cls.getFields(); for (Field field:fields){ System.out.println(field); } System.out.println("----------------" ); Field a=cls.getField("a" ); System.out.println(a); Person p = new Person (); Object value=a.get(p); System.out.println(value); a.set(p,"张三" ); System.out.println(p); System.out.println("================" ); Field[] fields1= cls.getDeclaredFields(); for (Field field:fields1){ System.out.println(field); } Field d=cls.getDeclaredField("d" ); d.setAccessible(true ); Object value2=d.get(p); System.out.println(value2); } }
获取Class类构造方法 获取构造方法后,可以使用构造方法创建实例化对象
Constructor<?>[] getConstructors()
、Constructor<T> getConstructor(Class<?>... parameterTypes)
获取public构造方法
Constructor<?>[] getDeclaredConstructors()
、Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
获取全部构造方法
constructor.newInstance()
使用构造方法创建实例化对象。
当使用无参数构造方法创建实例化对象时,可以简化为使用Class类对象操作:class.newInstace()
。
setAccessible()
方法适用
实例:Person类保持不变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package org.ReflectDemo;import org.domain.Person;import java.lang.reflect.Constructor;import java.lang.reflect.Field;public class ReflectDemo3 { public static void main (String[] args) throws Exception { Class cls=Person.class; Constructor[] constructors=cls.getConstructors(); for (Constructor constructor:constructors){ System.out.println(constructors); } Constructor<?> constructor=cls.getConstructor(String.class,int .class); System.out.println(constructor); Object person=constructor.newInstance("张三" ,23 ); System.out.println(person); System.out.println("-----------------" ); Constructor<?> constructor1=cls.getConstructor(); System.out.println(constructor1); Object person1=constructor1.newInstance(); System.out.println(person1); Object person2=cls.newInstance(); System.out.println(person2); } }
获取Class类成员方法
Method[] getMethods()
、Method getMethod(String name,Class<?>... parameterTypes)
获取public类成员方法。注意这里获取全部public类成员方法的时候会获取Object类的public成员方法
Method[] getDeclaredMethods()
、Method getDeclaredMethod(String name,Class<?>... parameterTypes)
获取全部类成员方法,当然也包括Object类
String method.getName()
获取方法名
String class.getName()
获取类名
method.invoke(Object obj,Class<?>...parameterTypes)
执行类成员方法
setAccessible()
方法适用
实例:Person类保持不变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package org.ReflectDemo;import com.sun.security.jgss.GSSUtil;import org.domain.Person;import java.lang.reflect.Method;public class ReflectDemo4 { public static void main (String args[]) throws Exception{ Class cls=Class.forName("org.domain.Person" ); Method eat_method=cls.getMethod("eat" ); System.out.println(eat_method); String eat_methodName=eat_method.getName(); System.out.println(eat_methodName); Person person=new Person (); eat_method.invoke(person); Method eat_method2=cls.getMethod("eat" ,String.class); System.out.println(eat_method2); eat_method2.invoke(person,"meet" ); Method[] methods=cls.getMethods(); for (Method method:methods){ System.out.println(method); } String className=cls.getName(); System.out.println(className); } }
Java反射与安全 P牛:Java反射可以给Java这门静态语言赋予动态特性。“⼀段代码,改变其中的变量,将会导致这段代码产⽣功能性的变化,我称之为动态特性 ”。
如下代码中execute
方法,根据传入的ClassName
参数不同,获取不同的Class
类对象。再根据MethodName
不同,获取该类对象下的方法。最后使用invoke
方法来执行该方法。
当不知道传入的参数是什么的时候,这段代码最终实现的功能是未知的。这便是动态特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.ReflectDemo;import java.util.Scanner;public class ReflectDemo5 { public void execute (String ClassName,String MethodName) throws Exception{ Class cls=Class.forName(ClassName); cls.getMethod(MethodName).invoke(cls.newInstance()); } public static void main (String args[]) throws Exception { Scanner sc = new Scanner (System.in); String ClassName=sc.next(); String MethodName=sc.next(); ReflectDemo5 ref=new ReflectDemo5 (); ref.execute(ClassName,MethodName); } }
这里有一个容易忽略的点,forName
是java.lang.Class
类的static方法,只要我们能拿到java.lang.Class
类便可以执行forName
,而在“获取Class类对象的四种方法”小节的最后,我们发现任何Class类都可以通过getClass()
方法来获取java.lang.Class
对象。
例如存在String a
:
当然在本例中的cls3,通过对java.lang.Class
对象进行操作,最后又获取到java.lang.Class
对象是没什么意义的,这里仅仅是要了解其方法。
forName
有三个函数重载
forName(String className)
forName(String name, boolean initialize,ClassLoader loader)
forName(Module module, String name) //模块部分是JDK9添加的特性,还未学习,暂不考虑学习此方法
第一个是常见的获取Class的方式。查看器源代码会发现其最后会返回forName0(className, true, ClassLoader.getClassLoader(caller), caller)
1 2 3 4 5 public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true , ClassLoader.getClassLoader(caller), caller); }
至于第二个方法,忽略中间代码,发现其最后返回forName0(name, initialize, loader, caller)
,由此可知,方法二是对方法一的一个封装。默认了initialize
和loader
两个参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException { Class<?> caller = null ; SecurityManager sm = System.getSecurityManager(); if (sm != null ) { caller = Reflection.getCallerClass(); if (loader == null ) { ClassLoader ccl = ClassLoader.getClassLoader(caller); if (ccl != null ) { sm.checkPermission( SecurityConstants.GET_CLASSLOADER_PERMISSION); } } } return forName0(name, initialize, loader, caller); }
至于两个参数的含义,initialize
表示是否进行初始化,loader
表示ClassLoader
,暂不考虑ClassLoader
的学习。
当initialize=true
的时候,使用forName来获取Class对象时,Class对象会自动初始化。
在Java中,存在三种初始化方法。分别是static{}
,{}
和构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.ReflectDemo;public class ReflectDemo7 { static { System.out.println("static" ); } { System.out.println("{}" ); } ReflectDemo7() { System.out.println("Reflect" ); } public static void main (String[] args) { ReflectDemo7 rf=new ReflectDemo7 (); } }
那么三者存在什么区别?
Person p = new Person(“zhangsan”,20); 该句话都做了什么事情?
因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。
执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
在堆内存中开辟空间,分配内存地址。
在堆内存中建立对象的特有属性。并进行默认初始化。
对属性进行显示初始化。
对对象进行构造代码块初始化。
对对象进行对应的构造函数初始化。
将内存地址付给栈内存中的p变量。
从上述可以看出在步骤2就已经运行static{}
方法,步骤四运行{}
方法,步骤六运行构造代码块。可以运行上面的代码判断顺序是否正确。
对比2、4、6三个步骤会发现,static{}
方法是对Class类初始化,发生在找到Class类对象的时候。另外两种方法则发生在步骤3分配内存之后。
而我们上面提到的:使用forName来获取Class对象时,Class对象会自动初始化。forName同样存在一个寻找Class类对象的过程,找到对象后执行static方法。但其没有分配内存,也就是后面两种方法并不会执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.ReflectDemo;public class ReflectDemo7 { static { System.out.println("static" ); } { System.out.println("{}" ); } ReflectDemo7() { System.out.println("Reflect" ); } public static void main (String[] args) throws Exception { Class cls=Class.forName("org.ReflectDemo.ReflectDemo7" ); } }
当我们能够操作如下代码中的className
参数时:
1 2 3 public void ref (String className) throws Exception{ Class cls=Class.forName(className); }
可以将className
指向我们构造的恶意类。其中的static{}
方法为构造的恶意方法。
1 2 3 4 5 6 7 8 9 static { try { Runtime rt = Runtime.getRuntime(); String command = new String ("calc" ); Process pr = rt.exec(command); pr.waitFor(); } catch (Exception e) { } }
除了使用forName()
方法指向恶意类外,也可以使用forName()
指向Java的内置类,通过这种方法可以省去import
。获取Java内置类之后,若存在无参数构造方法,只要使用class.newInstance()
,便可将其实例化(上面获取Class类构造方法提到过),而后利用其属性或方法。例如,如果我们可以获取到Runtime
类实例,便可以使用exec()
方法来执行任意命令。
但是很不幸的是,很多时候class.newInstance()
会失效。主要原因:
例如Runtime
类的无参数构造方法便是private
,当需要获取Runtime
实例化对象时,需要调用其静态方法getRuntime
来返回实例化对象。
这么做的目的涉及到Java的单例模式。
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)
如下代码,通过两次new产生的实例化对象是不同的。
1 2 3 4 5 6 7 8 9 10 package org.ReflectDemo;public class ReflectDemo8 { public ReflectDemo8 () {}; public static void main (String agrs[]) { ReflectDemo8 rf1=new ReflectDemo8 (); ReflectDemo8 rf2=new ReflectDemo8 (); System.out.println(rf1==rf2); } }
但是经过单例模式实例化的对象rf1和rf2是相同的。保证了线程中只存在一个ReflectDemo8
实例化对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.ReflectDemo;public class ReflectDemo8 { private static final ReflectDemo8 reflect=new ReflectDemo8 (); public static ReflectDemo8 getInstance () { return reflect; } private ReflectDemo8 () {}; public static void main (String agrs[]) { ReflectDemo8 rf1=ReflectDemo8.getInstance(); ReflectDemo8 rf2=ReflectDemo8.getInstance(); System.out.println(rf1==rf2); } }
回到正题,既然在Runtime
类中,getRuntime
可以获取实例化对象。那我们直接调用不就好了。
1 2 3 4 5 6 7 8 package org.ReflectDemo;public class ReflectDemo9 { public static void main (String[] args) throws Exception { Class clazz = Class.forName("java.lang.Runtime" ); clazz.getMethod("exec" ,String.class).invoke(clazz.getMethod("getRuntime" ).invoke(clazz),"calc.exe" ); } }
这么看有点麻烦。拆开来看。
1 2 3 4 5 6 7 8 9 package org.ReflectDemo;public class ReflectDemo9 { public static void main (String[] args) throws Exception { Class clazz = Class.forName("java.lang.Runtime" ); Object rt=clazz.getMethod("getRuntime" ).invoke(clazz); clazz.getMethod("exec" ,String.class).invoke(rt,"calc.exe" ); } }
通过forName
获取getRuntime
方法并调用,获取Runtime
实例化对象。再获取exec
方法并调用,来达到执行命令的目的。刚看到这里的时候有些疑惑。为什么在调用getRuntime
方法时invoke(clazz)
但是exec
方法需要invoke(rt)
,一个参数是Class
一个参数是Object
。查看了一下invoke
方法,发现其参数确实是Object。也没有其他的重载。
后面查看getRuntime
方法和exec
方法,发现getRuntime
方法是static。查阅资料并尝试后发现确实如此。Gavra-invoke-static-method
除了使用getRuntime
方法外。在获取Class类构造方法章节,我们提到过获取private
构造方法的方式:getDeclaredConstructors()
。恰巧在Runtime
类中存在private
构造方法。因此利用暴力反射同样可以获取Runtime
实例化对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.ReflectDemo;import java.lang.reflect.Constructor;public class ReflectDemo9 { public static void main (String[] args) throws Exception { Class clazz = Class.forName("java.lang.Runtime" ); Constructor constructor= clazz.getDeclaredConstructor(); constructor.setAccessible(true ); Runtime rt= (Runtime) constructor.newInstance(); clazz.getMethod("exec" ,String.class).invoke(rt,"calc" ); } }
解决了无参数构造方法私有后,再来解决不存在无参数构造方法的问题。
普通的有参数构造方法在上面的获取Class类构造方法中提到过了。比较难理解的是当参数为数组时该怎么处理。
以常用到的,可以任意命令执行的另一个类:ProcessBuilder
为例。其具有两个重载的构造方法。
ProcessBuilder(List command)
ProcessBuilder(String… command)
第一个好理解,只要传入的参数为List即可
1 2 3 4 5 6 7 8 9 10 11 package org.ReflectDemo;import java.util.Arrays;import java.util.List;public class ReflectDemo10 { public static void main (String[] args) throws Exception{ Class clazz=Class.forName("java.lang.ProcessBuilder" ); ((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe" ))).start(); } }
重点在于第二个。其参数为可变长参数,对于可变长参数,java在编译时会编译成数组。也就是说在java中,以下两种方法是等价的,不可重载的
ProcessBuilder(String… command)
ProcessBuilder(String[] command)
查看newInstance
方法,我们可以看到其参数为数组。
而ProcessBuilder
的第二个构造函数参数同样也是数组。两者结合,便成为了二维数组。
1 2 3 4 5 6 7 8 package org.ReflectDemo;public class ReflectDemo10 { public static void main (String[] args) throws Exception{ Class clazz=Class.forName("java.lang.ProcessBuilder" ); ((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String [][]{{"calc.exe" }})).start(); } }
至于这里为什么是二维数组。我们可以这么去理解Constructor
和newInstance
之间的过程