KotlinのtoXxx系の型変換用拡張関数とそれによってもたらされるコードの一貫性

どうも、久しぶりの記事です!

なんで書いたの?

Kotlinのコードの一貫性に関する個人的感想を社内開発メンバーに共有したのですが、もったいないからオープンにしようと思い久しぶりに記事を書いてみます。 超短いです。

概要

日々コードを書く中で、Kotlinがコードの一貫性を重視しているなぁとよく感じます。 例えば、toXxx()と呼ばれる多くの変換用の拡張関数が多数あり、多用しています。

例えば、Iterable<T>#toList()とかIterable<T>#toSet()とかそういうやつです。

これらの変換用の拡張関数はtoXxxという形式で統一されていることで、ちょっとしたメリットがあります。

メリット

直感的な表現力

toXxx()という形式は、ある型から別の型への変換を明示的に示すことができます。この明示的な命名と関数の形式は、コードを読んでいる人にとって何を行っているのかが直感的に理解しやすいです。

認知負荷の軽減

toXxx()一貫した命名規則のおかげで、類推で便利なAPIを呼び出すことができるようになります。

例えば、一度toInt(), toDouble()などの変換関数を知ってしまえば、toRegex()のような関数があるであろうと予想がつき、型を変換するという同様の目的で使えることを容易に想像がつきます。

この一貫性は、Kotlinを使ってプログラムを書く上で非常に価値が高いと感じています。特に、複数人での開発しているアプリコードベースにおいて、この一貫性はコードの品質やメンテナンス性を向上に大いに寄与するでしょう。

そういうプロジェクトではKotlinの初心者が参画してくることも多いと予想できるため、学習コストを下げることにも貢献してくれそうです。

ばいばい

超短いけど、伝えたかったことはこれだけです。

KotlinのtoXxx系の型変換用拡張関数とそれによってもたらされるコードの一貫性

どうも、久しぶりの記事です!

Kotlinのコードの一貫性に関する個人的感想を社内開発メンバーに共有したのですが、もったいないからオープンにしようと思い久しぶりに記事を書いてみます。 超短いです。

日々コードを書く中で、Kotlinがコードの一貫性を重視しているなぁとよく感じます。 例えば、toXxx()と呼ばれる多くの変換用の拡張関数が多数あり、多用しています。

例えば、Iterable<T>#toList()とかIterable<T>#toSet()とかそういうやつです。

これらの変換用の拡張関数はtoXxxという形式で統一されていることで、こんなメリットがあります。

  1. 直感的な表現力
    • toXxx()という形式は、ある型から別の型への変換を明示的に示すことができます。この明示的な命名と関数の形式は、コードを読んでいる人にとって何を行っているのかが直感的に理解しやすいです。
  2. 認知負荷の軽減
    • toXxx()一貫した命名規則のおかげで、類推で便利なAPIを呼び出すことができるようになります。例えば、一度toInt(), toDouble()などの変換関数を知ってしまえば、toRegex()のような関数があるであろうと予想がつき、型を変換するという同様の目的で使えることを容易に想像がつきます。この一貫性は、Kotlinを使ってプログラムを書く上で非常に価値が高いと感じています。特に、複数人での開発しているアプリコードベースにおいて、この一貫性はコードの品質やメンテナンス性を向上に大いに寄与するでしょう。そういうプロジェクトではKotlinの初心者が参画してくることも多いと予想できるため、学習コストを下げることにも貢献してくれそうです。

Javaのバージョンは最新に追随するか、LTSを乗り換えていくか

先日こんなことをつぶやいていた

どういうことかというと、最新バージョンに追随しようとした場合、buildpacksやもしかしたらその他のFWやライブラリがEOLになったjdkのサポートを直ちにきって、ほんの短い間バージョンを塩漬けにしておきたいと思ってもできない、という状況が起こり得るということ。

少なくともbuildpacksを利用する場合は、LTSを乗り換えていく戦略を取るほうが安全そう。

jdkのリリース後、すぐに乗り換えられるだけの体制が整っていれば別だが、そうじゃないならLTSを乗り換えていきましょう、というお話でした。

Spring Bootのバージョンを2.4.4にあげるときの落とし穴

概要

Spring Bootのバージョンを2.4.4に上げたタイミングで自動的に解決されるmysql-connector-javaのバージョンが8.0.23に変わります。

しかし、このバージョンからタイムゾーンを接続文字列に含めて明示してない(serverTimezoneなどを指定していない)場合に採用されるタイムゾーンが8.0.22以前はサーバ側(MySQLインスタンスの設定)だったにもかかわらず、8.0.23からは接続元(Spring Bootが動いている環境のタイムゾーン)に変わるので、日時や日付をLocalDateやLocalDateTimeを使っていると9時間ずれて動いてしまいます。

対応策

手っ取り早いのはserverTimezoneまたはconnectionTimeZoneを指定し、MySQLが動いている環境のタイムゾーンと一致させることっぽいです。いやはやハマった…

f:id:kogayushi:20210409222810p:plain

情報はここ dev.mysql.com

MDCInsertingServletFilterとSentryAppenderを組み合わせると一部のMDCのvalueがnullになっているせいで、処理途中でNPEになってしまいsentryにイベント通知できない

掲題の通りの事象に遭遇して、解決に手間取ったのでメモを残しておきます。

その時ぼやき半分につぶやいていたのはこちら。

MDCInsertingServletFilterからすると、valuenullをセットするのは正当な気がしています。

問題が顕在化している箇所はSentryAppenderがMDCの値をコピーする箇所であり、具体的にはCollectionUtils#shallowCopyを呼び出していてその中でnew ConcurrentHashMap<>(map)とするときに、ConcurrentHashMapのコンストラクタ内のガード節でnullチェックをしており、nullの場合はNPEをthrowしてしまっています。

MDCのvaluenullの場合は、空文字に変換すればよいのでは?と一瞬思いましたが、これはデータの改ざん(not加工)であるような気もしており、筋が悪そうです。

何らかの方法でConcurrentHashMapを辞めれれば良さそうな気がします。1

workaroundですがMDCInsertingServletFilterをコピペしてCustomMDCInsertingServletFilterという名前で作り、valuenullになりうる箇所はnullの場合に空文字にするという対応にしました。

package sample

import ch.qos.logback.classic.ClassicConstants
import org.slf4j.MDC
import java.io.IOException
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest

// オリジナルはch.qos.logback.classic.helpers.MDCInsertingServletFilter
// 値がないときはMDCのvalueにnullをputするが、それだとSentryAppender#createEvent内で、
// CollectionUtils.shallowCopyを使ってMDCの値をコピーしようとしているが、
// ConcurrentHashMapがnullのvalueを受け付けないので、NPEを送出してしまう
// OSS側で直してほしい気もするが、正しい挙動がなく直しようがなさそう
open class CustomMDCInsertingServletFilter : Filter {

    override fun destroy() {
        // do nothing
    }

    @Throws(IOException::class, ServletException::class)
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        insertIntoMDC(request)
        try {
            chain.doFilter(request, response)
        } finally {
            clearMDC()
        }
    }

    private fun insertIntoMDC(request: ServletRequest) {
        MDC.put(ClassicConstants.REQUEST_REMOTE_HOST_MDC_KEY, request.remoteHost)
        if (request is HttpServletRequest) {
            val httpServletRequest = request
            MDC.put(ClassicConstants.REQUEST_REQUEST_URI, httpServletRequest.requestURI)
            val requestURL = httpServletRequest.requestURL
            if (requestURL != null) {
                MDC.put(ClassicConstants.REQUEST_REQUEST_URL, requestURL.toString())
            }
            MDC.put(ClassicConstants.REQUEST_METHOD, httpServletRequest.method)
            MDC.put(ClassicConstants.REQUEST_QUERY_STRING, httpServletRequest.queryString ?: "") // nullになりうるのでnullだった場合は空文字に置換
            MDC.put(ClassicConstants.REQUEST_USER_AGENT_MDC_KEY, httpServletRequest.getHeader("User-Agent") ?: "") // nullになりうるのでnullだった場合は空文字に置換
            MDC.put(ClassicConstants.REQUEST_X_FORWARDED_FOR, httpServletRequest.getHeader("X-Forwarded-For") ?: "") // nullになりうるのでnullだった場合は空文字に置換
        }
    }

    private fun clearMDC() {
        MDC.remove(ClassicConstants.REQUEST_REMOTE_HOST_MDC_KEY)
        MDC.remove(ClassicConstants.REQUEST_REQUEST_URI)
        MDC.remove(ClassicConstants.REQUEST_QUERY_STRING)
        // removing possibly inexistent item is OK
        MDC.remove(ClassicConstants.REQUEST_REQUEST_URL)
        MDC.remove(ClassicConstants.REQUEST_METHOD)
        MDC.remove(ClassicConstants.REQUEST_USER_AGENT_MDC_KEY)
        MDC.remove(ClassicConstants.REQUEST_X_FORWARDED_FOR)
    }

    @Throws(ServletException::class)
    override fun init(arg0: FilterConfig) {
        // do nothing
    }
}

これググっても全くHITせず、OSS側のコードをデバッグしないと突き止められませんでした。

MDCInsertingServletFilterSentryAppenderを組み合わせて使っている事例は多い気がするので同じように困ってる人の役にたつ情報になってくれれば幸いです。

2021/04/06追記

ちょうどこの記事の投稿の直後ぐらいに問題が報告されて修正が入ったようです。 @wreulickeさん情報提供頂きありがとうございます!


  1. 執筆時点ではまだソースコードを解析していないため、影響範囲が不明です。

Atomic Designの段階の分類の指針について考えていること(2020/6/10バージョン)

概要

掲題についてだらだらと考えたことを表にまとめたのでメモに残しておきます。

大筋としてはそんなに間違えていないと思いますが、兼業フロントエンジニアなのであまり信じすぎないでください…。

前提

nuxt.js

分類とその説明

分類 該当フォルダ 説明
atoms components/atoms 原則一つのタグで表現できるもの。
標準のHTMLタグの見た目を装飾したり、
なんらかのアクションのトリガーを追加したものになる傾向が強い。
molecules components/molecules 2つ以上のタグで構成される意味のあるまとまり。
API呼び出しやvuexの更新といった外部の状態を直接操作するような振る舞いはない。
ルーセルやバリデーション込みの入力フィールドなどが該当する。
organisms components/organisms 2つ以上のタグで構成される意味のあるまとまり。
フォームやモーダル、テーブルが該当する。
コンポーネントを組合せて共闘動作させるための状態を持つことができる。
外部の状態を操作する振る舞いを持つことができる。
状態や振る舞いを持ってなくてもorganismsとして成立する
作成するコンポーネントをどれに分類するかで一番悩むもののはず。。
templates components/layouts organisms/molecules/atomsを配置する。
nuxtのlayoutsと混同しないように注意。
乱暴だが、いわゆるプレゼンテーショナルコンポーネントの考え方に近い。
単体で完結したorganismsのみで構成される場合、省略可(になるはず)
pages pages templateに、状態を流し込むのが役割。
これも乱暴だがコンテナコンポーネントの考え方に近い。
単体で完結したorganismsのみで構成される場合、
ただtemplatesを呼び出すだけになるのでその場合はtemplatesを省略可。

特徴と段階のマトリクス

項目 atoms molecules oarganisms templates pages 備考
振る舞い ない ない ある ない ある 一見、moleclesに見えても振る舞い(ビジネスロジック)があるならorganismsにする
分割可否 不可 これ以上分割できないものはatoms。
ただし、他のatomsのベースになりうるatomsはあり得るかもしれない。
例えばボタンコンポーネントに共通して備えさせたいpropsを定義するためのスーパータイプなど
標準のHTMLタグの数 原則1つ 2つ以上 2つ以上 2つ以上 2つ以上
コンポーネント組合せ数 n/a 2つ以上 2つ以上 2つ以上 2つ以上

io.lettuce.core.RedisCommandExecutionException: ERR no such key

以下のようなエラーを見かけたので調べてみた。

io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135)
    at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:108)
    at io.lettuce.core.protocol.AsyncCommand.completeResult(AsyncCommand.java:120)
    at io.lettuce.core.protocol.AsyncCommand.complete(AsyncCommand.java:111)
    at io.lettuce.core.protocol.CommandWrapper.complete(CommandWrapper.java:59)
    at io.lettuce.core.protocol.CommandHandler.complete(CommandHandler.java:646)
    at io.lettuce.core.protocol.CommandHandler.decode(CommandHandler.java:604)
    at io.lettuce.core.protocol.CommandHandler.channelRead(CommandHandler.java:556)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1434)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:965)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:648)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:583)
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:500)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:462)
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.lang.Thread.run(Thread.java:834)
org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR no such key
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:54)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:52)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44)
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42)
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:268)
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.convertLettuceAccessException(LettuceKeyCommands.java:817)
    at org.springframework.data.redis.connection.lettuce.LettuceKeyCommands.rename(LettuceKeyCommands.java:325)
    at org.springframework.data.redis.connection.DefaultedRedisConnection.rename(DefaultedRedisConnection.java:118)
    at org.springframework.data.redis.core.RedisTemplate.lambda$rename$16(RedisTemplate.java:932)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224)
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:184)
    at org.springframework.data.redis.core.RedisTemplate.rename(RedisTemplate.java:931)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveChangeSessionId(RedisOperationsSessionRepository.java:867)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.saveDelta(RedisOperationsSessionRepository.java:826)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository$RedisSession.access$000(RedisOperationsSessionRepository.java:703)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:421)
    at org.springframework.session.data.redis.RedisOperationsSessionRepository.save(RedisOperationsSessionRepository.java:247)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.commitSession(SessionRepositoryFilter.java:240)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.access$100(SessionRepositoryFilter.java:201)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:154)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:81)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.springframework.cloud.sleuth.instrument.web.ExceptionLoggingFilter.doFilter(ExceptionLoggingFilter.java:50)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at brave.servlet.TracingFilter.doFilter(TracingFilter.java:65)
    at org.springframework.cloud.sleuth.instrument.web.LazyTracingFilter.doFilter(TraceWebServletAutoConfiguration.java:145)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:566)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1588)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.Dispatcher.forward(Dispatcher.java:227)
    at org.eclipse.jetty.server.Dispatcher.error(Dispatcher.java:86)
    at org.eclipse.jetty.server.handler.ErrorHandler.doError(ErrorHandler.java:119)
    at org.eclipse.jetty.server.handler.ErrorHandler.handle(ErrorHandler.java:78)
    at org.springframework.boot.web.embedded.jetty.JettyEmbeddedErrorHandler.handle(JettyEmbeddedErrorHandler.java:55)
    at org.eclipse.jetty.server.Response.sendError(Response.java:655)
    at org.eclipse.jetty.server.handler.AbstractHandler.doError(AbstractHandler.java:100)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1339)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
    at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.Server.handle(Server.java:502)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:419)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
    at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
    at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
    at java.lang.Thread.run(Thread.java:834)

どうも、spring boot 2.1.x系のspring sessionとspring data redisの組み合わせを使っていると出るらしい。 issueも報告されている

以下の通り、spring boot 2.3.x系では直っているので、解決策はバージョンの最新化をすれば良さそう

github.com

これ日本語情報が全く無かったので、英語が苦手な人だと困りはてる気がしたのでキーワードだけでもと思いブログに情報を残しておきます。