しおしお

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

古のspring-data-jpaでNativeQueryとPageableを使ってハマった件

Spring Boot 1.5系のバッチ的な処理に急ぎ手を入れる必要があって、RepositoryにNativeQueryでページングするようなメソッド追加したら謎のエラーでめっちゃハマったお話です。*1

Spring Bootバージョン

1.5系の古いやつですね。過去の遺産じゃないかぎり使うことはないですね…

  id 'org.springframework.boot' version '1.5.22.RELEASE'

Repositoryの実装

Repositoryでは、NativeQueryを使ってページングさせるようなメソッドを定義しています。

@Query(
        nativeQuery = true,
        value = "select * from users ",
        countQuery = "select count(*) from users"
)
Page<UsersEntity> find(Pageable pageable);

動作検証用のコード

動作検証用にこんな感じのテストコードを書いています。

@Test
public void test() throws Exception {
    final Page<UsersEntity> result = sut.find(new PageRequest(0, 10));
    System.out.println("result = " + result);
}

実行結果

実行すると、InvalidJpaQueryMethodExceptionが発生してしまいます。

Caused by: org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting and/or pagination in method public abstract org.springframework.data.domain.Page siosio.datajpaexample.repository.UsersRepository.find(org.springframework.data.domain.Pageable)
    at org.springframework.data.jpa.repository.query.NativeJpaQuery.<init>(NativeJpaQuery.java:58)

例外が発生している、NativeJpaQueryの実装(↓)を見てみるとPageableパラメータを持つ場合には#pageableという文字列がSQL内にないとダメなようです。

public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
        EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {

    super(method, em, queryString, evaluationContextProvider, parser);

    Parameters<?, ?> parameters = method.getParameters();
    boolean hasPagingOrSortingParameter = parameters.hasPageableParameter() || parameters.hasSortParameter();
    boolean containsPageableOrSortInQueryExpression = queryString.contains("#pageable")
            || queryString.contains("#sort");

    if (hasPagingOrSortingParameter && !containsPageableOrSortInQueryExpression) {
        throw new InvalidJpaQueryMethodException(
                "Cannot use native queries with dynamic sorting and/or pagination in method " + method);
    }
}

SQL#pageableを追加して再実行してみよう

Repositoryの実装を修正して、SQL#pageableを追加してみます。

@Query(
        nativeQuery = true,
        value = "select * from users #pageable",
        countQuery = "select count(*) from users"
)
Page<UsersEntity> find(Pageable pageable);

対応後でも残念ながら別のエラーで落ちてしまいましたね…SQLを実行するところまではいっているようなのでどんなSQLを実行しようとしているのか見てみましょう。

could not execute query; nested exception is org.hibernate.exception.GenericJDBCException: could not execute query
org.springframework.orm.jpa.JpaSystemException: could not execute query; nested exception is org.hibernate.exception.GenericJDBCException: could not execute query
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:333)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244)

まさかのSQL内の#pageableが残ったままになってますね(´・ω・`) f:id:sioiri:20190817064721p:plain

エラーの回避方法

Spring Data JPA @Query | Baeldung4.3. Spring Data JPA Versions Prior to 2.0.4に回避方法が書いてありますね。
どうやら、Spring Data JPA 2.0.4より前の場合には、バグ?的なものがあるようです。

リンク先の回避方法を真似て、#pageableSQLコメントとして書いてあげます。大事なのは、#pageableの前後に改行を入れることですね。 改行忘れると、SQLの最後にくっつくlimitまでコメントになってしまいます。

@Query(
        nativeQuery = true,
        value = "select * from users \n -- #pageable \n",
        countQuery = "select count(*) from users"
)
Page<UsersEntity> find(Pageable pageable);

これで正常に実行できるようになりました。

result = Page 2 of 1 containing UNKNOWN instances

Spring Data JPAを2.0.4以降にしてみると

最初に#pageableの実装がないよと例外投げてたNativeJpaQueryから#pageableに関する実装が消えていますね。

public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryString,
        EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) {

    super(method, em, queryString, evaluationContextProvider, parser);

    Parameters<?, ?> parameters = method.getParameters();

    if (parameters.hasSortParameter() && !queryString.contains("#sort")) {
        throw new InvalidJpaQueryMethodException(
                "Cannot use native queries with dynamic sorting in method " + method);
    }
    this.resultType = getTypeToQueryFor();
}

ということで、Repositoryの実装はこんな感じで#pageableなしで書けるようになります。

@Query(
        nativeQuery = true,
        value = "select * from users",
        countQuery = "select count(*) from users"
)
Page<UsersEntity> find(Pageable pageable);

#pageableは消えたけど、#sortに関する実装がまだ残っているのでどうなるのか見てみます。 Repositoryの実装をソートのみに変えてみます。SQLには、#sortを含めてあげます。

@Query(
        nativeQuery = true,
        value = "select * from users #sort"
)
List<UsersEntity> find(Sort sort);

エラーにならず実行できました。#sortに関する処理は正しく動くようです。

result = [siosio.datajpaexample.entity.UsersEntity@6dff619a, siosio.datajpaexample.entity.UsersEntity@3f73d455, siosio.datajpaexample.entity.UsersEntity@24df2d20]

まとめ

バージョンアップして幸せになりたい。

おわり。

*1:色々と制約があってこうするしかなかった…

法人番号のチェックデジットを算出してみた

法人番号のチェックデジットの求め方は、法人番号を確認するページにあるチェックデジットの計算リンクから確認できます。

チェックデジットを求めるコード

川口市の法人番号を使ってチェックデジットを求めるコードを書いてみました。 頭ひと桁がチェックデジットなので結果として2が求められればOKですね。

fun main() {
    val corporateNumber = "2000020112038"
    data class Temp(val odd: Int, val even: Int) {
        fun addOdd(value: Int) = this.copy(odd = odd + value)
        fun addEven(value: Int) = this.copy(even = even + value)
    }

    val checkDigit = corporateNumber
            .substring(1)
            .reversed()
            .foldIndexed(Temp(0, 0)) { index, acc, c ->
                when ((index + 1) % 2) {
                    0 -> acc.addOdd(c.toInt())
                    else -> acc.addEven(c.toInt())
                }
            }
            .let { (it.odd * 2) + it.even }
            .let { 9 - (it % 9) }
    println("checkDigit = ${checkDigit}")
}

実行結果

チェックデジットが正しく求められたっぽいことがわかります!
f:id:sioiri:20190719233927p:plain

Spring Batchのchunkステップを並列で実行してみた

Spring Batchのchunkステップの各処理を並列実行してみました。

並列実行するためのジョブ定義

Readerで1から100までの連番を生成して、Writerでtestテーブルにbatch insertするだけのシンプルなchunkステップを持つジョブを使って試します。 並列実行されていることを確認するためにWriterでitemsをログに出力しています。

@Configuration
class BatchConfiguration(
        private val jobBuilderFactory: JobBuilderFactory,
        private val stepBuilderFactory: StepBuilderFactory,
        private val jdbcTemplate: JdbcTemplate
) {
    
    private val logger = LoggerFactory.getLogger(BatchConfiguration::class.java)
    
    @Bean
    fun job(): Job {
        return jobBuilderFactory.get("job")
                .incrementer(RunIdIncrementer())
                .start(step())
                .build()
    }

    @Bean
    fun step(): Step {
        val input = (1..100).iterator()
        return stepBuilderFactory.get("step")
                .chunk<Int, Int>(10)
                .reader(fun(): Int? {
                    return if (input.hasNext()) {
                        input.nextInt()
                    } else {
                        null
                    }
                })
                .writer {
                    logger.info("write size: ${it.size}, items: ${it}")
                    jdbcTemplate.batchUpdate("insert into test (id) values (?)", object : BatchPreparedStatementSetter {
                        override fun setValues(ps: PreparedStatement, i: Int) {
                            ps.setInt(1, it[i])
                        }

                        override fun getBatchSize(): Int {
                            return it.size
                        }
                    })
                }
                .build()
    }
}

並列実行の構成をせずにJOBを実行した結果

並列実行の構成を行わなかった場合、Writerの処理がmainスレッドで行われていることがわかります。

2019-07-14 07:11:45.608  INFO 22744 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2019-07-14 07:11:45.623  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2019-07-14 07:11:45.633  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
2019-07-14 07:11:45.642  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
2019-07-14 07:11:45.648  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
2019-07-14 07:11:45.653  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2019-07-14 07:11:45.659  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
2019-07-14 07:11:45.666  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [61, 62, 63, 64, 65, 66, 67, 68, 69, 70]
2019-07-14 07:11:45.672  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
2019-07-14 07:11:45.679  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
2019-07-14 07:11:45.685  INFO 22744 --- [           main] s.springbatchsample.BatchConfiguration   : write size: 10, items: [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
2019-07-14 07:11:45.711  INFO 22744 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{run.id=59}] and the following status: [COMPLETED]

並列実行するための構成を追加

並列実行するためのTaskExecutorが必要となるためインジェクションします。 TaskExecutorは、org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationが生成してくれるので、特にBeanを生成しなくても利用できます。

@Configuration
class BatchConfiguration(
        private val jobBuilderFactory: JobBuilderFactory,
        private val stepBuilderFactory: StepBuilderFactory,
        private val jdbcTemplate: JdbcTemplate,
        private val taskExecutor: TaskExecutor
) {

インジェクションしたTaskExecutorをステップ構築時に設定(taskExecutorメソッドに)します。 また、並列に実行されては困る部分は、適宜同期処理を入れる必要が有ります。今回のステップではReaderの処理が同時に実行されると同じ番号を返す必要があるため@Synchronizedを追加しています。

    @Bean
    fun step(): Step {
        val input = (1..100).iterator()
        return stepBuilderFactory.get("step")
                .chunk<Int, Int>(10)
                .reader(@Synchronized fun(): Int? {
                  // 省略
                })
                .writer {
                  // 省略
                }
                .taskExecutor(taskExecutor)
                .build()
    }

実行結果

Writerで出力しているログ内容から各タスクが並列実行(ログのスレッド名からスレッド数は8)されていることがわかります。
※デフォルトのスレッド数が8となっているので、この結果となります。

2019-07-14 07:40:51.342  INFO 24520 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2019-07-14 07:40:51.403  INFO 24520 --- [         task-2] s.springbatchsample.BatchConfiguration   : write size: 10, items: [1, 7, 11, 14, 17, 20, 24, 28, 32, 36]
2019-07-14 07:40:51.404  INFO 24520 --- [         task-3] s.springbatchsample.BatchConfiguration   : write size: 10, items: [3, 5, 21, 25, 29, 33, 37, 38, 39, 40]
2019-07-14 07:40:51.405  INFO 24520 --- [         task-1] s.springbatchsample.BatchConfiguration   : write size: 10, items: [2, 8, 10, 13, 16, 18, 22, 26, 31, 35]
2019-07-14 07:40:51.406  INFO 24520 --- [         task-4] s.springbatchsample.BatchConfiguration   : write size: 10, items: [4, 6, 9, 12, 15, 19, 23, 27, 30, 34]
2019-07-14 07:40:51.436  INFO 24520 --- [         task-5] s.springbatchsample.BatchConfiguration   : write size: 10, items: [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2019-07-14 07:40:51.445  INFO 24520 --- [         task-6] s.springbatchsample.BatchConfiguration   : write size: 10, items: [51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
2019-07-14 07:40:51.452  INFO 24520 --- [         task-7] s.springbatchsample.BatchConfiguration   : write size: 10, items: [61, 62, 63, 64, 65, 66, 67, 68, 69, 70]
2019-07-14 07:40:51.460  INFO 24520 --- [         task-8] s.springbatchsample.BatchConfiguration   : write size: 10, items: [71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
2019-07-14 07:40:51.467  INFO 24520 --- [         task-2] s.springbatchsample.BatchConfiguration   : write size: 10, items: [81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
2019-07-14 07:40:51.473  INFO 24520 --- [         task-4] s.springbatchsample.BatchConfiguration   : write size: 10, items: [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
2019-07-14 07:40:51.618  INFO 24520 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{run.id=63}] and the following status: [COMPLETED]

スレッド数を変更してみる

スレッド数は、application.propertiesで設定できます。 例えば、スレッド数を4としたい場合には、下のように設定します。

spring.task.execution.pool.core-size=4

実行結果

スレッド名が、task-1からtask-4までとなっていてスレッド数が4に制限されたことがわかります。

2019-07-14 07:48:49.039  INFO 24812 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2019-07-14 07:48:49.062  INFO 24812 --- [         task-1] s.springbatchsample.BatchConfiguration   : write size: 10, items: [3, 8, 11, 15, 19, 23, 27, 31, 35, 39]
2019-07-14 07:48:49.063  INFO 24812 --- [         task-2] s.springbatchsample.BatchConfiguration   : write size: 10, items: [1, 6, 10, 13, 17, 21, 25, 29, 34, 37]
2019-07-14 07:48:49.063  INFO 24812 --- [         task-4] s.springbatchsample.BatchConfiguration   : write size: 10, items: [2, 5, 9, 14, 18, 22, 26, 30, 33, 38]
2019-07-14 07:48:49.063  INFO 24812 --- [         task-3] s.springbatchsample.BatchConfiguration   : write size: 10, items: [4, 7, 12, 16, 20, 24, 28, 32, 36, 40]
2019-07-14 07:48:49.076  INFO 24812 --- [         task-4] s.springbatchsample.BatchConfiguration   : write size: 10, items: [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2019-07-14 07:48:49.083  INFO 24812 --- [         task-3] s.springbatchsample.BatchConfiguration   : write size: 10, items: [51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
2019-07-14 07:48:49.090  INFO 24812 --- [         task-2] s.springbatchsample.BatchConfiguration   : write size: 10, items: [61, 62, 63, 64, 65, 66, 67, 68, 69, 70]
2019-07-14 07:48:49.096  INFO 24812 --- [         task-1] s.springbatchsample.BatchConfiguration   : write size: 10, items: [71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
2019-07-14 07:48:49.101  INFO 24812 --- [         task-4] s.springbatchsample.BatchConfiguration   : write size: 10, items: [81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
2019-07-14 07:48:49.107  INFO 24812 --- [         task-3] s.springbatchsample.BatchConfiguration   : write size: 10, items: [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
2019-07-14 07:48:49.178  INFO 24812 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{run.id=64}] and the following status: [COMPLETED]

ステップで使用できるスレッド数を制限する

ステップ構築時に、throttleLimitを設定することで制限することができます。 デフォルトでは、スレッド数が8でthrottleLimitが4なので、スッテップでは同時に4スレッドのみが使われます。 多分ですが、Flowを使用して同時に複数のステップを実行するような構成とした場合に使うのかなと思います。

今回は、throttleLimitを2にしてWriterにスリープ処理を入れて制限がかかっていることを確認しやすくしています。

    @Bean
    fun step(): Step {
        val input = (1..100).iterator()
        return stepBuilderFactory.get("step")
                .chunk<Int, Int>(10)
                .reader(@Synchronized fun(): Int? {
                  // 省略
                })
                .writer {
                    logger.info("write size: ${it.size}, items: ${it}")
                    TimeUnit.SECONDS.sleep(5)
                    jdbcTemplate.batchUpdate("insert into test (id) values (?)", object : BatchPreparedStatementSetter {
                        override fun setValues(ps: PreparedStatement, i: Int) {
                            ps.setInt(1, it[i])
                        }

                        override fun getBatchSize(): Int {
                            return it.size
                        }
                    })
                }
                .taskExecutor(taskExecutor)
                .throttleLimit(2)
                .build()
    }

実行結果

ログから、2スレッド処理する毎に5秒間が開くのでthrottleLimitでスレッド数が制限されていることがわかります。

2019-07-14 08:07:10.815  INFO 26547 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2019-07-14 08:07:10.834  INFO 26547 --- [         task-1] s.springbatchsample.BatchConfiguration   : write size: 10, items: [1, 4, 6, 7, 9, 11, 13, 15, 17, 19]
2019-07-14 08:07:10.834  INFO 26547 --- [         task-2] s.springbatchsample.BatchConfiguration   : write size: 10, items: [2, 3, 5, 8, 10, 12, 14, 16, 18, 20]
2019-07-14 08:07:15.889  INFO 26547 --- [         task-3] s.springbatchsample.BatchConfiguration   : write size: 10, items: [21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
2019-07-14 08:07:15.898  INFO 26547 --- [         task-4] s.springbatchsample.BatchConfiguration   : write size: 10, items: [31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
2019-07-14 08:07:20.932  INFO 26547 --- [         task-5] s.springbatchsample.BatchConfiguration   : write size: 10, items: [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
2019-07-14 08:07:20.947  INFO 26547 --- [         task-6] s.springbatchsample.BatchConfiguration   : write size: 10, items: [51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
2019-07-14 08:07:25.974  INFO 26547 --- [         task-7] s.springbatchsample.BatchConfiguration   : write size: 10, items: [61, 62, 63, 64, 65, 66, 67, 68, 69, 70]
2019-07-14 08:07:25.990  INFO 26547 --- [         task-8] s.springbatchsample.BatchConfiguration   : write size: 10, items: [71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
2019-07-14 08:07:31.018  INFO 26547 --- [         task-2] s.springbatchsample.BatchConfiguration   : write size: 10, items: [81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
2019-07-14 08:07:31.037  INFO 26547 --- [         task-1] s.springbatchsample.BatchConfiguration   : write size: 10, items: [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
2019-07-14 08:07:36.095  INFO 26547 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{run.id=71}] and the following status: [COMPLETED]

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