当前位置:Java -> EclipseStore 高性能序列化器

EclipseStore 高性能序列化器

自从我20多年前学习了Java之后,我就一直想要一个简单的解决方案来序列化Java对象图,但是又不会带来Java所带来的序列化安全性和性能问题。它应该可以像下面这样实现...

byte[] data = serializer.serialize(objectGraph);
Node objectGraphDeserialized = serializer.deserialize(data);


您想知道如何通过新的开源项目EclipseSerializer来实现这一点吗?您来对了地方。

在我们看Open-Source项目EclipseStore Serializer之前,我想简要回顾一下来自Java序列化本身的挑战。这将是背景信息,以便了解项目的强大之处。

Java序列化简介

Java序列化是Java编程语言提供的一种机制,允许您将对象的状态转换为字节流。这个字节流可以很容易地存储在文件中、通过网络发送或以其他方式持久化。稍后,您可以反序列化字节流,重新构造原始对象,有效地保存和还原对象的状态。

以下是关于Java序列化的一些关键点:

Serializable接口: 要使Java类可序列化,它需要实现Serializable接口。这个接口没有任何方法;它充当标记接口,指示该类的对象可以被序列化。

   import java.io.Serializable;
   public class MyClass implements Serializable {
       // class members and methods
   }


序列化过程: 要对对象进行序列化,通常使用ObjectOutputStream。您创建此类的实例,并将您的对象写入其中。例如:

 try (FileOutputStream fileOut = new FileOutputStream("object.ser");
       ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
       MyClass obj = new MyClass();
       out.writeObject(obj);
   } catch (IOException e) {
       e.printStackTrace();
   }


反序列化过程: 要对对象进行反序列化,您使用ObjectInputStream。您从文件或网络中读取字节流,然后使用ObjectInputStream重新创建对象。

try (FileInputStream fileIn = new FileInputStream("object.ser");
       ObjectInputStream in = new ObjectInputStream(fileIn)) {
       MyClass obj = (MyClass) in.readObject();
       // Now 'obj' contains the deserialized object
   } catch (IOException | ClassNotFoundException e) {
       e.printStackTrace();
   }


版本考虑: 如果更改了一个序列化类的结构(例如添加或删除字段或更改其类型),则可能会导致旧的序列化对象的反序列化失败。Java提供了诸如serialVersionUID之类的机制来帮助处理版本和兼容性问题。

安全考虑: 序列化可能存在安全风险,特别是如果从不受信任的来源反序列化数据。在反序列化过程中可能会执行恶意代码。为了减轻这一风险,您应该仔细验证和清理您反序列化的任何数据,或者考虑使用JSON或XML等替代序列化机制。

自定义序列化: 在您的类中,您可以通过提供writeObjectreadObject方法来自定义序列化和反序列化过程。这些方法允许您控制对象状态如何写入和从字节流中读取。

总之,Java序列化是一个用于持久化对象并在网络上发送它们的宝贵功能。但是,它也带来了一些与版本和安全性相关的挑战,所以在处理不受信任的数据源时应谨慎使用。

安全问题有哪些?

Java序列化可能会引入一些安全问题,特别是当从不受信任的来源反序列化数据时。以下是与Java序列化相关的一些安全问题:

  • 远程代码执行: Java序列化最严重的安全风险之一是远程代码执行的可能性。当您反序列化一个对象时,Java运行时系统可以执行序列化数据中包含的任意代码。攻击者可以利用这一点在目标系统上执行恶意代码。这种漏洞可能导致严重的安全漏洞。
  • 拒绝服务(DoS): 攻击者可以创建一个具有大尺寸的序列化对象,导致过多的内存消耗,可能导致拒绝服务攻击。反序列化大对象可能会占用大量CPU和内存资源,导致应用程序减速或崩溃。
  • 数据篡改: 序列化的数据可能在传输或存储过程中被篡改。攻击者可以修改序列化字节流以改变反序列化对象的状态或引入漏洞。
  • 不安全的反序列化: 在没有适当验证的情况下反序列化不受信任的数据可能导致安全问题。例如,如果从不受信任的输入反序列化执行敏感操作的类,则攻击者可以操纵对象状态以执行未经授权的操作。
  • 信息泄露: 在对象被序列化时,可能会包含敏感信息。如果这些数据没有得到充分保护或加密,攻击者可能会获得敏感信息。

如何减轻序列化问题

要减轻这些安全问题,请考虑以下最佳实践:

  • 避免反序列化不受信任的数据: 如果可能的话,完全避免从不受信任的来源反序列化数据。而是对不受信任的数据使用更安全的数据交换格式,如JSON或XML。(或使用EclipseSerializer ;-)
  • 实施输入验证: 在反序列化数据时,验证和清理输入,以确保其符合预期的数据结构,并且不包含意外或恶意数据。
  • 使用安全管理器: Java的安全管理器可以用于限制反序列化代码的权限和行为。不过,值得注意的是在较新版本的Java中,安全管理器已被弃用。
  • 对类进行白名单处理: 限制可以反序列化的类到预定义的一组受信任的类。这可以防止对任意和潜在恶意的类进行反序列化。
  • 版本和兼容性: 在更改序列化类时要谨慎。使用serialVersionUID管理不同版本的序列化对象之间的版本和兼容性。
  • 安全库: 考虑使用第三方库,如Apache Commons Collections或OWASP Java序列化安全(Java-Serial-Killer),以帮助减轻已知的漏洞并防止常见攻击。

总之,Java序列化可能会引入严重的安全风险,尤其是处理不受信任的数据时。非常重要的是采取预防措施、验证输入,并考虑使用替代序列化方法或库来增强安全性。此外,保持Java运行环境的最新状态至关重要,因为较新的Java版本可能包含安全改进和已知漏洞的修复。

为什么JSON或XML不是JVM的完美解决方案?

许多论文和讲座建议通过使用XML或JSON来规避序列化的安全风险。这是要传输的数据的结构化表示。也存在安全问题,但我将在另一篇文章中讨论这些问题。然而,需要解决的是两个问题。首先,数据必须被转换为文本表示。这通常需要比纯二进制模型更多的数据量。此外,像图像的二进制数据这样的数据必须被记录,以便只能传输可打印或UTF-8字符。这个过程需要大量时间,而且当将其转换为XML并从XML转换回原始格式时通常需要大量的内存。

在大多数情况下导致问题的第二点是数据结构。在XML和JSON中,对象引用只能以更可管理的方式存储。这使得处理变得更加复杂,慢速和资源密集。尽管可以使用许多可靠的解决方案将Java对象转换为XML或JSON,我建议偶尔寻找新的方法。

EclipseStore:序列化器:实践部分

现在,让我们在这篇文章中来谈谈实际的东西。首先需要依赖。为此,我们在pom.xml中添加以下指令。第一个版本是在写这篇文章时准备的,而且有一个SNAPSHOT版本(1.0.0-SNAPSHOT)可以从存储库中获得。在这种情况下,您仍然必须使用SNAPSHOT 存储库

在pom.xml内部定义。

<dependency>
   <groupId>org.eclipse.serializer</groupId>
   <artifactId>serializer</artifactId>
   <version>{maven-version-number}</version>
</dependency>


一旦准备好并获取了依赖,剩下的事情会很快就进行。首先测试,我们创建了一个名为Node的类。每个Node可以有一个右孩子和一个左孩子。有了这样,我们就可以创建一棵树。

private String id;
private Node leftNode;
private Node rightNode;


例如,我创建了以下结构,然后使用序列化器对其进行了一次序列化和反序列化。

Node rootNode = new Node("rootNode");
Node leftChildLev01 = new Node("Root-L");
Node rightChildLev01 = new Node("Root-R");
leftChildLev01.addLeft(new Node("Root-L-R"));
leftChildLev01.addLeft(new Node("Root-L-L"));
rightChildLev01.addLeft(new Node("Root-R-L"));
rightChildLev01.addRight(new Node("Root-R-R"));
rootNode.addLeft(leftChildLev01);
rootNode.addRight(rightChildLev01);
Serializer<byte[]> serializer = Serializer.Bytes();
byte[] data = serializer.serialize(rootNode);
Node rootNodeDeserialized = serializer.deserialize(data);
System.out.println(rootNode.toString());
System.out.println(" ========== ");
System.out.println(rootNodeDeserialized.toString());


现在,让我们看看这是否也适用于Java对象图。为此,表示节点的类进行了更改,以便还可以定义一个父节点。现在可以建立循环。

public class GraphNode {
   private String id;
   private GraphNode parent;
   private List<GraphNode> childGraphNodes = new ArrayList<>();


我们以此处列出的图为例。

GraphNode rootNode = new GraphNode("rootNode");
GraphNode child01Lev01 = new GraphNode("child01Lev01");
GraphNode child02Lev01 = new GraphNode("child02Lev01");
rootNode.addChildGraphNode(child01Lev01);
rootNode.addChildGraphNode(child02Lev01);
child01Lev01.setParent(rootNode);
child02Lev01.setParent(rootNode);
GraphNode child01Lev02 = new GraphNode("child01Lev02");
GraphNode child02Lev02 = new GraphNode("child02Lev02");
child01Lev01.addChildGraphNode(child01Lev02);
child01Lev01.addChildGraphNode(child02Lev02);
child01Lev02.setParent(child01Lev01);
child01Lev02.setParent(child01Lev01);
GraphNode child01Lev03 = new GraphNode("child01Lev03");
GraphNode child02Lev03 = new GraphNode("child02Lev03");
child01Lev03.setParent(child02Lev01);
child02Lev03.setParent(child02Lev01);
child02Lev01.addChildGraphNode(child01Lev03);
child02Lev01.addChildGraphNode(child02Lev03);
//creating cycles
rootNode.addChildGraphNode(child01Lev03);
rootNode.setParent(child02Lev03);


这个图也没有任何问题地被处理,循环也没有引起任何问题。

Serializer<byte[]> serializer = Serializer.Bytes();
byte[] data = serializer.serialize(rootNode);
GraphNode rootNodeDeserialized = serializer.deserialize(data);


您可以使示例变得更加复杂,并尝试继承的微妙之处。还支持JDK17中的新数据类型。这意味着我有一个强大的工具可以处理各种任务。例如,在另一个名为EclipseStore的Eclipse项目中可以找到一种用于持续性的机制,基于这种序列化。但是您自己的小项目也可以受益于此。我将展示如何将其快速集成到残余服务中。

构建一个简单的REST服务

如果您想在Java中创建一个简单的REST服务,用于传输字节流,而不使用Spring Boot,您可以使用Java SE API和com.sun.net.httpserver包中的HttpServer类,该类允许您创建一个HTTP服务器。 

  • 我们在端口8080上创建一个HTTP服务器,并定义一个处理/api/bytestream请求的上下文。
  • ByteStreamHandler类处理用于上传字节流的POST请求和用于下载字节流的GET请求。
  • 对于POST请求,它读取传入的字节流,根据需要进行处理,并发送一个响应。
  • 对于GET请求,它发送一个预定义的字节流作为响应。

请记住,这只是一个简单的例子,您可以根据需要扩展它来处理更复杂的用例和错误处理,以满足您特定应用程序的需求。还要注意,com.sun.net.httpserver包是JDK的一部分,但它可能不会在所有Java发行版中都可用。

public class ByteStreamHandler implements HttpHandler {
   @Override
   public void handle(HttpExchange exchange) throws IOException {
       String requestMethod = exchange.getRequestMethod();
       if (requestMethod.equalsIgnoreCase("POST")) {
           // Handle POST requests for uploading byte streams
           handleUpload(exchange);
       } else if (requestMethod.equalsIgnoreCase("GET")) {
           // Handle GET requests for downloading byte streams
           handleDownload(exchange);
       }
   }

   private void handleUpload(HttpExchange exchange) throws IOException {
       // Get the input stream from the request
       InputStream inputStream = exchange.getRequestBody();
       // Read the byte stream and process it as needed
       ByteArrayOutputStream byteArrayOutputStream 
         = new ByteArrayOutputStream();
       byte[] buffer = new byte[1024];
       int bytesRead;
       while ((bytesRead = inputStream.read(buffer)) != -1) {
           byteArrayOutputStream.write(buffer, 0, bytesRead);
       }
       // Process the uploaded byte stream 
       // (e.g., save to a file or perform other actions)
       byte[] data = byteArrayOutputStream.toByteArray();
       // Do something with 'data'
       Serializer<byte[]> serializer = Serializer.Bytes();
       GraphNode deserialized = serializer.deserialize(data);
       //process the data
       System.out.println("deserialized = " + deserialized);
       // Send a response (you can customize this)
       String response = "Byte stream uploaded successfully.";
       exchange.sendResponseHeaders(200, response.length());
       OutputStream os = exchange.getResponseBody();
       os.write(response.getBytes());
       os.close();
   }
   private void handleDownload(HttpExchange exchange) throws IOException {
       // Simulate generating and sending a byte stream as a response
       String response = "Hello, Byte Stream!";
       Serializer<byte[]> serializer = Serializer.Bytes();
       byte[] data = serializer.serialize(response.getBytes());
       exchange.sendResponseHeaders(200, data.length);
       OutputStream os = exchange.getResponseBody();
       os.write(data);
       os.close();
   }
}


结论

我们审视了Java原始序列化的典型问题,以及使用开源的Eclipse序列化器项目从JVM到JVM进行通信时的繁琐实现是不必要的。在建模图形方面没有限制,因为它不仅处理了包括JDK17在内的当前新数据类型,图中的循环也不成问题。

使用Serializable接口也是不必要的,不影响处理。易于处理使得它可以用于细小的项目,例如使用内置JDK资源的REST服务。使用序列化器的大型项目包括开源项目EclipseStore。该项目提供了一个针对JVM的高性能持久性机制。

祝编码愉快

斯文

推荐阅读: 39.SpringBoot 打成的jar包和普通的jar包有什么区别 ?

本文链接: EclipseStore 高性能序列化器