在 Java 中使用 Google 的 Protocol Buffers

位置:首页>文章>详情   分类: Java教程 > 编程技术   阅读(92)   2024-06-12 06:20:04

Effective Java, Third Edition最近发布,我一直有兴趣确定此类 Java 开发书籍的更新,其最新版本仅涵盖 Java 6。此版本中显然有与 Java 7、Java 8 和 Java 9 密切相关的全新项目,例如项目 42 到 48在第 7 章(“Lambdas 和 Streams”)第 9 项(“首选 try-with-resources 而不是 try-finally”)和第 55 项(“明智地返回可选值”)。我(非常轻微地)惊讶地发现 Effective Java 的第三版有一个新项目不是专门由新版本的 Java 驱动的,而是由软件开发世界的发展驱动的独立于 Java 的版本。第 85 项(“首选 Java 序列化的替代方案”)促使我写这篇关于使用 Google 的 Protocol Buffers 和 Java 的介绍性文章。

Effective Java, Third Edition 的第 85 项中,Josh Bloch 以粗体字强调了以下与 Java 序列化相关的两个断言:

  1. 避免序列化利用的最好方法是永远不要反序列化任何东西。
  2. 没有理由在您编写的任何新系统中使用 Java 序列化。

在概述了 Java 反序列化的危险并做出这些大胆的声明之后,Bloch 建议 Java 开发人员使用他所说的(以避免在讨论 Java 时与术语“序列化”相关的混淆)“跨平台结构化数据表示”。 Bloch 表示,该类别中的领先产品是 JSONJavaScript Object Notation)和Protocol Buffersprotobuf) .我发现对 Protocol Buffers 的提及很有趣,因为我最近一直在阅读和使用 Protocol Buffers。在线详尽地介绍了 JSON(即使是 Java)的使用。我觉得 Java 开发人员对 Protocol Buffers 的认识可能不如对 JSON 的认识,因此感觉有必要发表一篇关于在 Java 中使用 Protocol Buffers 的文章。

GoogleProtocol Buffers 在其项目页面 中被描述为“一种用于序列化结构化数据的语言中立、平台中立的可扩展机制”。该页面补充说,“想想 XML,但更小、更快、更简单。”尽管 Protocol Buffers 的优点之一是它们支持以多种编程语言可以使用的方式表示数据,但本文的重点是专门介绍如何将 Protocol Buffers 与 Java 结合使用。

有几个与协议缓冲区相关的有用在线资源,包括主项目页面GitHub protobuf 项目页面proto3 语言指南proto2 语言指南 也可用)、Protocol Buffer Basics: Java 教程、Java 生成代码指南Java API (Javadoc)文档Protocol Buffers 发布页面Maven 存储库页面。本文中的示例基于 Protocol Buffers 3.5.1

Protocol Buffers Basics:Java 教程概述了将 Protocol Buffers 与 Java 结合使用的过程。它涵盖了比我在这里介绍的更多的可能性和使用 Java 时要考虑的事情。第一步是定义与语言无关的 Protocol Buffers 格式。这是在扩展名为 .proto 的文本文件中完成的。对于我的示例,我在文件 album.proto 中描述了我的协议格式,该文件显示在下一个代码清单中。

album.proto

syntax = "proto3";

option java_outer_classname = "AlbumProtos";
option java_package = "dustin.examples.protobuf";

message Album
{
  string title = 1;
  repeated string artist = 2;
  int32 release_year = 3;
  repeated string song_title = 4;
}

尽管上述协议格式的定义很简单,但涵盖的内容很多。第一行明确指出我正在使用 proto3 而不是假定的默认 proto2 ,如果没有明确指定则当前使用。以option开头的两行仅在使用此协议格式生成Java代码时才有意义,它们表示最外层类的名称以及将生成供Java使用的最外层类的包使用此协议格式的应用程序。

“message”关键字表示需要表示的是这个结构体,这里命名为“Album”。此构造中有四个字段,其中三个是 string 格式,一个是整数 (int32)。四个字段中的两个可以在给定消息中多次出现,因为它们使用 repeated 保留字进行注释。请注意,除了指定从该格式规范生成 Java 类的详细信息的两个 option 之外,我创建此定义时并未考虑 Java。

上面显示的 album.proto 文件现在需要“编译”到 Java 源类文件(AlbumProtos.java 包中的 dustin.examples.protobuf),这将允许写入和读取与定义的协议格式相对应的 Protocol Buffers 的二进制格式。此 Java 源代码文件的生成是使用包含在适当的基于操作系统的存档文件中的 protoc 编译器完成的。就我而言,因为我在 Windows 10 中运行此示例,所以我下载 并解压缩 protoc-3.5.1-win32.zip 以访问此 protoc 工具。下图描述了我使用命令 protoc 针对 album.proto 运行 protoc --proto_path=src --java_out=dist\generated album.proto

为了运行上面的代码,我在 album.proto 指向的 src 目录中有我的 --proto_path 文件,并且我创建了一个名为(但为空)的目录build\generated 生成的 Java 源代码将按照 --java_out 标志的指定放置。

指定包中生成的类的Java源码文件AlbumProtos.java有1000多行,生成类的源码这里就不一一列举了,GitHub上有。关于此生成的代码,需要注意的几件有趣的事情是缺少导入语句(所有类引用都使用完全限定的包名称)。有关 protoc 生成的 Java 源代码的更多详细信息,请参阅 Java 生成的代码 指南。重要的是要注意这个生成的类 AlbumProtos 仍然没有受到我自己的任何 Java 应用程序代码的影响,并且完全是从帖子前面显示的 album.proto 文本文件生成的.

AlbumProtos 提供生成的 Java 源代码后,我现在将生成此类的目录添加到我的 IDE 的源路径中,因为我现在将其视为源代码文件。我也可以将它编译成 .class.jar 以用作库。有了这个生成的 Java 源代码文件现在在我的源路径中,我可以将它与我自己的代码一起构建。

在继续这个例子之前,我们需要一个简单的 Java 类来表示 Protocol Buffers。为此,我将使用在下一个代码清单中定义的类 Album(也可以在 GitHub 上获得)。

Album.java

package dustin.examples.protobuf;

import java.util.ArrayList;
import java.util.List;

/**
 * Music album.
 */
public class Album
{
   private final String title;

   private final List<String> artists;

   private final int releaseYear;

   private final List<String> songsTitles;

   private Album(final String newTitle, final List<String> newArtists,
                 final int newYear, final List<String> newSongsTitles)
   {
      title = newTitle;
      artists = newArtists;
      releaseYear = newYear;
      songsTitles = newSongsTitles;
   }

   public String getTitle()
   {
      return title;
   }

   public List<String> getArtists()
   {
      return artists;
   }

   public int getReleaseYear()
   {
      return releaseYear;
   }

   public List<String> getSongsTitles()
   {
      return songsTitles;
   }

   @Override
   public String toString()
   {
      return "'" + title + "' (" + releaseYear + ") by " + artists + " features songs " + songsTitles;
   }

   /**
    * Builder class for instantiating an instance of
    * enclosing Album class.
    */
   public static class Builder
   {
      private String title;
      private ArrayList<String> artists = new ArrayList<>();
      private int releaseYear;
      private ArrayList<String> songsTitles = new ArrayList<>();

      public Builder(final String newTitle, final int newReleaseYear)
      {
         title = newTitle;
         releaseYear = newReleaseYear;
      }

      public Builder songTitle(final String newSongTitle)
      {
         songsTitles.add(newSongTitle);
         return this;
      }

      public Builder songsTitles(final List<String> newSongsTitles)
      {
         songsTitles.addAll(newSongsTitles);
         return this;
      }

      public Builder artist(final String newArtist)
      {
         artists.add(newArtist);
         return this;
      }

      public Builder artists(final List<String> newArtists)
      {
         artists.addAll(newArtists);
         return this;
      }

      public Album build()
      {
         return new Album(title, artists, releaseYear, songsTitles);
      }
   }
}

定义了一个 Java“数据”类 (Album) 和一个 Protocol Buffers 生成的可用于表示这张专辑的 Java 类 (AlbumProtos.java),我准备好编写 Java在不使用 Java 序列化的情况下“序列化”相册信息的应用程序代码。此应用程序(演示)代码位于 AlbumDemo 类中,该类在 GitHub 上可用,我将在本文中突出显示相关部分。

我们需要生成一个 Album 的示例实例以用于此示例,这是通过下一个硬编码清单完成的。

正在生成 Album 的示例实例

/**
 * Generates instance of Album to be used in demonstration.
 *
 * @return Instance of Album to be used in demonstration.
 */
public Album generateAlbum()
{
   return new Album.Builder("Songs from the Big Chair", 1985)
      .artist("Tears For Fears")
      .songTitle("Shout")
      .songTitle("The Working Hour")
      .songTitle("Everybody Wants to Rule the World")
      .songTitle("Mothers Talk")
      .songTitle("I Believe")
      .songTitle("Broken")
      .songTitle("Head Over Heels")
      .songTitle("Listen")
      .build();
}

Protocol Buffers 生成的类 AlbumProtos 包括一个嵌套的 AlbumProtos.Album 类,我将使用它以二进制形式存储我的 Album 实例的内容。下一个代码清单演示了这是如何完成的。

AlbumProtos.Album 实例化 Album

final Album album = instance.generateAlbum();
final AlbumProtos.Album albumMessage
   = AlbumProtos.Album.newBuilder()
      .setTitle(album.getTitle())
      .addAllArtist(album.getArtists())
      .setReleaseYear(album.getReleaseYear())
      .addAllSongTitle(album.getSongsTitles())
      .build();

如前面的代码清单所示,“构建器”用于填充 Protocol Buffers 生成的类的不可变实例。通过对该实例的引用,我现在可以在该实例上使用方法 toByteArray() 轻松地将实例的内容以 Protocol Buffers 的二进制形式写出,如下一个代码清单所示。

写二进制形式的AlbumProtos.Album

final byte[] binaryAlbum = albumMessage.toByteArray();

byte[] 数组读回到 Album 的实例中可以如下一个代码清单所示完成。

Album 的二进制形式实例化 AlbumProtos.Album

/**
 * Generates an instance of Album based on the provided
 * bytes array.
 *
 * @param binaryAlbum Bytes array that should represent an
 *    AlbumProtos.Album based on Google Protocol Buffers
 *    binary format.
 * @return Instance of Album based on the provided binary form
 *    of an Album; may be {@code null} if an error is encountered
 *    while trying to process the provided binary data.
 */
public Album instantiateAlbumFromBinary(final byte[] binaryAlbum)
{
   Album album = null;
   try
   {
      final AlbumProtos.Album copiedAlbumProtos = AlbumProtos.Album.parseFrom(binaryAlbum);
      final List<String> copiedArtists = copiedAlbumProtos.getArtistList();
      final List<String> copiedSongsTitles = copiedAlbumProtos.getSongTitleList();
      album = new Album.Builder(
         copiedAlbumProtos.getTitle(), copiedAlbumProtos.getReleaseYear())
         .artists(copiedArtists)
         .songsTitles(copiedSongsTitles)
         .build();
   }
   catch (InvalidProtocolBufferException ipbe)
   {
      out.println("ERROR: Unable to instantiate AlbumProtos.Album instance from provided binary data - "
         + ipbe);
   }
   return album;
}

如上一个代码清单所示,在调用生成类中定义的 InvalidProtocolBufferException 方法 static 期间,可能会抛出已检查的异常 parseFrom(byte[])。获取生成类的“反序列化”实例本质上是一行,其余几行从生成类的实例化中获取数据,并将该数据设置到原始 Album 类的实例中。

demonstration class 包括两行,打印出原始 Album 实例的内容和最终从二进制表示中检索到的实例。这两行包括对两个实例调用 System.identityHashCode() 以证明它们不是同一实例,即使它们的内容匹配。当使用前面显示的硬编码 Album 实例详细信息执行此代码时,输出如下所示:

BEFORE Album (1323165413): 'Songs from the Big Chair' (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]
 AFTER Album (1880587981): 'Songs from the Big Chair' (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]

从这个输出中,我们看到相关字段在两个实例中是相同的,并且这两个实例确实是唯一的。这比使用 Java 的“近乎自动的”序列化机制 实现Serializable 接口要多一些工作,但是与此方法相关的重要优点可以证明成本合理。在 Effective Java,第三版 中,Josh Bloch 讨论了与 Java 默认机制中的反序列化相关的安全漏洞,并断言“没有理由在您编写的任何新系统中使用 Java 序列化。

标签2: Java教程
地址:https://www.cundage.com/article/jcg-using-googles-protocol-buffers-java.html

相关阅读

Java HashSet 教程展示了如何使用 Java HashSet 集合。 Java哈希集 HashSet 是一个不包含重复元素的集合。此类为基本操作(添加、删除、包含和大小)提供恒定时间性...
SpringApplicationBuilder 教程展示了如何使用 SpringApplicationBuilder 创建一个简单的 Spring Boot 应用程序。 春天 是用于创建企业应...
通道是继 buffers 之后 java.nio 的第二个主要新增内容,我们在之前的教程中已经详细了解了这一点。通道提供与 I/O 服务的直接连接。 通道是一种在字节缓冲区和通道另一端的实体(通...
课程大纲 Elasticsearch 是一个基于 Lucene 的搜索引擎。它提供了一个分布式的、支持多租户的全文搜索引擎,带有 HTTP Web 界面和无模式的 JSON 文档。 Elasti...
解析器是强大的工具,使用 ANTLR 可以编写可用于多种不同语言的各种解析器。 在这个完整的教程中,我们将: 解释基础:什么是解析器,它可以用来做什么 查看如何设置 ANTLR 以便在 Java...
Java 是用于开发各种桌面应用程序、Web 应用程序和移动应用程序的最流行的编程语言之一。以下文章将帮助您快速熟悉 Java 语言,并迈向 API 和云开发等更复杂的概念。 1. Java语言...
Java中的继承是指子类继承或获取父类的所有非私有属性和行为的能力。继承是面向对象编程的四大支柱之一,用于提高层次结构中类之间的代码可重用性。 在本教程中,我们将了解 Java 支持的继承类型,...
Java Message Service 是一种支持正式通信的 API,称为 网络上计算机之间的消息传递。 JMS 为支持 Java 程序的标准消息协议和消息服务提供了一个通用接口。 JMS 提...
之前,我介绍了spring 3 + hibernate 集成 示例和struts 2 hello world 示例。在本教程中,我将讨论在将 spring 框架与 struts 与 hibern...
Java 项目中的一项常见任务是将日期格式化或解析为字符串,反之亦然。解析日期意味着你有一个代表日期的字符串,例如“2017-08-3”,你想把它转换成一个代表 Java 中日期的对象,例如Ja...