#基本概念
序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中,也便于进行网络传输。同时可以在另一个程序中对字节流数据进行反序列化恢复对象,以此实现多平台之间的通信、对象持久化存储。
-
序列化
把Java对象转换为字节序列的过程。ObjectOutputStream类的writeObject()方法可以实现序列化,它负责把一个Java对象写入一个字节流。
-
实现
java.io.Serializable
接口的类才可被序列化,且所有属性必须是可序列化的。(用transient或static关键字修饰的属性不能序列化) -
Serializable接口还有一个比较常见的子类Externalizable,它需要自己实现读写方法
readExternal()
和writeExternal()
。
-
-
反序列化
把字节序列恢复为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
解释一下重点部分:
- readNonProxyDesc()中调用resolveClass()函数根据需要反序列化的类的名字获取它的Class对象。很多防御方法就是在resolveClass方法中实现对反序列化类的校验。
- 在readOrdinaryObject()函数中,执行完readClassDesc()函数继续往下走,会调用readSerialDate()函数对对象属性进行赋值,并在此判断反序列化的类是否重写了readObject()函数,重写了便会通过反射调用该函数。否则调用defaultReadFields()对属性赋值,然后return。
#示例
-
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; } }
-
序列化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();
Class.forName("类名字符串")
装入类并初始化被加载类的静态属性和方法,返回Class的对象。不希望初始化类可以使用Class.forName("类名",是否初始化类,类加载器)
类名.class
返回Class的对象。JVM将使用类装载器,将类装入内存(前提是类还没有装入内存),不做类的初始化工作。实例对象.getClass()
返回运行时真正所指的对象所属类的Class的对象。 对类进行静态初始化、非静态初始化。
-
获取数组类型的Class对象需要使用Java类型的描述符方式
1 2
Class<?> doubleArray = Class.forName("[D"); //相当于double[].class Class<?> cStringArray = Class.forName("[[Ljava.lang.String;"); //相当于String[][].class
#反射调用
-
调用构造函数
反射需要先拿到类对象,通过类对象获取构造器对象,再通过构造器对象创建一个实例。
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
-
调用方法
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()); } }
getMethod
和getDeclaredMethod
区别:-
getMethod只能获取到当前类和父类的所有有权限的方法 (public)。
-
getDeclaredMethod能获取到当前类的所有成员方法 (不包含父类)。
method.invoke()
第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null。因为在Java中调用静态方法可以直接类名.方法名()
方式调用。它的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型。(多个参数值用","隔开)
-
-
访问属性
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 接口与实现类
|
|
代理类实现
|
|
Main函数
|
|
这样就能实现在不改动原先类的基础上,对功能方法进行扩展。但是静态代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。由此提出了动态代理的概念。
#动态代理
动态代理就是利用反射机制在运行时创建代理类。
创建动态代理类会使用到java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口。java.lang.reflect.Proxy
用于生成动态代理类Class、创建代理类实例。(该类实现了java.io.Serializable接口)
java.lang.reflect.InvocationHandler
接口用于调用Proxy类生成的代理类方法,动态代理类只有一个invoke方法。
动态代理的实现
|
|
Main函数执行
|
|
使用Proxy.newProxyInstance
来创建动态代理类实例,或使用Proxy.getProxyClass()
获取代理类对象再反射的方式来创建。
- 动态代理的必须是接口类,通过生成UserServices接口的实现类来代理接口的方法调用。
- 动态代理类会由
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
类有如下技术细节和特性:
-
该类继承于
java.lang.reflect.Proxy
并实现了被代理的接口类(UserServices),并通过ProxyGenerator动态生成接口类的所有方法。通过调用动态代理处理类 (Delegate) 的invoke方法获取执行结果。因为java.lang.reflect.Proxy类实现了java.io.Serializable接口,所以该类支持序列化。 -
该类重写了java.lang.Object类的toString、hashCode、equals方法。
-
如果生成了多个动态代理类,新生成的类名中的0会自增:
com.sun.proxy.$Proxy0/$Proxy1/$Proxy2
-
动态代理类符合Java对象序列化条件,它生成的类在反序列化/反序列化时不会序列化该类的成员变量,并且serialVersionUID为0L,也将是说将该类的Class对象传递给java.io.ObjectStreamClass的静态lookup方法时,返回的ObjectStreamClass实例将具有以下特性:
- 调用其 getSerialVersionUID 方法将返回0L。
- 调用其 getFields 方法将返回长度为零的数组。
- 调用其 getField 方法将返回null。
示例: