最近发布了 有效的Java第三版 ,我一直对确定此类Java开发书籍的更新感兴趣,该书籍的最新版本仅通过Java 6进行了介绍 。 在此版本中,显然有与Java 7 , Java 8和Java 9密切相关的全新项目,例如第7章(“ Lambda和流”)中的项目42至48,项目9(“建议尝试使用资源”最终尝试”)和第55条(“明智地退还可选方案”)。 我(非常有点)惊讶地发现, Effective Java第三版中有一个新项目,并不是由Java的新版本专门驱动的,而是由独立于Java版本的软件开发领域的开发驱动的。 第85条(“ Java序列化的首选替代产品”)是促使我写这篇介绍性文章的内容,内容涉及将Google的Protocol Buffers与Java结合使用 。
Josh Bloch在“ 有效Java,第三版”的条款 85中,以粗体强调了以下两个与Java序列化有关的断言:
- “ 避免序列化攻击的最佳方法是永远不要反序列化任何东西。 “
- “ 您没有理由在您编写的任何新系统中使用Java序列化。 “
在概述了Java反序列化的危险并做出了这些大胆的声明之后,Bloch建议Java开发人员使用他所谓的(跨平台的结构化数据表示形式)(以避免在讨论Java时与术语“序列化”相关的混淆)。 Bloch指出,该类别中的领先产品是JSON ( JavaScript对象表示法 )和协议缓冲区 ( protobuf )。 我发现提到协议缓冲区很有趣,因为最近我一直在阅读和使用协议缓冲区。 在线全面介绍了JSON(甚至Java)的用法。 我觉得在Java开发人员中对协议缓冲区的了解可能比对JSON的了解要少,因此,有必要在Java上使用协议缓冲区。
Google的协议缓冲区在其项目页面上被描述为“一种不依赖语言,不依赖平台的可序列化结构化数据的机制”。 该页面添加了“思考XML,但更小,更快,更简单”。 尽管协议缓冲区的优点之一是它们支持以可被多种编程语言使用的方式表示数据,但本文的重点仅在于将协议缓冲区与Java结合使用。
有许多与协议缓冲区相关的有用在线资源,包括主项目页面 , GitHub protobuf项目页面 , proto3语言指南 (也提供proto2语言指南 ), 协议缓冲区基础:Java教程, Java生成的代码指南 , Java API(Javadoc)文档 ,“ 协议缓冲区”发行页面和“ Maven存储库”页面 。 本文中的示例基于协议缓冲区3.5.1 。
协议缓冲区基础:Java教程概述了将协议缓冲区与Java结合使用的过程。 与使用Java相比,它涵盖了使用Java时要考虑的更多可能性和事情。 第一步是定义独立于语言的协议缓冲区格式。 这是在扩展名为.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 。 仅当使用此协议格式生成Java代码时,以option开头的两行才有意义,它们指示最外层类的名称以及该最外层类的软件包,该类将被生成以供Java应用程序使用此协议格式使用。
“ message”关键字表示此结构(此处称为“专辑”)需要表示。 此构造中有四个字段,其中三个是string
格式,一个是整数( int32
)。 四个字段中的两个在给定消息中可以不止一次存在,因为它们用repeated
保留字注释。 请注意,我创建此定义时没有考虑Java,除了两个option
s,它们指定了根据此格式规范生成Java类的详细信息。
所述album.proto
上述未示出的文件需要被“编译”到Java源类文件( AlbumProtos.java
在dustin.examples.protobuf
包),将允许写入和读出协议缓冲器的二进制格式的对应于定义的协议格式。 使用适当的基于操作系统的存档文件中包含的protoc
编译器可以完成Java源代码文件的生成。 就我而言,因为我正在Windows 10中运行此示例,所以我下载并解压缩了protoc-3.5.1-win32.zip
以便访问该protoc
工具。 下一张图像使用命令protoc --proto_path=src --java_out=dist\generated album.proto
描述了我对album.proto
运行protoc
。
对于运行上面的,我有我的album.proto
在文件src
目录中指出--proto_path
和我有一个创建(但空)目录下名为build\generated
生成的Java源代码被放置在由按规定--java_out
标志。
指定包中生成的类的Java源代码文件AlbumProtos.java
有1000多行,我不会在此处列出生成的类源代码,但是可以在GitHub上找到 。 关于此生成的代码,需要注意的几件有趣的事情是缺少导入语句(完全合格的程序包名称代替了所有类引用)。 Java生成的代码指南中提供了有关由protoc
生成的Java源代码的更多详细信息。 重要的是要注意,这个生成的类AlbumProtos
仍然不受我自己的任何Java应用程序代码的影响,并且仅由文章前面显示的album.proto
文本文件生成。
有了可用于AlbumProtos
的生成的Java源代码,我现在将在其中生成该类的目录添加到IDE的源路径中,因为现在将其视为源代码文件。 我也可以将其编译为.class
或.jar
用作库。 现在,在我的源路径中有了这个生成的Java源代码文件,我可以将其与自己的代码一起构建。
在继续本示例之前,我们需要一个简单的Java类来用Protocol Buffers表示。 为此,我将使用在下一个代码清单(也在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;}@Overridepublic 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
)并使用协议缓冲区生成的Java类(可表示该专辑)的情况下( AlbumProtos.java
),我准备编写Java应用程序代码以“序列化”专辑信息,而无需使用Java序列化。 该应用程序(演示)代码位于GitHub上可用的AlbumDemo
类中, 在本文中 ,我将重点AlbumDemo
该类。
我们需要生成一个用于示例的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();
}
协议缓冲区生成的类AlbumProtos
类包括一个嵌套的AlbumProtos.Album
类,我将使用该类以二进制形式存储我的Album
实例的内容。 下一个代码清单演示了如何完成此操作。
从Album
实例化AlbumProtos.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();
如前面的代码清单所示,“生成器”用于填充协议缓冲区生成的类的不可变实例。 参照该实例,我现在可以使用该实例上的toByteArray()
方法轻松地以Protocol Buffers二进制格式写出该实例的内容,如下面的代码清单所示。
写作AlbumProtos.Album
二进制形式
final byte[] binaryAlbum = albumMessage.toByteArray();
如下面的代码清单所示,可以完成将byte[]
数组读回Album
实例的操作。
从AlbumProtos.Album
二进制形式实例化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;
}
如最后一个代码清单所示,在调用生成的类中定义的static
方法parseFrom(byte[])
过程中,可能引发检查异常InvalidProtocolBufferException
。 获得生成的类的“反序列化”实例本质上是一行,其余几行从生成的类的实例中获取数据,并将该数据设置在原始Album
类的实例中。
演示类包括两行,这些行打印出原始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的实现序列化接口的“近乎自动” 序列化机制相比,这需要付出更多的努力,但是与这种方法相关联的重要优势可以证明成本合理。 Josh Bloch在有效的Java第三版中讨论了Java默认机制中与反序列化相关的安全漏洞,并断言“ 没有理由在编写的任何新系统中使用Java序列化。 ”
翻译自: https://www.javacodegeeks.com/2018/01/using-googles-protocol-buffers-java.html