はじめに
NSO のサービスパッケージは Java, Python, C を使用して記述し作成することが出来ます。いずれの言語をを使用した場合でも、何らかのエラー処理を行う必要が必ずあり、その際にそのエラー内容をログへ出力することがその後の問題解決に有用です。TAC ではお客様から頂くログを確認しますが、ログ出力がされていないために、詳細がわからないこともあります。Java での開発時には、以下についてご確認頂ければと思います。
Java の例外処理
Java 言語を使用して例外処理を行う場合、一般的に以下のように処理を try/catch で包含します。
try {
// 通常処理
} catch (Exception e){
// エラー処理
}
これによって、通常処理上で何らかの問題が発生した場合には、それらをそこで中断(それ以上のステップを実行せず)、エラー処理に移ります。また、Exception クラスでキャッチすると全てのエラーを含みますので、キャッチしたい例外のみを指定する場合も多いかと思いますが、ここでは省略しています。
独自処理用クラス
NSO が提供する Maapi クラスを使用しての処理などについて、ユーザが独自のユーティリティクラスを作成し、それを意識しないで使用できるようにする場合が多々あります。
以下は Utilクラスに実装された、getNumberOfDevices メソッドを使用して、NSO 上のデバイス数を表示するプログラムです。
9 public class Test {
10 public static void main(String[] args) throws Exception {
11 Util util = new Util();
12 try {
13 System.out.println(util.getNumberOfDevices());
14 System.out.println("other tasks...");
15 } catch (Exception e){
16 System.out.println(e.getMessage());
17 throw new Exception("failed something");
18 }
19 }
20 }
正常動作時には、以下のように表示されます。
$ java Test
3001
$
NSO を終了させた上で、再度実行してみます。キャッチした例外のメッセージを表示していますが、何が問題であったか、一切わかりません。
$ java Test
get size failed
Exception in thread "main" java.lang.Exception: failed something
at Test.main(Test.java:17)
$
スタックトレース
この例ではスタンドアロンアプリケーションとなっているため、main メソッドから実行していますが、サービスパッケージとして実行する場合は、NSOがそれを Reflection を呼び出します。いずれにしても例外が投げられる場合には、 そのスタックトレースが表示されます。
上記の例では、以下が表示されていました。サービスパッケージの場合に ncs-java-vm.log などに出力されるものも、この程度のものとなります。
Exception in thread "main" java.lang.Exception: failed something
at Test.main(Test.java:17)
この Test.java の 17 行目でエラーが合ったことはわかりますが、何が問題だったのでしょうか。
例外作成時の cause
Test クラスでは、例外を以下のように作成しておりました。
} catch (Exception e){
System.out.println(e.getMessage());
throw new Exception("failed something");
}
ここで、変数 e で受けた内容については、新しい例外が投げられる際には完全に喪失していることに注目してください。ログ上、問題発生後に確認出来るのは e.getMessage() のみです。
Exception クラスやそれを継承するクラスでは、2つ目の引数にその例外の原因を付加することが出来ます。
} catch (Exception e){
System.out.println(e.getMessage());
throw new Exception("failed something", e);
}
このように変更したあと、再度同じコードを実行します。
$ java Test
get size failed
Exception in thread "main" java.lang.Exception: failed something
at Test.main(Test.java:17)
Caused by: java.lang.Exception: get size failed
at Util.getNumberOfDevices(Test.java:28)
at Test.main(Test.java:13)
$
Test.java の 17 行目で失敗した原因が、ログ上に出力されるようになりました。Util クラス内に実装された getNumberOfDevices メソッド内の 28 行目が本当の原因だったとわかります。
もっともっと cause
独自クラス Util では、Maapi接続を行ったあと、/devices/device の数を数えています。現状の Util クラスのコードは以下のようになっています。上の例では、28 行目が問題となっていたことはわかりましたが、まだこれでは原因判明とはなりません。新規に作成される Exception には、その原因を付加するようにします。
22 class Util {
23 public int getNumberOfDevices() throws Exception {
24 try {
25 NavuList nl = getDeviceList();
26 return nl.size();
27 }catch(Exception e){
28 throw new Exception("get size failed");
29 }
30
31 }
32 private NavuList getDeviceList() throws Exception {
33 try {
34 Maapi m = connect();
35 NavuContext nc = new NavuContext(m);
36 int th = nc.startRunningTrans(Conf.MODE_READ);
37
38 NavuContainer root = new NavuContainer(nc);
39 NavuList nl = root.container(new Ncs().hash()).container("devices").list("device");
40 return nl;
41 }catch(Exception e){
42 throw new Exception("get device was failed");
43 }
44
45 }
46 private Maapi connect() throws Exception {
47 Socket s;
48 Maapi m;
49 try {
50 s = new Socket("localhost", Conf.NCS_PORT);
51 m = new Maapi(s);
52 m.startUserSession("admin",
53 InetAddress.getByName("localhost"),
54 "maapi",
55 new String[] { "admin" },
56 MaapiUserSessionFlag.PROTO_TCP);
57 return m;
58 }catch(Exception e){
59 throw new Exception("Connection Establishment was failed");
60 }
61 }
62 }
以下について変更したあと、再度実行します。
28 throw new Exception("get size failed", e);
42 throw new Exception("get device was failed", e);
59 throw new Exception("Connection Establishment was failed", e);
再度実行
$ java Test
get size failed
Exception in thread "main" java.lang.Exception: failed something
at Test.main(Test.java:17)
Caused by: java.lang.Exception: get size failed
at Util.getNumberOfDevices(Test.java:28)
at Test.main(Test.java:13)
Caused by: java.lang.Exception: get device was failed
at Util.getDeviceList(Test.java:42)
at Util.getNumberOfDevices(Test.java:25)
... 1 more
Caused by: java.lang.Exception: Connection Establishment was failed
at Util.connect(Test.java:59)
at Util.getDeviceList(Test.java:34)
... 2 more
Caused by: java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:607)
at java.net.Socket.connect(Socket.java:556)
at java.net.Socket.<init>(Socket.java:452)
at java.net.Socket.<init>(Socket.java:229)
at Util.connect(Test.java:50)
... 3 more
$
これによって、問題の原因が接続の失敗 (java.net.ConnectException) であることがわかりました。
考察
例外が発生する場合、それの処理をどのように行うかという判断は、色々な観点から行う必要があります。
その一つは、上で説明したようにエラー発生時にその原因を追うことが出来るようするという、機能面での問題解決です。これにより、本番環境で発生した問題をログから追うことが出来ます。原因が記録されていない場合は、デバッグ用のコードを追加した上でラボで再現試験を行い、調査する必要があります。
一方でその中身を隠したい意図がある場合は、原因を付加することでそれがある程度見えてしまうことになります。上の例では、Utilクラスに全てが実装されていますが、スタックトレースがUtilクラス使用者に見えてしまうため、その構造が一部見えることになります。