@RestController で別のエンドポイントに同じ処理を行わせる方法(エイリアス)

たとえばECサイトで、購入した商品の「変更処理 (/edit) 」と「キャンセル処理 (/cancel) 」があったとします。

でもサーバ側では実際には同じPUTリクエストで行われており、違いは渡ってくるPUTオブジェクトの中身だけ(変更点が数量フィールドなのか削除フラグなのかだけ)、ってことがありえます。

ただUI的にURLは分けないといけなかったりします。

そういう場合に便利です。

まず何も考えなければこうやって書いちゃいますね。

    @PutMapping("/edit")
    @ResponseStatus(HttpStatus.OK)
    public void updatePurchasedData(@RequestBody PurchaseDataPutRequest request) {
        requestProcessor.update(request);
    }

    @PutMapping("/cancel")
    @ResponseStatus(HttpStatus.OK)
    public void updatePurchasedData(@RequestBody PurchaseDataPutRequest request) {
        requestProcessor.update(request);
    }

@PutMappingvalue が違うだけで他は同じ、っていうのがいかにもダサいです。

わかりやすいDRY原則違反でもあります。

そこでこう書きましょう。

    @PutMapping({"/edit", "/cancel"})
    @ResponseStatus(HttpStatus.OK)
    public void updatePurchasedData(@RequestBody PurchaseDataPutRequest request) {
        requestProcessor.update(request);
    }

Spring Framework のソースを見てみるとわかりますが、

RequestMapping の valueString[] 型になってます。

なのでこういうことが可能なんですね〜

ZoneId を使った LocalDateTime から OffsetDateTime への変換

ZonedDateTime を経由する方法

    private static OffsetDateTime LocalToOffset(LocalDateTime localDateTime, ZoneId zoneId) {
        return ZonedDateTime.of(localDateTime, zoneId).toOffsetDateTime();
    }

atZoneメソッドを使う方法

    private static OffsetDateTime LocalToOffset(LocalDateTime localDateTime, ZoneId zoneId) {
        return localDateTime.atZone(zoneId).toOffsetDateTime();
    }

メソッドの冒頭に if 文で return させる場合に else をつけたほうがいい理由

「メソッドの冒頭で if 文で return させる」っていうのはどういうことかというと

    private static String getContents(HashMap<String, String> myMap, String key) {
        if (myMap == null) {
            return "";
        }
        return myMap.get(key);
    }

みたいなヤツのことです。

この場合だと、受け取った Map がnullだったときに NullPointerException になるのを防ぐために、メソッドの冒頭でnullチェックを行っています。 で、もしnullだったら空文字を返しておしまいなんですが、問題はこのあとですね。

選択肢は2つ。 ひとつは上記で書いたこれ

    private static String getContents(HashMap<String, String> myMap, String key) {
        if (myMap == null) {
            return "";
        }
        return myMap.get(key);
    }

もうひとつは、elseを付けるバージョン。

    private static String getContents(HashMap<String, String> myMap, String key) {
        if (myMap == null) {
            return "";
        } else {
            return myMap.get(key);
        }
    }

処理としてはどちらも同じです。 単に見た目の問題なんですが、違いが大きく出てくるのは以下の場合です。

if文以降(本処理)が長くなる場合

本処理が長くなる場合は、else をつけないほうがスッキリします。

  • else あり
    private static String getContents(HashMap<String, String> myMap, String key) {
        if (myMap == null) {
            return "";
        } else {
            String result = myMap.get(key);
            .
            .
            .
            .
            .
            .
            .
            .
            return result;
        }
    }
  • else なし
    private static String getContents(HashMap<String, String> myMap, String key) {
        if (myMap == null) {
            return "";
        }
        String result = myMap.get(key);
        .
        .
        .
        .
        .
        .
        .
        .
        return result;
    }

なので、個人的には else つけないほうが好きなんですが、 違う観点もあります。

「ここで else をつけなくても良いという判断は、if文の処理に依存している」

というものです。

つまり、ここで「elseなくてもいいじゃん」って思ったのは、 if の中で return してるからです。

より正確に言えば、if の中で return してることをちゃんと把握できているから、です。

もしここで return しないのであれば else は必要になるかもしれません。

例えば「mapがnullだったらデフォルトの値を利用して後続で整形して返す」という仕様変更があった場合、elseがあるとないとでは全然変わってきます。

  • else あり
    private static String getContents(HashMap<String, String> myMap, String key) {
        String value;
        if (myMap == null) {
            value = DEFAULT_VALUE;
        } else {
            value = myMap.get(key);
        }
        return optimizeFormat(value);
    }

ここで else がなければ、mapがnullの場合、当然NPEで落ちます。

  • else なし
    private static String getContents(HashMap<String, String> myMap, String key) {
        String value;
        if (myMap == null) {
            value = DEFAULT_VALUE;
        }
        value = myMap.get(key);
        return optimizeFormat(value);
    }

ちなみに最初の else 無しバージョンはこれ

    private static String getContents(HashMap<String, String> myMap, String key) {
        if (myMap == null) {
            return "";
        }
        return myMap.get(key);
    }

似てますね。

これが上記の挙動のおかしなメソッドに変貌するのはあっという間だと思います。

else をつけないほうがインデントが減ってぱっとみシンプルでいいんですが、のちのちの安易な修正によりバグを埋め込む可能性が若干残ってしまうんですね。

結論

else をつけるかつけないかの2択でいうと、場合にもよりますが「つけたほうが安全」ということになります。

ただ、、そもそもロジックの途中で return させることがあまり良くないですね、やっぱり。

OffsetDateTime - "2019-04-01T00:00:00+09:00" の "+09:00" って何??

OffsetDateTime っていうのはオフセット情報がついた日時のフォーマットです。

こういうやつ。

2019-04-01T00:00:00+09:00

で、ここの最後についている

+09:00

これがオフセットです。

厳密なところはISO 8601に定義されてるんですが

一旦気になるところだけ解説すると、

これはUTCからどれだけ時差があるか、を示しており、

詳しい人が見れば、どのあたりTimeZoneなのかがわかります。

まあ、経度が同じであれば ZoneId が異なっていてもオフセットは同じってことがありえる(1..n)ので、ある程度は、って感じです。

たとえばUTC+9時間はJST、Zone名でいうとAsia/Tokyoが思い浮かびますが、ロシアのヤクーツク時間である Asia/Yakutsk も同じオフセット(+09:00)を持っています。

なので「だいたいあのへん」「このうちのどれか」ってのはわかりますが、特定まではできないですね。

TimeZoneの特定までしたいのであれば、OffsetDateTime ではなく ZonedDateTime を使う必要があります。

で、これ

2019-04-01T00:00:00+09:00

は単に

2019-04-01T00:00:00です

という意味です。

決して

この時刻に9時間プラスして2019-04-01T09:00:00

のように計算しなければいけないわけではありません

そのまま日時を使えます。

ついでの情報として「ちなみになんだけど、この時刻はUTCから9時間プラスされています」っていう情報がくっついているだけです。

なのでこれをUTCに変換する場合は、この9時間前なので

日時としては2019-03-31T15:00:00となります。

ただこれだと LocalDateTime と見分けがつかないので、

OffsetDateTime の UTCである印として、最後にZをつけて

2019-03-31T15:00:00Z

になり、時刻としては

2019-04-01T00:00:00+09:00 = 2019-03-31T15:00:00Z

ということになります。

SpringBoot で OffsetDateTime を deserialize するときに Jackson が勝手に UTC に変換してしまう件について

よそのAPIが返してくる OffsetDateTime を処理するロジックを書いていたところ、テストが fail して、見てみると何故か UTC に変換されてたことがありました。

  • API が返している値

"2018-04-01T00:00:00+09:00"

"2018-03-31T15:00:00Z"

おお・・??・・・・おお、なるほどね。

まあ、、間違ってはいない。やりたいこともわかる。

でもオフセット消えちゃってるからどこのタイムゾーンかわからなくなっちゃってるじゃん。


そんでいろいろ調べてると

  • 犯人は Jackson っぽい

っていうことと

  • ADJUST_DATES_TO_CONTEXT_TIME_ZONE を無効にすると勝手なことをしなくなるらしい

ってのはわかりました。

じゃあサクッと無効にしてやりましょうかと思って更に調べてみたんですが

objectMapper の attribute を変えてやるみたいなのしか出てこなくて

こんなの

mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

いやいや、、そんなの手動でやってないし、じゃあどうしたかっていうと、答えは bootstrap.yml でした。

bootstrap.yml に以下のように設定

spring:
  jackson:
    deserialization:
      adjust_dates_to_context_time_zone: false

これで、取得した OffsetDateTime をそのまま受け取って使うことができるようになりました。

Mac Book でマルチジェスチャーが効かなくなったときの対処法

Mac Book って三本指で上にスワイプするとウィンドウ一覧が出たり、横にスワイプするとデスクトップを切り替えたりできて便利なんですが、ふとした時に効かなくなるんですよね。

 

そういうときの対処法がめっちゃ簡単だったので載せておきます。

  1. ターミナルアプリを立ち上げる
  2. killall Dock する

以上です。

 

もうちょっとだけ詳しく

⌘ + space を押してSpotlight を呼び出し、そこで term と打つとターミナルアプリが立ち上がります。黒い画面のアレです。

そこには文字が入力できるんですが、そこに

killall Dock 

 と入力してエンターを押す

これだけです。

マルチジェスチャーの親プロセスがDockになっているので、それを再起動すればもとに戻るんですね。

ちなみに sudo などは不要です。

副作用はない

これやっちゃってホントに大丈夫なの?とか心配かもしれませんが、大丈夫です。

Dock は kill されると勝手に再起動します。

git-flow の各ブランチをひとことで説明

developブランチ

共通の開発環境

featureブランチ

案件毎に作られる開発環境

releaseブランチ

「リリースするもの」リスト

masterブランチ

いま本番で動いているもの

hotfixブランチ

緊急用tmpブランチ

memo

  • 開発はdevelopから派生させたfeatureブランチで行い、テストまで終わったらdevelopブランチへマージする

(各開発者がdevelopの周りにあつまって個別に作業しているイメージ)

  • 試行錯誤は各featureブランチで行い、テストまで終わらせてからdevelopへマージするルールなので、developブランチは比較的安定する。
  • hotfixブランチはmasterから来てmasterに戻る。他は経由しない。
  • ただしリリース時にmasterブランチの他にdevelopブランチへマージしないと次回リリース時にデグレる。
  • developからmasterに直接行かずにreleaseブランチを経由することで、リリース対象を明確に区別することができる。