protoc-gen-java-dynamodb書いた / protoc pluginはじめて書いた
protobufでスキーマを書いて protoc-gen-java-dynamodb
に食わせると、そのスキーマのJavaの生成コードに「生成されたDynamoDBのエンティティクラスのコード」を良い感じにねじ込んでくれるというprotocプラグインを書きました。
syntax = "proto3"; package com.example.dynamodb; import "path/to/protoc-gen-java-dynamodb/protos/options.proto"; option java_package = "com.example.dynamodb"; option java_outer_classname = "ExampleEntityProto"; option (net.moznion.protoc.plugin.dynamodb.fileopt).java_dynamodb_table_name = "example-entity-table"; message ExampleEntity { string hash_key = 1 [(net.moznion.protoc.plugin.dynamodb.fieldopt).java_dynamodb_hash_key = true]; int64 range_key = 2 [(net.moznion.protoc.plugin.dynamodb.fieldopt).java_dynamodb_range_key = true]; bool bool_var = 3; }
例えばこのようなproto3を書いてプラグインに食わせると、 ExampleEntityProto.ExampleEntity.DynamoDBEntity
というinner classとして以下のようなコードが生成されます。
// protoc-gen-java-dynamodb plugin generated ((( @com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable(tableName = "example-entity-table") public static class DynamoDBEntity { private String hashKey; @com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute() @com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey() public String getHashKey() { return this.hashKey; } public void setHashKey(final String v) { this.hashKey = v; } private Long rangeKey; @com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute() @com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey() public Long getRangeKey() { return this.rangeKey; } public void setRangeKey(final Long v) { this.rangeKey = v; } private Boolean boolVar; @com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute() public Boolean getBoolVar() { return this.boolVar; } public void setBoolVar(final Boolean v) { this.boolVar = v; } public DynamoDBEntity() {} public DynamoDBEntity(final String hashKey, final Long rangeKey, final Boolean boolVar) { this.hashKey = hashKey; this.rangeKey = rangeKey; this.boolVar = boolVar; } public ExampleEntity toExampleEntity() { return new ExampleEntity.Builder() .setHashKey(this.hashKey) .setRangeKey(this.rangeKey) .setBoolVar(this.boolVar) .build(); } } public DynamoDBEntity toDynamoDBEntity() { return new DynamoDBEntity(this.getHashKey(), this.getRangeKey(), this.getBoolVar()); } // ))) protoc-gen-java-dynamodb plugin generated
なのでprotobufでdeserializeしたオブジェクトに対して例えば obj.toDynamoDBEntity()
としてやると生成された DynamoDBEntity
のインスタンスに変換ができ、それをDynamoDBMapper
等に食わせることが可能です。逆に DynamoDBEntity
のインスタンスにも toSomethingActualClass()
(SomethingActualClass
の部分は実際のクラスによって変わります) という変換コードが生えているので、実際のクラスのオブジェクトに変換することも容易です。
くわしい使い方についてはREADMEをご参照ください。
モチベーションとしては、例えばDynamoDB Streamsを使っている時なんかに良くありがちですが「データをインサートするコンポーネント」と「ストリームに乗ってきたデータを処理するコンポーネント」とが別々の言語で記述されていると、それぞれのコンポーネントでテーブル定義を二重に定義しなければならない、みたいな状況になることがあってそれが嫌。それであればprotobufで定義を書いておいてそれを各言語向けにコード生成して使えば良いのではないか、ということで作られたというのがこのプラグインです。
同じような悩みがある方、ぜひご利用ください。
今回protoc pluginを書いたのははじめてで、正直なところまったくのnewbieなので世界観を掴むところからのスタートだったのですが、最初の一歩としては@yuguiさんの記事が大変参考になりました。
この記事で基本的なことを把握しつつ、あとは既存のプラグイン実装 (e.g. https://github.com/Fadelis/protoc-gen-java-optional) を見ながら公式ドキュメントを読んで実装していく……というふうに進めたところなんとか形になりました。
Javaでprotocプラグインを書くときは素手でやると結構泥臭い部分 (例えばJavaのbuiltin typeのハンドリングなど) があるので、そのあたりは既にあるフレームワークを部分的に使うと楽で良いです。
例えばSalesforceが出しているコレなどを活用すると面倒な部分を自分で手書きしなくて大変便利。
あと犯した勘違いとしては「protocのプラグイン (つまりexecutableなjar) だけを配信すれば使えるから、ライブラリとしてMaven Centralとかにアップロードしなくても良い」というものがあり、確かに自前でprotoのoptionを提供しない場合それは正なのですが、仮に独自のoptionを提供している場合はそのoptionの情報をJavaのコードとして提供する必要があります。従ってMaven Centralなりなんなりでライブラリコードも配信する必要があります。なのでこのプラグインのライブラリもMaven Centralにアップロードされています。
なおGoで似たようなことをしたい時はsrikrsna/protoc-gen-gotagという便利なやつがあって、これはprotoファイルの中にコメントで書いておくと良い感じで生成コード中のGoのカスタムタグに値を埋め込んでくれるというナイスなやつです。
当初はJavaでもこういうことをやろうと意気込んでいたのですが、Javaのprotocのライブラリはprotoファイル中のコメントを読む機能が無かったり *1、Plugin Insertion Point (つまり生成コードを捻じ込める箇所) が限られていたり *2、仮に protoc-gen-gotag と同じアプローチを採ってprotocによって生成されたコードを構文解析してそこに値をブチ込むにしてもJavaだと色々と大変そうだなあ……などなど色々と制限があったので今回書いたプラグインの形に落ち着いた次第です。