システム系の例外は実行時例外+AOPでハンドリングするのがベスト
インフラ層のチェック例外はやはりJavaのBad Partだと思う
先日のJava言語のチェック例外は本当にGood Partなのか? - 達人プログラマーを目指してで、
インフラ層のフレームワークなどでは実行時例外が適切
ということを書いたのですが、この点についてもう少し詳しく考えてみたいと思います。
Java: The Good Partsの本ではRMIの章があるのですが、RMIではRemoteというマーカーインターフェースを継承しつつ、すべてのメソッドがRemoteExceptionというチェック例外を送出する規則となっています。
Java Good Parts(9.1節より引用) pubic interface StatRecorder extends Remote { void recordGame(BoxScore stats) throws RemoteException; Set<Player> getRoster(String forTeam) throws RemoteException; }
リモートの呼び出しはネットワークなどの障害が避けられなため、呼び出し側で必ず例外をチェックすることをコンパイル時に強制することは良い考えのように「理論上は」思われます。しかし、実際上はどうでしょうか?必然的に、以上のインターフェースの呼び出し側はそれより上位のレイヤーに属するクラスとなります。問題なのは、RemoteExceptionというインフラレイヤーでの例外を、以上のようなビジネスレイヤーのクラスが扱わなくてはならないところにあります。実際、この本のサンプルコードの呼び出し側は以下のように記述されていました。
Java Good Parts(9.1節より引用) public class StatReporterImpl { ... public Set<Player> getPlayers(String from, String team) { try { StatRecorder recorder = getRecorder(fromHost); if (recorder != null) { return recorder.getRoster(team); } else { return null; } } catch (RemoteException e) { System.out.println("Unable to find roster for team " + team); e.printStackTrace(); return null; } } }
上記では省略してしまいましたが、原文にはJavaDocが正確に記述されており、異常な場合はメソッドがnullを返すことが明記されているため、一応セーフなのだと思います。しかし、チェック例外をnull値に変換して返しているため、このクラスのさらに上位ではコンパイル時でなく実行時にnullのチェックが必要になっています。結局、RemoteExceptionというチェック例外は、コードを煩雑にしているだけでコンパイル時の安全性に寄与しているとは言えなくなっています。
上記ではnullを返していますが、アプリケーション層のクラス内ではシステムレベルの例外を適切に対処することは一般的に不可能なため、例外を上位に投げない限りこれは仕方のないことです。かといって、例外を上位にthrowsしたのでは、同様の処理が上位のレイヤーで必要になるばかりか、インフラレイヤーの例外を上位レイヤーが意識する必要が出てくるため、そもそもレイヤー化している意味がなくなってしまいます。
このように、システムレベルの例外をチェック例外として扱うことは、単にコードの記述が面倒になるというだけではなく、レイヤー化の妨げや不適切な事後条件(null値など)、コード重複などさまざまな悪影響があるのであり、どう考えても百害あって一利なしなのではないかと思います。そういう意味で、JavaSEやJavaEEのAPIのチェック例外のほとんどは、JavaのBad Partなのであると言い切ってしまってもよいのではないかと私は思うのです。
Spring Frameworkの第三の役割はJava APIの例外の適正化にある
意識的なのかJava: The Good Partsでは、SpringなどのフレームワークがJavaプラットフォームに与えた飛躍的な進化の部分には言及されていませんが、やはり、現代的なJavaのGood Partsを語る上では、こうした最近のフレームワークのことを考えるべきだと思います。そして、Spring Frameworkと言えば、第一にDI、第二にAOPということになるのですが、忘れてはならないのは第三の機能としての例外適正化です。この点については、以下のSpringの紹介記事でもJDBCを例として説明されています。
Introduction to the Spring Framework
Spring FrameworkではRemoteExceptionやSQLExceptionなどJava SEやJava EE標準のチェック例外に対応する実行時例外を定めているだけでなく、適切な例外の階層化も行っています。たとえば、データベースの例外を一律SQLExceptionとして扱う代わりに、例外の内容にしたがって、適切なDataAccessException(実行時例外)のサブクラスに変換して送出してくれるようになっています。この思想はフレームワーク全体として美しい形で統一されており、JDBCやRMIだけでなく、JMS、EJB、JNDI、JCAなどあらゆる標準APIの例外を同様に扱いやすい実行時例外として扱えるようになっています。なお、HibernateやJPAなど最近はフレームワークや標準APIそのものがチェック例外でなく、実行時例外を使う傾向になってきていますが、あらゆるAPIを統一された思想で扱えるというのはSpringのメリットだと思います。
システムレベルの実行時例外の確実なハンドリングはAOPで保障すべき
しかし、システムレベルの例外も、どこかで正しくハンドリングする必要があります。実行時例外を使うことで、チェック例外を使っていた時のように呼び出し側でのcatchが保障されなくなるのではという反論があるかと思います。この点については、現代のJavaではAOPという非常に便利な仕組みがあるという点を忘れてはならないと考えます。AOPはSpringやSeasar2などのフレームワークで簡単な仕組みが提供されていますし、このブログでも何回か紹介させていただいたようにAspectJのような本格的なツールも現在ではかなり身近に利用できるようになっています。
たとえば、あらゆるDaoクラスからDataAccessExceptionが送出されたタイミングで確実に処理をするのであれば、以下のようなアスペクトを作成することで簡単に対処できます。
public aspect DataAccessExceptionAspect { pointcut daoMethod() : execution(* *..*Dao.*(..)); after() throwing(DataAccessException ex) : daoMethod() { // ここで共通の例外処理。 } }
この方法であれば、サービスなど業務ロジック層のメソッドは完全にDataAccessExceptionのことを意識する必要がないだけでなく、アスペクト定義さえ間違えなければ、確実に一箇所で例外が処理されることも保障されます。このように、アプリケーションロジック中で個別に対処すべきでないシステム例外は実行時例外とし、例外処理はアスペクト内で行うというのがベストであると思います。
AspectJを使ってチェック例外を実行時例外的に扱えるようにする
あまり、知られていないかもしれませんが、AspectJにはチェック例外のソフト化という仕組みがあり、面倒なチェック例外をあたかも実行時例外のようにしてしまうことが可能です。この機能を使うことで、既存のチェック例外でSpringなどの仕組みが使えないケースでもthrows宣言やcatchの必要性を無くすことができます。チェック例外のソフト化には以下のような構文を使います。
declare soft : <ExceptionTypePattern> : <pointcut>;
たとえば、サービスの呼び出しに対して厄介なRMIのRemoteExceptionをソフト化するには
public aspect RemoteExceptionSofteningAspect { pointcut serviceMethodCall() : call(* *..*Service.*(..)); declare soft : RemoteException : serviceMethodCall(); }
のようなアスペクトを作成して、織り込むだけです。そうすると、サービスのシグネチャ上はRemoteExceptionを非チェックの実行時例外のように扱えるようになります。(org.aspectj.lang.SoftExceptionという実行時例外に自動的にラップされるようになる。)使い方を間違えると危険なところもありますが、場合によっては強力な簡易化の手段となります。