しおしお

IntelliJ IDEAのことなんかを書いてます

Spring Boot(MVC)でCORSを色々試してみた

雰囲気でCORSの設定してたので設定によってどんな結果になるか調べてみたよ。

クライアントとサーバのコード

クライアント

  • getとpostを行うだけの簡単なhtmlを使って試してみます
  • IntelliJさんについている簡易サーバからSpring Bootなアプリケーションにリクエストを投げる感じにします
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script
      src="https://code.jquery.com/jquery-3.4.1.min.js"
      integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
      crossorigin="anonymous"></script>
  <script>
    $(document).ready(function () {
      $('#get').click(function () {
        axios.get('http://localhost:8080/hello')
            .then(function (response) {
              console.log('get', response.status, response.data);
            });
      })
      
      $('#post').click(function () {
        axios.post('http://localhost:8080/hello', {
          id: 1,
          text: 'hello'
        })
            .then(function (response) { 
              console.log('post', response.status)
            })
      })
    });
  </script>
</head>
<body>
<button id="get">GET</button>
<button id="post">POST</button>
</body>
</html>

サーバ

  • getとpostのリクエストを簡単に処理するだけのやつです
@RestController
@RequestMapping("/hello")
class HelloController {

    @GetMapping
    fun get(): String {
        return "hello"
    }

    @PostMapping
    fun post(@RequestBody body: Body) {
        println("body = ${body}")
    }

    data class Body(
            private val text: String
    )
}

何も設定を行わずにクライアントからリクエストを投げてみる

デフォルト状態で、異なるドメインからリクエスト投げるとこんな感じにエラーになりますね。 f:id:sioiri:20190611090037p:plain

Controllerに@CrossOriginアノテーションを設定してみる

Controllerに対して、@CrossOriginアノテーションを設定すると、Controller毎に細かく設定ができます。 また、ハンドラメソッドに対して設定した場合には、より細かく設定できるようになります。

Controllerにアノテーションを設定した場合

Controllerクラスに@CrossOriginアノテーションを設定すると、このController内のハンドラメソッドがすべて許可されるようになります。 @CrossOriginvalue(origins)allowedHeadersを設定することで、より細かな設定もできるようになっています。

@RestController
@RequestMapping("/hello")
@CrossOrigin
class HelloController {
  // 省略
}

実行結果

@CrossOriginアノテーションを設定したことで、ちゃんと異なるドメインからの要求が処理されるようになりました。 f:id:sioiri:20190611095406p:plain

ハンドラメソッドにアノテーションを設定した場合

ハンドラメソッドに@CrossOriginアノテーションを設定すると、そのリクエストのみ許可されるようになります。 もし、Controllerにも設定されていた場合、Controllerとハンドラメソッドの設定がマージされる感じになります。

@RestController
@RequestMapping("/hello")
class HelloController {

    @GetMapping
    fun get(): String {
        return "hello"
    }

    @PostMapping
    @CrossOrigin
    fun post(@RequestBody body: Body) {
        println("body = ${body}")
    }

    data class Body(
            private val id: Int,
            private val text: String
    )
}

実行結果

今回は、POSTの処理だけに@CrossOriginアノテーションを設定したので、POST処理だけ成功しています。 f:id:sioiri:20190611093124p:plain

WebMvcConfigurerを使って横断的に設定してみる

WebMvcConfigurer#addCorsMappings を使うと、アプリケーション全体に対して設定を行うことができるようになります。 デフォルトでは、GET、HEAD、POSTしか許可されていないので、それ以外を許可する場合には allowedMethods を設定して上げる必要があります。

設定は、こんな感じにWebMvcConfigurerのBeanを登録してあげるだけですね。

@Configuration
class WebConfig {
    @Bean
    fun config(): WebMvcConfigurer {
        return object: WebMvcConfigurer {
            override fun addCorsMappings(registry: CorsRegistry) {
                registry.addMapping("/**")
                        .allowedOrigins("http://localhost:63342")
                        .allowedMethods("GET", "POST", "PUT")
            }
        }
    }
}

実行結果

個別のControllerに@CrossOriginアノテーションを設定しなくても処理が成功するようになりました。
f:id:sioiri:20190611095214p:plain

java.nio.file.Files#linesのclose忘れをIntelliJさんに教えてもらおう

デフォルトの設定では、AutoCloseableのclose忘れを教えてもらえないので、教えてもらえるようにInspectionsの設定を変更してあげましょう

設定手順

下の画像の流れで選択して、 AutoCloseable used without 'try'-with-resources にチェックを入れてあげましょう。 これで、Files#linesだけではなくAutoCloseableのtry-with-resources外での利用やclose忘れを検知できるようになります。

f:id:sioiri:20190604083414p:plain

Inspectionの実行結果

ちゃんと警告出るようになりました!

f:id:sioiri:20190604083903p:plain

Ktor 1.2.0(rc)で追加されたThymeleaf Featureを試してみた

KtorでThymeleafを試す - しおしおを書いたけど、Ktor 1.2.0(今はまだrc)からThymeleafが使えるようになるみたいなので試してみた。

gradle関連

gradle.properties

  • Ktorのバージョンを1.2.0系に設定する
ktor_version=1.2.0-rc2

build.gradle

  • ktor-thymeleaf をdependenciesに追加する
implementation "io.ktor:ktor-thymeleaf:$ktor_version"

サーバサイド

  • Thymeleaf をインストールする
  • installに渡すブロック内では、 Thymeleaf に関する設定を行う
    • この例では、クラスパス配下のテンプレートを使用する設定としている
  • respondには、viewでThymeleafが使われるようにするためにThymeleafContentを指定する
    • ThymeleafContentにはテンプレートの名前と、テンプレート内で使用するモデルを指定する
@Suppress("unused") // Referenced in application.conf
fun Application.module() {
    install(Thymeleaf) {
        setTemplateResolver(ClassLoaderTemplateResolver().apply { 
            prefix = "templates/"
            suffix = ".html"
            characterEncoding = "utf-8"
        })
    }

    routing {
        get("/") {
            call.respond(ThymeleafContent("test", mapOf("users" to Users(listOf(User(1, "user1"), User(2, "user2"))))))
        }
    }
}

class Users(private val users: List<User>) : Iterable<User> {
    override fun iterator(): Iterator<User> {
        return users.iterator()
    }

}

data class User(val id: Long, val name: String)

テンプレート

  • サーバサイドで設定したモデルの内容を表示するだけのシンプルなものにしています
<!DOCTYPE html >
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<ul>
  <!--/*@thymesVar id="users" type="siosio.Users"*/-->
  <!--/*@thymesVar id="user" type="siosio.User"*/-->
  <li th:each="user : ${users}" th:text="${user.id + ':' + user.name}"></li>
</ul>
</body>
</html>

実行結果

動きましたね。

f:id:sioiri:20190512072958p:plain

KtorでThymeleafを試す

Ktor標準ではThymeleafに対応していなかったので、Thymeleaf使えるかどうか試してみた感じです。
※Ktor1.2以降からThymeleaf機能が追加されたようです→Ktor 1.2.0(rc)で追加されたThymeleaf Featureを試してみた - しおしお

Thymeleaf用のFeatureを作る

ktor/FreeMarker.kt at master · ktorio/ktor · GitHubを参考に…
やってることは、こんな感じです。

  • ClassLoaderTemplateResolver の生成と初期設定
  • アプリケーションからの戻りが ThymeleafContent の場合に、Thymeleafを使ったレンダリング
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature
import io.ktor.http.content.OutgoingContent
import io.ktor.response.ApplicationSendPipeline
import io.ktor.util.AttributeKey
import io.ktor.util.cio.bufferedWriter
import kotlinx.coroutines.io.ByteWriteChannel
import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver

class ThymeleafContent(
        val template: String,
        val model: Map<String, Any>?
)

class Thymeleaf(private val engine: TemplateEngine) {

    companion object Feature : ApplicationFeature<ApplicationCallPipeline, ClassLoaderTemplateResolver, Thymeleaf> {
        override val key: AttributeKey<Thymeleaf> = AttributeKey("thymeleaf")

        override fun install(pipeline: ApplicationCallPipeline,
                             configure: ClassLoaderTemplateResolver.() -> Unit): Thymeleaf {
            val config = ClassLoaderTemplateResolver(Thread.currentThread().contextClassLoader)
                    .apply {
                        prefix = "templates/"
                        suffix = ".html"
                        characterEncoding = "UTF-8"
                    }
                    .apply(configure)
            val engine = TemplateEngine()
            engine.setTemplateResolver(config)
            val feature = Thymeleaf(engine)
            pipeline.sendPipeline.intercept(ApplicationSendPipeline.Transform) { value ->
                if (value is ThymeleafContent) {
                    val response = feature.process(value)
                    proceedWith(response)
                }
            }
            return feature
        }
    }

    private fun process(content: ThymeleafContent): ThymeleafOutgoingContent {
        return ThymeleafOutgoingContent(
                engine,
                content
        )
    }

    private class ThymeleafOutgoingContent(
            val engine: TemplateEngine,
            val content: ThymeleafContent
    ) : OutgoingContent.WriteChannelContent() {
        override suspend fun writeTo(channel: ByteWriteChannel) {
            channel.bufferedWriter(Charsets.UTF_8).use {
                val context = Context().apply {
                    setVariables(content.model)
                }
                engine.process(content.template, context, it)
            }
        }
    }
}

アプリケーションを作る

サーバサイド

  • 上で作ったThymeleaf Featureを使えるようにするためにインストールする
  • 処理結果として、ThymeleafContentを返す
package siosio

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.routing

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    install(Thymeleaf)

    routing {
        get("/hello") {
            call.respond(ThymeleafContent("index", mapOf("user" to User(1, "しおしお"))))
        }
    }
}

data class User(val id: Long, val name: String)

Thymeleaf用テンプレート

単純にUserクラスの内容を出力しているだけですね

<!DOCTYPE html >
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" xml:lang="ja" lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

<!--/*@thymesVar id="user" type="siosio.User"*/-->
<ul>
  <li th:text="${user.id}"></li>
  <li th:text="${user.name}"></li>
</ul>
</body>
</html>

実行結果

動きましたね(๑•̀ㅂ•́)و✧

f:id:sioiri:20190422063613p:plain

elastic4sでcase classを使った登録と検索

elastic4sを使って↓を試してみました。

  • case classの内容をElasticsearchに登録する
  • 検索結果をcase classマッピングして取得する

build.sbt

build.sbtには、elastic4sを追加します。

val elastic4sVersion = "6.5.1"
libraryDependencies ++= Seq(
  "com.sksamuel.elastic4s" %% "elastic4s-core" % elastic4sVersion,

  "com.sksamuel.elastic4s" %% "elastic4s-http" % elastic4sVersion,
)

データの登録

  • 登録対象のデータを持つ case class を定義します
  • Indexableを継承してcase classの内容を登録用のjsonに変換します
    この例では、elastic4sで定義されているObject Mapperを使って単純にjsonに変換しています
  • データの登録では、docに登録対象のデータを持つcase classを指定します
case class Data(id: Long, name: String)

implicit object SampleIndexable extends Indexable[Data] {

  override def json(t: Data): String = JacksonSupport.mapper.writeValueAsString(t)
}

private val client = ElasticClient(ElasticProperties("http://localhost:9200"))

client.execute {
  indexInto("sample" / "_doc").id("1").doc(Data(1, "sample1"))
}.await

データの検索

  • HitReaderを継承して検索結果の内容をcase classに変換します
  • 検索結果は、toをつかてcase classに変換して受け取ります
implicit object SampleHitReader extends HitReader[Data] {

  override def read(hit: Hit): Try[Data] = {
    Try(Data(hit.sourceAsMap("id").toString.toLong, hit.sourceAsMap("name").toString))
  }
}

val result = client.execute {
  search("sample") termQuery("name", "sample1")
}.await.result.to[Data]
println(result.headOption)

実行結果

登録したデータが検索できて、Dataクラスにマッピングできてるのが確認できます。

Some(Data(1,sample1))

IntelliJ IDEA 2019.1でのJUnit実行について…

IntelliJ IDEA 2019.1からはデフォルトではGradleでテスト実行されるようになったようです。

例えばテストを実行すると、こんな感じにGradleで実行されます。 f:id:sioiri:20190330074358p:plain

実行方法の設定変更方法

Settings -> Build, Execution, Deployment -> Build Tools -> Gradle -> Runner(↓の画面) から設定を変更します。 f:id:sioiri:20190330075733p:plain

設定内容はこんな感じになってます。

  • IntelliJでテスト実行したい場合は、Platform Test Runnerを選択します
  • テスト実行のたびに選択したい場合には、Chose per testを選択します
  • Gradleでテスト実行したい場合は、Gradle Test Runnerを選択します。

JUnit5&Gradle Test Runnerの組み合わせ

JUnit5を使っている場合、build.gradleにJUnit5用の設定がないとだめなので足してあげましょう。 Gradle&Junit5にあるように、build.gradleにいかが必要となります。

test {
  useJUnitPlatform()
}

Dockerコンテナ上で動いているJavaアプリケーションをIntelliJさんからデバッグしてみる

IntelliJ IDEA 2019.1 Release Candidate is Out! | IntelliJ IDEA Blogにあるように、 2019.1 からの新機能ですね。

Dockerfileの準備

デバッグ対象のアプリケーションをDockerで動かすためのDockerfileを作ります。
※docker-compose.ymlでもデバッグいけるみたいです。

とりあえず、デバッグできることだけを確認したいので、シンプルにこんな感じにしています。

FROM java:8

COPY build/libs/sample.jar .

CMD $JAVA_HOME/bin/java -jar sample.jar

実行構成の作成

  • 実行構成の追加からRemoteを選びます

f:id:sioiri:20190328085903p:plain

  • ポート番号やJavaのバージョンなどの設定を行います
    あとで必要になるので、Command line arguments for remote JVMの内容はコピーしておきましょう

f:id:sioiri:20190328090053p:plain

  • Before launchLaunch Docker before debugを選びます

f:id:sioiri:20190328090256p:plain

  • Launch Docker before debug の設定を行います
  • Custom Commandには、コピーしたCommand line arguments for remote JVMの内容を使ってリモートデバッグ可能な状態でアプリケーションを起動するようコマンドをを設定します((Custom Commandの設定、再度設定画面開くと消えちゃってますがおそらく設定自体は生きています))
  • 今回は、 java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar sample.jarと設定しています

f:id:sioiri:20190328090543p:plain

デバッグ実行してみる

こんな感じにコンテナ上で実行されているアプリケーションに対するデバッグが行なえます。 f:id:sioiri:20190328100035g:plain

・・・

Custom Command での上書き設定がちょっとめんどくさい感じですね