简介
咱们在程序中除了应用罕用的字符串进行数据传递之外,应用最多的还是JAVA对象。在JDK中,对象如果须要在网络中传输,必须实现Serializable接口,示意这个对象是能够被序列化的。这样就能够调用JDK本身的对象对象办法,进行对象的读写。
那么在netty中进行对象的传递可不可以间接应用JDK的对象序列化办法呢?如果不能的话,又应该怎么解决呢?
明天带大家来看看netty中提供的对象编码器。
什么是序列化
序列化就是将java对象依照肯定的程序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的java对象。
所以序列化的目标就是为了传输对象,对于一些简单的对象,咱们能够应用第三方的优良框架,比方Thrift,Protocol Buffer等,应用起来十分的不便。
JDK自身也提供了序列化的性能。要让一个对象可序列化,则能够实现java.io.Serializable接口。
java.io.Serializable是从JDK1.1开始就有的接口,它实际上是一个marker interface,因为java.io.Serializable并没有须要实现的接口。继承java.io.Serializable就表明这个class对象是能够被序列化的。
@Data@AllArgsConstructorpublic class CustUser implements java.io.Serializable{ private static final long serialVersionUID = -178469307574906636L; private String name; private String address;}
下面咱们定义了一个CustUser可序列化对象。这个对象有两个属性:name和address。
接下看下怎么序列化和反序列化:
public void testCusUser() throws IOException, ClassNotFoundException { CustUser custUserA=new CustUser("jack","www.flydean.com"); CustUser custUserB=new CustUser("mark","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); objectOutputStream.writeObject(custUserB); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUser custUser1 = (CustUser) objectInputStream.readObject(); CustUser custUser2 = (CustUser) objectInputStream.readObject(); log.info("{}",custUser1); log.info("{}",custUser2); } }
下面的例子中,咱们实例化了两个CustUser对象,并应用objectOutputStream将对象写入文件中,最初应用ObjectInputStream从文件中读取对象。
下面是最根本的应用。须要留神的是CustUser class中有一个serialVersionUID字段。
serialVersionUID是序列化对象的惟一标记,如果class中定义的serialVersionUID和序列化存储中的serialVersionUID统一,则表明这两个对象是一个对象,咱们能够将存储的对象反序列化。
如果咱们没有显示的定义serialVersionUID,则JVM会主动依据class中的字段,办法等信息生成。很多时候我在看代码的时候,发现很多人都将serialVersionUID设置为1L,这样做是不对的,因为他们没有了解serialVersionUID的真正含意。
重构序列化对象
如果咱们有一个序列化的对象正在应用了,然而忽然咱们发现这个对象如同少了一个字段,要把他加上去,可不可以加呢?加上去之后原序列化过的对象能不能转换成这个新的对象呢?
答案是必定的,前提是两个版本的serialVersionUID必须一样。新加的字段在反序列化之后是空值。
序列化不是加密
有很多同学在应用序列化的过程中可能会这样想,序列化曾经将对象变成了二进制文件,是不是说该对象曾经被加密了呢?
这其实是序列化的一个误区,序列化并不是加密,因为即便你序列化了,还是能从序列化之后的数据中晓得你的类的构造。比方在RMI近程调用的环境中,即便是class中的private字段也是能够从stream流中解析进去的。
如果咱们想在序列化的时候对某些字段进行加密操作该怎么办呢?
这时候能够思考在序列化对象中增加writeObject和readObject办法:
private String name; private String address; private int age; private void writeObject(ObjectOutputStream stream) throws IOException { //给age加密 age = age + 2; log.info("age is {}", age); stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); log.info("age is {}", age); //给age解密 age = age - 2; }
下面的例子中,咱们为CustUser增加了一个age对象,并在writeObject中对age进行了加密(加2),在readObject中对age进行了解密(减2)。
留神,writeObject和readObject都是private void的办法。他们的调用是通过反射来实现的。
应用真正的加密
下面的例子, 咱们只是对age字段进行了加密,如果咱们想对整个对象进行加密有没有什么好的解决方法呢?
JDK为咱们提供了javax.crypto.SealedObject 和java.security.SignedObject来作为对序列化对象的封装。从而将整个序列化对象进行了加密。
还是举个例子:
public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { CustUser custUserA=new CustUser("jack","www.flydean.com"); Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES"); IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes()); enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv); SealedObject sealedObject= new SealedObject(custUserA, enCipher); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(sealedObject); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); SealedObject custUser1 = (SealedObject) objectInputStream.readObject(); CustUser custUserV2= (CustUser) custUser1.getObject(deCipher); log.info("{}",custUserV2); } }
下面的例子中,咱们构建了一个SealedObject对象和相应的加密解密算法。
SealedObject就像是一个代理,咱们写入和读取的都是这个代理的加密对象。从而保障了在数据传输过程中的安全性。
应用代理
下面的SealedObject实际上就是一种代理,思考这样一种状况,如果class中的字段比拟多,而这些字段都能够从其中的某一个字段中主动生成,那么咱们其实并不需要序列化所有的字段,咱们只把那一个字段序列化就能够了,其余的字段能够从该字段衍生失去。
在这个案例中,咱们就须要用到序列化对象的代理性能。
首先,序列化对象须要实现writeReplace办法,示意替换成真正想要写入的对象:
public class CustUserV3 implements java.io.Serializable{ private String name; private String address; private Object writeReplace() throws java.io.ObjectStreamException { log.info("writeReplace {}",this); return new CustUserV3Proxy(this); }}
而后在Proxy对象中,须要实现readResolve办法,用于从系列化过的数据中重构序列化对象。如下所示:
public class CustUserV3Proxy implements java.io.Serializable{ private String data; public CustUserV3Proxy(CustUserV3 custUserV3){ data =custUserV3.getName()+ "," + custUserV3.getAddress(); } private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); CustUserV3 result = new CustUserV3(pieces[0], pieces[1]); log.info("readResolve {}",result); return result; }}
咱们看下怎么应用:
public void testCusUserV3() throws IOException, ClassNotFoundException { CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com"); try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(custUserA); } try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject(); log.info("{}",custUser1); } }
留神,咱们写入和读出的都是CustUserV3对象。
Serializable和Externalizable的区别
最初咱们讲下Externalizable和Serializable的区别。Externalizable继承自Serializable,它须要实现两个办法:
void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
什么时候须要用到writeExternal和readExternal呢?
应用Serializable,Java会主动为类的对象和字段进行对象序列化,可能会占用更多空间。而Externalizable则齐全须要咱们本人来管制如何写/读,比拟麻烦,然而如果思考性能的话,则能够应用Externalizable。
另外Serializable进行反序列化不须要执行构造函数。而Externalizable须要执行构造函数结构出对象,而后调用readExternal办法来填充对象。所以Externalizable的对象须要一个无参的构造函数。
netty中对象的传输
在下面的序列化一节中,咱们曾经晓得了对于定义好的JAVA对象,咱们能够通过应用ObjectOutputStream和ObjectInputStream来实现对象的读写工作,那么在netty中是否也能够应用同样的形式来进行对象的读写呢?
很遗憾的是,在netty中并不能间接应用JDK中的对象读写办法,咱们须要对其进行革新。
这是因为咱们须要一个通用的对象编码和解码器,如果应用ObjectOutputStream和ObjectInputStream,因为不同对象的构造是不一样的,所以咱们在读取对象的时候须要晓得读取数据的对象类型能力进行完满的转换。
而在netty中咱们须要的是一种更加通用的编码解码器,那么应该怎么做呢?
还记得之前咱们在解说通用的frame decoder中讲过的LengthFieldBasedFrameDecoder? 通过在实在的数据后面加上数据的长度,从而达到依据数据长度进行frame辨别的目标。
netty中提供的编码解码器名字叫做ObjectEncoder和ObjectDecoder,先来看下他们的定义:
public class ObjectEncoder extends MessageToByteEncoder<Serializable> {
public class ObjectDecoder extends LengthFieldBasedFrameDecoder {
能够看到ObjectEncoder继承自MessageToByteEncoder,其中的泛型是Serializable,示意encoder是从可序列化的对象encode成为ByteBuf。
而ObjectDecoder正如下面咱们所说的继承自LengthFieldBasedFrameDecoder,所以能够通过一个长度字段来辨别理论要读取对象的长度。
接下来咱们具体理解一下这两个类是如何工作的。
ObjectEncoder
先来看ObjectEncoder是如何将一个对象序列化成为ByteBuf的。
依据LengthFieldBasedFrameDecoder的定义,咱们须要一个数组来保留实在数据的长度,这里应用的是一个4字节的byte数组叫做LENGTH_PLACEHOLDER,如下所示:
private static final byte[] LENGTH_PLACEHOLDER = new byte[4];
咱们看下它的encode办法的实现:
protected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception { int startIdx = out.writerIndex(); ByteBufOutputStream bout = new ByteBufOutputStream(out); ObjectOutputStream oout = null; try { bout.write(LENGTH_PLACEHOLDER); oout = new CompactObjectOutputStream(bout); oout.writeObject(msg); oout.flush(); } finally { if (oout != null) { oout.close(); } else { bout.close(); } } int endIdx = out.writerIndex(); out.setInt(startIdx, endIdx - startIdx - 4); }
这里首先创立了一个ByteBufOutputStream,而后向这个Stream中写入4字节的长度字段,接着将ByteBufOutputStream封装到CompactObjectOutputStream中。
CompactObjectOutputStream是ObjectOutputStream的子类,它重写了writeStreamHeader和writeClassDescriptor两个办法。
CompactObjectOutputStream将最终的数据msg写入流中,一个encode的过程就差不多实现了。
为什么说差不多实现了呢?因为长度字段还是空的。
在最开始的时候,咱们只是写入了一个长度的placeholder,这个placeholder是空的,并没有任何数据,这个数据是在最初一步out.setInt中写入的:
out.setInt(startIdx, endIdx - startIdx - 4);
这种实现也给了咱们一种思路,在咱们还不晓得音讯的实在长度的时候,如果心愿在音讯之前写入音讯的长度,能够先占个地位,等音讯全副读取结束,晓得实在的长度之后,再替换数据。
到此,对象数据曾经全副编码结束,接下来咱们看一下如何从编码过后的数据中读取对象。
ObjectDecoder
之前说过了ObjectDecoder继承自LengthFieldBasedFrameDecoder,它的decode办法是这样的:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { ByteBuf frame = (ByteBuf) super.decode(ctx, in); if (frame == null) { return null; } ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver); try { return ois.readObject(); } finally { ois.close(); } }
首先调用LengthFieldBasedFrameDecoder的decode办法,依据对象的长度,读取到实在的对象数据放到ByteBuf中。
而后通过自定义的CompactObjectInputStream从ByteBuf中读取到实在的对象,并返回。
CompactObjectInputStream继承自ObjectInputStream,是和CompactObjectOutputStream相同的操作。
ObjectEncoderOutputStream和ObjectDecoderInputStream
ObjectEncoder和ObjectDecoder是对象和ByteBuf之间的转换,netty还提供了和ObjectEncoder,ObjectDecoder兼容的ObjectEncoderOutputStream和ObjectDecoderInputStream,这两个类能够从stream中对对象编码和解码,并且和ObjectEncoder,ObjectDecoder齐全兼容的。
总结
以上就是netty中提供的对象编码和解码器,大家如果心愿在netty中传递对象,那么netty提供的这两个编码解码器是最好的抉择。
本文已收录于 http://www.flydean.com/14-8-netty-codec-object/
最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
欢送关注我的公众号:「程序那些事」,懂技术,更懂你!