Java:例外の原因を知り、ユーザにわかりやすく通知する



問題

Javaの例外には若干のメッセージが含まれるのだが、それをそのままユーザに表示するわけにはいかない。また、トップの例外メッセージには途中の無駄な例外クラスが含まれている。

例えばこうだ。

javax.ws.rs.ProcessingException: java.net.SocketTimeoutException: Read timed out
    at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:260)
    at org.glassfish.jersey.client.ClientRuntime.invoke(ClientRuntime.java:254)
(略)
Caused by: java.net.SocketTimeoutException: Read timed out
    at java.base/java.net.SocketInputStream.socketRead0(Native Method)

あるいは

javax.ws.rs.ProcessingException: java.net.ConnectException: Connection refused: connect
    at org.glassfish.jersey.client.internal.HttpUrlConnector.apply(HttpUrlConnector.java:260)
(略)
Caused by: java.net.ConnectException: Connection refused: connect
    at java.base/java.net.PlainSocketImpl.connect0(Native Method)
(略)

いずれにしても、例外が多段になるに連れて途中の例外クラス名がメッセージに含まれてしまう。これを除去し、既知の例外であれば、ユーザに日本語で通知するようにする。

例外の根本原因を保持するクラス

例外クラスをExceptionCause.getに入れれば、根本原因をExceptionCauseオブジェクトとして戻す。

public class ExceptionCause {

  public final Class<?>clazz;
  public final String message;

  public ExceptionCause(Class<?>clazz, String message) {
    this.clazz = clazz;
    this.message = message;
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof ExceptionCause)) return false;
    ExceptionCause that = (ExceptionCause)o;
    return this.clazz.equals(that.clazz) && this.message.equals(that.message);
  }

  @Override
  public int hashCode() {
    return clazz.hashCode() + message.hashCode() * 7;
  }

  @Override
  public String toString() {
    return clazz + "," + message;
  }

  public static ExceptionCause get(Throwable e) {
    while (e.getCause() != null) e = e.getCause();
    return new ExceptionCause(e.getClass(), e.getMessage());
  }
}

あとは、既知のパターンについて、マップを通じて日本語メッセージを取得する。

  private static final Map<ExceptionCause, String>PROCESSING_EXCEPTION_MAP = 
    new HashMap<ExceptionCause, String>() {{
      put(new ExceptionCause(java.net.ConnectException.class, "Connection refused: connect"),
        "サーバに接続できません"
      );
      put(new ExceptionCause(java.net.SocketTimeoutException.class, "Read timed out"), 
        "サーバからの応答がありません");
    }};

  public static SomeException processingException(ProcessingException ex) {    
    ExceptionCause cause = ExceptionCause.get(ex);    
    String message = PROCESSING_EXCEPTION_MAP.get(cause);
    if (message != null) return new SomeException(ex, message);
    return new SomeException(ex, cause.message);
  }