JDBな人生  専門的なことから日常的なことまで~ まぁ自由きままに書いていきます。
2018年08月 / 07月<< 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 >>09月

アクセスランキング

[ジャンルランキング]
コンピュータ
303位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
45位
アクセスランキングを見る>>

Gradle/Spring Boot/MySQL (+Phinx)のプロジェクトをGoogle App Engine/Cloud SQLにデプロイする

久々に技術系の記事です。最近ちょっとしたWEBアプリケーションを開発しているのですが、今回は初めて、運用にGoogle Cloudを使ってみることにしました。ある程度開発が進み、一度デプロイしようと思ったら色々と問題が。かれこれ三日もハマってしまいました。

サンプルはいろいろとあるのですが、Gradleを利用、かつCloud SQLに接続しているようなものが見つけられず、見よう見まねで何とか動かしたというところです。

以下、参考になったサイトと簡単な手順だけ書き残しておきます。

0. Google Cloud SDKをインストールする

開発用のマシンからデプロイする場合はこれを入れておきます。

Cloud SDK | Google Cloud
https://cloud.google.com/sdk/

1. PhinxをCloud SQLに接続する

こちらは単純でした。ドキュメント通りにCloud SQL Proxyをインストール・起動した後、phinx.ymlに接続先としてlocalhostを指定します。(3306はおそらく使用中なのでポートも指定することになると思います。)

Cloud SQL Proxy を使用して MySQL クライアントを接続する | Cloud SQL for MySQL | Google Cloud
https://cloud.google.com/sql/docs/mysql/connect-admin-proxy?hl=ja

2. App Engineで起動するための改造

こちらも基本ドキュメント通りに進めますが、Gradleの記法には要注意です。

getting-started-java/appengine-standard-java8/springboot-appengine-standard at master · GoogleCloudPlatform/getting-started-java · GitHub
https://github.com/GoogleCloudPlatform/getting-started-java/tree/master/appengine-standard-java8/springboot-appengine-standard

build.gradle

// 衝突するものを排除する
configurations {
    compile.exclude group: 'org.springframework.boot', module: "spring-boot-starter-tomcat"
    compile.exclude group: 'org.slf4j', module: "jul-to-slf4j"
    compile.exclude group: 'org.apache.logging.log4j', module: "log4j-to-slf4j"
    compile.exclude group: 'ch.qos.logback', module: "logback-classic"
}

// AppEngineでの起動のため
apply plugin: 'com.google.cloud.tools.appengine'
apply plugin: 'war'

buildscript {
    // ...
    dependencies {
        // ...
        classpath 'com.google.cloud.tools:appengine-gradle-plugin:+'
    }
}

dependencies {
    // ...
    providedCompile group: 'javax.servlet', name: 'servlet-api', version: '2.5'
    compile group: 'com.google.cloud.tools', name: 'appengine-gradle-plugin', version: '1.3.5'
}


@SpringBootApplicationのクラスにSpringBootServletInitializerを継承させます。
(SpringBootServletInitializerを継承したクラスをここ以外の場所に置いてもAppEngineでは起動しませんでした。)

ServerApplication.kt

// ...
@SpringBootApplication
class ServerApplication : SpringBootServletInitializer() {
    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(ServerApplication::class.java)
    }
}

fun main(args: Array) {
    runApplication(*args)
}



上記ドキュメントの"Out of memory errors"の対処法だけ、本文中の方法では直らなかったので、以下のページを参考にしました。

springboot-appengine-standard logging configuration · Issue #284 · GoogleCloudPlatform/getting-started-java · GitHub
https://github.com/GoogleCloudPlatform/getting-started-java/issues/284#issuecomment-349119391

application.properties

# ...
logging.level.root=INFO
logging.level.org.springframework.test.web.servlet.result=DEBUG


あとはローカルならappengineRun、デプロイならappengineDeployを実行すればいけるはずです。(もちろん諸々の初期設定は必要ですが。)

3. Cloud SQLに接続する

こちらもドキュメント通りに進めますが、設定の方法に注意が必要です。

Google Cloud SQL の使用 | Java の App Engine スタンダード環境 | Google Cloud
https://cloud.google.com/appengine/docs/standard/java/cloud-sql/?hl=ja

build.gradle

dependencies {
    // ...
    runtime('mysql:mysql-connector-java')
    compile group: 'com.google.cloud.sql', name: 'mysql-socket-factory', version: '1.0.5'
}


appengine-web.xml

<use-google-connector-j>true</use-google-connector-j>


application-development-google.properties

spring.datasource.url=jdbc:google:mysql://インスタンス接続名/DB名
spring.datasource.username=ユーザ名
spring.datasource.password=パスワード
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
spring.datasource.driver-class-name=com.mysql.jdbc.GoogleDriver


DriverClassの指定はこれでいいのかよくわかりませんが、とりあえずこの指定で動いているのでこうしておきます。

---

以上が三日間の苦闘の末の成果(?)です。最終的な設定ファイルが定まるまで53回もデプロイしていました。ほとんど実験ですね。ドキュメントをちゃんと読むのってやっぱり大事です。

VPSにデプロイするのと比べると、楽ができる部分が多そうなので、うまいこと活用していきたいと思います。あとはどのくらいの料金がかかりそうなのかですね。

クライアント側ではangularの新しいやつ(一体バージョンはいくつまで上がったのでしょうか?)を使おうと思っているので、また何かあれば記事にします。
   Kotlin/Spring Boot/GAE    TB(0)    CM(0)    EDIT    ページ↑

Kotlin + Mockitoでany<T>()やeq<T>()を使いたい

今年頭から、APIの開発をMojolicious/PerlからSpring Boot/Kotlinに移行しています。今回は初のKotlinの記事です。

KotlinはJava完全互換を謳っているので、当然Java向けのモックライブラリであるMockitoもきちんと動くということになるはずです。しかし、実際には以下のような状況ではうまく動きません。

java - Is it possible to use Mockito in Kotlin? - Stack Overflow
http://stackoverflow.com/questions/30305217/is-it-possible-to-use-mockito-in-kotlin

まずモック対象のメソッドがこんな感じだとします。
ここでのポイントは引数の`String`がNon-Nullになっていることです。
class UserService{ 
    fun findUserByToken(token: String) {
    // 定義
    }
}

そしてモックを実行します。「特定の値が指定されたとき」であれば次の書き方ができます。
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken("token string")
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.eq("token string"))

条件を特に指定しない場合、次の書き方ができます。
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.anyString())
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.any())
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.anyObject())

さて、この記事にたどり着いた人ならお分かりかと思いますが、これらの5つの記法のうち、実際に動くのは最初の一つのみです。
こんな例外が発生します。
java.lang.IllegalStateException: Mockito.any() must not be null

その原因はMockitoの実装にあります。MockitoのSourceを見ればわかるのですが、`any()`や`anyObject()`を呼ぶと何が起きるのかというと、
①内部で条件の登録を処理する
②メソッドの戻り値としては`null`を返す
という具合です。
https://github.com/mockito/mockito/blob/master/src/main/java/org/mockito/ArgumentMatchers.java
public static <T> T any() {
    return anyObject();
}

public static <T> T anyObject() {
    reportMatcher(Any.ANY);
    return null;
}

この戻り値`null`はどこに行くかというと、mockされた`findUserByToken(token: String)`に行きます。が、問題はこのtokenはNon-NullなStringだということです。親切なKotlinは、このようなNon-Nullな引数を持つメソッドを呼び出すときに、事前に「渡す予定の」引数がnullかどうかチェックしてくれます。

つまり、テストコードがKotlinのSourceからJavaのコードにコンパイルされるときに、このようなメソッド呼び出しは
①引数として渡す予定の値がnullかどうかチェックする、nullなら例外を投げる
②実際にメソッドを呼び出す
という2段階の処理として生成されるということです。KotlinのNon-Nullの実装の機構がそうなっている、という話ですね。
そのため、any()の戻り値がmockされたuserServiceのインスタンスに届く前に、Kotlinのnullチェックに引っ掛かってしまい、敢え無く例外で落ちる、ということになるわけです。

解決策は、このnullチェックを何とかしてくぐり抜ければ良いということになりますが、そのための方法が、「総称型扱いでnullを渡す」というものです。この場合だと、「なぜか」上記のコンパイル時の処理で①(渡す引数のnullチェック)が省略されます。Kotlinの実装依存の解決策といえばそう、ということになりますね。
fun <T> nullNotNull(): T {
    return null as T
}

// 落ちる
userServiceMock.findByToken(null);

// 落ちる
userServiceMock.findByToken(null as String);

// 落ちない
userServiceMock.findByToken(nullNotNull());
※Kotlinでは「渡す予定の引数のnullチェック」と「引数としてもらった変数のnullチェック」の両方を行うので、これがmockでなければ三つとも落ちます。ここではMockitoが中身をオーバライドするので、引数としてもらった変数のチェックは実行されません。

一般的な解決策としては、次のようなclassを作っておいて、Mockito.any()やMockito.eq()の代わりに使えばなんとかなります。
class KotlinMockitoHelper {
    companion object {
        fun <T> any(): T {
            return Mockito.any()
                    ?: null as T
        }

        fun <T> eq(value: T): T {
            return if (value != null)
                Mockito.eq(value)
            else
                null
                        ?: null as T
        }
    }
}

なんだかすっきりしない結論ですね…。
とりあえず、Stack Overflowで出ていた解決策でどうして解決できるんだ??と調べてみた次第です。改めてポイントをまとめると、
①Mockito.any()の戻り値はnullである、
②<T>に対してはnullチェックをサボる、というところです。
これだと、今後の実装でまた動かなくなる、ということもあり得ると思います。まあ、モックするというテスト手法は下火になりつつあるという話もあるので、モックせずにうまく書いていく、という方針をとるのも手なのかもしれません。
   Kotlin/Spring Boot/GAE    TB(0)    CM(0)    EDIT    ページ↑

プロフィール

JDB Luigi

Author:JDB Luigi
どこにでもいるようなありふれた人間・・・という訳でもなく、かと言って怪しい宗教を信仰する変人という訳でも無い。

基本的に掲載しているコード等は煮ていただいても焼いていただいても結構ですが、利用は自己責任にてお願いいします。
また、バグ・アドバイス等もしあればよろしくお願いします。