RestTemplateで意図せずURLエンコードさせないための注意点

こんにちは。 エキサイト株式会社の三浦です。

URLには、特殊な意味を持っていたり使えなかったりする文字があり、それらを使いたい場合はURLエンコードを掛ける必要があります。 そのため、とある条件下ではJavaのRestTemplateは自動的にURLエンコードを掛けてくれます。

ですが、場合によってはエンコードされてしまうと問題がある場合もあります。

今回は、RestTemplateで意図せずURLエンコードさせないための注意点を紹介します。

URLエンコードをすると問題があるパターン

URLには、特殊な意味があったり使えない文字があります。 例えば日本語は、使えない文字の1つです。 日本語を使いたい場合は、以下のように変換する必要があります。

// これを
https://サンプル

// こうする
https://%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB

これでURLとして使えるようになるのですが、実はこの % はURLで特殊な意味を持つ文字となっています。 そのため、うっかりもう一回URLエンコードを掛けてしまうと、以下のように更に変換されてしまいます。

// これが
https://%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB

// こうなる
https://%25E3%2582%25B5%25E3%2583%25B3%25E3%2583%2597%25E3%2583%25AB

この状態だと受け取り側も2回デコードを掛ける必要が出てきてしまうため、避ける必要があります。

RestTemplateとエンコード

ところで、JavaでURLリクエストをする際は、RestTemplateを使うという方が多いと思います。 例えば、以下のように使うことができます。

// 1. URLエンコードを掛けた上でURL文字列を作成
String uriString = UriComponentsBuilder.fromUriString("https://sample")
    .queryParam("sample_query", "サンプル")
    .build()
    .encode()
    .toUriString();

// 2. 作ったURL文字列を使ってGETリクエストを送信
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
    uriString,
    HttpMethod.GET,
    null,
    String.class
);

ただし、これだと実は問題があります。 まず、 1. URLエンコードを掛けた上でURL文字列を作成 をした結果、以下のような文字列が発行されます。

// 1. URLエンコードを掛けた上でURL文字列を作成
String uriString = UriComponentsBuilder.fromUriString("https://sample")
    .queryParam("sample_query", "サンプル")
    .build()
    .encode()
    .toUriString();

// uriString = https://sample?sample_query=%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB

これは正しくURLエンコードされた文字列になります。 しかし、実際に 2. 作ったURL文字列を使ってGETリクエストを送信 にて送信されるURLは以下になります。

// 2. 作ったURL文字列を使ってGETリクエストを送信
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
    uriString,
    HttpMethod.GET,
    null,
    String.class
);

// 送信されたURL : https://sample?sample_query=%25E3%2582%25B5%25E3%2583%25B3%25E3%2583%2597%25E3%2583%25AB

見ての通り、二重エンコードされています。 実は文字列を渡して restTemplate.exchange を実行すると、自動的にURLエンコードが掛けられます。 もちろん有用な場合もありますが、知らずに使っていると上記のように意図しないエンコード結果になってしまうでしょう。

RestTemplateで自動的にエンコードさせない方法

では、どうすれば自動エンコードさせないようにできるのでしょうか。 実は、文字列ではなくURI型で渡せばエンコードされずにリクエストが実行されます。

// 1. URLエンコードを掛けた上でURIを作成
URI uri = UriComponentsBuilder.fromUriString("https://sample")
    .queryParam("sample_query", "サンプル")
    .build()
    .encode()
    .toUri();

// 2. 作ったURIを使ってGETリクエストを送信
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
    uri,
    HttpMethod.GET,
    null,
    String.class
);

このようにすれば、作成したURIそのままでリクエストが実行されます。

最後に

意図しないURLエンコードは、当然ですが意図しないリクエストにつながる恐れがあります。 基本的にはRestTemplateにはURI型を渡すようにし、エンコードをかける際は UriComponentsBuilder で明示的に実行するのが良いでしょう。

XTechPodcastのアイコンをデザインしてみた話。

f:id:excite_ny:20211026103245p:plain

こんにちは!21卒デザイナーの山﨑です。

業務中に役員以上がいるMessengerグループに追加され、こんなメッセージが届きました。

「XTechのPodcastのアイコン作ってくれない?」

突然のオーダーに超焦りながら、内容とアイコンのイメージをヒアリングを行いました。

  • 内容:XTech役員・手嶋とスタートアップ業界の経営者や投資家との対談を週に1度程度配信

  • 配信媒体PodcastSpotify

  • タイトル:XTVオフレコ対談(アイコンのどこかに入れる)

  • アイコンイメージ:XTechパーカーやTシャツ同様、かっこいい感じ

ちなみにXTechのパーカーのデザインはこんな感じです!(以前はXTechのTシャツとパーカーを11案を出してました)

f:id:excite_ny:20211020184646p:plain

ビジネス系・Tech系・サイバー系など…いろいろ考えた結果…8案ほど提案しました!

f:id:excite_ny:20211020183416p:plain

f:id:excite_ny:20211020183432p:plain

個人的には配信者の顔がある方がリスナーの安心感が強まると思い、顔写真多めの案になりました!

この8案を提出し決定したのは…こちらです!

f:id:excite_ny:20211020184954j:plain

めっちゃ画像でかくなってしまってすみません!

個人的に顔写真ありを推していたのもあり、このアイコンが決定したのは意外でした。

・毎回配信者本人が登場しない可能性がある(別の社内の方と対談する場合がある)

・スタートアップ オフレコ対談はXTechのラジオでなので個人を指定する写真は使用しない

以上の理由で、顔写真ありは採用されなかったのかなと思いました。

このアイコンは、パーカーのイメージに合わせたサイバーな雰囲気で製作しました。

「スタートアップ オフレコ対談」第一回目はなんとメルカリ共同創業者の方に聞く創業秘話です!

私自身知らなかった話を聞けて、すごく勉強になったので是非聞いてみてください🙇‍♀️

podcasts.apple.com

PRレビュー指摘を自動化しました (EOF + new line)

こんにちは、ヘルスケア開発部でエンジニアをやっている 大澤 です。

本日はPRのレビュー指摘の自動化として 一枚で完結するgithub actionを作成しましたので、
汎用性がある程度高いのでこちらを紹介することにしました。

作成した動機

弊社の開発部では github flow (厳密には異なります) を利用して開発を行っております。
業務時に PR を やり取りしています。

しかしながらレガシープロジェクトで template engine用の linter が入っていません。
部内コーディング規約で決まっている "EOF の直前は必ず改行" を入れるというものが決まっておりまして、 linter が無いため、この修正を忘れてしまい、 "EOF の直前は必ず改行してください" という指摘を受けて差し戻しを受けてしまうことが何度か有りました。

  • 添付画像のような状態のときに指摘を受けるわけですね。
    • f:id:hibikiosawa4388:20211019184434p:plain
      • 左: EOFだけ
      • 右: new line + EOF の場合
        • 🚫 の有無で判断が可能です。

コーディング規約の違反の指摘をしていただけるのはありがたいです。
しかし、本来 PR は ロジックを見て問題がないことを確認する作業が大きいです。
コーディング規約違反ならば linter を入れて自動的チェック・自動修正を入れたりできれば、
レビュアーもレビュイーもより本質的なコードレビューや議論に時間を割くことができます。

コーディング規約の違反の指摘についてCI化し、自動化しようと思い立ちました。

課題

  • レガシープロジェクト故に単純にlint checkを入れると過去の数百、数千といったファイルについて差分が発生してしまう。
    • linter導入時に大きなチェックが伴うとlinter の導入自体が難しくなりがち
  • プログラミング言語と異なる template engine の linter が .editorconfig にしか存在しない。
    • eclint という.editorconfig をコーディング規約をチェック・修正するツールは有るが ignore 設定が存在しない

解法

  • レガシープロジェクト故に単純にlint checkを入れると過去の数百、数千といったファイルについて差分が発生してしまう。
  • template engine の linter が .editorconfig にしか存在しない。
    • => PRで発生する差分ファイルのみを対象に linter の CI を実行するように変更
      • => できたのが上述のgithub action

また下記の個人的な要望を満たすものとして作成しました。

github action の中身

下記の内容をリポジトリ直下の 下記のようなファイルとして設置していただくだけで利用できます。

.github/workflows/check_eof_with_new_line.yml

name: CHECK-EOF-WITH-NEW-LINE

on:
  pull_request:
    types: [synchronize, opened, reopened]
    branches:
      - '**'

jobs:
  check-eof-with-new-line:
    runs-on: ubuntu-latest
    steps:
      # clone repository to be able to merge master
      - name: checkout repository
        uses: actions/checkout@v2
        with:
          ref: ${{ github.event.pull_request.head.ref }}
          fetch-depth: 0
      - name: setting for git
        # 1.2. setting for git merge command (require user.email and user.name)
        run: |
          git config --global user.email "xxxxxx.ci@users.noreply.github.com" && \
          git config --global user.name "xxxxxx.ci"
      # install eclint only
      - uses: actions/setup-node@v2
        with:
          node-version: '12'
      - run: |
          rm package.json package-lock.json || :
      - run: npm init -y
      - run: npm install -g eclint
      # get diff files and eclint check files
      - run: |
          rm .editorconfig || :
      - run: |
          for j in  $( \
                  git --no-pager diff --name-only ${{ github.event.pull_request.head.ref }}  origin/${{ github.event.pull_request.base.ref }} | xargs file | grep ".*: .* text" | sed "s;\(.*\): .* text.*;\1;" \
              ); \
          do
              eclint check "$j" --insert_final_newline; \
          done

github action について行ごとの詳細な説明

checkout
  • fetch-depth: 0 というオプションを利用することで branch の情報が残るようにして checkout する
    • このオプションを付けないと current branch を squash して 1 commit だけになるように checkout してしまいます。
      • Q.つけないとどうなるの?
      • A.この後に行う git diff で branch 等を指定しても存在しないとエラーが発生してしまいます。
- name: checkout repository
  uses: actions/checkout@v2
  with:
    ref: ${{ github.event.pull_request.head.ref }}
    fetch-depth: 0
git の設定を追加
  • 一部 git command では user の情報を要求してくるので、適当に宣言しておきます。
- name: setting for git
  # 1.2. setting for git merge command (require user.email and user.name)
  run: |
    git config --global user.email "xxxxxx.ci@users.noreply.github.com" && \
    git config --global user.name "xxxxxx.ci"
eclint コマンドを install します。
  • node、npm と eclint を install しておきます。
  • global install なので関係はありませんがなんとなく package.json や package-lock.json も消しておきます。
    • ※ 元々 local install で実行しようとしたけど上手く行かなかったので残ってしまった部分(消し忘れとも言う)
# install eclint only
- uses: actions/setup-node@v2
  with:
    node-version: '12'
- run: |
    rm package.json package-lock.json || :
- run: npm init -y
- run: npm install -g eclint
余計な指摘をしないように.editorconfigを削除する。
  • .editorconfig を削除します。
    • ちなみにコマンドの後ろについている || : は対象のファイルが無くてもコマンドのexitステータスを0にする小技
  • ※ 今回は "EOF の直前は必ず改行" だけの指摘を行いたいので、他の指摘をしないために すでに .editorconfig が存在していれば削除します。
    • ※ もし "EOF の直前は必ず改行" だけではなく .editorconfig の規約違反を全て検知してほしい場合は削除は不要です。
- run: |
  rm .editorconfig || :
差分ファイルのみを対象に CI を実行する
  1. git diff の機能を利用して PR の差分対象ファイル(binaryを除く)を抽出
    • git --no-pager diff --name-only ${{ github.event.pull_request.head.ref }} origin/${{ github.event.pull_request.base.ref }}
      • PR 上で差分ファイルとして出てくるファイル一覧を取得します。
    • xargs file | grep ".*: .* text" | sed "s;\(.*\): .* text.*;\1;"
      • 差分ファイルとして出てくる一覧にはバイナリが入っている事があるのでこれを取り除きテキストファイルのみの差分ファイル一覧を取得します。
  2. for で check を実行する
    • 上記で取得したファイルについて eclint 逐次実行します。
- run: |
      for j in  $( \
      git --no-pager diff --name-only ${{ github.event.pull_request.head.ref }}  origin/${{ github.event.pull_request.base.ref }} | xargs file | grep ".*: .* text" | sed "s;\(.*\): .* text.*;\1;" \
          ); \
      do
          eclint check "$j" --insert_final_newline; \
      done

まとめ

  • この github action を設置することで以下の事柄について解決することができました。

    • linter を導入時に CI 導入のために大きな修正差分が発生する
      • OR 導入時に常にredなCIになる
    • "EOF の直前は必ず改行" という指摘の往復をなくして効率化
  • CI の実行例

    • red
      • f:id:hibikiosawa4388:20211019190559p:plain
      • f:id:hibikiosawa4388:20211019190607p:plain
    • green:
      • f:id:hibikiosawa4388:20211019184448p:plain

指摘が自動化されてレビューの指摘の往復が減りました。
それにレガシーにlinterを導入できてみんなハッピーですね。

めでたしめでたし

蛇足1

  • 上記 github action に下記の変更を加えるとeditorが自動的にfixしてくれるので更に便利になります。
    • github action で .editorconfig を削除しないようにする
    • .editorconfig を リポジトリに設置

蛇足2

このPRの差分対象にのみ特定のCIを実行するというのはとても応用が効きます。
これを参考にして面白い活用方法を見つけてみてください。

remote_addrとX-Forwarded-Forについて

エキサイトのしばたにえんです。 remote_addrとX-Forwarded-Forの違いについてよくわからなかったので調べました。

remote_addr

アクセス元のIP。直前のIPを持ちます。

Client
↓
LoadBalancer(remote_addr は Client)
↓
ApplicationServer(remote_addr は LoadBalancer)
Javaでremote_addrを取得する
@RequestMapping
public class IpController {
    @GetMapping("ip")
    public String getIp(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getRemoteAddr();
    }

X-Forwarded-For

ロードバランサーやプロキシを経由する度にアクセス元の情報が追加されていきます。,付きで追加されるので注意

Client(X-Forwarded-For は "")
↓
LoadBalancer(X-Forwarded-For は "Client")
↓
ApplicationServer(X-Forwarded-For は "Client, LoadBalancer")
JavaでX-Forwarded-Forを取得する
@RequestMapping
public class IpController {
    @GetMapping("ip")
    public String getIp(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getHeader("X-Forwarded-For");
    }

DBの不要なカラムを削除する

エキサイト株式会社の武藤です。

現在は担当サービスであるE・レシピのリビルドを進めており、技術負債の解消も行ってきました。 レシピデータを管理するテーブルには、現在の仕様で使われていないカラムが50個ほどあり、それらを削除しました。

今まで、仕様追加によって新たにデータ管理する際には、既存テーブルへのカラム追加で対応されてきました。 その仕様がフロントエンドやアプリから使われなくなっても、削除が困難なために放置され続けてきました。

不要なカラムやそれを参照しているコードは、開発時にノイズになるため、削除したほうがよいでしょう。 今回は、不要なカラムを削除するにあたって、バックアップテーブルとカラム削除後のテーブルを用意し、ダブルライト方式で安全に行った手順を紹介します。

不要カラムのピックアップ

まずは、現在使われていないカラムをリストアップしました。 運用経験や命名から使われていないカラムを推測します。

次に、リストアップしたカラムがコード上で使われていないことを確認します。 GitHubIDE上でカラム名を検索し、参照がないことを確認しました。 アプリケーションによっては、DBから取得した値が変数に格納された時にスネークケースからキャメルケースに変換されることがあります。 その点も加味しながら検索ワードを変えて調査していきます。

最終的に、表示に使われていないことを確認するため、フロントエンド、AndroidiOSなどクライアント側のコードを調査する必要があります。

DB接続から表示までと調査範囲は広く、アプリケーションの規模によっては複数のプロジェクトに跨るため、大変な作業です。。

アプリケーションから不要カラムの参照を削除

アプリケーションからリストアップした不要なカラムの参照を削除していきます。

リリース後、エラーの確認期間を数日設けます。

バックアップテーブル、削除後テーブルのダブルライトを実装する

アプリケーション側でダブルライトを実装します。

手順については後述しますが、問題発生時に切り戻しができるようにバックアップテーブルを作成します。 バックアップテーブルと削除後テーブルで更新内容を同期するためにダブルライトを行います。

この段階ではリリースせず、切り替えの準備をしておきます。

不要カラム削除の実施手順

ここまででカラム削除の準備ができました。 次にDBへの変更の順番を考えていきます。

E・レシピの場合は、DBの書き込みを一時停止してもサービス利用に影響がないので、一時的に書き込みを停止する判断をしました。

バックアップテーブルの作成

カラム削除前テーブルからバックアップを作成し、アプリケーションのリード先をバックアップテーブルに変更します。

f:id:excite-mthiroshi:20211015170744p:plain
カラム削除前テーブルをバックアップし、アプリケーションのリード先を変更

図では、レシピデータを管理するrecipeテーブルに、古いキャンペーンのカラムが放置された状態と仮定しています。

不要カラム削除前のテーブルを参照されないようにリネーム

外部キー制約を無効化します。削除対象のテーブルへの外部キー制約があると、制約の参照元のテーブルに影響が出る可能性があるためです。

カラム削除前テーブルをリネームして参照されない状態にします。 削除ではなく念の為リネームで留めておきます。

f:id:excite-mthiroshi:20211015171208p:plain
カラム削除前テーブルをリネーム

不要カラム削除後のテーブルにデータを投入

カラム削除後のテーブルを作成し、バックアップテーブルからSELELCT, INSERTでデータ投入をします。

f:id:excite-mthiroshi:20211015171746p:plain
カラム削除後テーブルを作成しデータ投入

アプリケーションのダブルライトを反映

その後、DBの外部キー制約を有効に戻し、アプリケーション側のバックアップテーブルとカラム削除後テーブルへのダブルライトをリリースします。

f:id:excite-mthiroshi:20211017174707p:plain
ダブルライトを反映

ダブルライトのリリース後、アプリケーション側で問題がないことを確認し、書き込み処理を再開します。

その後、数日経過を見て問題がなければ、ダブルライトを停止、バックアップテーブルを削除して完了です。 ダブルライトにすることで、切り替え後に問題があれば向き先を戻せばよいので、そこがメリットです。

最後に

ダブルライト方式のテーブルの不要カラム削除の手順について説明しました。

レシピサービスとして根幹となるテーブルの不要カラム削除は、影響を考えると慎重に進める必要があり、大変な作業でした。

今後はメンテナンスのしやすさも考慮して開発がしたいと思いました。

RestTemplateで、「+」をURLエンコードしてリクエストする方法

こんにちは。 エキサイト株式会社の三浦です。

URLでは、使えない・特殊な意味を持つ文字があります。 そういった文字を使いたい場合はURL用のエンコードを掛けてからリクエストをするのですが、RestTemplateではデフォルトでは「+」がエンコードされないという問題があります。

今回は、その問題を解決する方法を紹介します。

URLの特殊文字

URLとは、例えば 「https://tech.excite.co.jp/ 」のような文字列です。 このURLでは、使えなかったり特殊な意味を持つ文字が存在します。

例えば、日本語をそのまま使うことはできません。 「https://サンプル/」のようなURLを使いたい場合は、URLエンコードを掛けて「https://%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB/」のようにする必要があります。

また、URL上で特殊な意味を持つ文字もあります。 上記でも使われている「:」や「/」などの文字は、URL上の区切りなどで利用されるために、特殊な意味を持たないただの文字として使いたい場合はエンコードする必要があります。 「https://aaa/bbb/」のURLで、間に挟まれている「/」を特殊な意味を持たない文字として扱いたい場合は、「https://aaa%2Fbbb/」とする必要があります。

そして、そういったエンコードが必要な文字には「+」も含まれています。

RestTemplateとURLエンコード

RestTemplateでリクエストを送る場合は、下記のように送る場合が多いと思います。

// 文字列をもとにURLを作成(エンコードもする)
URI uri = UriComponentsBuilder.fromUriString("https://sample/")
    .queryParam("sample_query", "sample+")
    .build()
    .encode()
    .toUri();

// 作ったURLを使ってGETリクエストを送信
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
    uri,
    HttpMethod.GET,
    null,
    String.class
)

この場合、 UriComponentsBuilder がURLエンコードを掛けてくれます。 ただ、実は落とし穴があり、「+」の文字がエンコードされません。

実は歴史的経由があり、「+」はそのまま使える文字として認識されているようです。 ですが、リクエストを受け取る側の処理によっては「+」がエンコードされている必要があるので、このままでは問題が生じる可能性があります。

RestTemplateで「+」をエンコードする方法

では、どのようにして「+」をエンコードすればよいのでしょうか。

いくつか方法があるのですが、例えば

// 文字列をもとにURLを作成(エンコードもする)
URI uriWithNoEscapedPlus = UriComponentsBuilder.fromUriString("https://sample/")
    .queryParam("sample_query", "sample+")
    .build()
    .encode()
    .toUri();

// 「+」を文字置換でエンコードする
String strictlyEscapedQuery = StringUtils.replace(uriWithNoEscapedPlus.getRawQuery(), "+", "%2B");

// エンコード後のクエリに置き換える
URI uri = UriComponentsBuilder
    .fromUri(uriWithNoEscapedPlus)
    .replaceQuery(strictlyEscapedQuery)
    .build(true)
    .toUri();

// 作ったURLを使ってGETリクエストを送信
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
    uri,
    HttpMethod.GET,
    null,
    String.class
)

このようにすればエンコードが可能です。 これ以外にも RestTemplate の途中に同様の処理を挟むようInterceptorを使うという方法もあります。

処理の本質(文字置換をする)は同じなので、お好みの方でやってもらえればと思います。

最後に

「+」がURLで使用される場面はそこまで多くはないため、引っかかってしまうとかなり詰まってしまうこともあると思います。 そういった場合に、参考になれば幸いです。

SpringBootでapplication.yaml以外の外部リソースファイルを読み込む

エキサイト株式会社エンジニアの佐々木です。アプリケーションを開発していると設定が増えてくると思いますが、増えてくるとファイルが増えて大変です。また、モジュラーモノリスのような構成にすると設定を別々に管理した方が都合がいいことがでてきます。SpringBootで試してみます。

前提

SpringBoot2.4以上で使用ができます。

設定ファイルで指定する方法

下記のような database.yaml のファイルがあるとします。

database:
  host: localhost
  port: 33067
  schema: hogehogea

application.yamlファイルではないので、SpringBoot標準では読み込みません。application.yamlに次の設定を追記します。

spring:
  config:
    import: classpath:/database.yaml

上記の設定で設定を分けられます。

クラスに書く方法

設定ファイル

デフォルトのままapplication.yamlをそのまま読み込めなかったと思うので、property(.ini)ファイルにします。(factoryクラスを書けば読み込み可能です)

database.host = localhost
database.port = 3306
database.schema = hogehoge

クラスファイル

クラスファイルは下記のようになります。

@Data
@Configuration
@ConfigurationProperties(prefix = "database")
@PropertySource("classpath:database.properties")
public static class DatabaseConfig {
    private String host;
    private Integer port;
    private String schema;

    @Bean
    public DatabaseConfig databaseConfig(){
        return new DatabaseConfig();
    }
}

@PropertySourceにファイル名を書くと読み込んでくれます。アノテーションがたくさんついていますが、下記にまとめます。

@Data : lombokというライブラリを使って、Getter/Setterのメソッド等を自動的に生成してくれます
@Configuration: 設定ファイルクラスであることを明示します
@ConfigurationProperties: prefixで指定した文字列の配下とクラスのフィールド名が一致しているものにマッピングします
@PropertySource: 設定ファイルの置き場所を指定します
@Bean: DIコンテナに登録します

記述量は多くなりますね。ちなみに、@PropertySourceがデフォルトでYAMLに対応するというのは、GithubのIssueで拒否されています。(spring.config.importがでてきたから?)

まとめ

SpringBoot2.4以降は、spring.config.importを使うのが良さそうです。ニッチなネタですが、意外とソースが古かったりニーズにはずれていたりします。参考になれば幸いです。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

「第一回テクデザ総会」を開催しました!

こんにちは。 エキサイト株式会社の三浦です。

2021年10月8日、第一回目となる「テクデザ総会」を開催しました!

f:id:excite-takayuki-miura:20211011110649p:plain
第一回テクデザ総会

テクデザ総会

テクデザ総会は、コミュニケーションの活性化を主目的としたエキサイト・iXITの技術者の総会となります。 総会とは書きましたが格式張ったものではなく、今回のZoomでの開催中もたくさんのコメントを頂きながらの進行になりました。

総会は、

  • 各事業部のトピックスの発表
  • Zoomのブレイクアウトルーム機能を使った交流
  • 投票を募って決定した多数の大賞の授与

をメインのコンテンツとしています。

各事業部のトピックスの発表

エキサイトやiXITの各事業部の技術部長などの協力を得て、各事業部の上期のトピックスを発表してもらいました。 なかなか自分が所属している事業部以外でやっていることは把握しにくいところがあるので、この発表で他事業部のことを知ってもらい、交流のきっかけにしてもらいたいという意図があります。

Zoomのブレイクアウトルーム機能を使った交流

Zoomにはブレイクアウトルームという、参加者を任意の人数に分けて別々のルームに入れるという機能があります。 その機能を使い、約3~4名ほどの人数に参加者を分けて、それぞれのルームで話し合ってもらいました。

この交流は2回行いました。 1回目は自己紹介や雑談を5分ほど行ってもらって交流を深めてもらい、2回目はエキサイトやiXITに欲しい文化・ルール・制度等について10分ほど話し合ってもらいました。

今まで交流がなかった人と交流してもらうことで、技術組織全体のコミュニケーションを活性化することを目的としていましたが、実際に今まで話す機会がなかった人と話すことができたと非常に好評で、次回も是非取り入れたいところです。

投票を募って決定した多数の大賞の授与

事前にGoogleアンケートで投票を募り、様々な項目に関する大賞の受賞者を決定して、総会にて授与しました。 大賞は合計でなんと21個、投票の結果同数だった人を含め計23名への授与となりました。

大賞としては正直かなり数が多いですが、お祭り感を出してコミュニケーションを活性化させるという目的は達成できたと考えています。

全体として、非常にうまく行った総会だったと思います。 あとはこれが、今後の技術組織全体のコミュニケーションのさらなる活性化へと繋がっていけば言うことなしです。

最後に

組織のコミュニケーションの活性化は、一朝一夕でうまくいくものではありません。 今回のような総会を継続的に行いつつ、他にも様々な施策を行い、活性度を上げていきたいと思います。

axios通信時にNuxt.jsのローディングアニメーションを実装する

はじめに

こんにちは!SaaS事業部エンジニアの小川です。

普段はSaaS事業部でKUROTEN.という経営管理SaaSの開発をしています。

今回は、Nuxt.jsでaxios通信時にローディングアニメーションを表示する方法を紹介します。

NuxtのLoading設定(Nuxt標準)

基本的にNuxt.jsでローディングアニメーションを実装する場合、nuxt.config.jsのloadingオプションをtrueにする方法がよく使われていると思います。 ここでいうローディングは、ページ遷移の間の読み込み時に動作します。

# nuxt.config.js
export default {
    loading: true
}

また、こちらのローディングオプションにはオブジェクトを渡してあげることで、ある程度細かく見た目を変えることもできます。(以下では一例を紹介)

# nuxt.config.js
export default {
    loading: {
        progress-bar-height: 2px, // プログレスバーの高さ
        rtl: false // プログレスバーの向きを指定
    }
}

さらに見た目をカスタマイズしたい場合は以下のようにコンポーネントを用意し、 NuxtのLoadingオプションにコンポーネントを設定することも可能です。

// nuxt.config.js
export default {
    Loading: './commponents/Loading'
}

axios通信時にLoadingを動かす

ここからが本題です。 Nuxt.jsでaxios通信時にローディング処理を行いたい場合、以下の2つの手法があります。

this.loading.startとthis.loading.finishで通信中を取得

公式で紹介されている方法で、 axios通信している箇所にstartfinishの処理を追加するだけで実装できるのでお手軽です。 あとは先ほど紹介したNuxtのLoadingオプションをtrueにすれば従来どおりの方法でローディングが動作します。

// ローディング処理を入れたい箇所
this.loading.start ()
hoge () // axiosの処理
this.loading.finish ()

storeとpluginのaxiosでローディングの状態を管理する

上で述べた手法でもaxios通信時にローディング処理を走らせることができますが、 呼び出し処理を追加するたびにstartとfinishの記述が必要になってしまうため 通信処理が複数あると開発者の負担が大きいように思います。

そこで、storeでローディングの状態管理をすることで、axios通信時の処理を一括して記述する方法をご紹介します。

まずはloadingの状態を管理するためのstoreを追加します。

// ./store/loading.js

export const state = () => ({
  loadingCount: 0
})

export const getters = {
  loadingCount: (state) => state.loadingCount,
  isLoading: (state) => state.loadingCount > 0
}

export const mutations = {
  incrementLoadingCount (state) {
    ++state.loadingCount
  },
  decrementLoadingCount (state) {
    if (state.loadingCount > 0) {
      --state.loadingCount
    }
  }
}

export const actions = {
  incrementLoadingCount ({ commit }) {
    return commit('incrementLoadingCount')
  },
  decrementLoadingCount ({ commit }) {
    return commit('decrementLoadingCount')
  }
}

次にaxiosを用いた通信中にLoadingCountが1以上になるようpluginsのaxios.jsに先ほど作成したアクションを追加します。 このpluginsはaxiosのリクエストやレスポンスをフックにして呼び出されるので、共通化したい場合にとても便利です。

// ./plugins/axios.js

export default function({ $axios, store }) {
    $axios.onRequest(config => {
        store.dispatch('loading/incrementLoadingCount')
    })

    $axios.onResponse(() => {
        store.dispatch('loading/decrementLoadingCount')
    })
    
    $axios.onError(e => {
        store.dispatch('loading/decrementLoadingCount')
    })
}

あとは上述で作成したLoadingCountをトリガーにして読み込みのオーバーレイやアニメーションを表示すれば完成です。 以下は通信中にVuetifyのプログレスバーを動かすサンプルです。LoadingCountをトリガーに使うだけなので、自由度は非常に高いです。

// ./components/Loading.vue
<template>
  <v-progress-linear
    :active="isLoading"
    :indeterminate="isLoading"
    :height="loadingHeight"
    absolute
    bottom
    :color="loadingColor" />
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    name: 'Loading',
    data () {
      return {
        loadingHeight: '5',
        loadingColor: '#4DB6AC'
      }
    },
    computed: {
      ...mapGetters({
        isLoading: 'loading/isLoading'
      })
    }
  }
</script>

先程実装したLoadingコンポーネントをHeaderコンポーネントで呼び出し、layoutsのdefaultへ渡すことで、全ページにローディング処理の実装ができます。

// ./components/Header.vue

<template>
  <v-app-bar
    color="primary"
    class="app-bar"
    :height="height"
    fixed
    app>
    <loading />
  </v-app-bar>
</template>

<script>
  import Loading from '@/components/Loading'

  export default {
    name: 'Header',
    components: {
      Loading
    },
    data () {
      return {
        height: '50'
      }
    }
  }
</script>
// ./layouts/default.vue

<template>
    <div>
        <Header />
    </div>
</template>

<script>
import Header from '@/components/Header'

export default {
    components: {
        Header
    }
}
</script>

これでaxios通信が行われている最中にLoadingアニメーションが実行されます。

axios通信時のLoadingの様子

最後に

axios通信時の手法の肝はLoadingCountという状態を使うことなので、簡単に応用が効くと思います。 axios通信時にローディング処理を入れたいという方は一度試してみてはいかがでしょうか!

SQLServerのWHERE句は大文字小文字でもヒットしてしまう問題

エキサイトしばたにえんです。 早速ですが、

WITH hogefuga_table AS (
    SELECT 'hogefuga' AS word
)
SELECT * FROM hogefuga_table 
WHERE word = 'HOGEFUGA';

こちらhogefugaがヒットされてしまいます。 WHERE句の大文字小文字を判別してくれません。

SQLServerのWHERE句で大文字小文字を判別するためにcollateが使えます。

WITH hogefuga_table AS (
    SELECT 'hogefuga' AS word
)
SELECT * FROM hogefuga_table 
WHERE word = 'HOGEFUGA' COLLATE Japanese_CS_AS_KS_WS;

とすればWHERE句の大文字小文字を判別してくれるようになり、ヒットされることはありません。

COLLATEには他にも種類があります。 例えば、「ぽ」、「ぼ」、「ボ」、「ポ」の判別を「したくない」場合

WITH hogefuga_table AS (
    SELECT 'ボボぼーボボーボぽ' AS word
)
SELECT * FROM hogefuga_table 
WHERE word = 'ぽぼポーぽぽーぽボ' COLLATE Japanese_CI_AI;

とするとボボぼーボボーボぽヒットしてくれるようになります。 他にもCOLLATEには種類があるので必要に応じて使い分けるといいかもしれません。

git branchの結果を時間順に表示

エキサイトのしばたにえんです。
時間順にソートするgit branchのoptionの紹介です
「さっき作ったブランチが見つからない」、
「ターミナルのタブも消してしまってhistoryでも見つからない」
って時に便利です。

新しい順の表示
git branch --sort=-authordate
古い順の表示
git branch --sort=authordate

新しい順の表示なんかは何かと便利だと思いますので使ってみてください。
git branch --helpで他のオプションも確認できるので調べてみてもいいかもしれません。

LocalDateTime型をrequestから受け取るカスタムアノテーションを作る

エキサイトのしばたにえんです。 早速ですが カスタムアノテーションの作成をしていきます。 リクエストからLocalDateTimeを受け取る時に@JsonFormatを使って受け取ると思いますが、この時にpatternを書く必要があります。

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime date;

こんな感じです

pattern = "yyyy-MM-dd'T'HH:mm:ss" 毎回これを書いていくのはめんどうなのと patternが間違ったりでミスをする可能性が出てきます。

そんな時にはカスタムアノテーションを作ると便利かもしれません。 簡単です。

@JacksonAnnotationsInside
@Retention(RetentionPolicy.RUNTIME)
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
public @interface LocalDateTimeRequest {
}

としてアノテーションを登録して、

@LocalDateTimeRequest
private LocaldateTime date;

とすれば、毎回patternを書く必要がなくなります。 便利なので是非使ってみてください

SpringBootで設定ファイル(application.yaml)を一括で読み込む

エキサイト株式会社エンジニアの佐々木です。SpringBootでの設定ファイル(application.yaml)を一括で読む方法のメモになります。

前提

SpringBoot2.4以上で検証しています。

起動クラスの設定

ソースコードは下記になります。

起動クラスに@ConfigurationPropertiesScanを付与します。これで、@ConfigurationPropertiesアノテーションを読みにいきます。

@SpringBootApplication
@ConfigurationPropertiesScan  // ConfigurationPropertiesを読みにいくおまじない
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

単一項目な設定ファイル

設定ファイルの項目名と変数名が一致していればバインドされます。

設定ファイル(appliation.yaml)

設定ファイルは下記になります。

env:
  host: local
  port: 8080

読み取りクラス

読み取りクラスは下記のように定義します。

@Getter
@RequiredArgsConstructor
@ConstructorBinding          // この設定がないと Setterが必要になるのでつけておく
@ConfigurationProperties(prefix = "env")   // prefixで指定された部分のyamlを読みいく
public class Config {
    private final String host;
    private final Integer port;
}

@ConfigurationPropertiesアノテーションapplication.yamlのどこを読むかを設定します。@ConstructorBindingアノテーションで、setterがなくても値がセットされるようにします(つけておかないとSetterメソッドが必要になります)。@Getter@RequiredArgsConstructorLombokアノテーションです。Getterの生成とコンストラクタの定義を省略できます。yamlの中のプロパティ名とクラスのプロパティ名を合わせると、バインドしてくれます。型は気にしなくても、StringやIntegerくらいなら対応してくれます。設定されたものは、DIをすれば、使えます。

出力

{
"host": "local",
"port": 8080
}

Key-Value型の読み取り

Key-Value形式の設定は、Map型を使いKey-Valueをオブジェクトにバインドします。

設定ファイル

regionの中にjausのようなkey-valueのファイルがあるとします。

env:
  host: local
  port: 8080
  region:
    ja: 日本
    us: アメリカ

読み取りクラス

読み取りでは、Map型を使いJavaオブジェクトに変換していきます。

@Getter
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "env")
public class Config {
    private final String host;
    private final Integer port;
    private final Map<String,String> region; // Map型で定義(ジェネリクスは相応の型を指定)
}

上記で、region変数にKey-Value型のデータがバインドされます。

出力

{
"host": "local",
"port": 8080,
"region": {
    "ja": "日本",
    "us": "アメリカ"
    }
}

プロパティがネストされたapplication.yaml

ネストされた設定ファイルもバインド可能です。項目名と変数名が一致していれば自動的にバインドしてくれます。

設定ファイル

ネストされた設定ファイルです。

env:
  host: local
  port: 8080
  people:
    - name: hogehoge
      age: 18
    - name: fugafuga
      age: 30

読み取りクラス

階層がある場合は、staticな内部クラスを書くか、外部クラスに定義して型を指定するかになります。変数名が一致すればクラスに合わせて読み込みまれます。

@Getter
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "env")
public static class Config {
    private final String host;
    private final Integer port;
    private final List<People> people;

    @Getter
    @RequiredArgsConstructor
    static class People {
        private String name;
        private Integer age;
    }
}

staticな内部クラスでも@Getter、@RequiredArgsConstructorアノテーションは必要になります。

出力

{
  "host": "local",
  "port": 8080,
  "region": {
    "ja": "日本",
    "us": "アメリカ"
  },
  "people": [
    {
      "name": "hogehoge",
      "age": 18
    },
    {
      "name": "fugafuga",
      "age": 30
    }
  ]
}

まとめ

@Valueで1つずつ読み込む方法もありますが、記述が多くなるので、クラス定義だけでできる方法を記載します。Yamlとの脳内変換が多くなってくるので、ネストの深さがでてくると、冗長でも@Valueの方が楽な場合もあるかもしれません。適材適所でお使いください。

最後に

エキサイト株式会社では、新卒・中途(大卒・高専卒・高卒問わず)バックエンドエンジニア・アプリエンジニア・フロントエンジニア・クリエイティブを募集しております。興味がありましたらご連絡をお待ちしております。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

Javaで1日の始まりと終わりの時刻を簡単に取得する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 既存サービスのリビルドをするにあたり、日付まわりの処理を記述することが多くなってきました。 日付まわりの処理を誤ると、未公開のデータを取得できてしまうこともあるため慎重にコーディングしたいです。 そこで得た知見として、Javaで1日の始まりと終わりの時刻を取得する簡単な方法について紹介します。

環境

LocalTime.MIN / LocalTime.MAX を使う

LocalTimeのクラス変数であるMIN / MAXを使用することで、1日の始まりと終わりの時刻を取得することができます。 これを使用することで、自分で0時0分0秒を作成したり、23時59分59秒のLocalTimeを作成する必要がなくなるため、積極的に使っていきたいです。

public LocalDateTime getStartDate(LocalDateTime date) {
    return LocalDateTime.of(
            date.toLocalDate(),
            LocalTime.MIN
    );
}

public LocalDateTime getEndDate(LocalDateTime date) {
    return LocalDateTime.of(
            date.toLocalDate(),
            LocalTime.MAX
    );
}

参考

docs.oracle.com

SpringBootでスネークケースのリクエストパラメータを受け取る方法

エキサイト株式会社エンジニアの佐々木です。古いシステムをSpringBootリビルドしており、リクエストパラメータの命名が統一されていないというのがあったので、対応方法の一例を紹介します。

@RequestParamで解決する

@RequestParamname属性で指定できます。

@GetMapping
public String index(@RequestParam(name = "last_name") String lastName){
    return lastName;
}

curl "http://localhost:8080?last_name=buzz"
"buzz"

この方法は、パラメータが少ないときはいいのですが、多くなってくるとこのメソッドが辛くなっていきます。次はオブジェクトでの受け取り方法です。

変数名を変更する

オブジェクトで受け渡すときは、変数名を変更すると対応できます。last_nameの変数名を

@RestController
@RequestMapping
public class DemoController {

    @RequestMapping
    public String index(Form form){
        return "";
    }

    @Data
    static class Form {

        private String last_name;

    }
}

curl "http://localhost:8080?last_name=buzz"
{
  "last_name": "buzz"
}

これだと、レスポンスされるデータや内部で使うデータもスネークケースになってしまいます。Javaは、キャメルケースが通常なので、変換が面倒になります。

@ConstructorPropertiesで整える

スネークケースが必要な箇所だけ、@ConstructorPropertiesを付与します。

@RestController
@RequestMapping
public class DemoController {

    @RequestMapping
    public Form index(@Valid Form form){
        return "";
    }

    @Data
    static class Form {

        private String firstName;
        @NotEmpty
        private String lastName;
        private String phoneNumber;

        @ConstructorProperties({"last_name","phone_number"})
        public Form(String lastName, String phoneNumber){
            this.lastName = lastName;
            this.phoneNumber = phoneNumber;
        }
    }
}


// バリデーションも効きます
curl "http://localhost:8080?firstName=fizz&last_name=buzz&phone_number=123"
{
  "lastName": "buzz",
  "phoneNumber": "123",
  "firstName": "fizz"
}

curl "http://localhost:8080?firstName&phone_number=123"
// Validation Error.

検索とかは受け取るパラメータが多かったりするんで、中にはスネークケースのものがあったりします。1つだけ例外があった場合に、すべてのパラメータをコンストラクターで定義しないといけないのは辛いので、必要な箇所だけでいいようにします。ここらへんは賢いと思います。

最後に

パラメータの命名等は、コンパイル等では弾けず、linterでも見落とす部分があるので、最終APIでカバーすることになると思いますが、この程度でよければ解決できそうなので、ぜひ利用してみてください。

エキサイトではバックエンドエンジニア、アプリエンジニア、フロントエンジニア、UI/UXデザイナーを積極採用中です。ぜひご連絡ください。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com