SpringでVision API を使ってみる

# :cloud:  使ったAPI

- [Cloud Vision API](https://cloud.google.com/vision/)

# サンプルを動かす

## :key: 認証情報の設定

[ライブラリを使ったサンプルコード](https://cloud.google.com/vision/docs/reference/libraries)で、１行目から引っかかった。

```java
ImageAnnotatorClient vision = ImageAnnotatorClient.create();
```

認証情報を内部で持っているのだが、認証に関しての手順はすっ飛ばしてた。

[Cloud API サービスに対する認証](https://cloud.google.com/vision/docs/common/auth)に書いてある通りに手順を実行してサービスアカウントのJSONファイルをダウンロードした。

どうやってその内容をライブラリに渡すか。。
ネットで調べると大体専用の環境変数に突っ込んでパスを参照するように書かれていた。
恐らくサンプルコードはそれで動くのだろう。

でも環境変数からパス取得って。。
ちゃんとアプリケーションプロパティから取りたい。

ImageAnnotatorClientのJavadocを読んでみると次のような記載が。

```java
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()` なるメソッドを発見。

```java
public static GoogleCredentialsProvider.Builder defaultCredentialsProviderBuilder() {
  return GoogleCredentialsProvider.newBuilder().setScopesToApply(DEFAULT_SERVICE_SCOPES);
}
```

`Credentials`のサブクラスを見て`GoogleCredentialsProvider`の実装を見たら`GoogleCredentials`のインスタンスが作れれば何とかいけそう。
`GoogleCredentials`クラスを見ていたらそれらしきメソッドを発見。

こんな風に作る

```java
String jsonFilePath = "..."; // JSONキーファイルのパス
FileInputStream jsonFileInputStream = new FileImputStream(jsonFilePath);
GoogleCredentials myCredentials = GoogleCredentials.fromStream(jsonFileInputStream);
```

##:shield: 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](https://github.com/netty/netty-tcnative/issues/151)

Tomcat使うなとか、諦めてJetty使うように変更したPRとか見てしまったので、もうおとなしくJettyにしました。
結果的にTomcatに戻したのですが、その方法は後述します。

まずはspring-bootの設定からtomcatを取り除き、jettyに変更する作業。

```diff:pom.xml
	<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にうまく変換できれば！

# :cherry_blossom: Spring実装

## JSON変換モジュールの捜索

DTOを見る限りJacksonの`ObjectMapper`では色々設定しないとうまくシリアライズできそうになかったので、ライブラリ側でJSONに変換できるモジュールが無いか探してみた。

Vision APIのライブラリで使用しているリクエストやレスポンス周りのクラスを改めて見てみると全て`GeneratedMessageV3`やら`Message`を継承している。

ここからはかなり時間がかかって、どう辿ったかは記憶が曖昧。。
何とか`JsonFormat`なるクラスを見つけて内部クラスの`JsonPrinter`で`Message.Builder`を引数に取るそれらしきメソッドを見つけた。

これで何とか`HttpMessageConverter`が実装できる。

## Jacksonを使わないJSON用の`HttpMessageConverter`を実装

もう単純に何も迷わずシンプルに実装してみる。
まずは`Printer`と`Parser`のインスタンス生成。

```java
private Printer printer = JsonFormat.printer().omittingInsignificantWhitespace();
private Parser parser = JsonFormat.parser().ignoringUnknownFields();
```

次に`AbstractHttpMessageConverter`を継承して実装する。
メインどころだけ記載します。まずはJSONから`Message`クラスに変換するロジック

```java
@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`を参考にしました。
（...ってかこのクラスで良かったんじゃないか。。？）

次に書き出しの方です。

```java
@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の変換が可能です。
こんな風に。

```java
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](https://github.com/uich/demo-spring-google-cloud)に上げてます。
結果テキストの英語を日本語に変換するために[Translate API](https://cloud.google.com/translate/)も使ってます。
また、サービスアカウント用のJsonKeyファイルではなく、API-Keyにも対応しています。
良ければ覗いてみてください。