Java安全|反射最细讲解

Java
285
0
0
2023-06-15
标签   Java反射

什么是反射?

JAVA 安全可以从反序列化漏洞说起,反序列化漏洞又可以从反射说起。反射是⼤多数语⾔⾥都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将 Java 这种静态语⾔附加上动态特性。可能说完这一两句话大家还是不知道反射是个啥玩意,现在为了让大家容易理解,先为大家提出一个需求,通过这个需要来引出反射。需求如下:

根据配置文件re.properties指定信息,创建对象并调用方法。

 classfullpath=com.lxflxf.Cat

method=hi 

这样的需求在学习框架时很多, 即在通过外部文件配置,在不修改源码的情况下,来控制程序。

我们使用现有技术可以做到吗?咱们可以动手写一下。

首先创建配置文件,写入上述内容,然后创建一个类,写入如下内容:

 public class Cat {

     private  String name = "小猫";

    public  void  hi(){
        System.out.println("hi" + name);
    }
} 

传统的方法是不是我们可以先new一个对象,然后再调用它的方法。写法如下:

 Cat cat = new Cat();
cat.hi(); 

通过传统方法,确实可以调用hi()方法,但是这和我们的需求不一样,这里我们是要根据配置文件re.properties指定信息来完成。到了这里,有同学就说了,咱们可以通过 IO流 的方式来读取配置文件的信息。好,咱们用代码来写一下。

使用Properties来读写配置文件。案例代码如下:

 Properties properties = new Properties();
        properties.load(new FileInputStream("src//re.properties"));
        String classfullpath = properties.get(" Class fullpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classfullpath" + classfullpath);
        System.out.println("methodName=" + methodName); 

运行一下,发现成功读取到内容。

然后需要创建对象,怎么创建对象呢?有同学就说了,咱们可以直接 new classfullpath ,这样不就好了嘛?嗯,想法不错,下回不要想了。不要忘记了,我们现在的classfullpath可是 字符串 类型,怎么能去 new 呢。所以现有技术是做不到这个事情的。那么这里就要引入我们要讲的重点—— 反射机制 。

为了能更好地理解反射,这里先写一个小案例,然后再去解释。

第一步、加载类,返回Class类型的对象cls

 Class cls = Class.forName(classfullpath); 

第二步、通过cls得到你加载的类 com.lxflxf.Cat 的对象实例

 Object o = cls.newInstance(); 

可能有同学会问,你怎么知道这里拿到的是com.lxflxf.Cat呢,我们可以打印一下来看看, System.out.println(o.getClass()) 输出结果如下:

第三步、通过cls得到你加载的类 com.lxflxf.Cat 的 methodName 的方法对象,我们可以在反射中,把方法视为对象。

 Method method = cls.getMethod(methodName); 

最后、通过method1调用方法、也就是通过方法对象来实现调用方法

 method.invoke(o); 

在这里我们也能发现反射和传统方法的区别了,传统方法是对象.方法(),反射中呢,是方法.invoke(对象)。那我们运行一下,看看能否输出方法里的内容呢,如下:

说到这里大家脑海里应该也有了反射的概念。其实反射机制还有一个优点,那就是可以通过外部文件配置,在不修改源码的情况下,来控制程序。比如这里,我在Cat类下面再写一个方法,cry()方法,代码如下:

 public void cry(){
    System.out.println(name + "......喵喵喵");
} 

如果我们使用传统方法,要调用这个方法,是不是就要修改代码了,比如 cat.cry(); 这样的,那通过反射,我们只需要修改配置文件就可以了,在配置文件re.properties中,将method=hi改为method=cry,就可以了。

运行,发现成功调用并输出了内容,实现了改配置文件,不改代码,完成了解藕。

反射机制

上文中,通过一个小案例来简单的了解了一下反射,现在来系统的说一下。 反射机制允许程序在执行期借助于 Reflection API取得任何类的内部信息(比如成员变量、构造器、成员方法等等),并能操作对象的属性及方法。 加载完类后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类似的结构。为了便于理解,在这里为大家画一下 Java反射机制 原理示意图。如下:

然后现在做一个小小的总结,Java反射机制可以完成:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时得到任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的成员变量和方法
  • 生成动态代理

反射相关的主要类型如下:

、Java.long.Class:代表一个类,Class对象表示某个类加载后在堆中的对象
、 Java.lang .reflect.Method:代表类的方法
、Java.lang.reflect.Field:代表类的成员变量
、Java.lang.reflect. Constructor :代表类的 构造方法  

上文的案例代码中,我们使用了Method和Class相关的方法,现在演示一下,通过Field来拿到成员变量,代码如下:

 Field name = cls.getField("name");
System.out.println(name.get(o)); 

发现成功拿到了成员变量的值

Class类分析

接下来对Class类特点进行一下梳理。先看看Class类图

我们发现它的父类仍然是Object。

然后第二点是, Class类 对象不是new出来的,而是系统创建的 。这里怎么理解呢,还记得上面咱们画的原理图吗? Class类是由loadClass()方法完成类加载,生成了某个类对应的Class类对象 。现在为大家演示一下。写如下案例代码:

 Class<?> aClass = Class.forName("com.lxflxf.Cat"); 

然后这这句代码的前面下一个断点,进行调试。成功进入ClassLoader类中,到了loadClass()方法。如下:

接下来说第三点, 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次 。现在写一个小案例来验证一下这个事情,通过ha shCode来判断,写如下几行代码:

 Class<?> cls = Class.forName("com.lxflxf.Cat");
Class<?> cls = Class.forName("com.lxflxf.Cat");
System.out.println(cls.hashCode());
System.out.println(cls.hashCode()); 

执行结果如下图,值相同

最后关于Class类对象还有两点说一下,一是每个类的实例都会记得自己是由哪个Class实例所生成,二是Class对象可以完整地得到一个类的完整结构,通过一系列的API。

Class类常用方法

这里通过写小案例的方式,为大家说说Class类常用方法,首先新建一个Car类,代码如下:

 public class Car {
    public String brand;
    public int price;
    public String color;
} 

然后我要获取到Car类对应的Class对象,这里用到的就是forName()方法:

 String classAllPath = "com.lxflxf.Car";
//获取到Car类对应的Class对象
Class cls = Class.forName(classAllPath); 

我们可以输出一下

 System.out.println(cls);
System.out.println(cls.getClass()); 

第一个输出的是cls对象,是哪个类的Class对象,第二个输出的是cls运行类型,如下图:

如果我想要得到包名,可以通过getPackageName()方法,可以通过 System.out.println(cls.getPackageName()) ,输出内容为 com.lxflxf 。如果想得到类名,可以通过getName()方法。还有一个很重要的方法,那就是创建对象实例:newInstance(),案例如: Object o = cls.newInstance(); ,这里也需要注意一点,在 JDK 1.9往上,不再使用newInstance()。还可以通过getField()获取到属性。还有一些其他方法,这里就不一一举例了。列了一个表格,如下:

前面说了这么多,那哪些类型有Class对象呢?如下列表:

  • 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
  • interface:接口
  • 数组
  • enum : 枚举
  • annotation : 注解
  • 基本数据类型
  • void

案例代码如下:

 Class<String> cls = String.class;  //外部类
Class< Serializable > cls2 = Serializable.class;  //接口
Class<Integer[]> cls = Integer[].class;   //数组
Class<Deprecated> cls = Deprecated.class;   //注解
System.out.println(cls);
System.out.println(cls);
System.out.println(cls);
System.out.println(cls); 

输出结果如下:

动态加载

在文章最开始,就说了一下,通过“反射”,我们可以将Java这种静态语⾔附加上动态特性,换句话说,就是反射机制是Java实现动态语言的关键,也就是通过反射实现类动态加载。怎么理解呢,就是在运行时加载需要的类,如果运行时不用该类,则不报错,降低了依赖性。

举个例子吧

新建一个Java文件,命名为ClassLoad,写入如下代码

 Scanner scanner = new Scanner(System.in);
System.out.println("请输入数字");
 String  key = scanner.next();
switch (key){
    case "":
        System.out.println("我等于");
    case "":
        Class<?> cls = Class.forName("Person");
        Object o = cls.newInstance();
        Method m = cls.getMethod("hi");
        m.invoke(o);
        System.out.println("ok!");
        break;

} 

这里,我没有写Person类,但是程序编译的时候是不会报错的。也就是说,等到程序执行到 case “2” ,里面时才会发生报错,也就是上文中提到的 在运行时加载需要的类,如果运行时不用该类,则不报错,这就是动态加载。 我们现在来运行看一眼。先输入1程序正常,然后输入2报错。

现在是不是理解了动态加载了呢。

类加载

可能还有一些同学想要了解,比如,类加载过程到底是怎么样的呢?其实类加载大体分为三个阶段(加载阶段(Loading)、链接阶段(验证、准备、解析)、初始化阶段(initalization)),这里画一张图来便于理解。

如果感觉小编写得不错,请素质三连:点赞+转发+关注。我会努力写出更好的作品分享给大家。更多JAVA进阶学习资料小编已打包好,可以关注私信找我领取哦!