使ったAPI
サンプルを動かす
認証情報の設定
ライブラリを使ったサンプルコードで、1行目から引っかかった。
ImageAnnotatorClient vision = ImageAnnotatorClient.create();
認証情報を内部で持っているのだが、認証に関しての手順はすっ飛ばしてた。
Cloud API サービスに対する認証に書いてある通りに手順を実行してサービスアカウントのJSONファイルをダウンロードした。
どうやってその内容をライブラリに渡すか。。
ネットで調べると大体専用の環境変数に突っ込んでパスを参照するように書かれていた。
恐らくサンプルコードはそれで動くのだろう。
でも環境変数からパス取得って。。
ちゃんとアプリケーションプロパティから取りたい。
ImageAnnotatorClientのJavadocを読んでみると次のような記載が。
This class can be customized by passing in a custom instance of ImageAnnotatorSettings to create(). For example:
InstantiatingChannelProvider channelProvider =
ImageAnnotatorSettings.defaultChannelProviderBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(myCredentials))
.build();
ImageAnnotatorSettings imageAnnotatorSettings =
ImageAnnotatorSettings.defaultBuilder().setChannelProvider(channelProvider).build();
ImageAnnotatorClient imageAnnotatorClient =
ImageAnnotatorClient.create(imageAnnotatorSettings);
おぉ、myCredentials
が作れればそれを使っていいのね。
FixedCredentialsProvider
は渡したCredentials
をそのまま供給してくれるみたいだから、Credentials
が作れれば!
で、このCredentials
のインスタンス、どうやって作ればいいのかね・・?
もう一度、ImageAnnotatorSettings
クラスを眺めてみる。
defaultCredentialsProviderBuilder()
なるメソッドを発見。
public static GoogleCredentialsProvider.Builder defaultCredentialsProviderBuilder() {
return GoogleCredentialsProvider.newBuilder().setScopesToApply(DEFAULT_SERVICE_SCOPES);
}
Credentials
のサブクラスを見てGoogleCredentialsProvider
の実装を見たらGoogleCredentials
のインスタンスが作れれば何とかいけそう。
GoogleCredentials
クラスを見ていたらそれらしきメソッドを発見。
こんな風に作る
String jsonFilePath = "..."; // JSONキーファイルのパス
FileInputStream jsonFileInputStream = new FileImputStream(jsonFilePath);
GoogleCredentials myCredentials = GoogleCredentials.fromStream(jsonFileInputStream);
## SSL関連のエラーとの戦い
これでサンプルコード動かすと次のエラーが出た。
java.lang.IllegalArgumentException: Jetty ALPN/NPN has not been properly configured.
at io.grpc.netty.GrpcSslContexts.selectApplicationProtocolConfig(GrpcSslContexts.java:174) ~[grpc-netty-1.2.0.jar:1.2.0]
at io.grpc.netty.GrpcSslContexts.configure(GrpcSslContexts.java:151) ~[grpc-netty-1.2.0.jar:1.2.0]
at io.grpc.netty.GrpcSslContexts.configure(GrpcSslContexts.java:139) ~[grpc-netty-1.2.0.jar:1.2.0]
at io.grpc.netty.GrpcSslContexts.forClient(GrpcSslContexts.java:109) ~[grpc-netty-1.2.0.jar:1.2.0]
・・・
エラー発生箇所を見てみるとSSLのプロバイダーはJDKになってて、そのなかでJetty関連のクラスがあるかを判定している。
・・・Tomcatやし!
OpenSSLでも良かった気がするのだが、なんでOpenSSLを使うようにならなかったのか、もう少し追ってみた。
io.grpc.netty.GrpcSslContexts.defaultSslProvider()
が途中で呼ばれているのだがここで決定していて、更に追ってみるとOpenSsl.isAvailable()
の戻りがfalse
になっていたようだ。
何故だ?
OpenSsl
クラスの初期化で原因がわかりそうなデバッグログが吐かれるようになっていたのでログレベルを下げてみて再度実行。
Failed to initialize netty-tcnative; OpenSslEngine will be unavailable. See http://netty.io/wiki/forked-tomcat-native.html for more information.
と表示されていた。
ん? netty-tcnative
なるライブラリが必要なのか?URLも載っていたのでアクセスしてみる。
書いてある通り netty-tcnative-boringssl-static
のライブラリをpom.xml
に追記して再度実行してみた。
これでOpenSSLを使ってくれるはず。
と思ってもう一度実行してら次はこんな感じ。
java.lang.UnsatisfiedLinkError: no provided in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1864) ~[na:1.8.0_65]
at java.lang.Runtime.loadLibrary0(Runtime.java:870) ~[na:1.8.0_65]
at java.lang.System.loadLibrary(System.java:1122) ~[na:1.8.0_65]
at org.apache.tomcat.jni.Library.<init>(Library.java:80) ~[tomcat-embed-core-8.5.14.jar:8.5.14]
at org.apache.tomcat.jni.Library.initialize(Library.java:180) ~[tomcat-embed-core-8.5.14.jar:8.5.14]
at io.netty.handler.ssl.OpenSsl.initializeTcNative(OpenSsl.java:417) ~[netty-handler-4.1.8.Final.jar:4.1.8.Final]
Issueでも上がってた。
netty/netty-tcnative#151 java.lang.UnsatisfiedLinkError: no provided in java.library.path using spring boot/tomcat embedded
Tomcat使うなとか、諦めてJetty使うように変更したPRとか見てしまったので、もうおとなしくJettyにしました。
結果的にTomcatに戻したのですが、その方法は後述します。
まずはspring-bootの設定からtomcatを取り除き、jettyに変更する作業。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-tomcat</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-jetty</artifactId>
+ </dependency>
これでやっと正常に動きました。
こんな時間かかるならRestTemplate
からAPI叩いた方が早かったのでは?
いや、APIはシンプルだが何だかんだDTOの準備に手間取る。。
じゃあ、DTOはライブラリのものを使って、あとはJSONにうまく変換できれば!
Spring実装
JSON変換モジュールの捜索
DTOを見る限りJacksonのObjectMapper
では色々設定しないとうまくシリアライズできそうになかったので、ライブラリ側でJSONに変換できるモジュールが無いか探してみた。
Vision APIのライブラリで使用しているリクエストやレスポンス周りのクラスを改めて見てみると全てGeneratedMessageV3
やらMessage
を継承している。
ここからはかなり時間がかかって、どう辿ったかは記憶が曖昧。。
何とかJsonFormat
なるクラスを見つけて内部クラスのJsonPrinter
でMessage.Builder
を引数に取るそれらしきメソッドを見つけた。
これで何とかHttpMessageConverter
が実装できる。
Jacksonを使わないJSON用のHttpMessageConverter
を実装
もう単純に何も迷わずシンプルに実装してみる。
まずはPrinter
とParser
のインスタンス生成。
private Printer printer = JsonFormat.printer().omittingInsignificantWhitespace();
private Parser parser = JsonFormat.parser().ignoringUnknownFields();
次にAbstractHttpMessageConverter
を継承して実装する。
メインどころだけ記載します。まずはJSONからMessage
クラスに変換するロジック
@Override
protected Message readInternal(Class<? extends Message> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// Content-Typeとエンコードを特定
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.getDefaultCharset());
// 指定クラスのBuilderクラスを生成
Message.Builder builder = getMessageBuilder(clazz);
// BuilderインスタンスにJSONをマージ
this.parser.merge(new InputStreamReader(inputMessage.getBody(), charset), builder);
// ビルド!
return builder.build();
}
とそんな難しいことしてないです。
ただ、Message.Builder
インスタンスの生成だけはReflection経由で生成しています。
これはProtobufHttpMessageConverter
を参考にしました。
(...ってかこのクラスで良かったんじゃないか。。?)
次に書き出しの方です。
@Override
protected void writeInternal(Message message, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// Content-Typeとエンコードを特定
MediaType contentType = outputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = this.getDefaultContentType(message);
}
Charset charset = contentType.getCharset();
if (charset == null) {
charset = this.getDefaultCharset();
}
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputMessage.getBody(), charset);
// JSONの書き出し
this.printer.appendTo(message, outputStreamWriter);
outputStreamWriter.flush();
}
ここまでできれば後は簡単です。
RestTemplate
に突っ込んでMessage
を継承しているクラスなら何も気にせずにJSONの変換が可能です。
こんな風に。
BatchAnnotateImagesRequest batchRequest = BatchAnnotateImagesRequest.newBuilder()
.addRequests(request)
.buidl();
RestTemplate restTemplate = new RestTemplate(Collections.singletonList(new GoogleMessageHttpMessageConverter()));
BatchAnnotateImagesResponse response = restTemplate.postForObject(VISION_API_URL, batchRequest,
BatchAnnotateImagesResponse.class);
まとめ
- 無理にSpring使わなくてもJetty環境ならそのままLibrary使えば良いと思う
- Tomcat環境でもそこまでたくさん実装しなくてもLibraryの恩恵は受けられる
- Spring実装の場合はgRPCではなくなる
- Libraryの実装ではリトライ設定もよしなに設定されているのでSpringの場合は別途設定する必要がある(逆に言えばリトライ設定が見える化できるからSpringの方が良いかもしれない)
今回のコードもGitHubに上げてます。
結果テキストの英語を日本語に変換するためにTranslate APIも使ってます。
また、サービスアカウント用のJsonKeyファイルではなく、API-Keyにも対応しています。
良ければ覗いてみてください。