Статьи

Использование буферов протокола Google с Java

Эффективная Java, третье издание было недавно выпущено, и я был заинтересован в поиске обновлений для этой классной книги по разработке Java, последнее издание которой освещалось только в Java 6 . Очевидно, что в этом издании есть совершенно новые элементы, тесно связанные с Java 7 , Java 8 и Java 9, такие как пункты с 42 по 48 в главе 7 («Лямбды и потоки»), пункт 9 («Предпочитать попытку с ресурсами» попробовать-наконец-то ») и пункт 55 (« Разумное возвращение опционов »). Я был (очень немного) удивлен, осознав, что в третьем издании Effective Java появился новый элемент, который не был специально создан новыми версиями Java, но вместо этого был обусловлен разработками в мире разработки программного обеспечения, независимыми от версий Java. Этот пункт, пункт 85 («Предпочитать альтернативы сериализации Java»), побудил меня написать вступительную статью об использовании буферов протокола Google с Java .

В пункте 85 « Эффективной Java», третье издание , Джош Блох выделяет жирным шрифтом следующие два утверждения, относящиеся к сериализации Java:

  1. « Лучший способ избежать использования сериализации — никогда не десериализовать что-либо. «
  2. « Нет никакой причины использовать сериализацию Java в любой новой системе, которую вы пишете. «

После описания опасностей десериализации Java и создания этих смелых утверждений, Bloch рекомендует разработчикам Java использовать то, что он называет (чтобы избежать путаницы, связанной с термином «сериализация» при обсуждении Java) «кросс-платформенные представления структурированных данных». Блох утверждает, что ведущими предложениями в этой категории являются JSON ( JavaScript Object Notation ) и Protocol Buffers ( protobuf ). Мне показалось интересным упоминание о протокольных буферах, потому что я в последнее время немного читал и играл с протокольными буферами Использование JSON (даже с Java) подробно описано в Интернете. Я чувствую, что осведомленность о буферах протокола может быть меньше среди разработчиков Java, чем осведомленность о JSON, и поэтому чувствую, что публикация по использованию буферов протокола с Java оправдана.

Протокол Google Buffers описан на странице проекта как «не зависящий от языка, не зависящий от платформы расширяемый механизм для сериализации структурированных данных». На этой странице добавлено: «Подумайте, XML, но меньше, быстрее и проще». Хотя одно из преимуществ протокольных буферов заключается в том, что они поддерживают представление данных таким образом, чтобы их могли использовать несколько языков программирования, основное внимание в этом посте уделяется исключительно использованию протокольных буферов с Java.

Существует несколько полезных сетевых ресурсов, связанных с буферами протокола, включая главную страницу проекта, страницу проекта GitHub protobuf , Руководство по языку proto3 (также доступно Руководство по языку proto2 ), Основы буфера протокола: руководство по Java, Руководство по созданию сгенерированного кода Java , Документация по API Java (Javadoc) , страница выпуска буферов протокола и страница репозитория Maven . Примеры в этом посте основаны на буферах протокола 3.5.1 .

Основы буфера протокола: в руководстве по Java описан процесс использования буферов протокола с Java. Он охватывает гораздо больше возможностей и вещей, которые следует учитывать при использовании Java, чем я расскажу здесь. Первый шаг — определить независимый от языка формат буферов протокола. Это сделано в текстовом файле с расширением .proto . Для моего примера я описал мой формат протокола в файле album.proto который показан в следующем листинге кода.

album.proto

01
02
03
04
05
06
07
08
09
10
11
12
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 вместо предполагаемого default2, который в настоящее время используется, когда это явно не указано. Две строки, начинающиеся с option, представляют интерес только при использовании этого формата протокола для генерации кода Java, и они указывают имя самого внешнего класса и пакет этого самого внешнего класса, который будет сгенерирован для использования приложениями Java для работы с этим форматом протокола. ,

Ключевое слово «message» указывает, что эта структура, названная здесь «Album», является тем, что должно быть представлено. В этой конструкции четыре поля, три из которых имеют string формат, а одно — целое число ( int32 ). Два из четырех полей могут существовать более одного раза в данном сообщении, поскольку они снабжены repeated зарезервированным словом. Обратите внимание, что я создал это определение без учета Java, за исключением двух option которые определяют детали генерации классов Java из этой спецификации формата.

Файл album.proto показанный выше, теперь необходимо «скомпилировать» в файл исходного класса Java ( AlbumProtos.java в пакете dustin.examples.protobuf ), который позволит записывать и читать двоичный формат буферов протокола, который соответствует определенному протоколу. формат. Это поколение файла исходного кода Java выполняется с protoc компилятора 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 файл album.proto в каталоге src указывает --proto_path и у меня был созданный (но пустой) каталог с именем build\generated для сгенерированного исходного кода Java, который будет помещен в соответствии с --java_out флаг.

Файл исходного кода Java сгенерированного класса AlbumProtos.java в указанном пакете содержит более 1000 строк, и я не буду перечислять этот сгенерированный исходный код класса здесь, но он доступен на GitHub . Среди нескольких интересных вещей, которые следует отметить в этом сгенерированном коде, — отсутствие операторов импорта (полные имена пакетов используются вместо всех ссылок на классы). Более подробная информация о исходном коде Java, созданном protoc , доступна в руководстве Java Generated Code . Важно отметить, что на этот сгенерированный класс AlbumProtos до сих пор не влиял ни один из моих собственных кодов Java-приложений, и он генерируется исключительно из текстового файла album.proto показанного ранее в посте.

Сгенерированный исходный код Java, доступный для AlbumProtos , теперь я добавляю каталог, в котором этот класс был сгенерирован, в исходный путь моей IDE, потому что сейчас я рассматриваю его как файл исходного кода. Я мог бы альтернативно скомпилировать его в .class или .jar для использования в качестве библиотеки. Теперь, когда этот сгенерированный файл исходного кода Java находится в моем исходном пути, я могу создать его вместе со своим собственным кодом.

Прежде чем идти дальше в этом примере, нам нужен простой класс Java для представления с помощью буферов протокола. Для этого я буду использовать класс Album , определенный в следующем листинге кода (также доступный на GitHub ).

Album.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
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-класс «data» ( Album ) и сгенерированный Java-классом Protocol Buffers класс для представления этого альбома ( AlbumProtos.java ), я готов написать код приложения Java для «сериализации» информации об альбоме без использования Java. сериализации. Этот код приложения (демонстрационный) находится в классе AlbumDemo который доступен на GitHub и в котором я выделю соответствующие части в этом посте.

Нам нужно сгенерировать образец экземпляра Album для использования в этом примере, и это будет достигнуто с помощью следующего жестко закодированного списка.

Создание образца экземпляра Album

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 * 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 AlbumProtos.Album из Album

1
2
3
4
5
6
7
8
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() для этого экземпляра, как показано в следующем листинге кода.

Написание двоичной формы AlbumProtos.Album

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

Считывание массива byte[] обратно в экземпляр Album может быть выполнено, как показано в следующем листинге кода.

Создание Album из бинарной формы AlbumProtos.Album

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * 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 показанными ранее, выходные данные выглядят так:

1
2
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, третье издание , Джош Блох обсуждает уязвимости безопасности, связанные с десериализацией в механизме Java по умолчанию, и утверждает, что « нет причин использовать сериализацию Java в любой новой системе, которую вы пишете. »

Опубликовано на Java Code Geeks с разрешения Дастина Маркса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Использование буферов протокола Google с Java

Мнения, высказанные участниками Java Code Geeks, являются их собственными.