Java反射笔记2

反射的优点和缺点

  1. 优点:可以动态的创建和使用对象,使用灵活,没有反射机制,就无法正常使用框架技术
  2. 缺点:使用反射基本是解释执行,对执行速度有影响
    构建代码查看时间差距:

首先一个Cat类,中间的输出语句可要可不要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package reflection;
#Cat.java
public class Cat {
public Cat(){
}
public Cat(String name){
}
public void hi(){
// System.out.println("1");
}
public void cry(){
// System.out.println("2");
}
}

然后构建代码

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 reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
m1();
m2();
}
public static void m1(){
Cat cat = new Cat();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
cat.hi();
}
long end = System.currentTimeMillis();
System.out.println("传统方法耗时:"+(end - start));
}
public static void m2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("reflection.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
hi.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射方法耗时:"+(end - start));
}
}

上面的前一段是传统方法调用,下面是反射来调用
图片

因为我这里循环次数不是很大,差距也只是个位数倍数,将i调整为10亿,再运行

图片

时间差距几乎是很大的

反射调整优化-关闭访问调查:

1. Method和Field.Constructor对象都有setAccessible()方法
2. setAccessible作用是启动和禁用访问安全检查的开关
3. 参数值为true表示反射的对象在使用时取消访问检查,提高反射的检查,参数值为false则表示反射的对象执行访问检查
1
2
3
4
5
6
7
8
9
10
11
12
13
public  static void m3() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("reflection.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
hi.setAccessible(true);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
hi.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射方法m3耗时:"+(end - start));

}

图片

优化效果如图,但效果不是特别好,但聊胜于无

Class类:

  1. Class也是类,因此也继承Object类
  2. Class类对象不是new出来的,是系统创建(见笔记1的程序运行图解)
    比如说:
1
Cat cat = new Cat();

在这一步进行打断点debug,step进去首先进入ClassLoader.java里的loadClass方法,此时方法里的String name参数刚好指代上面的Cat方法。
如果使用反射方法进行创建对象

1
Class cls = Class.forName("Cat");

经过多次方法的调动,最后也会调用一个loadClass方法加载一个Cat类的Class类对象
3.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

4.每个类的实例都会记得自己是由哪个Class实例所生成

5.通过Class可以完整的得到一个类的完整结构,通过一系列API

6.Class对象是存放在堆里的

7.类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据

获取Class对象:

  1. 代码阶段/编译阶段:
    Class.forName()

前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException,实例:

1
Class cls1 = Class.forName("java.lang.Cat");

多用于配置文件,读取类的全路径,加载类
2.Class类阶段(加载阶段):

类.class

前提:若已知具体的类,通过类的class获取,该方式最为安全可靠,程序性能最高

实例:

1
Class cls2 = Cat.class;

多用于参数传递,比如通过反射得到对应构造器对象
3.Runtime运行阶段:

对象.getClass()

前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象

实例:

Class clazz = 对象.getClass();

通过创建好的对象,获取Class对象

4.通过类加载器来获取到类的Class对象

  • 先得到类加载器 car
    1
    ClassLoader classLoader = car.getClass().getClassLoader();
  • 通过类加载器得到Class对象
    1
    Class cls = classLoader.loadClass(classAllPath)
    以上获得的Class对象都是同一个(因为内存里对于一个类只允许有一个Class类对象)

5.基本数据类型按照如下方式得到Class类对象

Class cls = 基本数据类型.class

6.基本数据类型对应的包装类,可以通过.TYPE得到Class类对象

Class cls = 包装类.TYPE

到此为止,基本的反射语法都能看懂了,那我们学习某个知识肯定到最后也是为了Bypass也就是绕过。在安全中,使用反射的一大目的也是为bypass。

forName有两个函数重载:

1
2
Class forName(String name) 
Class forName(String name, **boolean** initialize, ClassLoader loader)

第一个就是我们常见获取class方式,其实也就是第一种的封装
可以理解为下面

1
2
3
Class.forName(className)
==
Class.forName(className,true,currentLoader)

一般情况下,forName第一个参数是类名,第二个代表是否初始化,第三个参数就是类加载器
类加载器就是告诉JVM如何去加载这个类,在这里JAVA默认根据类名来加载类,这个类名是类的完整路径,比如:java.lang.Runtime

第二个参数是initialize,决定是否初始化

在反射中如果使用功能.class来创建Class对象的引用时,不会自动初始化,如果使用forName()会自动初始化Class对象

(代码中test.class对象的本质就是test.class文件加载到内存中的内容)

这里初始化,可以理解为原来在C语言的中给一个变量赋初值,将这个思想转换到这里,也就是给一个类赋一个初始状态

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint { 
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class); }
public TrainPrint()
{
System.out.printf("Initial %s\n", this.getClass());
}
}

以上三个代码块,运行顺序分别是static,empty block以及最后的构造函数
static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯, 但在当前构造函数内容的前⾯。

所以说forName中第二个参数就是告诉JVM是否执行类初始化

假设有一个函数,其中参数name可控且会执行初始化:

1
2
public void ref(String name) throws Exception {  Class.forName(name); 
}

那么我们可以构造一个类,来执行我们的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e)
{
// do nothing
}
}
}

其中我们将特意设置的恶意代码放在static中