栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java 反序列化(一)必备基础知识

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java 反序列化(一)必备基础知识

反序列化基础 简介

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。把字节序列恢复为对象的过程称为对象的反序列化。

序列化的条件

一个类是否可以序列化的条件

  1. 该类需要实现java.io.Serializable接口
为什么会有序列化和反序列化

序列化和反序列化的用途主要是用于客户端和服务端交互数据,假如此时一台主机A和一台主机B。如果

例子

首先定义一个Person类,实现了Serializable接口。

package com.darkerbox.deserialization;

import java.io.IOException;
import java.io.Serializable;

public class Person implements Serializable {
    String name;
    String age;

    public Person() {
    }

    public Person(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException, IOException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行命令
        Runtime.getRuntime().exec("calc.exe");
    }
}

再写一个序列化的类

package com.darkerbox.deserialization;

import java.io.*;

public class Ser {
    public static void main(String[] args) throws IOException {
        Person person = new Person();
        person.name = "Vicl1fe";
        person.age = "30";
        // 实例化一个对象输出流。
        ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream(new File("ser")));
        // 序列化 person这个对象
        obj.writeObject(person);
        obj.close();

    }
}

再写一个反序列化的类

package com.darkerbox.deserialization;

import java.io.*;

public class Des {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 实例化一个对象输入流。
        ObjectInputStream obj = new ObjectInputStream(new FileInputStream(new File("ser")));
        // 反序列化对象,然后强转为Person对象。
        Person person = (Person)obj.readObject();

        System.out.println(person.getName());
        System.out.println(person.getAge());
        obj.close();

    }
}

可以看到在序列化的时候,实例化了一个Person对象。并且设置Name为Vicl1fe,Age为30。然后序列化这个对象到ser文件中。
使用WinHex打开这个文件,如果文件头是 AC ED 00 05,那么基本上该文件就是序列化后的文件。

然后在反序列化类中定义ObjectInputStream对象,然后使用readObject方法反序列化为Object对象,然后强转为Person对象,就可以得到正确的对象。

简单的反序列化漏洞

我们给Person类添加一个readObject方法。最终如下

package com.darkerbox.deserialization;

import java.io.IOException;
import java.io.Serializable;

public class Person implements Serializable {
    String name;
    String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException, IOException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行命令
        Runtime.getRuntime().exec("calc.exe");
    }
}

这里,我们不需要重新生成序列化文件,可以直接运行反序列化代码。成功弹出计算器。


为什么呢?因为如果实现Serializable的类重写了readObject方法,当反序列化调用readObject方法的时候就会调用重写后的readObject方法。

注意in.defaultReadObject();这里调用的默认的defaultReadObject方法。必须调用这个方法,才可以正确的进行反序列化获取对象的属性值。

反射 什么是反射

Java语言中 一种 动态(运行时)访问、检测 & 修改它本身的能力

Java 类的成员包括以下三类:

  • 属性字段
  • 构造函数
  • 方法

通过反射我们可以实例化任意类的对象。调用任意方法。(个人认为)

例子

正常的实例化对象的方式

// 正常的实例化对象的方式,直接new一个对象。
Person person = new Person();
person.setName("Vicl1fe");
person.setAge("30");

反射获取对象的方式

// 反射获取对象
// 反射获取对象
// 获取Class对象clz
Class clz = Class.forName("com.darkerbox.deserialization.Person");
// 调用getMethod方法可以获取一个'方法对象',参数1是方法名,参数2是该方法的参数的类型。
Method method = clz.getMethod("setName", String.class);
// 调用getConstructor方法来获取'构造函数对象'
Constructor constructor = clz.getConstructor();
// 构造函数对象调用netInstance来实例化一个真正的对象。
Object object = constructor.newInstance();
// '方法对象'调用invoke方法可以执行该方法。参数需要传入一个对象,后面是参数值
method.invoke(object,"30");
// 获取getName方法对象。
Method method1 = clz.getMethod("getName");
// 调用getName方法
System.out.println(method1.invoke(object));

如果我们一开始不知道我们应该实例化什么对象,那么我们就不能使用正常实例化对象的方式来获取对象。

但是可以通过反射的方式来获取对象。只要知道类的全路径名就可以获取该类的所有信息,并且可以实例化该类的对象。

获取反射中的Class对象

在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的 Class 对象。

1.使用Class.forName方法获取,此方法需要知道类的全路径名

Class clz = Class.forName("java.lang.String");

2.使用.class方法

Class clz = String.class;

3.使用类对象的 getClass() 方法。

String str = new String("Hello");
Class clz = str.getClass();
通过反射创建类对象

通过反射创建类对象主要有两种方式:通过 Class 对象的 newInstance() 方法、通过 Constructor 对象的 newInstance() 方法。

1.通过 Class 对象的 newInstance() 方法。

Class clz2 = Person.class;
Person apple = (Person)clz2.newInstance();

2.通过 Constructor 对象的 newInstance() 方法

Class clz3 = Person.class;
Constructor constructor1 = clz.getConstructor();
Person person1 = (Person)constructor1.newInstance();

通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。

调用有参构造函数方法如下

Class clz4 = Person.class;
Constructor constructor4 = clz.getConstructor(String.class, String.class);
Person person4 = (Person)constructor4.newInstance("Vicl1fe", "20");
通过反射获取类属性、方法、构造器

我们通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性。

Class clz6 = Person.class;
Field[] fields = clz6.getFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

而如果使用 Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性:

Class clz6 = Person.class;
Field[] fields = clz6.getDeclaredFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

与获取类属性一样,当我们去获取类方法、类构造器时,则是getMethod、getConstructor,
如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。

参考

https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html

代理 静态代理

这种代理方式需要代理对象和目标对象实现一样的接口。

优点:

  • 可以在不修改目标对象的前提下扩展目标对象的功能。

缺点:

  • 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
  • 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。

举一个例子
首先定义一个接口:

public interface Rental {
    // "卖"的操作
    public void sale();
}

定义一个委托类,这个类主要是为了实现Rental接口,添加具体的对象。

public class Entrust implements Rental {

    // 实现接口"卖"的操作,添加具体的操作对象,最终实现"卖房子"。
    public void sale() {
        System.out.println("卖房子");
    }
}

定义一个代理类

public class AgentRental implements Rental {

    private Rental target; // 被代理对象

    public AgentRental(Rental target) {
        this.target = target;
    }

    public void sale() {
        System.out.println("房子卖500w"); // 添加新的操作
        target.sale(); // 调用Entrust委托类的sale方法
    }
}

最后定义一个测试类

public class main {
    // 静态代理使用示例
    public static void consumer(Rental subject) {
        subject.sale();
    }
    public static void main(String[] args) {
        Rental test = new Entrust();
        System.out.println("---使用代理之前---");
        consumer(test);
        System.out.println("---使用代理之后---");
        consumer(new AgentRental(test));
    }
}

运行结果如下

---使用代理之前---
卖房子
---使用代理之后---
房子卖500w
卖房子

仔细想一下静态代理和没有静态代理的区别。

通过上面的代码,可以看出静态代理的优点,假如有一天你要便宜点,卖400w。不需要修改Entrust类,只需要修改代理类AgentRental即可。不需要修改最底层的代码。

但这个是我们通过代理类进行实现更改的方法,如果当我们需要过多的代理类对委托类进行修改的情况下,则可能出现下图情况:

由此可以我们得知此静态代理的缺点:

当我们的接口类需要增加和删除功能的时候,委托类和代理类都需要更改,不容易维护。

同时如果需要代理多个类的时候,每个委托类都要编写一个代理类,会导致代理类繁多,不好管理。

因为java静态代理是对类进行操作的,我们需要一个个代理类去实现对委托类的更改操作,针对这个情况,我们可以利用动态代理来解决,通过程序运行时自动生成代理类。
------------摘自https://xz.aliyun.com/t/9197#toc-2

相当于Rental每增加一个方法,就会增加至少一个代理类。

动态代理

Java动态代理位于Java.lang.reflect包下,我们一般就仅涉及Java.lang.reflect.Proxy类与InvocationHandler接口,使用其配合反射,完成实现动态代理的操作。

静态代理与动态代理的区别主要在:

  • 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
  • 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中

动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

首先看看InvocationHandler这个接口。

这个接口只有一个invoke方法。其他全是注释。这个接口负责提供调用代理操作。

Proxy类:负责动态构建代理类

接口和委托类就用上面静态代理的例子,这里可以直接写动态代理的代码

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicAgent implements InvocationHandler {
    // target变量为委托类对象
    private Object target;
    public DynamicAgent(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 添加自定义的委托逻辑
        System.out.println("房子卖500w");
        // 调用委托类的方法
        Object result = method.invoke(target,args);
        return result;
    }
    // 实现 java.lang.reflect.InvocationHandler.invoke()方法

}

测试类。

import com.darkerbox.proxy.Entrust;
import com.darkerbox.proxy.Rental;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class main {
    public static void main(String[] args) {
        // 获取委托类的实例对象
        Entrust testEntrust = new Entrust();
        // 获取CLassLoader
        ClassLoader classLoader = testEntrust .getClass().getClassLoader();
        // 获取所有接口
        Class[] interfaces = testEntrust .getClass().getInterfaces();
        // 获取一个调用处理器
        InvocationHandler invocationHandler = new DynamicAgent(testEntrust);
        // 查看生成的代理类
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        // 创建代理对象
        Rental proxy = (Rental) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        // 调用代理对象的sayHello()方法
        proxy.sale();
    }
}

运行结果

房子卖500w
卖房子

一步步分析。
首先在测试类的main方法。实例化了一个委托类的对象。

// 获取委托类的实例对象
Entrust testEntrust = new Entrust();

然后又获取类的CLassLoader和所有接口。

// 获取CLassLoader
ClassLoader classLoader = testEntrust .getClass().getClassLoader();
// 获取所有接口
Class[] interfaces = testEntrust .getClass().getInterfaces();

创建一个调用处理器。里面实现了invoke方法。

// 获取一个调用处理器
InvocationHandler invocationHandler = new DynamicAgent(testEntrust);

设置为true,会在项目目录下生成com.sun.proxy目录及代理类的class文件,

// 查看生成的代理类,
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

如下图

根据classLoader和interfaces和上面创建的调用处理器这三个而实例化一个Proxy代理对象。

// 创建代理对象
Rental proxy = (Rental) Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);

当调用代理对象的任何方法时,都会先调用处理器的invoke方法。

// 调用代理对象的sale方法
proxy.sale();
参考

https://segmentfault.com/a/1190000011291179
https://xz.aliyun.com/t/9197#toc-2

JNDI

等之后单独一篇文章写吧

javassit

Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。

引入包


  org.javassist
  javassist
  3.25.0-GA

编写创建一个Person类

package com.darkerbox.javassit;


import javassist.*;


public class ssit_test {

    
    public static void createPseson() throws Exception {
        ClassPool pool = ClassPool.getDefault();

        // 1. 创建一个空类
        CtClass cc = pool.makeClass("Person");

        // 2. 新增一个字段 private String name;
        // 字段名为name
        CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
        // 访问级别是 private
        param.setModifiers(Modifier.PRIVATE);
        // 初始值是 "xiaoming"
        cc.addField(param, CtField.Initializer.constant("xiaoming"));

        // 3. 生成 getter、setter 方法
        cc.addMethod(CtNewMethod.setter("setName", param));
        cc.addMethod(CtNewMethod.getter("getName", param));

        // 4. 添加无参的构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = "xiaohei";}");
        cc.addConstructor(cons);

        // 5. 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0=this / $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        // 6. 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        ctMethod.setModifiers(Modifier.PUBLIC);
        ctMethod.setBody("{System.out.println(name);}");
        cc.addMethod(ctMethod);

        //这里会将这个创建的类对象编译为.class文件
        cc.writeFile("./");
    }

    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行之后会在项目目录下生成一个class文件。我们来看一下这个文件内容。

public class Person {
    private String name = "xiaoming";

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public Person() {
        this.name = "xiaohei";
    }

    public Person(String var1) {
        this.name = var1;
    }

    public void printName() {
        System.out.println(this.name);
    }
}

将两个代码进行对比,就理解了。

调用生成的类对象

1.通过反射方式调用

 Object person = cc.toClass().newInstance();
 Method setnamme = person.getClass().getMethod("setName",String.class);
 setnamme.invoke(person,"小马");

 Method print = person.getClass().getMethod("printName");
 print.invoke(person);

参考

https://www.cnblogs.com/rickiyang/p/11336268.html

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/644081.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号