「Booost」ハッカソン型インターンを開催しました。

エキサイト株式会社では、9月9日(木)〜13日(月)に「Booost」ハッカソンインターンを開催しました。

学生向けの長期インターンは以前から実施していますが、ハッカソン型のインターンは初開催でした。 「学生生活をエキサイトさせるサービス」というテーマで、5日間かけてサービスの企画から開発までを行いました。

スケジュールについて

2日間でサービスを企画し、3日間で開発するというスケジュールでした。 サービスの企画は学生のみで行い、 開発からエキサイト株式会社とiXIT株式会社のエンジニアがメンターとしてサポートに入りました。

9月 9日(木) サービス企画1日目

9月10日(金) サービス企画2日目、中間発表

9月11日(土) 開発1日目

9月12日(日) 開発2日目

9月13日(月) 開発3日目、最終発表、オンライン懇親会

ハッカソンでの使用技術について

開発に集中していただきたかったので開発環境用意の負担を減らすため、 Docker・Laravel・Next.js・MySQL・Redisのプロジェクトを用意していました。 また、サービスを本番環境にリリースできるように、学生分のAWSアカウントを用意し、EC2等を利用できる状態にしていました。

ベースは用意していましたが、実際に使用する技術に関しては学生の皆様にお任せしていたので、チームによって違う技術構成になりました。

各チームが、サービスのコンセプトやメンバーの技術スタック、挑戦したい技術等で判断していて、チームの色が出ていました。各チームのサービス企画内容のリンク先に、使用技術について書いていますので、ぜひご覧ください。

コミュニケーションについて

コミュニケーションツールとしては、Discordを利用し、チームごとのボイスチャンネルとテキストチャンネルを用意しました。 ハッカソン中は、ボイスチャンネルに入って開発しており、話し合いがすぐできる環境になっていました。

毎日、朝会と夕会を行い、チームの進捗状況を人事やメンターが都度確認できるようにしていました。

メンターについて

エキサイト株式会社とiXIT株式会社のエンジニアがメンターとして参加しました。

チームごとにメンターが2〜3名付きまして、チームのボイスチャンネルに入って開発の状況を聞きながら、 タスクの優先順位付けやスケジューリング等のプロジェクトマネージメントから、 データベース設計やコーディング等の技術的なことまで、プロダクトの完成を目指して技術問わずサポートしていました。

各チームのサービス企画内容

各チーム、最終発表までにデモができる段階まで完成させてくれました。

以下のリンクに、サービスの詳細が記載されています。

topaz.dev topaz.dev topaz.dev topaz.dev

ハッカソン 結果

優勝:

本音ターン

受賞理由:

サービスの完成度が高く、実機と管理画面のデモが完璧動いていた点が素晴らしかったです。 学生に関する社会的な課題の解決を目指しているところがより実用的で良くアイデアも高い評価がされました。

特別賞:

Lian

受賞理由:

サービスデザインの完成度の高い点や、 短期間の間で、想定していた機能を作り上げていた点が評価されました。

参加者のブログ

参加してくださった学生さんがブログ記事を書いてくださりましたので、こちらもご覧ください。とても有り難いです。

exciteのハッカソン型インターン「Booost」で特別賞もらえた話 - Qiita

最後に

ハッカソン初心者の学生もいる中で、学生やメンター含め全メンバーが切磋琢磨し、最後発表まで開発をやり遂げてくれました。 また学生の皆様に、このハッカソンを通してチーム開発や技術的な知識について学びを得ることができたと言っていただけました。 運営としても、学生の皆様のチャレンジ力に刺激され、様々な学びを得ることができました。 どちらとも学びのある良い5日間となりました。

弊社では、今回のようなハッカソンインターンや、長期インターン等の用意しています。 また、一緒に働いてくれる仲間を絶賛募集しております! ご興味のある方は、以下のリンクからぜひご応募ください!

www.wantedly.com

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年、そしてそれ以上と続いていったらと思います。

Spring Bootでコンポーネント名が重複してエラーになったときの解決策

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 Spring Bootでコンポーネント名が重複したときに、ConflictingBeanDefinitionExceptionが発生してエラーとなってしまいました。 その原因と解決策についてまとめました。

別のパッケージの同一のコンポーネント

例としてItemに関するItemServiceについて考えてみます。 ItemServiceはWebとアプリで処理が少し異なるため、それぞれで実装します。

  • WebのService
package com.sample.service.item;

import org.springframework.stereotype.Service;

@Service
public class ItemServiceImpl implements ItemService {
    @Override
    public Item getItemByID(Long id) {
      /* 処理 */
    }
}
  • アプリのService
package com.sample.app.service.item;

import org.springframework.stereotype.Service;

@Service
public class ItemServiceImpl implements ItemService {
    @Override
    public Item getItemByID(Long id) {
      /* アプリ固有の処理 */
    }
}

上記2つは似ていますが、パッケージが異なることに注意が必要です。 通常、同一のクラス名であったとしても、パッケージが異なれば問題なく利用できます。 しかし、ConflictingBeanDefinitionExceptionが発生しコンパイルできずにエラーとなってしまいました。

ConflictingBeanDefinitionException: 
Annotation-specified bean name 'itemServiceImpl' for bean class [com.sample.app.service.item.ItemServiceImpl] conflicts with existing,
non-compatible bean definition of same name and class [com.sample.service.item.ItemServiceImpl

原因

コンポーネントの場合、同一のクラス名が2つ以上存在するときにコンパイルできずにエラーとなってしまいます。 これは、Spring BootがDIするときに、どちらのBean名もItemServiceImplとなってしまうからです。 そのため、どちらのSpring BootがどちらのItemServiceImplをDIしてよいのか判別できなかったため、ConflictingBeanDefinitionExceptionが発生してしまいました。

解決策1:コンポーネントプレフィックスに文字列を付与する

一番単純でわかりやすいのはクラス名の前にAppAdminなどの文字列を付与することです。 これで問題なくコンパイルおよび実行することができます。

package com.sample.app.service.item;

import org.springframework.stereotype.Service;

@Service
public class AppItemServiceImpl implements AppItemService {
    @Override
    public void getItemByID(Long id) {
      /* アプリ固有の処理 */
    }
}

解決策2:アノテーションの引数に文字列を指定する

@Controller@Service@Repository@Componentなどのアノテーションの引数に独自のBean名を定義します。 これにより、同じクラス名のコンポーネントが複数存在しても問題なく実行することができるようになります。

package com.sample.service.item;

import org.springframework.stereotype.Service;

@Service("AppItemServiceImpl")
public class ItemServiceImpl implements ItemService {
    @Override
    public void getItemByID(Long id) {
      /* 処理 */
    }
}

このとき、アノテーションの引数には変数を渡すことができます。 これで管理するのもよいかもしれません(ただし面倒だとは思います)。

@Service(ComponentName.APP_SERVICE_IMPL)

おわりに

Spring Bootでコンポーネント名が重複してエラーになったときの原因と2つの解決策についてまとめました。 個人的には、命名規則を決めて、管理者用ならAdminを、アプリ用ならAppプレフィックスに付与したほうがよいのかなと考えています。 例えば、解決策2で運用したときにItemServiceIDEでファイル検索したときに、ファイルパスまで含めて見ないとたどり着けないため、少し不便に感じるからです。 ただ、どちらの実装方法も大きく異なることはないため、最終的には好みになりそうです。

どこまでアプリケーションを「完璧」にすべきか

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

アプリケーションを作るときや改修するとき、すべてのエンジニアが「完璧なアプリケーションを作ろう」と一度は思うはずです。

しかし、シンプルなコードで済むアプリケーションならともかく、複雑になればなるほど「完璧」を達成するのは難しくなっていきます。

今回は、どこまで「完璧」を目指すべきか、持論を語っていきたいと思います。 なおこちら、あくまで持論であり、必ずしも会社の方針と100%一致するとは限らないので、あらかじめご了承下さい。

完璧なアプリケーションとは

まず最初に、「完璧なアプリケーション」とは何でしょうか? それはもちろん、「いついかなる時も問題が起きない」アプリケーションです。

例えば、「2つの値を渡すとその合計値を返す」というアプリケーションの場合、正の数・負の数・非整数でも計算が可能であるとか、(仕様次第ですが)IntegerやLong値の限界を超えても問題なく動作する、などが「完璧」の条件として挙げられます。

この程度であれば問題ありませんが、ではWebサイトであればどうでしょうか? 考えられる条件としては、

  1. サイトにアクセスすれば、必ず想定通りの情報がサイトに載っている
  2. いつサイトにアクセスしても、一定以下の時間でページが表示される

でしょうか。 一見条件も少なく、簡単に達成できるような気もしてきますが、Webサイトというのは様々な状況下に陥ることが多々あります。 例えば、

  • 急に大量のアクセスが来る
  • 外部からの攻撃を受ける
  • ごく限られた条件下でしか起きない、コード内の潜在的なバグが発火する
  • 運用上のミス等で、DBなどのデータソース内のデータに不整合が生じる
  • インフラ周りに物理的な障害が発生する
  • クラウドを使用している場合、クラウド側に問題が発生する

などが挙げられるでしょう。 これら(もちろんここで挙げていないその他様々な原因も)をすべて解決し、「完璧なアプリケーション」を作り上げることは可能なのでしょうか?

「完璧」でなく「ベストエフォート」なアプリケーションを作る

個人的には、これら全てを完全に解決することはかなり難しいと考えています。

もちろん、例えば「急に大量のアクセスが来る」の対処法として「サーバ/コンテナが自動スケールするようにする」ですとか、「外部からの攻撃を受ける」の対処法として「セキュリティ対策を万全にする」など様々な解決策はありますが、それでも自動スケールが完了するまでの数分間はページ速度の急激な低下、もしくはタイムアウトはどうしても発生してしまうでしょうし、外部からの攻撃も新しい種類のものが出てきてしまえばまた新しく対処法を考える必要が出てきます。

さらに言えば、例えばごく限られた条件でしか起きない問題に対してどこまで対応するべきでしょうか? 1年に1~2回、しかも1回あたり数秒程度しか起きない問題に対して、何日も何週間もかけて原因調査・対応をするべきなのでしょうか?

私もしばらくこういった問題に頭を悩ませてきましたが、他のエンジニアと相談したり、色々と考えた結果、「完璧」ではなく「ベストエフォート」こそ目指すべきアプリケーションなのではないか、と考えるようになりました。

アプリケーション、及びそれらの上で展開されるサービスには、もちろん譲れない点があります。 例えば、「1日6時間はエラーでサイトが見られない」などといったサービスは、いくらなんでも受け入れられないでしょう。 可能な限り、いつでも見られるようにする必要はあります。

しかし、例えば「年間で99.9%の時間のサイトの動作は保証する(0.1%の時間はエラーでサイトが見られない可能性がある)」だとしたらどうでしょう? この程度であれば許容範囲、というサービスならあるのではないでしょうか? なおこれは、1年 = 365日 = 8760時間の0.1%なので、8.7時間ほどエラーが許容される、ということになります。

考えてみると、そもそもインターネットという仕組みそのものがベストエフォートという考え方で作られています。 今回の例であるWebサイトでいえば、その上で作られているアプリケーションがベストエフォートを目指すのは、ある意味自然なのかもしれません。

また有名なAWSも、サービスレベルアグリーメントという形で、「完璧」ではなく「ベストエフォート」でサービスを作っていることを宣言しています。

もちろん、サービスの内容によってどこまで許容されるかは変わってきます。 ニュースメディアサイトと医療用のオンライン手術サービスのようなものであれば、許容されるエラーのレベルは全く違うことでしょう。

ただしなんにせよ、やみくもに「完璧」なアプリケーションを作ることを考えるのではなく、まずはそのサービスでどこまでエラーが許容されるかを考えるべき、というのはすべてのアプリケーションに共通しているのではないでしょうか。 そして、許容されるエラーレベルに対して、適切に「ベストエフォート」なアプリケーションを作っていくことが、開発速度と開発精度の両方を兼ね備えた開発方法なのかもしれません。

最後に

AWSのように非常に大きく、インフラそのものをサービスとして提供するサービスであれば、最初から「~~%までの正常動作は保証する」というように宣言をしています。 一方で、いわゆる一般的なサービス・アプリケーションであれば、暗黙的に「まれに障害が発生することもある」という認識はありつつも、どこまでエラーが許容されるかという話し合いをすることは少ないのではないでしょうか? 特に自社開発であれば、その傾向は大きなものとなるでしょう。

ですがエラーや障害というのは、そのサービスが大きくなればなるほどいつか必ず起きるものとなっていきます。 例え自社開発のサービスであっても、一度はエラー許容レベルに関して話し合って決めることで、開発速度と精度のバランスを取ることができ、長期的に見てサービスの開発に良い影響を与えるのではないでしょうか。

はじめてのFlutter

iXIT株式会社の堀です。

新しい事をはじめたいと思い、グループ会社でも採用が決まったFlutterの勉強をはじめました。
2018年末にGoogleからFlutterがリリースされ、2年以上も経つのでネット上にも情報がたくさん載っています。 同じようにこれから始める人の参考になればと思い記事書かせて頂きます。(技術的要素はほぼありません)

何本か入門記事を読んだあと、環境構築。
Android StudioXcodeもすでにインストール済みだったので、あっという間に環境が出来上がりました。
最近のフレームワークは手とり足取り感満載ですね!
その後は、毎度おなじみ「Hello World」かと思いきや、用意されているサンプルアプリはカウンターアプリでした。
ソース内にコメントもいっぱい!(英語だけど)

カウンターアプリ
カウンターアプリ

Dartの入門書を見ながら、軽くソースを解析。 これだけのステップ数でスタイルもまとめて出来ちゃうなんてFlutterすごいかも。 (ただし、規定外のデザインにする時大変そうな気が・・・)

次の1歩として新しいアプリを作ろうと思い、見つけたのがこちら。

eh-career.com

解説も細かく書かれており、入門アプリにはちょうど良さそう!
早速、記事を見ながらプログラミング・・・
あれ?記事通りに書いてるのにエラーが・・・
スペルミスかなっと思いコピペ。それでもエラーが解消されないので、エラーメッセージで検索。 いっぱい出てきました。「その書き方もう使えないよ〜」ってコメントでした。 最近よく聞く「破壊的変更」ってやつですね。
下位互換はせず、新しく作り直してシンプルにする!って方針は賛成ですが、古い記事に惑わされて情報の海で溺れないように要注意です。

上記の解説ページとバージョン違いで出たエラーを潰しながら、どうにか完成。
Githubリポジトリ見てもつまらないので、Excite Tech Blogを表示できるように改良。

記事一覧アプリ
記事一覧アプリ

もうちょっとステップアップしてFirebaseと絡めたアプリを作りたいなと思っていたところ、程よいサンプルアプリ見つけました!

github.com

Github上にソースだけでなくFlowchartや利用するAPIリンクなどもキレイに纏められています。
Youtube3本立てのTutorialもあるのでこちらを見ながら実装。
(解説は英語ですが聞き取りやすい話し方ですし、コード見ながらなので分かりやすいです)
バージョン違いによるエラーを解消しつつ、Tutorial#2まで完了。

投票アプリ
投票アプリ

引き続き、完成を目指して進めて行きたいと思います。

YoutubeにはTutorialがたくさん載っています。
英語だからと諦めてしまうには勿体ないくらい素敵な動画も多いので、みなさんもぜひ挑戦してみてください!

エキサイトは「iOSDC Japan 2021」に協賛します!

今週金曜日から開催される「iOSDC Japan 2021」に、エキサイトはシルバースポンサー、Tシャツスポンサーとして協賛します。 イベント概要については、以下をご覧ください。

iOSDC Japan 2021

開催日時 : 2021年9月17日(金)〜 9月19日(日)

開催場所 : オンライン開催

公式サイト :

iosdc.jp

公式Twitter :

twitter.com

最後に

エキサイトでは一緒に働いてくれる仲間を絶賛募集しております!

また、長期インターンも歓迎していますので、ご連絡お待ちしております!!🙇‍♀️

www.wantedly.com

そして、iOSDCトークンはこちらになります!

#エキサイト

それでは皆さま、 iOSDC Japan 2021を一緒に楽しんでいきましょう!

DefaultErrorAttributesについて

エキサイト株式会社の中尾です。

RestControllerAdviceで拾えない例外処理を拾う方法を記載します。

よくある方法は、ExceptionHandlerで全てcatchする方法だと思います。

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleNotFoundException(Exception exception) {
        this.writeLog(exception.getStackTrace());
        return "all Exception catch";
    }

しかし、これだとResponseStatusが全てINTERNAL_SERVER_ERRORになります。 ResponseStatusを指定しない場合、ステータスOKになってしまいます。

なぜこのようなことしないといけないのでしょうか?

そもそもデフォルトのエラーレスポンスはいかになっています。

{
  "timestamp": "2021-09-04T20:25:31.581+09:00",
  "status": 404,
  "error": "Not Found",
  "trace": "org.springframework.web.servlet.NoHandlerFoundException: No handler found for GET /aaa\n\tat org.springframework.web.servlet.DispatcherServlet.noHandlerFound(DispatcherServlet.java:1278)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1041)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:626)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:733)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.servlet.resource.ResourceUrlEncodingFilter.doFilter(ResourceUrlEncodingFilter.java:67)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:94)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:142)\n\tat org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:834)\n",
  "message": "No handler found for GET /aaa",
  "path": "/aaa"
}

エラー情報ですぎですよね。 本番環境でこれだと困ると思います。 以下の設定を追加することで、traceログを非表示にできます。

server.error.include-message: never
server.error.include-binding-errors: never
server.error.include-stacktrace: never
server.error.include-exception: false

traceとmessageが消えました。

shogo.nakao@localhost: $ curl http://localhost:8080/aaa | jq                                                                                                    [/Users/shogo.nakao]
{
  "timestamp": "2021-09-04T20:34:27.516+09:00",
  "status": 404,
  "error": "Not Found",
  "path": "/aaa"
}

しかし、アプリケーションによって、エラーレスポンスの形は変わると思います。 アプリケーションに合わせてエラーレスポンスを変えてあげましょう。

@Component
@Profile("!local")
public class CustomErrorAttributes extends DefaultErrorAttributes {

    /**
     * error response default setting 
     *
     * @param webRequest
     * @param options
     * @return
     */
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        final String message = this.getMessage(webRequest, this.getError(webRequest));
        Map<String, Object> customErrorAttribute = new HashMap<>();
        customErrorAttribute.put("error",
                new ErrorResponse.ErrorData()
                        .setMessage(message));
        return customErrorAttribute;
    }
}
{
  "error": {
    "message": 404
   }
}

解説します。

DefaultErrorAttributesを継承することで、デフォルトエラーレスポンスの型を変えられます。 @Profile("!local")を設定することでローカル環境以外の場合に適応させます。 ローカル環境では詳細なトレースログみたいですからね。 どっちにしてもコンソールログに詳細なログは出力されますが。

final String message = this.getMessage(webRequest, this.getError(webRequest));

こちらで、this.getMessageとthis.getErrorはDefaultErrorAttributesの実装を見てください。

RestControllerAdviceについて

エキサイト株式会社の中尾です。

SpringBootでexceptionが発生したとき、特定のエラーレスポンスで返す方法を記載します。

以下、コードになります。

@RestControllerAdvice
@Slf4j
public class ExceptionController {
    @ExceptionHandler({BadRequestException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleBindException(Exception exception) {
        this.writeLog(exception.getStackTrace());
        return exception.getMessage();
    }

    @ExceptionHandler({NoHandlerFoundException.class})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleNotFoundException(Exception exception) {
        this.writeLog(exception.getStackTrace());
        return "404 Not Found";
    }

    private void writeLog(StackTraceElement[] stackTraceElements){
        Arrays.stream(stackTraceElements)
                .limit(10L)
                .forEach(e -> log.error(e.toString()));
    }
}
public class BadRequestException extends RuntimeException {
    public BadRequestException(String s) {
        super(s);
    }
}
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false

解説します。

RestControllerAdviceはRestControllerと違ってGetMapping/PostMappingとかは記載せず、ExceptionHandlerで該当のExceptionが発生したときResponseStatusを返します、

spring.pleiades.io

動作検証

shogo.nakao@localhost: $ curl http://localhost:8080                                                                                     [/Users/shogo.nakao]
XXXX must not be empty

404の場合、springの設定ファイルにthrow-exception-if-no-handler-found: trueとadd-mappings: falseをプロパティファイルに設定した上でRestControllerAdviceに追加してください。

shogo.nakao@localhost: $ curl http://localhost:8080/aaa                                                                                                         [/Users/shogo.nakao]
404 Not Found

うまくいきました。

なお、以下のようにExceptionHandlerになにも指定しない場合、全てのエラーをキャッチすることができます。

    @ExceptionHandler
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleNotFoundException(Exception exception) {
        this.writeLog(exception.getStackTrace());
        return "all Exception catch";
    }

RestControllerAdviceには関係ありませんが、writeLogにstackTraceのエラーを10行ぐらいまで出力する設定になります。

        Arrays.stream(stackTraceElements)
                .limit(10L)
                .forEach(e -> log.error(e.toString()));

GitHub上でVisual Studio Codeを起動してコードレビューをする

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 少し前にGitHub上でVisual Studio Codeを起動してコードレビューをすることができるようになりました。 実際に試してみたので共有します!

Visual Studio Codeを起動する

プルリクエストのページから「.(ドット)」を押すことで、ブラウザ上でVisual Studio Codeを起動することができます。

f:id:excite-kazuki:20210824095702p:plain

下記画像のページが出てきて少し待ちます。

f:id:excite-kazuki:20210905222910p:plain

その後、エディタが開きました!

f:id:excite-kazuki:20210905222945p:plain

プルリクエストを確認する

左のタブ一覧から「GitHub Pull Request」をクリックします。

f:id:excite-kazuki:20210905224229p:plain

ファイルとファイルの差分を見ることができます。 f:id:excite-kazuki:20210905224354p:plain

実際にGitHub上で見る「File Changed」と同じことが確認できます。

f:id:excite-kazuki:20210905224516p:plain

URLを見ると、トップレベルドメインGitHub.comVisual Studio Code.devになっていることが確認できます。

コードレビューをする

行番号の横のプラスボタンをクリックするとコメントをすることができます。 ただし、複数行選択してコメントができないことや、斜体や太字、箇条書きなどのボタンが用意されていないため、 完全上位互換にはならないことに注意が必要です。

f:id:excite-kazuki:20210905231217p:plain

実際にプルリクエストのページを見ると、コメントされていることが確認できます。

f:id:excite-kazuki:20210905231227p:plain

ファイル単位で差分を確認できるため、GitHub上でのみコードレビューをしている人にとっては、新たな選択肢が増えたのかなと思います。

ローカル環境でコードレビューをする方法

ローカルのVisual Studio Codeであれば拡張機能GitHub Pull Requests and Issues」を導入することでプルリクエストの作成や、コードレビュー、Issueの作成をすることができます。

f:id:excite-kazuki:20210824092038p:plain

また、JetBrains IDEであればPull Requestsタブから同様のことができます。

f:id:excite-kazuki:20210824092354p:plain

おわりに

GitHub上でVisual Studio Codeを起動してコードレビューする方法についてかんたんにまとめてみました。 コードレビューの選択肢が増えたのはとても嬉しいです!