障害調査のコツ(ログの調査方法など)

新卒とかに何度も説明しているので、再共有時用のメモです

とにかくまずはアプリケーションが出力しているログを見ることです。

大抵の場合は、答えか大きなヒントがそこに書いてあります。

特にフレームワークの出力しているエラーメッセージとスタックトレースをよく見ましょう。

自分たちが書いたコードでも、異常データが入力された場合やデータ不整合を検知した箇所ではわかりやすいメッセージと共にエラーログを出力しておきましょう。半年か1年後の自分が救われます。(もっと早いかも?)

ログにヒントが無いときはどうしましょうか。

  • 現象が無応答状態ならThread dumpの各Threadの状態を観察する
  • OutOfMemoryによる突然死の場合はheap dump(発生時に出力するように設定するのを忘れないこと)を、Eclipse Memory Analyzerなどで解析する
  • Stop The Worldっぽい場合は、gc.logでFull GCが起きてないか確認する

といった感じのことをすれば大抵の場合は原因を突き止められると思います。

上記でも特定できない突然死の場合はOSのOOM Killerを疑いましょう。OOM Killerでないにしても、/var/log/messagesなどの/var/log配下のログを見ると、OSがプロセスを落としている痕跡が残っているかもしれません。

他にも症状によって疑うところはたくさんあると思いますので、上記は一例だと思ってください。

ちなみにネットワーク周りの調査だとパケットキャプチャするっていう手もあります。

例えばkeep aliveの設定が効いているかとかハーフコネクションが起きてないかとかですね。

意図した電文や正常な電文がやり取りされているか確認する手段はあるので、知っておくといつか役に立つかもしれません

私もパケットキャプチャの勉強をちゃんとやろうと思ってすべく、パケットキャプチャの教科書という書籍を買いました。

パラパラめくった感じでは、なかなか良さそうです。

まだ読んでませんが^w^

DAO(Repository)の単体テストについて

DbSetUpとAssertj-DBの組み合わせがとても良さそうです。

さらに組み込みDBのH2と組み合わせてSpringのTest系の機能を組み合わせるとほぼ理想通りのDAOのテストがかけるようになりました。

具体的には以下が実現出来ています。

  1. h2でテスト実行するからMySQLなどの本物のDBが要らない
  2. テスト実行後のデータが残るのでテスト失敗時のデータが残せる
  3. テストクラスごとにh2のデータベースが起動するので、テストの並列実行が可能(=高速化)
  4. テストデータを簡単に投入できる
  5. データを簡単に検証できる

コードの完全版は用意できませんでしたが、DbSetUpとAssertj-DBの簡単なサンプルを示しておきます。

DBのセットアップはこんな感じで、

@Before
public void setUp() {
    Operation ops = sequenceOf(DELETE_ALL, INSERT_ROLE);
    DbSetup dbSetup = new DbSetup(this.destination, ops);
    dbSetup.launch();
}

テーブルの中身の検証はこんな感じです。

Assertions.assertThat(someTable)
          .hasNumberOfRows(5)
          .row(0)
          .value("some_column").isEqualTo("awesomeValue")
          .value("other_column").isEqualTo("fantastic");

量が多くなると流石に見づらくなりますが、JavaでDBのテストを表現できると書きやすくていいですね。

この組み合わせは実際に業務で利用しています。

分散トレーシングをテーマにして勉強会に登壇してきた

だいぶ前ですが、とある企業様との合同勉強会で、分散トレーシングの説明とデモを行ってきました。

そのサンプルコードのURLを共有します。

https://github.com/kogayushi/ist-distributed-tracing-demo

スライドの元となるmarkdownファイルもこのリポジトリに入っています。

https://github.com/kogayushi/ist-distributed-tracing-demo/blob/master/What_is_distributed_tracing.md

分散トレーシングなにそれ?って人が多かったですが、ネタを仕込んでいたこともあり笑いが生まれていました。

勉強会の質としてどうだったかは…わかりませんw

私はなぜ今からKotlinを学ぶのか

それなりにJavaのことはわかってきた気がするので、よりよいコードを書けるようになるべくKotlinを学びはじめました

Kotlinに着目したきっかけは特に思い出せません。

better Javaと言われているからJavaより書きやすいんだろうな、くらいの印象からスタートだった気がします。

学び始めると魅力がどんどんわかってきて、学びがあるたびにリアルタイムにツイートしてました。

特に演算子オーバーロードは気に入りました。(他の言語にもありますが…

値オブジェクト同士の演算するときに四則演算子で表現できたらいいなぁってずっと思ってました。

たとえばコレクションオブジェクト同士のマージを.addAllとかではなくlist + listで表現したいです。

Kotlinならこれが実現できそうです。

whenもいい感じに使えそうです。

仕様を知ったその瞬間はこんな感想でしたが、用途を考えていたらすぐ閃きました。

OOPなコードを心がけていると、バリエーションが生まれるところでStrategy/Policy/Stateパターンを使ったりしますが、そのクラスをディスパッチするcreation methodを作る必要もあります。 Kotlinならこのcreation methodをいい感じにかけそうです。

という感じでまだKotlin In Actionを読んでいる途中なのですが、本の数章読んだだけでコードがきれいに描けそうなイメージが湧いてきました。 引き続き、学んでいきます( ・`д・´)

言語やFWなどの選定基準

みなさんは言語やFWなどの選定基準について考えたことがあるでしょうか?

私は使いたいと思ったFWなどを採用するかどうかを考える場合に、以下を基準にしています。

  • プロダクションで利用されている事例が多い
    • バグが改修される速度が早い
    • あるいは未対応でもissueに報告されているので、調査コストが低くなる
  • 枯れてきている(枯れている)
    • バグが改修されている確率が高い
    • 未改修でも回避策などのノウハウが溜まっている
  • コミュニティが活発
    • 上記の2点の情報が流通している
    • またそれだけ関心を持った人が多いので、多機能化しサポートも手厚くなる傾向が強い

しかし、上記の観点でダメだったとしても、目的を達成するために他の手段がなければ採用することもあります。

文字にすると当たり前な考え方のように見えますが、言語やFWがどうやって選定されているか考えたことはあまりないのではないでしょうか?

何でテックリードまたはアーキテクトはこれを選んだんだ!と不満に思うことなら多々あると思いますが。。。(私もありました笑

テックリードの気持ちになって、選ばれた技術がどんな課題(Why)があってどんな問題(What)をどうやって(How)解決してくれているのかを考えてみるとスキルアップにつながる気づきが得られるかもしれません

話が少しずれてしまいますが、上記のWhat/How/Whyはサイモン・シネックのゴールデンサークル理論というものです。

とても勉強になるのでぜひ観てほしいです。

単純に話として面白いです。あと聞き取りやすい英語なので勉強にも使えます。

Spring Security 5.xでログイン失敗時にmessages.propertiesで上書きしているにもかかわらず常に"Bad credentials"が表示されてしまう

messages.propertiesで、メッセージを上書きしているにも関わらず、デフォルトのBad credentialsというメッセージが表示されてしまう不具合に遭遇しました。

どうも、AbstractUserDetailsAuthenticationProviderにinjectionされているMessageSourceが不正なようです。

AbstractUserDetailsAuthenticationProviderの継承クラスを自作して、それをProviderManagerにセットして…なんて自分で色々組み立てているときに再現するようです。

対策として、こんな感じでAbstractUserDetailsAuthenticationProviderの継承クラスに自分でMessageSourceをセットしてあげると直りました。

    MyConcreteAuthenticationProvider athenticationProvider() {
        MyConcreteAuthenticationProvider provider = new MyConcreteAuthenticationProvider();
        provider.setUserDetailsService(this.userDetailsService());
        provider.setPasswordEncoder(passwordEncoder);
        provider.setMessageSource(messageSource); // <= ここがポイント
        return provider;
    }

ひしだまさんのサイトの対応策とこちらで、似たり寄ったりかもしれませんが、こういう解決策もあるよ、ということでせっかく見つけたのでメモを残しておきます。

昔作ったOOPなサンプルコードが出てきたからブログに残しておく

大昔に作ったOOPなコードの紹介記事を世に出していなかったので、恥ずかしいけど記録として公開しておきます。

今書けばもっと洗練できると思います(けど管理職になってから多忙で、その時間がないです…)。

オブジェクト指向プログラミングのサンプルコードご紹介

OOPらしく書くとコードはどう変わるか〜

題材

パスワードポリシー

Goal

以下の例を知って、OOPをやりたくなってもらう

  • 複雑さを抑え込んだ変更に強いコード
  • OOPらしく書くとコードが読みやすくなる
  • 手続き型で作ると複雑さを抑え込めず読み難い

Story

  • サービスクラスをターゲットに手続き型で書いたコードとOOPなコードの複雑さの違いを測る
  • ありそうな仕様変更を書いてみて違いを知る
    • その結果からOOPの効果の程を知る

具体例

要求仕様(ユーザストーリー)

  • パスワード変更
    1. パスワードポリシーを満たせていたらパスワードを変更する
    2. 満たせていない場合はエラーを返す

パスワードポリシー

  1. パスワードの長さは8文字以上20文字以内
  2. 英数字大文字小文字を最低1回使用する
  3. ユーザ名と一致しない
  4. 現在のパスワードと一致しない
  5. 氏名(アルファベット)を含まない
    • 大文字小文字は区別しない
  6. メールアドレスと一致しない
  7. 電話番号と一致しない

サンプルコード

https://github.com/kogayushi/ist-oop-refactoring-password-policy

Procedural

Production Code
public void changePassword(String id,String newPassword){
    User user = this.userRepository.userFromId(UUID.fromString(id));
    if (newPassword.length() < 8 || newPassword.length() > 20) {
        String msg = "inputted password violated password length policy";
        throw new ViolatedPasswordPolicyException(msg);
    } // omitted the middle, coz it's too long.
    if (INCLUDING_UPPER_CASE_ALPHABET_AT_LEAST_ONE.matcher(newPassword).find() == false) {
        String msg = "inputted password violated character policy";
        throw new ViolatedPasswordPolicyException(msg);
    }
    this.userRepository.updatePassword(user.getId(), new Password(newPassword));
}
特徴
  • ポリシーを満たすかチェックするif文が10個以上並んでいる(例では略した)
  • ポリシーにルールが増えるたびにif文が増える
  • つまり、パスワード変更というユーザストーリーの中にパスワードポリシーの詳細が漏れている

OOP

Production Code
public void changePassword(ChangePasswordCommand command) {

    UUID id = command.getId();
    User user = this.userRepository.userFromId(id);

    PasswordPolicy policy = this.policyFactory.generatePasswordPolicyFor(user);
    Password password = new Password(command.getNewPassword());

    this.policy.validate(password);

    this.userRepository.updatePassword(id, password);
}
特徴
  • 分岐(if文等)がない
  • パスワードポリシーの詳細をサービスクラスが知らない(知らなくて済む)
  • ポリシーのみを切り離して単体テストが可能

サンプルコードの複雑さを観測してみる

  • Cyclotic Complexityについては後述
Procedure/OOP Cyclotic Complexity
Procedure 13
OOP 1
  • 手続き型も13なら、悪くはない
  • OOPは1と最小値

What's cyclomatic complexity

  • コードの複雑さを表す指標
  • if文などで分岐が増えるとポイントが1増える
  • if文やswitchがなければcyclomatic complexityは1(最小値)
  • 分岐がなくてもメソッドが2つ生えてたら最低で2
  • 参考URL(不正確らしいがわかりやすいので紹介)
目安
循環的
複雑度
複雑さの状態 バグ
混入確率
10以下 非常に良い構造 25%
30以上 構造的なリスクあり 40%
50以上 テスト不可能 70%
75以上 いかなる変更も誤修正を生む 98%

サンプルコードの複雑さを観測してみる(再掲)

Procedure/OOP Cyclotic Complexity
Procedure 13
OOP 1
  • 現時点ではどちらも悪くはない
  • OOPで書いたほうが指標で判断すれば優れていると言える

仕様変更 is coming !

その1:ユーザ名も変更可能にしたい

  • パスワードと同じポリシーを適用したい

Procedural

修正ポイント
  1. changePasswordをコピペしてchangeUsernameを作成
  2. changePasswordchangeUsernameの重複を排除
Production Code
public void changeUsername(User user,String newUsername) {
    User user = this.userRepository.userFromId(UUID.fromString(id));
    this.validateCommonPolicy(user,  newPassword, "password");
    this.userRepository.updatePassword(user.getId(), new Password(newPassword));
}
private void validateCommonPolicy(User user, String newAuthenticationFactor, String name) {
    if (newAuthenticationFactor.length() < 8 || newAuthenticationFactor.length() > 20) {
        log.warn("inputted password violated password length policy");
        throw new ViolatedPasswordPolicyException(msg);
    } // omitted, a lot.
}

OOP

修正ポイント
  1. PasswordPolicyをコピペしてUsernamePolicyを作成
  2. generatePasswordPolicyForをコピペしてgenerateUsernamePolicyForを作成
  3. changePasswordをコピペしてchangeUsernameを作成
  4. PasswordPolicyUsernamePoilcyの重複を排除
Production Code
public void changeUsername(ChangeUsernameCommand command) {
    UUID id = command.getId();
    User user = this.userRepository.userFromId(id);

    UsernamePolicy policy = this.policyFactory .generateUsernamePolicyFor(user);
    Username username = new Username(command.getNewUsername());
    this.policy.validate(username);
    this.userRepository.updateUsername(id, username);
}

その2:ユーザ名の文字長の制限は
4文字以上10文字以内にしたい

Procedure

修正ポイント
  • 文字長チェックのif分をvalidateCommonPolicyからchangePasswordとchangeUsernameにコピー
  • usernameの文字長下限のチェックを8から4に変更
Production Code
public void changeUsername(String id, String newUsername) {
    // omitted
    if (newUsername.length() < 4 || newUsername.length() > 20) {
        String msg = "inputted uername violated username length policy";
        log.warn(msg); // it's warn just for testing.
        throw new ViolatedPolicyException(msg);
    }
    this.validateCommonPolicy(newUsername, user, "username");
    this.userRepository.updateUsername(user.getId(), new Username(newUsername));
}

OOP

修正ポイント
  • LengthPolicyをcommonPolicyから両方にコピペ
  • usernameの方だけ文字長を8から4に変更
Production Code
public PasswordPolicy generatePasswordPolicyFor(User user) {
    Set<Policy> policies = generateCommonPolicy(user);
    policies.add(new LengthPolicy(8, 20));

    return new PasswordPolicy(policies);
}

public UsernamePolicy generateUsernamePolicyFor(User user) {
    Set<Policy> policies = generateCommonPolicy(user);
    policies.add(new LengthPolicy(4, 20));

    return new UsernamePolicy(policies);
}

その3:ユーザ名はメールアドレスとの重複可

Procedure

修正ポイント
  • メールアドレスとの一致チェックのif文をvalidateCommonPolicyからchangePasswordに移動
Production Code
public void changePassword(String id, String newPassword) {
    // omitted
    if (newPassword.length() < 8 || newPassword.length() > 20) {
        String msg = "inputted password violated password length policy";
        log.warn(msg); // it's warn just for testing.
        throw new ViolatedPolicyException(msg);
    }
    if (newPassword.equals(user.getPerson().getContactInformation().getMailAddress().getValue())) {
        String msg = "inputted password violated not same with mail address policy";
        log.warn(msg);
        throw new ViolatedPolicyException(msg);
    } // omitted, a lot.
}

OOP

修正ポイント
  • NotSameWithMailAddressPolicyをgenerateCommonPolicyから移動
Production Code
public PasswordPolicy generatePasswordPolicyFor(User user) {
    Set<Policy> policies = generateCommonPolicy(user);

    policies.add(new LengthPolicy(8, 20));
    policies.add(new NotSameWithMailAddressPolicy(user.getPerson().getContactInformation().getMailAddress()));

    return new PasswordPolicy(policies);
}

まとめ

手続き型を振り返ると

  • 共通ロジックをメソッドにくくりだすことで重複が排除できた
    • しかし、確かに重複は排除出来たが複雑度が減ったわけではない
  • ユーザ名とパスワードのポリシーの違いが増えるたびに複雑度は増える

OOPらしく書くと何が違うか?

  • ユーザ名を変更するメソッドが増えても複雑度は1しかあがらない
  • パスワードポリシーのルールが変わったとしてもサービスクラス(ビジネスロジック)に影響がない
  • 1つ1つの処理(今回はポリシー)に名前がつくので全体的に読みやすい
  • サービスクラスがシンプル(分岐がない)になる

今回は利用側のクラスに着目したが…

  • 利用される側であるPolicyやFactoryについても仕様変更に影響してcyclomatic complexityのポイントがあがることはない
  • cyclomatic complexityはあくまで指標
    • 肝心なのは、業務ルールがカプセル化されることにより利用側のクラスがその詳細を知らずに済むこと
    • 業務の複雑さはなくせない。しかし、狭い範囲に閉じ込めることで変更に強くなる

例えば今回は

  • ポリシーをオブジェクトにしたことで
    • パスワードリセット機能など、パスワードを変更する他の機能でも利用できる(再利用性)
    • メールアドレスのアカウント部と一致してはいけない、などのポリシーの変更に強い(変更容易性)

Let's do the OOP !