技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

java:例外発生後に例外のメッセージを書き変える

例外オブジェクトにはメッセージが格納されており、getMessage()メソッドでそのメッセージを取得することができます。
しかし、このメッセージはコンストラクタでのみ設定可能であり、メッセージを後で変更するメソッドは用意されていないため、例外クラスに用意されている手段では例外発生後にメッセージを書き変えることはできません。
 
しかし、リフレクションを使用することで、メッセージを後で書き変えることが可能です。
メッセージはThrowableクラスのprivateのクラス変数"detailMessage"に保持されるため、これをリフレクションで書き変えます。
 
サンプルコードは以下の通りです。
 
【サンプルコード】
・ExceptionTest.java
import java.io.IOException;
import java.lang.reflect.Field;

public class ExceptionTest {

  public static void main(String[ ] args) {

    try {
      method();
    } catch (IOException e) {
      System.out.println(e.getMessage());
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  static void method() throws Exception {

    try {

      // メッセージ付きで例外を発生させる
      throw new IOException("hoge");

    } catch (Exception e) {

      // detailMessageフィールドの定義を取得
      Field fieldDefinition =
        Throwable.class.getDeclaredField("detailMessage");

      // フィールドをアクセス可能に設定
      fieldDefinition.setAccessible(true);

      // 例外オブジェクトのメッセージを変更する
      fieldDefinition.set(e, e.getMessage() + "fuga");

      // 例外を再スロー
      throw e;

    }

  }

}
 
【実行結果】
・コンソール(標準出力)
hogefuga
 
・コンソール(標準エラー出力
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by ExceptionTest (file:/C:/pleiades/workspace/Hello/build/classes/) to field java.lang.Throwable.detailMessage
WARNING: Please consider reporting this to the maintainers of ExceptionTest
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
 
----
 
ちなみに、リフレクション時に発生してしまう標準エラー出力ですが、筆者の環境(Java10)では抑止する良い手段が見つかりませんでした。
以下の2つの手段を試しています。
 
1.JVM引数で抑止
"--illegal-access=deny"を指定したら逆に異常終了するようになってしまいました。
下記ページによると、Java11も不可、Java8は可(そもそもリフレクション時の標準エラー出力が出ない)だそうです。
 
Java - Javaの開発環境を作る過程でのエラー文について|teratail
https://teratail.com/questions/153595
 
2.標準エラー出力の出力ストリームを変更
通常、"System.setErr(自作PrintStreamオブジェクト);"を記述することで標準エラー出力の出力先を変更し、コンソールに出力されないようにすることができるのですが、今回のケースではそれができませんでした。
"System.err.close();"を記述した場合はコンソールへの出力を抑止することができたので、リフレクション時のWARNINGメッセージでは"System.err"を直接使用しているのではないかと思います。
なお、"System.err"はクローズしたらリオープンすることができないので、"System.err.close();"を実行してしまうと、以降は予期せぬ例外等が発生しても標準エラー出力として出力できなくなってしまいます。
("System.setErr(System.out);"で標準出力として出力することならできますが…)

ゴンペルツ曲線(信頼度成長曲線)とは

ゴンペルツ曲線(信頼度成長曲線)とは、テストで発見されるバグ数をグラフにしたものです。
横軸に時間、縦軸に累積バグ数をとる場合、下記のようなグラフになります。

f:id:akira2kun:20201219180016j:plain

 
テストを開始した直後は、テスト手順が確立していないため、バグはなかなか見つかりません。
テスト手順が確立した後は、時間が経つにつれて順調にバグが見つかります。
テストが終盤に近づくと、残っているテストケースはテスト困難なレアケースになるので、再びバグを見つけにくくなります。
 
具体的にいくつのバグが出るかは、プロジェクトの特性によって変化します。
現場毎に、プロジェクトの規模と発見されるバグ数の統計を取り、その統計を元にいくつのバグが出るのかを具体的な数値に落とし込みます。
 
もし、バグが大量に見つかる(aのケース)ようであれば、テスト開始時点の品質が悪い疑いがあります。
この場合、品質管理者は、バグの原因を確認し、前工程で見つけるべきバグが大量に見つかっていないかどうかを見る必要があります。
前工程で見つけるべきバグが大量に発見された場合は、バグの対応でテストの進捗が悪くなることを防ぐために、もう一度前工程に戻って品質強化を行う必要があります。
 
また、発見されるバグが少ない(bのケース)ようであれば、テストケースの作り込みが甘い疑いがあります。
この場合、品質管理者は、テストに使われているテストケースを確認し、テストの目的に照らし合わせてテストケースの網羅性が十分かを見る必要があります。
テストケースの網羅性が不十分な場合は、現在のテスト工程で見つけるべきバグが見つからないことにより次工程やリリース後に悪影響を及ぼすのを防ぐため、テストケースの修正を指示する必要があります。
 
品質管理者には成果物の一つ一つを細かく見る程の時間はないため、上記のような数値管理を行い、プロジェクトの状況を把握しようとします。
(人間の体に例えると、健康診断で数値を出し、疑わしい数値が出た場合に医師が検査を行う、という例えになります)
そのため、作業者としては、正直に数値を出すことで、品質管理者が適切に管理を行えるようにする必要があります。

java:引数で渡した参照型変数をメソッド内で変更する書き方について

参照型変数を引数としてメソッドに渡し、呼び出し先のメソッドの中でその参照型変数に変更を入れた場合、呼び出し元でもその変更内容を参照することができます。
ソースコードで言うと、以下のような形で呼び出し元に影響を与えることができます。
(isDisplay[0]の変更は呼び出し元にも反映されます。実行すると、"Hello World!"が出力され、"Error!"は出力されません。)
 
public class HelloWorld {
  public static void main(String[ ] args){

    boolean isDisplay = {false,false};

    getIsDisplay(isDisplay);

    if (isDisplay[0]) {
      System.out.println("Hello World!");
    }
    if (isDisplay[1]) {
      System.out.println("Error!");
    }

  }

  public static void getIsDisplay(boolean isDisplay) {
    isDisplay[0] = true;
  }
}
 
このような書き方は、C言語のレガシーなコードではよく見かけますが、javaではあまり見かけることがないと思います。
レガシーな現場に入ったことが無い方は、見慣れていない、違和感がある、と感じる方が多いと思います。
 
javaでは、以下のように戻り値により呼び出し元に影響を与える書き方の方が主流です。
 
public class HelloWorld {
  public static void main(String[ ] args){

    boolean[] isDisplay = {false,false};

    isDisplay[0] = getIsDisplay();

    if (isDisplay[0]) {
      System.out.println("Hello World!");
    }
    if (isDisplay[1]) {
      System.out.println("Error!");
    }

  }

  public static boolean getIsDisplay() {
    return true;
  }
}
 
ビジネスロジックを知り尽くしている場合は、前者のようにメソッドに参照型変数を渡してメソッドの中で好きに変更させる形にした方が楽にコーディングできます。
しかし、ビジネスロジックをよく知らない担当者がソースコードを調査したり改修したりする場合、後者のように戻り値を通して呼び出し元に影響を与える形にしないと、読む必要があるソースコードの範囲が広がり、調査・改修が困難になります。
今回の例で言うと、"isDisplay[1]"に変更が入っていないことを知るために、前者のコードではメソッドの中まで見る必要がありますが、後者のコードではメソッドの中まで見る必要はありません。
 
現在の開発現場では、複数の担当者が入れ替わり立ち替わりでソースコードを改修し続けるのが主流ですので、後者の書き方の方が望まれます。
現行のソースコードが既に前者の形で書かれているのであれば、それに合わせた方が良い場合もありますが、そうではないなら後者の書き方で書くべきでしょう。

Excel:ピボットテーブルの使い道と作り方(テストデータ付き)

ピボットテーブルとは、集計作業を行う機能のことです。
自力で集計用の表を作成して関数を書くことでも集計作業は可能ですが、ピボットテーブルを用いればマウス操作だけで簡単に集計作業を行うことができます。
 
今回は、試しにピボットテーブルで架空の体力測定結果を集計してみます。
握力について、男女で差があるのか、測定月で差があるのかを、平均値を出すことで確認します。
 
なお、使用するデータは以下からダウンロードできます。
手を動かしながら覚えたい方はぜひ。
https://1drv.ms/x/s!AivF3bzWXOzukHtXk5hscKYqkLkM
 
----
 
1.集計作業を行う表をドラッグで選択し、挿入>ピボットテーブルを選択する。

f:id:akira2kun:20201204213710j:plain

2.ドラッグした範囲が自動的に選択されているので、OKボタンを押下する。

f:id:akira2kun:20201204213734j:plain

 

3.ピボットテーブルのシートが生成されるので、列に測定月、行に性別、値に握力(kg)をドラッグして選択する。

f:id:akira2kun:20201204213749j:plain

 

4.「合計 / 握力(kg)」の右にある矢印を選択し、値フィールドの設定を選択する。

f:id:akira2kun:20201204213805j:plain

 

5.デフォルトでは合計が選択されているので、平均を選択し、OKボタンを押下する。

f:id:akira2kun:20201204213821j:plain

 

6.性別ごと、また測定月ごとで、握力(kg)の平均値が算出される。

f:id:akira2kun:20201204213842j:plain

 

 これで、男女では大きな差があるが、測定月では大きな差がないことが一目でわかるようになりました。

障害の発生原因の切り分けのポイント

テストや本番運用で障害が発生した場合、既知の障害等で原因が明らかな場合を除き、対応のために原因を調査する必要があります。
原因を調査する上ではどこに原因があるのかの切り分けが必要になります。
以下では、切り分け作業を行う上でのポイントを順を追って説明します。
 
1.障害が発生した状況を保全する
障害が発生したデータやその状況を記録したログは後の調査で使いますので、誤って更新したり削除したりしないように保全する必要があります。
テスト中であればそのデータを用いたテストは中断する必要がありますし、場合によってはコピーして別の場所に補完する必要があります。
 
2.期待される値との比較を行う
データやログを見て、設計上期待される値と異なる点を探していきます。
コーディングの単純な誤りであれば、大抵の場合は、データが期待値と異なるようになった箇所やログの出力内容から原因を特定できます。
場合によっては、その期待値自体が合っているのかどうか、設計や要件の確認・再検討が必要になる場合もあります。
 
3.障害発生手順を確立させる
ログの出力内容が不足している場合や原因が込み入っている場合は、データやログを見ただけでは原因がわからない場合があります。
その場合は、障害が発生した時と同じ手順で障害の再現を確認します。
障害が再現すれば、障害を発生させる手順が確立されたということになります。
厄介なのは障害が再現しなかった場合で、一意キー(顧客番号や受付番号の類)が障害発生時と異なることに注目して一意キーと結びつくデータを洗い出す、ランダムに処理が変わる箇所がないか(乱数を使っている箇所や振り分け先がランダムなロードバランサー等に注目する)、という観点で再現しなかった理由を調査し、障害発生手順を確立させます。
どうしても確立できない場合は、ハード障害である可能性を視野に、障害発生時の状況をまとめてハードウェアを提供するベンダーに確認する、というアクションを起こす必要がある場合もあります。
(私の経験上、「太陽フレアでビットが入れ替わってしまった」という冗談のような原因を告げられたこともあります)
 
4.再現手順を元に開発環境で原因調査を行う
再現手順が確立したら、開発環境で再現手順を試し、徐々に原因を絞り込んでいきます。
もし環境を変えたことで障害が再現しなくなった場合、環境問題である可能性が出てきます。
(例えば、マスタデータの不備、機器の構成の違いに起因する問題、等)
障害が再現する場合、開発環境ではデバッグを入れて変数の内容を表示することができますので、それをログの代わりにして原因調査を進めることができます。
また、開発環境ではデータも自由に書き変えられますので、データを少しずつ書き変えて挙動を確かめることでも、原因調査を進められます。

java:例外を先に生成して後でthrowする

多くのソースコードでは、例外を発生させると同時にthrowしています。
しかし、例外クラスもクラスの一つであり、newするとオブジェクトが生成されますので、先に例外クラスのオブジェクトを生成し、オブジェクトとしてやりとりした後、後でthrowすることが可能です。
 
独自例外クラスを使用している場合は、このテクニックを用いると便利なことがあるかもしれません。
 
以下、サンプルコードです。
 
【サンプルコード】
・ExceptionTest.java
import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionTest {

  public static void main(String[] args) {

    Exception ex = null;
    ex = exceptionMaker();

    try {
      throw ex;
    } catch (FileNotFoundException e1) {
      System.out.println("FileNotFoundException");
    } catch (IOException e2) {
      System.out.println("IOException");
    } catch (Exception e3) {
      System.out.println("Exception");
    }

  }

  static Exception exceptionMaker() {

    Exception ex = null;

    ex = new FileNotFoundException();
    ex = new IOException();

    return ex;

  }

}
 
【実行結果】
IOException

テーブル結合のキーはテーブル間で同一の値を用いること

当たり前のことなのですが、テーブル結合の際には、結合に必要なキーはテーブル間で同一の値を用いて、結び付けができるようにする必要があります。
プログラムでキーとなる値を順次生成する場合、テーブルへのinsertの際にテーブル毎に別々の値を生成してしまうというミスをしないようにする必要があります。
 
--------------------
 
例えば、以下のようなテーブルが存在するとします。
 
・利用料金テーブル
create table usagefee
(
usage_id CHAR(10) NOT NULL, --利用番号
use_date CHAR(8) NOT NULL, --利用日
shop_name CHAR(40) NOT NULL, --店名
fee DECIMAL(10) NOT NULL, --利用料金
PRIMARY KEY (usage_id)
);
 
・利用料金明細テーブル
create table usagefee_detail
(
usage_id CHAR(10) NOT NULL, --利用番号
detail_id CHAR(3) NOT NULL, --枝版
item_name CHAR(40) NOT NULL, --商品名
fee DECIMAL(10) NOT NULL, --利用料金
PRIMARY KEY (usage_id,detail_id)
);
 
--------------------
 
結合する時に指定するキーは利用番号になるため、以下のように利用番号には同一の値を使用する必要があります。
 
insert into usagefee values("0000000001","20201201","Shop_A",10000);
insert into usagefee_detail values("0000000001","001","Item_A",5000);
insert into usagefee_detail values("0000000001","002","Item_B",5000);
select *
from  usagefee p
join  usagefee_detail c
on   p.usage_id = c.usage_id;
 
Query OK, 1 row affected (0.01 sec)
Query OK, 1 row affected (0.01 sec)
Query OK, 1 row affected (0.00 sec)
+----------+--------+---------+-----+----------+---------+---------+----+
|usage_id |use_date|shop_name|fee |usage_id |detail_id|item_name|fee |
+----------+--------+---------+-----+----------+---------+---------+----+
|0000000001|20201201|Shop_A  |10000|0000000001|001   |Item_A  |5000|
|0000000001|20201201|Shop_A  |10000|0000000001|002   |Item_B  |5000|
+----------+--------+---------+-----+----------+---------+---------+----+
2 rows in set (0.00 sec)
 
--------------------
 
しかし、利用番号がプログラム中で生成する連番であり、かつプログラム中で利用料金テーブルと利用料金明細テーブルで別々に連番を付与していた場合、利用番号で結びつかなくなってしまいます。
 
insert into usagefee values("0000000001","20201201","Shop_A",10000);
insert into usagefee_detail values("0000000002","001","Item_A",5000);
insert into usagefee_detail values("0000000002","002","Item_B",5000);
select *
from  usagefee p
join  usagefee_detail c
on   p.usage_id = c.usage_id;
 
Query OK, 1 row affected (0.01 sec)
Query OK, 1 row affected (0.01 sec)
Query OK, 1 row affected (0.00 sec)
Empty set (0.00 sec)
 
こうなると、利用料金テーブルと利用料金明細テーブルを結合するという使い方ができなくなり、後で困ることになります。
更に、既にサービスイン後であり過去データの補正が必要になった場合、その補正も困難になる場合があります。