Java反序列化基础

#基本概念

序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中,也便于进行网络传输。同时可以在另一个程序中对字节流数据进行反序列化恢复对象,以此实现多平台之间的通信、对象持久化存储。

  • 序列化

    把Java对象转换为字节序列的过程。ObjectOutputStream类的writeObject()方法可以实现序列化,它负责把一个Java对象写入一个字节流。

    1. 实现java.io.Serializable接口的类才可被序列化,且所有属性必须是可序列化的。(用transient或static关键字修饰的属性不能序列化)

    2. Serializable接口还有一个比较常见的子类Externalizable,它需要自己实现读写方法readExternal()writeExternal()

    序列化数据的分析(包含SUID计算)

  • 反序列化

    把字节序列恢复为Java对象的过程。ObjectInputStream类的readObject()方法用于反序列化。

    Tips: 反序列化需要被反序列化的类必须存在且serialVersionUID值必须一致。序列化类的serialVersionUID静态变量用于标识Java类的序列化"版本"。如果序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认serialVersionUID值。

    通过分析跟踪源码发现,在反序列化过程中只通过反射创建反序列化的类的对象以及对该对象属性进行赋值操作,不调用对象的任何方法(包括构造方法,重写的readObject()方法除外)。如果该对象的属性的值是一个类的话,要求这个类必须是可序列化的(实现了Serializable接口),因为它会在给这个属性赋值的时候再次对该类进行反序列化。

    1
    2
    3
    4
    5
    6
    7
    
    ObjectInputStream.readObject
    ObjectInputStream.readUnshared
    XMLDecoder.readObject
    Yaml.load
    XStream.fromXML
    ObjectMapper.readValue
    JSON.parseObject
    

    解释一下重点部分:

    1. readNonProxyDesc()中调用resolveClass()函数根据需要反序列化的类的名字获取它的Class对象。很多防御方法就是在resolveClass方法中实现对反序列化类的校验。
    2. 在readOrdinaryObject()函数中,执行完readClassDesc()函数继续往下走,会调用readSerialDate()函数对对象属性进行赋值,并在此判断反序列化的类是否重写了readObject()函数,重写了便会通过反射调用该函数。否则调用defaultReadFields()对属性赋值,然后return。

#示例

  1. User类

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    package test1;
    
    import java.io.Serializable;
    
    class User implements Serializable{
        private String name;
        public void setName(String name) {
            this.name=name;
        }
        public String getName() {
            return name;
        }
    }
    
  2. 序列化User类

     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 test1;
    
    import java.io.*;
    
    public class serialize {
        public static void main(String[] args) throws Exception {
            User user=new User();
            user.setName("gogo");
    
            byte[] serializeData=serialize(user);
            FileOutputStream fout = new FileOutputStream("user.bin");
            fout.write(serializeData);
            fout.close();
            User user2=(User) unserialize(serializeData);
            System.out.println(user2);
        }
    
    //序列化
        public static byte[] serialize(final Object obj) throws Exception {
            ByteArrayOutputStream btout = new ByteArrayOutputStream();
            ObjectOutputStream objOut = new ObjectOutputStream(btout); //把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流
            objOut.writeObject(obj); // 通过writeObject,将obj对象进行序列化到了btout里
            return btout.toByteArray();
        }
    
    //反序列化
        public static Object unserialize(final byte[] serialized) throws Exception {
            ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
            ObjectInputStream objIn = new ObjectInputStream(btin); // ObjectInputStream负责从一个字节流读取Java对象
            return objIn.readObject();// 通过ObjectInputStream的readObject方法直接读取对象了
        }
    

    运行结果(Java序列化数据标志: aced 0005开头)

    序列化数据信息是将对象信息按照一定规则组成的,根据这个规则也可以逆向推测出数据信息中的数据类型等信息。可使用该工具分析: SerializationDumper

#Java 反射

反射定义: 对于任意一个类,都能够得到这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能称为Java反射机制。

#Class对象

每个类被加载进入内存之后,系统就会为该类生成一个对应的java.lang.Class对象,通过该Class对象就可以访问到JVM中的这个类。这个对象包含了完整的类的结构、方法和属性等信息。

  • Class对象表示正在运行的Java应用程序中的类和接口。
  • Class对象没有公共构造方法,由Java虚拟机自动构造。
  • Class对象用于提供类本身的信息,比如有几种构造方法、普通方法和有多少属性。

获取方法

  • 获取Runtime类的Class对象

    1
    2
    3
    4
    5
    6
    7
    8
    
    //通过class的完整类名使用forName获取
    Class  runtimeClass1 = Class.forName("java.lang.Runtime");
    //通过静态变量class获取
    Class  runtimeClass2 = java.lang.Runtime.class;
    //通过Classloader获取
    Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");
    //通过对象的getClass()获取
    Class  runtimeClass4 = java.lang.Runtime.getRuntime().getClass();
    
    1. Class.forName("类名字符串")装入类并初始化被加载类的静态属性和方法,返回Class的对象。不希望初始化类可以使用Class.forName("类名",是否初始化类,类加载器)
    2. 类名.class返回Class的对象。JVM将使用类装载器,将类装入内存(前提是类还没有装入内存),不做类的初始化工作。
    3. 实例对象.getClass()返回运行时真正所指的对象所属类的Class的对象。 对类进行静态初始化、非静态初始化。
  • 获取数组类型的Class对象需要使用Java类型的描述符方式

    1
    2
    
    Class<?> doubleArray = Class.forName("[D"); //相当于double[].class
    Class<?> cStringArray = Class.forName("[[Ljava.lang.String;"); //相当于String[][].class
    

#反射调用

  1. 调用构造函数

    反射需要先拿到类对象,通过类对象获取构造器对象,再通过构造器对象创建一个实例。

    1
    2
    3
    4
    5
    6
    7
    
    public class serialize {
    public static void main(String[] args) throws Exception {
       Class UserClass=Class.forName("test1.User");
       Constructor constructor=UserClass.getConstructor(String.class);
       User user=(User) constructor.newInstance("gogo");
       System.out.println(user.getName());
    }
    

    Tips: 反射调用内部类的时候需要使用$来代替.。如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成: com.anbai.Test$Hello

  2. 调用方法

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    public class serialize {
        public static void main(String[] args) throws Exception {
            Class UserClass=Class.forName("test1.User");
            Constructor constructor=UserClass.getConstructor(String.class);
            User user=(User) constructor.newInstance("gogo");
            Method method = UserClass.getDeclaredMethod("setName", String.class);
            //如果有私有方法,可以通过setAccessible来获取访问权限
            method.setAccessible(true);
            method.invoke(user, "ada");
            System.out.println(user.getName());
        }
    }
    

    getMethodgetDeclaredMethod区别:

    1. getMethod只能获取到当前类和父类的所有有权限的方法 (public)。

    2. getDeclaredMethod能获取到当前类的所有成员方法 (不包含父类)。

    method.invoke()第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null。因为在Java中调用静态方法可以直接类名.方法名()方式调用。

    它的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型。(多个参数值用","隔开)

  3. 访问属性

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    public class serialize {
        public static void main(String[] args) throws Exception {
            Class UserClass=Class.forName("test1.User");
            Constructor constructor=UserClass.getConstructor(String.class);
            User user=(User) constructor.newInstance("gogo");
            Field field= UserClass.getDeclaredField("name");
            field.setAccessible(true);// name是私有属性,需要先设置可访问
                 field.get(user); //通过反射获取属性
            field.set(user, "12131");
            System.out.println(user.getName());
    }
    

    修改final关键字修饰的成员变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     // 反射获取Field类的modifiers(字段的修饰符)
     Field modifiers = field.getClass().getDeclaredField("modifiers");
     // 设置modifiers修改权限
     modifiers.setAccessible(true);
     // 修改成员变量的Field对象的modifiers值
     //getModifiers() 返回字段的修饰符,它是一个int,不同的bit表示不同的含义。
     modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
     // 修改成员变量值
     field.set(类实例对象, 修改后的值);
    

#Java 动态代理

介绍动态代理之前先来了解一下静态代理,帮助理解代理的作用。

#静态代理

如果需要对类增强一些功能。直接在原始代码的基础上直接修改容易出错。这时可以写一个代理类,引入之前的类然后在代理类中添加需要的方法。

UserServices 接口与实现类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//用户接口
public interface UserServices {
    void getName(String name);
}
//一个实现类UserRealm
public class UserRealm implements UserServices{
  public void getName(String name){
     System.out.println(name);
  }
}

代理类实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//UserRealmProxy 实现UserServices接口
class UserRealmProxy implements UserServices{
  private UserServices userservices;

  public UserRealmProxy(UserServices userservices){
    this.userservices = uservices;
  }
//重写getName函数,在原本输出名字的基础上增加功能
  @Override
  public void getName(String name){
    System.out.println("UserRealmProxy Start....");
    this.userservices.getName();
    System.out.println("UserRealmProxy End....")
  }
}

Main函数

1
2
3
4
5
6
7
class Main{
    public static void main(String[] args) {
        //实现一个类实例userservices
        UserServices userServices = new UserRealm();
        UserRealmProxy proxy = new UserRealmProxy(userServices);
        proxy.getName("xxxx");
    }

这样就能实现在不改动原先类的基础上,对功能方法进行扩展。但是静态代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。由此提出了动态代理的概念。

#动态代理

动态代理就是利用反射机制在运行时创建代理类。

创建动态代理类会使用到java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。java.lang.reflect.Proxy用于生成动态代理类Class、创建代理类实例。(该类实现了java.io.Serializable接口)

java.lang.reflect.InvocationHandler接口用于调用Proxy类生成的代理类方法,动态代理类只有一个invoke方法。

动态代理的实现

 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
interface UserServices{
    public void getName(String name);
}
//写一个类实现UserServices接口,后面对该方法进行扩展
class UserReal implements UserServices{
    @Override
    public void getName(String name){
        System.out.println(name);
    }
}

//对功能进行扩展的类(必须要实现InvocationHandler接口)
class Delegate implements InvocationHandler{
    private Object object;
    public Delegate(Object object){
        this.object = object;
    }
   //重写InvocationHandler接口中的方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Delegate Start.....");
        Object res = method.invoke(object,args);
        System.out.println("Delegate End.....");
        return res;
    }
}

Main函数执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class DynamicTest {
    public static void main(String[] args) {
        UserReal user = new UserReal();
        Delegate delegate = new Delegate(user);
				//指定动态代理类的类加载器,即生成完成后的代理对象的类加载器
				//定义动态代理生成的类实现的接口,需要被增强的接口列表(数据)
				//动态代理的处理类,增强逻辑
        UserServices userService1 = (UserServices) Proxy.newProxyInstance(UserServices.class.getClassLoader(), new Class[] {UserServices.class}, delegate);//生成动态代理类
				//这样可以给UserReal类对UserServices接口的实现所有方法进行功能扩展
        userService1.getName("xn");
    }
}

使用Proxy.newProxyInstance来创建动态代理类实例,或使用Proxy.getProxyClass()获取代理类对象再反射的方式来创建。

  1. 动态代理的必须是接口类,通过生成UserServices接口的实现类来代理接口的方法调用。
  2. 动态代理类会由java.lang.reflect.Proxy.ProxyClassFactory创建。ProxyClassFactory会调用sun.misc.ProxyGenerator类生成该类的字节码,并调用java.lang.reflect.Proxy.defineClass0()方法将该类注册到JVM。

#动态代理生成的$ProxyX类分析

java.lang.reflect.Proxy类是通过创建一个新的Java类(类名为com.sun.proxy.$ProxyX) 的方式来实现类方法代理功能的。

动态代理生成出来的com.sun.proxy.$ProxyX类有如下技术细节和特性:

  1. 该类继承于java.lang.reflect.Proxy并实现了被代理的接口类(UserServices),并通过ProxyGenerator动态生成接口类的所有方法。通过调用动态代理处理类 (Delegate) 的invoke方法获取执行结果。因为java.lang.reflect.Proxy类实现了java.io.Serializable接口,所以该类支持序列化。

  2. 该类重写了java.lang.Object类的toString、hashCode、equals方法。

  3. 如果生成了多个动态代理类,新生成的类名中的0会自增: com.sun.proxy.$Proxy0/$Proxy1/$Proxy2

  4. 动态代理类符合Java对象序列化条件,它生成的类在反序列化/反序列化时不会序列化该类的成员变量,并且serialVersionUID为0L,也将是说将该类的Class对象传递给java.io.ObjectStreamClass的静态lookup方法时,返回的ObjectStreamClass实例将具有以下特性:

    1. 调用其 getSerialVersionUID 方法将返回0L。
    2. 调用其 getFields 方法将返回长度为零的数组。
    3. 调用其 getField 方法将返回null。

    示例:

#参考

  1. Java 动态代理
  2. Java 序列化/反序列化_「Java Web安全」
  3. 序列化之Java默认序列化技术ObjectOutputStream与ObjectInputStream
加载评论