JAVA 压缩和序列化
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA 压缩和序列化相关的知识,希望对你有一定的参考价值。
参考技术A压缩和序列化主要用在数据的存储和传输上,二者都是由IO流相关知识实现,这里统一介绍下。
全部章节传送门:
Java I/O类支持读写压缩格式的数据流,你可以用他们对其他的I/O流进行封装,以提供压缩功能。
GZIP接口比较简单,适合对单个数据流进行压缩,在Linux系统中使用较多。
ZIP格式可以压缩多个文件,而且可以和压缩工具进行协作,是经常使用的压缩方法。
JAR(Java Archive,Java 归档文件)是与平台无关的文件格式,它允许将许多文件组合成一个压缩文件。为 J2EE 应用程序创建的 JAR 文件是 EAR 文件(企业 JAR 文件)。
JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。在 JAR 中包含特殊的文件,如 manifests 和部署描述符,用来指示工具如何处理特定的 JAR。
如果一个Web应用程序的目录和文件非常多,那么将这个Web应用程序部署到另一台机器上,就不是很方便了,我们可以将Web应用程序打包成Web 归档(WAR)文件,这个过程和把Java类文件打包成JAR文件的过程类似。利用WAR文件,可以把Servlet类文件和相关的资源集中在一起进行发布。在这个过程中,Web应用程序就不是按照目录层次结构来进行部署了,而是把WAR文件作为部署单元来使用。
一个WAR文件就是一个Web应用程序,建立WAR文件,就是把整个Web应用程序(不包括Web应用程序层次结构的根目录)压缩起来,指定一个.war扩展名。下面我们将第2章的Web应用程序打包成WAR文件,然后发布
要注意的是,虽然WAR文件和JAR文件的文件格式是一样的,并且都是使用jar命令来创建,但就其应用来说,WAR文件和JAR文件是有根本区别的。JAR文件的目的是把类和相关的资源封装到压缩的归档文件中,而对于WAR文件来说,一个WAR文件代表了一个Web应用程序,它可以包含 Servlet、html页面、Java类、图像文件,以及组成Web应用程序的其他资源,而不仅仅是类的归档文件。
在命令行输入jar即可查看jar命令的使用方法。
把对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable的对象才能被序列化。对象序列化包括如下步骤:
对象反序列化的步骤如下:
创建一个可以可以序列化的对象。
然后进行序列化和反序列化测试。
serialVersionUID: 字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。
JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。
为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。
控制序列化字段还可以使用Externalizable接口替代Serializable借口。此时需要定义一个默认构造器,否则将为得到一个异常(java.io.InvalidClassException: Person; Person; no valid constructor);还需要定义两个方法(writeExternal()和readExternal())来控制要序列化的字段。
如下为将Person类修改为使用Externalizable接口。
transient修饰符仅适用于变量,不适用于方法和类。在序列化时,如果我们不想序列化特定变量以满足安全约束,那么我们应该将该变量声明为transient。执行序列化时,JVM会忽略transient变量的原始值并将默认值(引用类型就是null,数字就是0)保存到文件中。因此,transient意味着不要序列化。
静态变量不是对象状态的一部分,因此它不参与序列化。所以将静态变量声明为transient变量是没有用处的。
Java技术专题「序列化系列」深入挖掘FST快速序列化压缩内存的利器的特性和原理
FST的概念和定义
FST序列化全称是Fast Serialization Tool,它是对Java序列化的替换实现。既然前文中提到Java序列化的两点严重不足,在FST中得到了较大的改善,FST的特征如下:
- JDK提供的序列化提升了10倍,体积也减少3-4倍多
- 支持堆外Maps,和堆外Maps的持久化
- 支持序列化为JSON
FST序列化的使用
FST的使用有两种方式,一种是快捷方式,另一种需要使用ObjectOutput和ObjectInput。
直接使用FSTConfiguration提供的序列化和反序列化接口
public static void serialSample()
FSTConfiguration conf = FSTConfiguration.createAndroidDefaultConfiguration();
User object = new User();
object.setName("huaijin");
object.setAge(30);
System.out.println("serialization, " + object);
byte[] bytes = conf.asByteArray(object);
User newObject = (User) conf.asObject(bytes);
System.out.println("deSerialization, " + newObject);
FSTConfiguration也提供了注册对象的Class接口,如果不注册,默认会将对象的Class Name写入。这个提供了易用高效的API方式,不使用ByteArrayOutputStreams而直接得到byte[]。
使用ObjectOutput和ObjectInput,能更细腻控制序列化的写入写出:
static FSTConfiguration conf = FSTConfiguration.createAndroidDefaultConfiguration();
static void writeObject(OutputStream outputStream, User user) throws IOException
FSTObjectOutput out = conf.getObjectOutput(outputStream);
out.writeObject(user);
out.close();
static FstObject readObject(InputStream inputStream) throws Exception
FSTObjectInput input = conf.getObjectInput(inputStream);
User fstObject = (User) input.readObject(User.class);
input.close();
return fstObject;
FST在Dubbo中的应用
-
Dubbo中对FstObjectInput和FstObjectOutput重新包装解决了序列化和反序列化空指针的问题。
-
并且构造了FstFactory工厂类,使用工厂模式生成FstObjectInput和FstObjectOutput。其中同时使用单例模式,控制整个应用中FstConfiguration是单例,并且在初始化时将需要序列化的对象全部注册到FstConfiguration。
-
对外提供了同一的序列化接口FstSerialization,提供serialize和deserialize能力。
FST序列化/反序列化
FST序列化存储格式
基本上所有以Byte形式存储的序列化对象都是类似的存储结构,不管class文件、so文件、dex文件都是类似,这方面没有什么创新的格式,最多是在字段内容上做了一些压缩优化,包括我们最常使用的utf-8编码都是这个做法。
FST的序列化存储和一般的字节格式化存储方案也没有标新立异的地方,比如下面这个FTS的序列化字节文件
00000001: 0001 0f63 6f6d 2e66 7374 2e46 5354 4265
00000010: 616e f701 fc05 7630 7374 7200
格式:
Header|类名长度|类名String|字段1类型(1Byte) | [长度] | 内容|字段2类型(1Byte) | [长度] | 内容|…
- 0000:字节数组类型:00标识OBJECT
- 0001:类名编码,00标识UTF编码,01表示ASCII编码
- 0002:Length of class name (1Byte) = 15
- 0003~0011:Class name string (15Byte)
- 0012:Integer类型标识 0xf7
- 0013:Integer的值=1
- 0014:String类型标识 0xfc
- 0015:String的长度=5
- 0016~001a:String的值"v0str"
- 001b~001c:END
从上面可以看到Integer类型序列化后只占用了一个字节(值等于1),并不像在内存中占用4Byte,所以可以看出是根据一定规则做了压缩,具体代码看FSTObjectInput#instantiateSpecialTag
中对不同类型的读取,FSTObjectInput也定义不同类型对应的枚举值:
public class FSTObjectOutput implements ObjectOutput
private static final FSTLogger LOGGER = FSTLogger.getLogger(FSTObjectOutput.class);
public static Object NULL_PLACEHOLDER = new Object()
public String toString() return "NULL_PLACEHOLDER"; ;
public static final byte SPECIAL_COMPATIBILITY_OBJECT_TAG = -19; // see issue 52
public static final byte ONE_OF = -18;
public static final byte BIG_BOOLEAN_FALSE = -17;
public static final byte BIG_BOOLEAN_TRUE = -16;
public static final byte BIG_LONG = -10;
public static final byte BIG_INT = -9;
public static final byte DIRECT_ARRAY_OBJECT = -8;
public static final byte HANDLE = -7;
public static final byte ENUM = -6;
public static final byte ARRAY = -5;
public static final byte STRING = -4;
public static final byte TYPED = -3; // var class == object written class
public static final byte DIRECT_OBJECT = -2;
public static final byte NULL = -1;
public static final byte OBJECT = 0;
protected FSTEncoder codec;
...
FST序列化和反序列化原理
对Object进行Byte序列化,相当于做了持久化的存储,在反序列的时候,如果Bean的定义发生了改变,那么反序列化器就要做兼容的解决方案,我们知道对于JDK的序列化和反序列,serialVersionUID对版本控制起了很重要的作用。FST对这个问题的解决方案是通过@Version注解进行排序。
在进行反序列操作的时候,FST会先反射或者对象Class的所有成员,并对这些成员进行了排序,这个排序对兼容起了关键作用,也就是@Version的原理。在FSTClazzInfo中定义了一个defFieldComparator比较器,用于对Bean的所有Field进行排序:
public final class FSTClazzInfo
public static final Comparator<FSTFieldInfo> defFieldComparator = new Comparator<FSTFieldInfo>()
@Override
public int compare(FSTFieldInfo o1, FSTFieldInfo o2)
int res = 0;
if ( o1.getVersion() != o2.getVersion() )
return o1.getVersion() < o2.getVersion() ? -1 : 1;
// order: version, boolean, primitives, conditionals, object references
if (o1.getType() == boolean.class && o2.getType() != boolean.class)
return -1;
if (o1.getType() != boolean.class && o2.getType() == boolean.class)
return 1;
if (o1.isConditional() && !o2.isConditional())
res = 1;
else if (!o1.isConditional() && o2.isConditional())
res = -1;
else if (o1.isPrimitive() && !o2.isPrimitive())
res = -1;
else if (!o1.isPrimitive() && o2.isPrimitive())
res = 1;
// if (res == 0) // 64 bit / 32 bit issues
// res = (int) (o1.getMemOffset() - o2.getMemOffset());
if (res == 0)
res = o1.getType().getSimpleName().compareTo(o2.getType().getSimpleName());
if (res == 0)
res = o1.getName().compareTo(o2.getName());
if (res == 0)
return o1.getField().getDeclaringClass().getName().compareTo(o2.getField().getDeclaringClass().getName());
return res;
;
...
从代码实现上可以看到,比较的优先级是Field的Version大小,然后是Field类型,所以总的来说Version越大排序越靠后,至于为什么要排序,看下FSTObjectInput#instantiateAndReadNoSer方法
public class FSTObjectInput implements ObjectInput
protected Object instantiateAndReadNoSer(Class c, FSTClazzInfo clzSerInfo, FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws Exception
Object newObj;
newObj = clzSerInfo.newInstance(getCodec().isMapBased());
...
else
FSTClazzInfo.FSTFieldInfo[] fieldInfo = clzSerInfo.getFieldInfo();
readObjectFields(referencee, clzSerInfo, fieldInfo, newObj,0,0);
return newObj;
protected void readObjectFields(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, FSTClazzInfo.FSTFieldInfo[] fieldInfo, Object newObj, int startIndex, int version) throws Exception
if ( getCodec().isMapBased() )
readFieldsMapBased(referencee, serializationInfo, newObj);
if ( version >= 0 && newObj instanceof Unknown == false)
getCodec().readObjectEnd();
return;
if ( version < 0 )
version = 0;
int booleanMask = 0;
int boolcount = 8;
final int length = fieldInfo.length;
int conditional = 0;
for (int i = startIndex; i < length; i++) // 注意这里的循环
try
FSTClazzInfo.FSTFieldInfo subInfo = fieldInfo[i];
if (subInfo.getVersion() > version ) // 需要进入下一个版本的迭代
int nextVersion = getCodec().readVersionTag(); // 对象流的下一个版本
if ( nextVersion == 0 ) // old object read
oldVersionRead(newObj);
return;
if ( nextVersion != subInfo.getVersion() ) // 同一个Field的版本不允许变,并且版本变更和流的版本保持同步
throw new RuntimeException("read version tag "+nextVersion+" fieldInfo has "+subInfo.getVersion());
readObjectFields(referencee,serializationInfo,fieldInfo,newObj,i,nextVersion); // 开始下一个Version的递归
return;
if (subInfo.isPrimitive())
...
else
if ( subInfo.isConditional() )
...
// object 把读出来的值保存到FSTFieldInfo中
Object subObject = readObjectWithHeader(subInfo);
subInfo.setObjectValue(newObj, subObject);
...
从这段代码的逻辑基本就可以知道FST的序列化和反序列化兼容的原理了,注意里面的循环,正是按照排序后的Filed进行循环,而每个FSTFieldInfo都记录自己在对象流中的位置、类型等详细信息:
序列化:
- 按照Version对Bean的所有Field进行排序(不包括static和transient修饰的member),没有@Version注解的Field默认version=0;如果version相同,按照version, boolean, primitives, conditionals, object references排序
- 按照排序的Field把Bean的Field逐个写到输出流
- @Version的版本只能加不能减小,如果相等的话,有可能因为默认的排序规则,导致流中的Filed顺序和内存中的FSTFieldInfo[]数组的顺序不一致,而注入错误
反序列化:
- 反序列化按照对象流的格式进行解析,对象流中保存的Field顺序和内存中的FSTFieldInfo顺序保持一致
- 相同版本的Field在对象流中存在,在内存Bean中缺失:可能抛异常(会有后向兼容问题)
- 对象流中包含内存Bean中没有的高版本Field:正常(老版本兼容新)
- 相同版本的Field在对象流中缺失,在内存Bean中存在:抛出异常
- 相同的Field在对象流和内存Bean中的版本不一致:抛出异常
- 内存Bean增加了不高于最大版本的Field:抛出异常
所以从上面的代码逻辑就可以分析出这个使用规则:@Version的使用原则就是,每新增一个Field,就对应的加上@Version注解,并且把version的值设置为当前版本的最大值加一,不允许删除Field
另外再看一下@Version注解的注释:明确说明了用于后向兼容
package org.nustaq.serialization.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
/**
* support for adding fields without breaking compatibility to old streams.
* For each release of your app increment the version value. No Version annotation means version=0.
* Note that each added field needs to be annotated.
*
* e.g.
*
* class MyClass implements Serializable
*
* // fields on initial release 1.0
* int x;
* String y;
*
* // fields added with release 1.5
* @Version(1) String added;
* @Version(1) String alsoAdded;
*
* // fields added with release 2.0
* @Version(2) String addedv2;
* @Version(2) String alsoAddedv2;
*
*
*
* If an old class is read, new fields will be set to default values. You can register a VersionConflictListener
* at FSTObjectInput in order to fill in defaults for new fields.
*
* Notes/Limits:
* - Removing fields will break backward compatibility. You can only Add new fields.
* - Can slow down serialization over time (if many versions)
* - does not work for Externalizable or Classes which make use of JDK-special features such as readObject/writeObject
* (AKA does not work if fst has to fall back to 'compatible mode' for an object).
* - in case you use custom serializers, your custom serializer has to handle versioning
*
*/
public @interface Version
byte value();
public class FSTBean implements Serializable
/** serialVersionUID */
private static final long serialVersionUID = -2708653783151699375L;
private Integer v0int
private String v0str;
准备序列化和反序列化方法
public class FSTSerial
private static void serialize(FstSerializer fst, String fileName)
try
FSTBean fstBean = new FSTBean();
fstBean.setV0int(1);
fstBean.setV0str("v0str");
byte[] v1 = fst.serialize(fstBean);
FileOutputStream fos = new FileOutputStream(new File("byte.bin"));
fos.write(v1, 0, v1.length);
fos.close();
catch (Exception e)
e.printStackTrace();
private static void deserilize(FstSerializer fst, String fileName)
try
FileInputStream fis = new FileInputStream(new File("byte.bin"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[256];
int length = 0;
while ((length = fis.read(buf)) > 0)
baos.write(buf, 0, length);
fis.close();
buf = baos.toByteArray();
FSTBean deserial = fst.deserialize(buf, FSTBean.class);
System.out.println(deserial);
System.out.println(deserial);
catch (Exception e)
e.printStackTrace();
public static void main(String[] args)
FstSerializer fst = new FstSerializer();
serialize(fst, "byte.bin");
deserilize(fst, "byte.bin");
参考资料
https://github.com/RuedigerMoeller/fast-serialization
以上是关于JAVA 压缩和序列化的主要内容,如果未能解决你的问题,请参考以下文章
Java技术专题「性能优化系列」针对Java对象压缩及序列化技术的探索之路