JavaとPythonとGoのHTTPステータスコードの実装を調査した

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 Spring Boot / Javaで既存システムのリビルドを進めてHTTPステータスコードの実装を調べている途中に、ふと他の言語の実装が気になり、普段趣味で使用しているPythonとGoのソースコードを見てみました。 本記事では、JavaPython、GoのHTTPステータスコードの実装について紹介します。

調査環境

今回調査した環境は以下のとおりです。

  • JavaSpring Framework 5.3.7)
    • org/springframework/http/HttpStatus.java
  • Python 3.9.7
    • http/__init__.py
  • Go 1.17
    • src/net/http/status.go

Javaステータスコード

JavaSpring Framework)のステータスコードenumを使って実装されていました。 ステータスコードごとに要素が存在し、とてもわかりやすい実装になっています。 また、HttpStatusには2xx系のステータスコードかどうかを判定するis2xxSuccessful()メソッドや、エラーかどうかの判定するisError()メソッドが実装されています。ここに挙げたメソッド以外にもHTTPステータスコードに関係する便利なメソッドがいくつか実装されているので、必要に応じて活用していきたいです。

public enum HttpStatus {
    OK(200, Series.SUCCESSFUL, "OK"),
    CREATED(201, Series.SUCCESSFUL, "Created"),
    ACCEPTED(202, Series.SUCCESSFUL, "Accepted"),

   public boolean is2xxSuccessful() {
        return (series == Series.SUCCESSFUL);
    }


    public boolean isError() {
        return (is4xxClientError() || is5xxServerError());
    }

    private final int value;
    private final Series series;
    private final String reasonPhrase;

    HttpStatus(int value, Series series, String reasonPhrase) {
        this.value = value;
        this.series = series;
        this.reasonPhrase = reasonPhrase;
    }

    public int value() {
        return this.value;
    }
}

使い方

HttpStatusはvalue()メソッドを呼び出すことでステータスコードを取得することでき、getReasonPhrase()メソッドを呼び出すことでメッセージを取得することができます。

// 引数にHttpStatus.CREATEDを渡して実行
public void check(HttpStatus status) {
    System.out.println(status.value()); 
    // → 201
    
    System.out.println(status.getReasonPhrase());
    // → Created
}

Pythonステータスコード

Pythonの標準ライブラリのステータスコードは、IntEnumを継承して実装されていました。 Javaステータスコードの実装と比較的似ていますが、こちらはステータスコードのみ実装されており、ちょっとしたことで使える便利なメソッドの実装はありませんでした。

class HTTPStatus(IntEnum):
    OK = 200, 'OK', 'Request fulfilled, document follows'
    CREATED = 201, 'Created', 'Document created, URL follows'
    ACCEPTED = (202, 'Accepted', 'Request accepted, processing continues off-line')

使い方

HTTPStatusは、valueを呼び出すことでステータスコードを取得することができ、phraseを呼び出すことでメッセージを取得することができます。 また、IntEnumを継承しているためint型と比較することができます。

# 引数にHTTPStatus.CREATEDを渡して実行
def check(status: http.HTTPStatus):
    print(status.value)
    # → 201

    print(status.phrase)
    # → Created

    print(status == http.HTTPStatus.CREATED)
    # → True

    print(status == 201)
    # → True

Goのステータスコード

Goには上記に挙げたenumのような機能はありません。 Goでは、定数とmapを組み合わせてHTTPステータスコードを実装しています。 そのため、上記2つとは異なり、HttpStatus型のようなものはないため、ステータスコードはint型として扱わなければなりません。

package http

const (
    StatusOK       = 200 // RFC 7231, 6.3.1
    StatusCreated  = 201 // RFC 7231, 6.3.2
    StatusAccepted = 202 // RFC 7231, 6.3.3
)

var statusText = map[int]string{
    StatusOK:       "OK",
    StatusCreated:  "Created",
    StatusAccepted: "Accepted",
}

func StatusText(code int) string {
    return statusText[code]
}

使い方

とてもシンプルです!

// 引数にhttp.StatusCreatedを渡して実行
func check(code int) {
    fmt.Println(code)
    //→ 201

    fmt.Println(http.StatusText(code))
    // → Created
}

おわりに

本記事ではJavaPython、GoのHTTPステータスコードの実装についてまとめました。 実際にソースコードを比較してみると、各言語の特色が出てきて面白かったです。 最後まで読んでいただき、ありがとうございました!

Javaでクエリパラメータの日時データを受け取る方法

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

APIで、日付情報をクエリパラメータとして受け取るという場面はそう珍しいものではありません。 ただし日付情報の問題点として、様々なフォーマットが存在しうるというものがあります。

今回は、渡されるフォーマットに合わせて日付情報を受け取る方法を説明します。

日付情報とフォーマット

日付情報というのは、例えば「2020年1月1日 0時0分0秒」みたいなものです。 ただ、これを示すフォーマットはいくつかあります。 例えば、

  • 2020-01-01 00:00:00
  • 2020/1/1 00:00:00
  • 20200101000000

などなど…。 先程の日本語を織り交ぜた「2020年1月1日 0時0分0秒」もそうですし、日本語以外でもそのようなフォーマットはあるでしょう。

では、これらをJavaで考慮してAPIのクエリパラメータとして受け取れるようにするにはどうすれば良いでしょうか?

@DateTimeFormat

実は、Spring Bootであれば @DateTimeFormat というものを使えば簡単にできます。

実装方法は、

class SampleRequest {
    LocalDateTime sampleDateTime;

    public SampleRequest(
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime sampleDateTime
    ) {
        this.sampleDateTime = sampleDateTime;
    }
}

こんな感じで、引数にアノテーションとしてつけるだけでOKです。 アノテーション内の pattern で、受け取りたいフォーマットを指定します。

また、もし複数パターンで来ることがある場合でも、

class SampleRequest {
    LocalDateTime sampleDateTime;

    public SampleRequest(
        @DateTimeFormat(pattern = "[yyyy-MM-dd HH:mm:ss][yyyyMMddHHmmss]") LocalDateTime sampleDateTime
    ) {
        this.sampleDateTime = sampleDateTime;
    }
}

このように指定することで複数パターンを許容することができます。

ただし注意点として、 LocalDateTime 型(年月日・時間の両方を持つことが前提の型)では、

class SampleRequest {
    LocalDateTime sampleDateTime;

    public SampleRequest(
        @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDateTime sampleDateTime
    ) {
        this.sampleDateTime = sampleDateTime;
    }
}

のように日付だけで受け取ろうとするとエラーになるのでご注意下さい。

最後に

日付のフォーマットは、言語によって取り扱い方の難易度が変わってきます。 Javaであれば上記のようにフォーマットの指定が必要ですが、言語によってはあまり気にせず受け取れたりする場合もあるでしょう。

ただ、最初はゆるい制限の言語を使っていたために日付のフォーマットはそこまで気にしていなかったが、仕様や言語が変わって硬いフォーマット指定が必要になって大変…ということも起こり得るので、普段から日付のフォーマットの統一は心がけると良いでしょう。

もうキャッシュの実装は怖くない!

はじめに

こんにちは。エキサイト株式会社で長期インターンをさせていただいている岡崎です。

今回は私が学んだキャッシュについての記事を書かせていただきます。

「もう分かっているんだが?」というエンジニアの皆さん向けではなく、Spring Bootでキャッシュを初めて実装する人向けの記事となっております。

そもそもキャッシュとは?

キャッシュとは、アクセスしたwebページの情報を一時的に保存していく技術です。

キャッシュを用いることで1からページを読み込むことがないので、動作の速さが期待できるぞってことですね。

キャッシュの流れは以下の通りになっています。

  1. コントローラまたはサービスは@Cacheableがついたメソッドを呼び出す
  2. Cache AOPが提供する@Cacheableにキーが渡される。CacheManegarを利用して、Hash Mapからデータを取得する。キャッシュデータが取得できた場合はコントローラまたはサービスへキャッシュデータを返却し、キャッシュデータが取得できない場合は次を実行する。
  3. Cache AOPは引数を渡し、定義されたサービスメソッドを実行し、戻り値を取得する。Cache AOPは2で特定されたキャッシュキーで取得した戻り値をCache Manegerを利用してHash Mapへデータとして格納する。
  4. Cache AOPはコントローラまたはサービスへ戻り値を返却する。

Cache AOP

キャッシュ機能の入り口になるインターフェース

Cache Maneger

キャッシュ機能をコントロールしてくれる

キャッシュのデータはredisが保持してくれます。

このredisはデータをメモリに保存してくれる高速なデータストアのことです。

キャッシュの実装は?

はい、ようやく本題ですね。

じゃあ、それってどうやって実装するの?という話です。

Spring Bootではキャッシュ機能のためのライブラリがすでに用意されています。

 キャッシュする必要があるメソッドに@Cacheableをつける

ますはここです。

この@Cacheableとは、キャッシュを有効化にするためのアノテーションです。

キャッシュを行いたいメソッドに対し、

@Cacheable(

cachename = “名前を任意でつける”

key = “キーがあればキーをつける。なければなくてよい”

condition = “キャッシュをする条件があればここでつける。なくてもよい”

)

public void sample(Integer sample, String sample) {

とつけます。

これだけでいい場合もあるのですが、現在だとそれだけではないときも多いと思います。

散々見たエラーはCould not read JSON: Cannot construct instance ofというものでした。

データのやり取りのイメージは以下です。

f:id:ooo-ka999:20210907105750p:plain

このアノテーションをつけることだけでうまくいく場合もありますが、そうでないこともあります。

例えばLocalDateTimeです。 うまくデフォルトのものだと変換ができなくて落ちてしまいます。 なので、

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime sampleTime;

のようにアノテーションをつけてあげる必要があります。

そのほかにも、こんな事例がありました。 Jacksonでは、publicのフィールド、publicのゲッターを対象としてしまうので、対象にしたくないものは@JsonIgnoreをつける必要がありました。これで対象から外すためです。

使用例は以下の通りです。

@JsonIgnore
public Boolean isHogeHoge(String hogehoge) {
 return this.hogehoge == hogehoge;
}

また、デシリアライズ時にデフォルトコンストラクタが使用されてしまうので、デフォルトコンストラクタでデシリアライズしてほしくない場合は、@JsonCreatorを使ってデシリアライズ時のコンストラクタを使用してあげる必要があります。また、このときにprivateなインスタンス変数を使う場合は@JsonPropertyを用いてマッピングしたいキーの名前を指定してあげる必要がありました。

使用例は以下の通りです。

@JsonCreator
public SampleModel(@JsonProperty("sample") String sample) {
 this.sample=sample;
}

まとめ

キャッシュはキャッシュの対象にきちんと必要なアノテーションをつけてあげることが大切でした。キャッシュって奥深いんだなと今回では学びました。 まだまだ未熟なエンジニアですが、どんどん成長していきたいと思います。

また、余談ですがエキサイトのインターンはとても楽しく、充実しています! 興味ある人にはとてもおすすめしたいです。私はまだ引き続きインターンは続くので、残りの時間も成長できるように頑張っていきたいです。

ここまで読んでいただき、ありがとうございました

PHPDoc の @uses を使って、可変関数の未使用警告を抑制する

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

PHP の可変関数

PHP には可変関数という機能があります。

www.php.net

関数名を変数にすることで、動的に呼び出したい関数を変更できます。

<?php

class Demo {

    function foo() {
        echo "foo";
    }
}

$functionName = 'foo';
$demo = new Demo();

$demo->$functionName();

使い方によっては有用な機能かもしれません。

しかし、intelliJ (PhpStrom) でファイルを開いてみると、未使用関数としてグレーアウトされてしまいます。

f:id:excite-mthiroshi:20210913201930p:plain
intelliJ で可変関数呼び出しをすると未使用関数として扱われる

未使用のコードは、運用開発の際にノイズになるので、可能な限り削除してしまう方がよいでしょう。しかし、可変関数呼び出しのコードがある場合、プロジェクト全体に検索をかけて、使われていないことの確認が必要です。 また、未使用関数の場合、定義元へコードジャンプも効かないため、修正するファイルの切り替え等の作業が煩雑になってしまいます。

PHPDoc の @uses を使おう

intelliJ で可変関数がグレーアウトされてしまうのは、静的解析を基に未使用と判断されるからです。 そこで、 PHPDoc の @uses を使って適切に呼び出し関係を補足してあげましょう。

f:id:excite-mthiroshi:20210913202215p:plain
呼び出し元で @uses をつける

可変関数が未使用関数ではなくなり、色が付きました。また、これでコードジャンプもできるようになりました。

最後に

PHP の可変関数について、intelliJ で表示される未使用警告の回避方法を説明しました。

長年運用されてきたソフトウェアは、使われなくなったコードが溜まってきてしまいます。Doc 以外にも、多くの言語で静的解析ツールが用意されていますので、それらを上手く駆使して無駄がないコードを保っていきましょう。

参考

PhpStorm の静的解析機能をさらに活用するための3つのアノテーション | バシャログ。

@uses & @used-by — phpDocumentor

Spring Bootでクエリパラメータの順序が異なるURIを比較する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 UriComponentsのreplaceQueryParam()を使用してクエリパラメータの値を書き換えたときに、書き換える前と書き換えた後とでクエリパラメータの順序が異なってしまい、 単体テストで落ちてしまいました。 本記事では、クエリパラメータの順序が異なるURIを比較する方法について共有します。

概要

クエリパラメータの順序が異なる2つのURIを用意します。 このとき、uri1とuri2を比較するとfalseになります。

URI uri1 = URI.create("https://example.com?page=2&per_page=10");
URI uri2 = URI.create("https://example.com?per_page=10&page=2");

System.out.println(Objects.equals(uri1, uri2)); // → false

これは、URIがクエリパラメータを文字列で保持しており、equalsで比較したときにクエリパラメータは"page=2&per_page=10""per_page=10&page=2"の文字列比較を行っているため、falseになってしまうからです。 クエリパラメータの順序が異なったとしても同一のものとして比較したいときは、少し工夫する必要があります。

解決策

URIをUriComponentsに変換することで容易に比較することができるようになります。 UriComponentsではクエリパラメータをMap<K, List<V>>のラッパーであるMultiValueMapで保持しているため、クエリパラメータの順序が異なるものを比較したときにtrueを返します。

UriComponents u1 = UriComponentsBuilder.fromUri(uri1).build();
UriComponents u2 = UriComponentsBuilder.fromUri(uri2).build();

System.out.println(Objects.equals(u1, u2)); 
// → true

実際にUriComponentsのクエリパラメータの中身を見てみると、確かにMap形式で保持されていることが確認できます。

System.out.println(u1.getQueryParams());
// → {page=[2], per_page=[10]}

注意点

MultiValueMapは1つのキーに対して複数の値を扱うことができます。 そのため、クエリパラメータに同一のキーが複数指定されている場合、Listに変換されるため、falseを返す可能性があります。

URI uri3 = URI.create("https://example.com?page=2&per_page=10&page=3");
URI uri4 = URI.create("https://example.com?page=3&per_page=10&page=2");

UriComponents u3 = UriComponentsBuilder.fromUri(uri3).build();
UriComponents u4 = UriComponentsBuilder.fromUri(uri4).build();

System.out.println(u3.getQueryParams());
// → {page=[2, 3], per_page=[10]}

System.out.println(u4.getQueryParams());
// → {page=[3, 2], per_page=[10]}

System.out.println(Objects.equals(u3, u4));
// → false

したがって、同一のキーが複数指定される場合は注意しなくてはなりません

おわりに

UriComponentsを利用してクエリパラメータの順序が異なるURIを比較する方法についてまとめました。 クエリパラメータの順序が異なるURIを比較したいときって一度はあるのかなと思います。 URIをUriComponentsにして比較を行うことで、クエリパラメータ順序が異なっていたとしてもtrueを返すようになります。 「クエリパラメータの順序が異なるURIでも同じものとして扱いたい!」といった方に本記事が参考になれば幸いです。

参考

docs.oracle.com

spring.pleiades.io

mysqlslap で MySQL の負荷エミュレーションをしてみる

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

オンプレにあるDBを稼働中のRDSに統合する際に、mysqlslapを使った負荷エミュレーションをしたので、使い方を紹介します。

mysqlslapとは

mysqlslapとは、負荷エミュレーションをするコマンドです。クライアントの接続数やクエリ数などを設定して、実行時間を計測してくれます。 dev.mysql.com

mysqlCLIクライアントをインストールした際に一緒にいくつかの便利コマンドが入っているみたいです。mysqlslapもその一つです。

実行例

75クライアントから75クエリーのUPDATE文の実行を10回繰り返す実行例です。

DB設定

DBホスト : demo_db
mysqlユーザ : demo_user
mysqlパスワード : demo_password
ポート : 3306
mysqlslap \
--no-defaults \
--auto-generate-sql \
--engine=innodb \
--create-schema=demo_schema \
--password=demo_password \
--host=demo_db \
--user=demo_user \
--port=3306 \
--number-char-cols=3 \
--iterations=10 \
--concurrency=75 \
--auto-generate-sql-write-number=10 \
--auto-generate-sql-add-autoincrement \
--auto-generate-sql-load-type=update \
--number-of-queries=75
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
    Running for engine innodb
    Average number of seconds to run all queries: 1.200 seconds
    Minimum number of seconds to run all queries: 1.068 seconds
    Maximum number of seconds to run all queries: 1.371 seconds
    Number of clients running queries: 75
    Average number of queries per client: 1

1回の試行の実行時間の平均が1.200秒、最小値が1.068秒、最大値が1.371秒でした。 このように、パラメータを調整して実行時間を計測します。

設定したオプションの説明です。

--no-defaults : デフォルト値を読み込まない
--engine : 使用するストレージエンジン
--create-schema : スキーマ
--password : DBのパスワード
--host : DBのホスト
--user : DBのユーザ
--port : ポート番号
--iterations : 試行回数
--concurrency : クライアント数
--auto-generate-sql : SQLを自動で生成
--auto-generate-sql-write-number : 各スレッドで実行する行挿入の回数
--auto-generate-sql-add-autoincrement : AUTO_INCREMENT カラムを自動生成されたテーブルに追加
--auto-generate-sql-load-type : テストの負荷タイプを指定
--number-of-queries : 各クライアントのクエリー数の指定
--number-char-cols : --auto-generate-sql が指定されている場合に使用する VARCHAR カラムの数

これ以外にも様々なオプションがありますので、公式ドキュメントを御覧ください。

検証したこと

今回は、オンプレDBからRDSで稼働中のDBに相乗りさせる形で移行を考えていました。その場合、オンプレDB分の負荷がRDSにそのままかかってきますので、それに耐えられるかを検証しました。

まずは、オンプレDBにかかっている負荷をzabbixから確認しました。それを基にmysqlslapでRDSに負荷をかけて、実行時間を確認していきました。最終的には、zabbixで確認した負荷より大幅に高い負荷をかけても実行時間が十分に短かったので、移行しても耐えられると判断しました。

そして、実際にRDSへ統合してCloudWatchで負荷を確認したところ、詰まることなく捌けていました。

事前に負荷検証をすることで、安全にDB移行ができました。

最後に

オンプレDBのAWS移行にあたって、RDSに対して負荷検証をしてみました。 mysqlslapで簡単に負荷検証ができるので、ぜひ使ってみてください。

第4回定期勉強会「Flutter勉強会」

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

毎月恒例の定期勉強会が今月も開催されました。 今回は「Flutter勉強会」ということで、エキサイトでも使われ始めたFlutterの話になります。

なお過去開催分は以下になりますので、よければ御覧ください。

tech.excite.co.jp

tech.excite.co.jp

tech.excite.co.jp

Flutter勉強会

Flutterは、公式サイトで以下のように紹介されています。

Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, desktop, and embedded devices from a single codebase.

端的に言えば、「単一コードからモバイルアプリやwebなど様々なデバイス用のアプリケーションを生成できるフレームワーク」です。 その利便性から徐々にいろいろなサービスに取り入れられていっており、エキサイトでも導入され始めています。

今回は実際にエキサイト内でFlutterに触っているエンジニア3名に、

  • Flutterとは何か
  • どんな特徴があるのか
  • 触ってみた所感

を発表してもらいました。

Flutter自体の説明は以上の通りで、その利便性は言うまでもないですが、実際に使っているからこそわかる

  • 具体的なディレクトリ構造やコードはどうなっているのか
  • Swiftなどのネイティブコードの開発と比べた時のメリット・デメリット
  • 広告など、実際にアプリケーションコードを書く上で必要になってくる要素をどのように実装すべきなのか

などを教えてもらうことができました。

単に「Flutterっていいよね」だけではなく、こんな面倒な点がある、デメリットがあるというところも発表されていて、Flutterを技術選定の選択肢に入れる上で有意義な勉強会になったと感じています。 こうした生の声は、導入しはじめの技術では特に共有のメリットが大きいので、Flutter以外の技術でもぜひ勉強会をしていきたいところです。

最後に

定期勉強会は第4回目ということで、ついに四半期分をやったということになりました。 今後も半年、1年、そしてそれ以上と続いていったらと思います。