Java 反射机制 学习笔记
前言
上次学了Java RMI相关的知识后,感觉收获挺大的,今天跟着Epicccal师傅继续学习Java 反射机制,这篇文章算是读完师傅文章做的一些记录吧
1.什么是 Java Reflection
可以用一句话来说明什么是 Java 反射机制
Java 反射机制允许运行中的Java程序获取自身的信息, 操作类和对象的内部属性.
当然, 这样说太简明了, 很多没了解过 Java 的同学( 比如我 )可能会一头雾水 . 往细一点说 , 可以给出如下定义 :
Java 反射机制是指在程序运行时 , 对于任何一个类 , 都能知道这个类的所有属性和方法 , 对于任何一个实例对象 , 都能调用该对象的任何一个属性和方法 .
Java中这种 " 动态获取信息 " 和 " 动态调用属性方法 " 的机制被称为 Java 反射机制.
实例对象可以通过反射机制获取它的类 , 类可以通过反射机制获取它的所有方法和属性 . 获取的属性可以设值 , 获取的方法可以调用 .
Java 反射机制的功能可分为如下几点 :
在程序运行时查找一个对象所属的类 .
在程序运行时查找任意一个类的成员变量和方法 .
在程序运行时构造任意一个类的对象 .
在程序运行时调用任意一个对象的方法 .
2.反射的主要用途
反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
举一个例子,在运用 Struts 2 框架的开发中我们一般会在 struts.xml 里去配置 Action,比如:
<action name="login"
class="org.ScZyhSoft.test.action.SimpleLoginAction"
method="execute">
<result>/shop/shop-index.jsp</result>
<result name="error">login.jsp</result>
</action>
配置文件与 Action 建立了一种映射关系,当 View 层发出请求时,请求会被 StrutsPrepareAndExecuteFilter 拦截,然后 StrutsPrepareAndExecuteFilter 会去动态地创建 Action 实例。比如我们请求 login.action,那么 StrutsPrepareAndExecuteFilter就会去解析struts.xml文件,检索action中name为login的Action,并根据class属性创建SimpleLoginAction实例,并用invoke方法来调用execute方法,这个过程离不开反射。
对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。
3.反射的基本运用
1.查找一个对象所属的类
如何获取一个类( java.lang.Class )呢? 总的而言有三种方法 .
obj.getClass()
Class.forName(className)
className.class
具体的使用方法如下所示
这里再强调一下 forName 这个方法,下面是H0t-A1r-B4llo0n师傅文章的原话
这里提到了这个函数可以选择是否会进行类的初始化,这里我们需要注意一点,执行了类的初始化也就会相应的执行一些类的静态代码块中的内容
通过上图可以看到,类的初始化时执行了静态代码块中的内容,构造函数的内容并没有执行,构造函数的内容是在类对象的实例化时才执行的,这得分清
那也就是说 , 如果我们能控制一个类 , 那么就可以通过在类中添加包含恶意代码的静态代码块 . 当类初始化时 , 默认会自动执行恶意代码. 如下所示
设有test类
public class test {
public test(String name) throws ReflectiveOperationException
{
Class.forName(name);
}
public static void main(String[] args) throws ReflectiveOperationException
{
test class1 =new test("lmonstergg");
//System.out.println("通过obj.getClass()获取类名"+class1.getClass()); //当我们知道实例对象名称时
//System.out.println("通过Class.forName(className)获取类名"+Class.forName("test")); //当我们知道了某个类的名称时
//System.out.println("通过className.class获取类名"+test.class); //当我们已经加载了某个类时
}
}
以及构造恶意的 lmonstergg 类
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class lmonstergg {
static
{
try {
Process p = Runtime.getRuntime().exec("whoami"); //执行系统命令
InputStream is=p.getInputStream(); //获取进程p的标准输出流作为输入字节流
InputStreamReader isr=new InputStreamReader(is); //将字节流转换为字符流
BufferedReader br=new BufferedReader(isr); //为字符流提供缓冲区,便于读取整块数据
String line=null;
while((line=br.readLine())!=null)
{
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在该类静态代码块中 , 通过 java.lang.Runtime.getRuntime().exec() 执行系统命令 , 并将返回字节流转换为字符流 , 存入缓冲区后逐行读取并输出 .
当调用 test 类时 , 会自动执行恶意代码 .
2.查找一个类的方法
如何获取某一个类的所有方法呢? 总的来说有三种方法 .
className.getMethod(functionName , [parameterTypes.class])
className.getMethods()
className.getDeclaredMethods()
具体使用方法如下图所示 , 这里参考了 sczyh30 师傅的 深入解析Java反射(1) , 在原有代码的基础上进行了修改
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> c = methodClass.class;
Object object = c.newInstance();
Method[] methods = c.getMethods();
Method[] declaredMethods = c.getDeclaredMethods();
//获取methodClass类的add方法
Method method = c.getMethod("add", int.class, int.class);
//getMethods()方法获取的所有方法
System.out.println("getMethods获取的方法:");
for(Method m:methods)
System.out.println(m);
//getDeclaredMethods()方法获取的所有方法
System.out.println("getDeclaredMethods获取的方法:");
for(Method m:declaredMethods)
System.out.println(m);
}
}
class methodClass {
public final int fuck = 3;
public int add(int a,int b) {
return a+b;
}
public int sub(int a,int b) {
return a+b;
}
}
getMethod() : 返回类中一个特定的方法 . 其中第一个参数为方法名称 , 后面的参数为方法的参数对应 Class 的对象 .
getMethods() : 返回某个类的所有公用(public)方法 , 包括其继承类的公用方法 .
getDeclaredMethods() : 返回某个类或接口声明的所有方法 , 包括公共、保护、默认(包)访问和私有方法 , 但不包括其继承类的方法 .
补充一些内容
Class<?> : 定义了一个泛型类 , 其中 <?> 代表不确定类的类型 , 具体细节可以参考 程序鱼师傅 JAVA泛型通配符T,E,K,V区别,T以及Class\<T>,Class\<?> 的区别 一文
for(Method m:methods) 循环获取methods集合中的内容 , 把每一项赋值给变量m .
输出信息中的美元符号( $ )代表内部类 .
3.构造任意一个类的对象
上文提到了可以通过三种方式来获取类 , 那么如果获取一个实例对象呢 ?
通过 className.newInstance() 来构建一个实例对象.
我们都知道在类实例化时会调用构造函数 , 而构造函数又分为 " 有参构造函数 " 和 " 无参构造函数 " . 然而 className.newInstance() 没有参数 , 只能调用无参构造函数 . 如果我们想要调用有参构造函数 , 就必须依赖于 Class 类的 getConstructor() 方法 .
通过 Class 类的 getConstructor() 方法 , 可以获取 Constructor 类的一个实例 , Constructor 类也存在一个 newInstance() 方法 , 不过该方法可以携带参数 . 用该方法来创建实例对象可以调用有参构造函数 .
例子如下
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class test {
public test()
{
System.out.println("调用无参构造函数");
}public test(String name)
{
System.out.println("调用有参构造函数"+name);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class<?> cls=Class.forName("test");
test obj1= (test) cls.newInstance();
test obj2=(test) cls.getConstructor(String.class).newInstance("调用有参构造函数");
}
}
因此 , 我们可以通过 newInstance() 方法来构造任何一个类的对象 . 并且可以选择是调用其无参构造方法 , 还是有参的构造方法 ,总结用法如下
className.newInstance()
className.getConstructor( parameterType ).newInstance( parameterName )
4.调用任意一个实例对象的方法
一般来说 , 可以通过 objectName.functionName() 这种格式来调用实例方法
但是在很多情况下 , 你并不知道类名, 也就无法 new 出实例对象 , 更别提调用实例对象的方法了 . 当遇到这种情况时 , 就需要使用 Java 反射来调用实例对象的方法了 .
仔细看过上文的师傅应该有一些思路了 .
不知道类怎么办 ?
我们可以通过 obj.getClass() , Class.forName(className) , className.class 来获取类.
不知道类有哪些方法怎么办 ?
我们可以通过 className.getMethod(functionName , [parameterTypes.class]) , className.getMethods() , className.getDeclaredMethods() 来获取类的方法.
不能 new 出实例对象怎么办 ?
我们可以通过 className.newInstance() , className.getConstructor().newInstance() 来构造实例对象 .
那如何调用实例对象的方法呢 ?
通过 invoke() 方法来调用任何一个实例对象的方法 !
这是invoke() 函数的定义
下面来个例子试一试
Method.invoke(obj , args[])
如上文所说的 , 通过Java反射机制来获取类 , 获取类的方法 , 构造实力对象 , 最终调用实例方法 .
这里再额外说一点
如果要调用的方法是静态的 , 则忽略 obj 参数 .
这个点其实比较好理解 , 我们知道Java中调用静态方法是无需创建实例对象的** , 所以这里可以省略 obj 参数 .
如果要调用的方法的形参个数为 " 0 " , 那么 args[] 数组的长度可以为 " 0 " 或者 " null " .
这个点其实也没啥说的 , args[] 数组本就是要调用方法的参数 , 既然目标方法没有参数 , 这里自然也就不用写 .
参考文章
https://www.guildhab.top/2020/04/java-rmi-%e5%88%a9%e7%94%a82-java-%e5%8f%8d%e5%ba%8f%e5%88%97%e5%8c%96%e6%bc%8f%e6%b4%9e/
https://www.sczyh30.com/posts/Java/java-reflection-1/#4%E3%80%81%E8%8E%B7%E5%8F%96%E6%96%B9%E6%B3%95