Having explored the GWT RPC exception limitations, on this post we will take another route of simplifying GWT exception handling. In short the idea is by extending the parent RemoteServiceServlet we will inject a code so the RPC Servlet treat our business runtimes as a good friend. We are lazy developers and we would like to be able to throw business exceptions without having to declare them on the each RPC method (Which probably contradicts the whole Java exception contract.. :) ).

Hooking the business exception / Injecting it into GWT SerializationPolicy (aka fooling the GWT SerializationPolicy).

When an error propagates within a GWT service this error needs to be declared within the RPC method signature, otherwise bad things happen. The GWT SerilizablePolicy marshaling will complain about the type not being recognized and return 500 Server error response. Well we could easily overcome this limitation by creating a dummy hook method with only purpose of white-listing the business exception type within the given RPC signature Serialization policy:


interface AbstractRemoteServiceAsync {
   void hookException(AsyncCallback<Void> callback);
}

interface AbstractRemoteService extends RemoteService {
   void hookException() throws MyException;
}

class AbstractRemoteServiceServlet extends RemoteServiceServlet
                 implements AbstractRemoteService {
   /**
    * This method only reson vivre is to hook our business MyException type
    * into GWT Serialization policy
    */
   @Override
   public String hookException() throws MyException {
      // Blank
   }
}
/**
 * The business exception
 */
public class MyException extends RuntimeException {
	private static final long serialVersionUID = 1L;
}

From that point on, AbstractRemoteServiceServlet methods will treat MyException as a white-listed friend and no more SerializationPolicy warnings will happen.

Next thing we want to do is, automate this process so we don’t have to declare those hook methods on every RPC Servlet right? Well polymorphism on help.

Overriding RemoteServiceServlet default exception flow.

Let’s open the RemoteServiceServlet implementation. Luckily this class follows a template method pattern with hook methods ready for extending. What we will acomplish is extending RemoteServiceServlet class and inject a behaviour handling our business exceptions (code in red).

And this is my humble contribution to GWT community. All sources and project are hosted at Google Code:

/**
 * This class overrides RemoteServiceServlet to provide automatic exception
 * handling for known business exceptions
 */
public class AbstractRemoteServiceServlet extends RemoteServiceServlet
    implements AbstractRemoteService {

  /**
   * Overriding method so we can inject business exception type discovery
   * and handling
   */
  @Override
  public String processCall(String payload) throws SerializationException {
    try {
      RPCRequest rpcRequest = RPC.decodeRequest(payload, this.getClass(),
          this);
      return handleProcessCall(this, rpcRequest);
    } catch (IncompatibleRemoteServiceException ex) {
      log("Incompatible error was thrown while processing this call.", ex);
      return RPC.encodeResponseForFailure(null, ex);
    }
  }
  /**
   * Tis method digest the given throwable and tries to find any know business
   * exception. When a known business error is found it is being flushed (the
   * business type) to response stream, thus client callback receives the known
   * type but not the default GWT UnexpectedException.
   */
  @Override
  protected void doUnexpectedFailure(Throwable e) {
    discoverAndWriteError(e, e);
  }

  /**
   * This method reson vivre is to hook the recognized MyException type into
   * GWT Serialization policy
   */
  @Override
  public void hookException() throws MyException {
    // Blank
  }
  /**
   * Method handles GWT RPC call and on failure it digests the error,
   * finds supported business exception and handles them gracefully.
   */
  private String handleProcessCall(Object target, RPCRequest rpcRequest)
      throws SerializationException {

    Method serviceMethod = rpcRequest.getMethod();
    Object[] params = rpcRequest.getParameters();
    SerializationPolicy serializationPolicy = rpcRequest
        .getSerializationPolicy();
    try {
      return RPC.invokeAndEncodeResponse(target, serviceMethod, params,
          serializationPolicy);
    } catch (UnexpectedException ex) {
      Throwable cause = ex.getCause();
      // This could be a list of supported types ..etc..
      // Leaving it up to your imagination
      if (cause.getClass().equals(MyException.class)) {
        throw new MyException();
      }
      return encodeResponse(cause.getClass(), cause, true,
          serializationPolicy);
    }
  }

  /**
   * This method recursively iterates through given 'masterError'
   * finding any supported failures and handling the latter
   * appropriately.
   */
  private void discoverAndWriteError(Throwable masterError, Throwable e) {
    if (e == null) {
      writeError(new UnexpectedException(masterError.getMessage(),
          masterError));
      return;
    }

    Throwable cause = e.getCause();
    if (cause == null) {
      writeError(new UnexpectedException(masterError.getMessage(),
          masterError));
      return;
    }
    // This could be a list of supported types, etc..
    // Leaving it up to your imagination
    if (cause.getClass().equals(MyException.class)) {
      writeError((MyException)cause);
      return;
    } else {
      // continue with 'recursion' cause discovery
      discoverAndWriteError(masterError, cause);
    }
  }

  /**
   * This method writes known business exception the response HTTP packet
   * Code borrowed from com.google.gwt.user.server.rpc.RPCServletUtils
   */
  private void writeError(Throwable cause) {
    // Send SC_OK/200 status, serialize the gwt error, flush it to response,
    // and let the client deal with the business error
    HttpServletResponse resp = getThreadLocalResponse();
    try {
      resp.setContentType("text/plain");
      resp.setStatus(HttpServletResponse.SC_OK);
      String excMsg = RPC.encodeResponseForFailure(null, cause);
      resp.getWriter().write(excMsg);

    } catch (IOException e) {
      log("Failed to send failure to client", e);

    } catch (SerializationException e) {
      log("Failed to send failure to client", e);
    }
  }

  /**
   * Borrowed from RPC.java. Returns a string that encodes the results of an
   * RPC call. Private overload that takes a flag signaling the preamble of
   * the response payload.
   */
  private static String encodeResponse(Class responseClass, Object object,
      boolean wasThrown, SerializationPolicy serializationPolicy)
      throws SerializationException {

    ServerSerializationStreamWriter stream = new
              ServerSerializationStreamWriter(serializationPolicy);
    stream.prepareToWrite();
    if (responseClass != void.class) {
      stream.serializeValue(object, responseClass);
    }
    String bufferStr = (wasThrown ? "//EX" : "//OK") + stream.toString();
    return bufferStr;
  }
}

With the new RemoteService classes being created above, our GWT Services will derive from them as bellow:


@RemoteServiceRelativePath("greet")
interface GreetingService extends AbstractRemoteService {
  /* Notice, no exception is being declared*/
  String greetServer(String name);
}

interface GreetingServiceAsync extends AbstractRemoteServiceAsync {
  /* Notice, no exception is being declared*/
  void greetServer(String input, AsyncCallback callback);
}

class GreetingServiceImpl extends AbstractRemoteServiceServlet implements
    GreetingService {
  /* Notice, no exception is being declared*/
  public String greetServer(String input) {
    {
      throw new MyException();
    }
  }
}

The last piece of course is executing the service and see it in real browser:

Feel free to explore project sources at Google Code.

Something else not directly related to this post. You may have noticed, GWT RPC exceptions are being transmitted to browser as a JSON HTTP packet of 200 OK response with //EX[org.roussev.MyException|1|23|4|5] signature.