文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

GAE/Java8+kotlin+Spark Frameworkをデプロイするまでに起きたエラーのまとめ

GAE/Java8がベータに達したので、早速試したわけですが、実はデプロイしてサイトが動くまでにアレコレ躓いた部分があったので、その対応をまとめます。


f:id:treeapps:20170917230836p:plain

www.bunkei-programmer.net

先程↑この記事を書いたわけですが、実はこれを計測するまで色々躓いて嵌ったポイントがあったので、全部まとめておきます。

環境

前回の記事と同じです。

IDE IntelliJ IDEA Ultimate v2017.2.4
GAE Runtime Java8 beta
ビルド・デプロイ Gradle v4.2
Language kotlin v1.1.4-3
Framework Spark Framework v2.6.0
GAEリージョン asia-northeast1

エラーとその対応

Q、gradleの場合appengine-web.xml, web.xmlってどこに置くの?

A、$PROJECT_ROOT/src/main/webapp/WEB-INF配下に置く。

gradleでappengine-gradle-pluginを使ってデプロイを試みた時、以下のエラーが起きました。「src/main/appengine」と表示されているので、ここに配置するのかな?と思いました。

FAILURE: Build failed with an exception.

* What went wrong:
A problem was found with the configuration of task ':appengineStage'.
> Directory '/Users/tree/IdeaProjects/string-utility/src/main/appengine' specified for property 'stagingConfig.appEngineDirectory' does not exist.

言われたとおりに「src/main/appengine」配下にappengine-web.xmlとweb.xmlを配置してgradleでデプロイを試みると、今度は以下のエラーが起きました。

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':appengineStage'.
> app.yaml not found in the App Engine directory.

app.yamlが無いと言われました。GAE/Javaスタンダード環境の場合はapp.yamlではなくappengine-web.xmlを使う(app.yamlも使える???)ので、「src/main/appengine」に配置せず、「src/main/webapp/WEB-INF」に配置すると、app.yamlを要求されなくなります。

Q、IntelliJ IDEAでデプロイすると全ページNotFoundになる。

A、Gradleでビルドしないとlib配下にjarが展開されないから。

これは最初嵌まりました。てっきりIntelliJから簡単にデプロイできてやったー!と思っていたのですが、一向に動かず、調べてみるとサーバ側にjarファイルが全く配置されていませんでした。つまり、ビルドされていなかったわけです。

結局IntelliJ上で、メニュー -> Tools -> Google Cloud Tools -> Deploy To Appengine... で表示される以下からGAEに正常にデプロイする方法は解りませんでした。

f:id:treeapps:20170924224816p:plain

結局gradleからデプロイする事で解決しました。

Q、ローカルでは普通に動くのに、GAEにデプロイしたらNotFoundになる。

A、web.xmlにSparkFilterの設定が必要。

GAE/Java8はJetty9で動いており、warファイルで動くようで、web.xmlに設定が無いとJettyがエントリーポイントを見つける事ができず起動できない、ということです。

sparkjava.com

<filter>
    <filter-name>SparkFilter</filter-name>
    <filter-class>spark.servlet.SparkFilter</filter-class>
    <init-param>
        <param-name>applicationClass</param-name>
        <param-value>com.company.YourApplication</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>SparkFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

com.company.YourApplicationの部分を、起動用のクラスを指定して下さい。

例えば「com.hogehoge.Main」等と入力します。.ktという拡張子の記述は不要です。または、kotlinでもFilterを設定可能です。

Q、./gradlew appengineDeploy時に認証エラーが起きる。

A、OAuth認証とgcloudのアカウント設定が必要です。

./gradlew appengineDeployを実行すると、以下のエラーが発生しました。

$ ./gradlew appengineDeploy
ERROR: (gcloud.app.deploy) You do not currently have an active account selected.
Please run:

  $ gcloud auth login

to obtain new credentials, or if you have already logged in with a
different account:

  $ gcloud config set account ACCOUNT

to select an already authenticated account to use.

FAILURE: Build failed with an exception.

* What went wrong:

ここに書いてあるように、順番に2つのコマンドを実行します。「gcloud auth login」でOAuth認証し(ブラウザが自動起動するので簡単です)、「gcloud config set account アカウント」でgcloud設定をします。アカウント部分はプロジェクトIDを設定します。

これで「./gradlew appengineDeploy」が通るようになります。

Q、./gradlew appengineDeployでバージョンがyyyyMMddHHmmssになってしまう。

A、gradle側にバージョンを指定します。

具体的には、以下の「version」の部分です。「version」指定をしない場合、自動的にバージョンがyyyyMMddHHmmssになります。

appengine {  // App Engine tasks configuration
    deploy {   // deploy configuration
        version = 'test'
        stopPreviousVersion = true  // default - stop the current version
        promote = true              // default - & make this the current version
    }
}

Q、java.lang.IllegalAccessException: Class spark.servlet.SparkFilter can not access a member of class com.hoge.Application with modifiers "private"

A、SparkApplicationをimplementsする必要があります。

無事GAEにデプロイしてみたものの、以下のエラーが発生しました。

java.lang.IllegalAccessException: Class spark.servlet.SparkFilter can not access a member of class com.stringutility.Application with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.Class.newInstance(Class.java:437)
	at spark.servlet.SparkFilter.getApplication(SparkFilter.java:104)
	at spark.servlet.SparkFilter.getApplications(SparkFilter.java:131)
	at spark.servlet.SparkFilter.init(SparkFilter.java:66)
	at org.eclipse.jetty.servlet.FilterHolder.initialize(FilterHolder.java:139)
	at org.eclipse.jetty.servlet.ServletHandler.initialize(ServletHandler.java:873)
	at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:349)
	at org.eclipse.jetty.webapp.WebAppContext.startWebapp(WebAppContext.java:1406)
	at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1368)
	at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:778)
	at org.eclipse.jetty.servlet.ServletContextHandler.doStart(ServletContextHandler.java:262)
	at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:522)
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:68)
	at com.google.apphosting.runtime.jetty9.AppVersionHandlerMap.createHandler(AppVersionHandlerMap.java:244)
	at com.google.apphosting.runtime.jetty9.AppVersionHandlerMap.getHandler(AppVersionHandlerMap.java:182)
	at com.google.apphosting.runtime.jetty9.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:97)
	at com.google.apphosting.runtime.JavaRuntime$RequestRunnable.dispatchServletRequest(JavaRuntime.java:650)
	at com.google.apphosting.runtime.JavaRuntime$RequestRunnable.dispatchRequest(JavaRuntime.java:612)
	at com.google.apphosting.runtime.JavaRuntime$RequestRunnable.run(JavaRuntime.java:582)
	at com.google.apphosting.runtime.JavaRuntime$NullSandboxRequestRunnable.run(JavaRuntime.java:776)
	at com.google.apphosting.runtime.ThreadGroupPool$PoolEntry.run(ThreadGroupPool.java:263)
	at java.lang.Thread.run(Thread.java:745)

GAE/JavaはJetty上でwarファイルを起動する必要があります。つまり、サーブレットで動きます。

サーブレットがエントリーポイントを見つけ出せるように、SparkApplicationをimplementsします。

kotlinの場合は以下のように記述します。

object Application : SparkApplication {

    data class Person(
            val cd: Int,
            val name: String
    )

    override fun init() {
        get("/api/json", Route { req, res ->
            Person(cd = 123, name = "やっほー")
        }, JsonTransformer())
    }

}

これでOKです。  ・・・・本当でしょうか?

Q、まだ java.lang.IllegalAccessException: Class spark.servlet.SparkFilter can not access a member of class com.hoge.Application with modifiers "private" が起きる。

A、シングルトンでは駄目なようです。

object Application : SparkApplication {

これを以下に変更します。

class Application : SparkApplication {

これでようやくデプロイしたサイトが正常動作するようになります。

しかしobjectをclassに変更すると、以下がコンパイルエラーになってしまい、今度はローカルでSparkが起動できなくなります。

//    @JvmStatic
    fun main(args: Array<String>) {

ここから察するに、GAE用のエントリーポイントと、ローカル用のエントリーポイントは分けた方がいい、という事でしょうかね。

そもそもローカルのGAEがまだjava1.8+jetty9に対応しておらず、SparkApplicationを実装したクラスを直接実行するしかなく、まだまだ開発環境に問題を抱えています。

雑感

GAE/Javaをkotlinで動かす例がまだネット上では少なく、色々嵌ったので、情報を残してみました。

一応公式のサンプルプロジェクトがあるものの、本当に最低限で、更にgradleではなくmavenなのでした。

github.com

まだまだ今後ハマるポイントが増える可能性がありますが、随時更新しようと思います。