Android und Grails über JSON miteinander sprechen lassen

By | 2. November 2010

Ist eine Weile her seit meinem letzten Eintrag 🙂 Gestern habe ich mich mit einem Kollegen über die Kommunikation zwischen einer Android App und einem Grails-Backend unterhalten. Ich freue mich immer über eine Herausforderung, also habe ich ein kleines Projekt aufgesetzt, das dieses Problem löst.

Das Command-Pattern ist eines meiner liebliengs-Entwirfsmuster. Es eignet sich hervorragend zum Bau von Schnittstellen zwischen zwei unterschiedlichen Systemen. In diesem Fall zwischen Grails (serverseitig) und Android (clientseitig). Der Vorteil des Command-Patterns ist, dass es wunderbar mit dem Open/closed principle harmoniert. Denn die Schnittstelle umfasst, platt gesagt, einen Service mit einer execute Methode, die einen Request bekommt und einen Response liefert. Wie die konkreten Implementierungen der einzelnen Request/Response Paare aussehen, ist nicht wichtig, die Schnittstelle bleibt die Selbe.

Die Funktionsweise ist wie folgt:

  1. Auf der Android-Seite wird eine Anfrage in Form einer Action erzeugt und an einen asynchronen Service übergeben.
  2. Dieser Service serialisiert die Anfrage nach JSON und schickt die Nachricht per POST zum Server.
  3. Auf Server Seite wird die Nachricht deserialisiert und an einen synchronen Service übergeben, der damit was anzufangen weis.
  4. Die Antwort serialisiert der Server ebenfalls nach JSON und schickt sie zurück.
  5. Auf Android Seite wird die Antwort deserialisiert und das Callback des asynchronen Service aufgerufen.

Dies ist nichts Revolutionäres und funktioniert so oder so ähnlich in einigen Projekten. Ich möchte nun mit dem Code konkret werden. (Mir fehlte die Fantasie für einen tollen Namen, also habe ich das Projekt MDI – mobile data interface – genannt) Um das alles beisammen zu halten, habe ich ein neues Maven Projekt mit drei Modulen aufgesetzt:

...
  <groupId>de.fivews.mdi</groupId>
  <artifactId>mdi</artifactId>
  <packaging>pom</packaging>
  <version>0.1</version>
  <modules>
    <module>core</module>
    <module>server</module>
    <module>android</module>   
  </modules>
...

Wichtig ist in dem server– und android-Modul eine Abhängikeit zu dem core-Modul zu definieren.

...
  <dependency>
      <groupId>${pom.groupId}</groupId>
      <artifactId>core</artifactId>
      <version>0.1</version>
    </dependency>
...

Core – gemeinsame Code-Basis

Das erste Modul core ist das umfangreichste und bildet die Schnittstelle zwischen der Client-App und dem Server. (Ich habe die Bezeichner Action, Result und ActionHandler an das Dispatch Modul in GWT angelehnt.)
bildschirmfoto-2010-11-03-um-181627

In diesem Modul sind Interfaces und Klassen definiert, die sowohl auf der Client- als auch für die Serverseite genutzt werden. Das Result Interface wird von allen Beans implementiert, die ein Ergebnis enthalten, das für den Client bestimmt ist.

public interface Result extends Serializable {}

Analog zu dem Result gibt es das Action Interface. Dieses Interface sorgt für zweierlei.

  1. Die Bindung an einen bestimmten Result. Es wird von der konkreten Implementierung vorgegeben, von welchem Typ das Ergebnis sein muss.
  2. Die getResultClass-Methode liefert die Klasse des Results. Wir werden später sehen, dass ich den Typen benötige, da die Generics mir zur Laufzeit nicht weiter helfen.
public interface Action<T extends Result> extends Serializable {
  public Class<T> getResultClass();
}

Weiter geht es mit dem ActionHandler. So ein Handler ist jeweils für ein Action/Result Pärchen verantwortlich. Mit anderen Worten, nimmt ein Handler eine bestimmte Action entgegen, macht irgendwas damit und liefert ein bestimmtes Result-Objekt. Zu jedem Action/Result Paar gibt es genau einen ActionHandler. Es ist sinnvoll für jede Action gleich einen Mock-ActionHandler zu implementieren, der per Dependency Injection in eine Testumgebung eingebunden werden kann. Zudem liefert die getActionClass-Methode die konkrete Klasse der Action, für die der Handler verantwortlich ist. Auch dazu später…

public interface ActionHandler<A extends Action<R>, R extends Result> {
 
  /**
   * Handles an action and returns the appropriate result
   *
   * @param action The action to handle
   * @return The result of the action
   * @throws Exception If an action could not be executed
   */
  public R execute(A action) throws Exception;
 
  /**
   * @return The class of the action this handler is responsible for.
   */
  public Class<A> getActionClass();
}

Nun benötigen wir eine Instanz, die darüber entscheidet, welche Action, von welchem ActionHandler bearbeitet wird. Dafür habe ich mir den Dispatcher ausgedacht. Der Dispatcher führt eine interne Liste über die verfügbaren ActionHandler und delegiert jede Action zu dem entsprechenden Handler, sofern denn einer verfügbar ist.

public interface Dispatcher {
 
  /**
   * Executes an action and returns the result
   *
   * @param action The action to execute
   * @param <R> The result type
   * @param <A> The action type
   * @return The result
   * @throws DispatchException If an action raises an exception
   */
  public <R extends Result, A extends Action<R>> R execute(A action) throws DispatchException;
}

Passend dazu existiert auch eine asynchrone Variante für den Client. (Es ist nichts ärgerlicher als eine blockierende UI.) Der Unterschied liegt in der Rückgabe des Ergebnisses. In der Asynchronen Variante wird das Ergebnis(oder der Fehler) in einem Callback zurück gegeben.

public interface AsyncDispatcher {
  /**
   * Executes an action and returns the result
   *
   * @param action The action to execute
   * @param callback The callback with the result
   * @param <R> The result type
   * @param <A> The action type
   */
  public <R extends Result, A extends Action<R>> void execute(A action, AsyncCallback<R> callback);
}

Zu guter letzt habe ich auch noch einen konkreten Anwendungsfall modelliert und zwar die Abfrage nach der Schnittstellen-Version.

// GotVersion.java
public class GotVersion implements Result {
  private Version version;
  ...
}
 
// GetVersion.java
public class GetVersion implements Action<GotVersion> {
  public Class<GotVersion> getResultClass() {
    return GotVersion.class;
  }
}
 
// MockGetVersionHandler.java
public class MockGetVersionHandler implements ActionHandler<GetVersion, GotVersion> {
 
  public GotVersion execute(GetVersion action) throws Exception {
    return new GotVersion(new Version("1.0"));
  }
 
  public Class<GetVersion> getActionClass() {
    return GetVersion.class;
  }
}

Damit ist die Arbeit an der Schnittstelle fürs erste abgeschlossen. Die Erweiterung findet durch die Implementierung von jeweils Action + Result + ActionHandler statt. Danach zähle ich die Version in der pom.xml hoch, installiere das Modul in meinem Maven Repository und damit steht es dem Server und Client zur Verfügung.

Server – Grails

Der Server ist eine Grails Anwendung. Damit die Kommunikation mit dem Client funktioniert muss ich drei Dinge tun:

  1. Einen Dispatch Service implementieren
  2. Dem Dispatch Service alle Handler mitgeben
  3. Einen Controller implementieren, der die JSON Nachricht verarbeitet und an den Dispatch Service delegiert.

Die Implementierung der Dispatcher Schnittstelle ist denkbar einfach. Es wird eine Liste der verfügbaren ActionHandler im Konstruktor erwartet. Sie werden in einer Map auf die Klasse der Action abgebildet, für die der ActionHandler verantwortlich ist. Wird die execute-Methode aufgerufen, sucht sich der Service den passenden Handler raus und delegiert den Aufruf.

class DispatchService implements Dispatcher {
  def handlers = [:]
 
  def DispatchService(handlerList = []) {
    handlerList.each { handler ->
      handlers.put(handler.actionClass, handler)
    }
  }
 
  Result execute(Action action) {
    def handler = handlers.get(action.getClass())
    if (handler == null) {
      throw new DispatchException("Handler not found for action class " + action.getClass())
    }
 
    try {
      return handler.execute(action)
    }
    catch (Exception e) {
      throw new DispatchException(e)
    }
  }
}

Die zu verwendenden ActionHandler konfiguriere ich über die config/spring/resources.groovy

beans = {
 
  /**
   * The service that delegates the action requests to the responsible handler instances.
   * To extend the service, just instantiate a new handler bean and put it in the list 
   */
  dispatchService(DispatchService, [
          new MockGetVersionHandler()
  ])
}

Schließlich ist der Controller zu implementieren, der die JSON Nachricht verarbeitet. Da aus dem JSON String nicht erkennbar ist, welche Action gerade übertragen wird, schickt der Client den voll qualifizierten Klassen Namen als Parameter mit. Ich kenne keine Bibliothek, die so einfach wie GSON funktioniert, wenn es um die (De)serialisierung von JSON Daten geht. Um eine Action zu deserialisieren, benötigt GSON die konkrete Klasse der Action. Diese lädt der Controller über Class.forName()

package de.fivews.mdi.server.controller
 
import com.google.gson.Gson
 
class DispatchController {
  static packagePrefix = "de.fivews.mdi"
  def dispatchService
  def gson = new Gson()
 
  /**
   * Interface entry point for mobile data requests.
   * Expects the following parameters:
   * - a The full qualified action class name
   * - d The JSON data that will be transformed into an instance
   *     of the action class
   */
  def index = {
    /* Action package must start with de.fivews.mdi */
    if (params.a && !params.a.startsWith(packagePrefix)) {
      render(text: gson.toJson(new Error(error: 'action not supported')))
    }
    /* Is JSON data given? */
    else if (!params.d) {
      render(text: gson.toJson(new Error(error: 'data missing')))
    }
    /* Prerequisites are given. Continue processing... */
    else {
      try {
        def actionClass = params.a ? Class.forName(params.a) : null
        /* Is action name given? */
        if (!actionClass) {
          render(text: gson.toJson(new Error(error: 'action missing')))
        }
        /* Process request and returns the result as JSON text */
        else {
          render text: processRequest(params.d, actionClass)
        }
      }
      catch (Exception e) {
        /* Don't show too much information */
        render(text: gson.toJson(new Error(error: "unexpected error")))
      }
    }
  }
 
  /**
   * Handles the given JSON payload by decoding the action and executing
   * the action request. The result will be encoded as JSON and returned
   *
   * @param data The JSON payload
   * @param actionClass The action class of the JSON payload
   * @return The result as JSON
   */
  private def processRequest(d, actionClass) {
    def action = gson.fromJson(d, actionClass)
    def result = dispatchService.execute(action)
    gson.toJson(result)
  }
 
  /**
   * An instance of this class will be returned if an error is raised 
   */
  private static class Error {
    def String error;
  }
}

Sobald der Request im index-Closure angekommen ist, überprüft der Controller ob die Daten (params.d) und der Klassen-Name(params.a) der Action die Voraussetzungen erfüllen. Wenn nicht, erzeugt er eine Fehlermeldung. Soweit alles passt, lädt der Classloader mit Class.forName() die Klasse der Action und der Controller deserialisiert mit Hilfe von GSON die Nachricht in die entsprechende Action. Der Dispatcher delegiert die Action an den verantwortlichen ActionHandler und liefert das Ergebnis an den Controller, der das Ergebnis wieder nach JSON serialisiert und an den Client liefert. Puhh 🙂

Client – Android

An dieser Stelle ist die Arbeit schon fast erledigt. Auf der Clientseite ist es ratsam eine asynchrone Variante des Dispatcher's zu nutzen. So wird die Anwendung nicht blockiert. Meine Android-Activity sieht deshalb wie folgt aus:

...
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    // URL = "http://localhost:8080/server/dispatch"
    final AsyncDispatcher dispatcher = new HttpAsyncDispatcher(URL, new DefaultHttpClient());
    dispatcher.execute(new GetVersion(), new AsyncCallback<GotVersion>() {
      public void onSuccess(GotVersion result) {
        Log.i(TAG, "result = " + result);
      }
 
      public void onError(Throwable throwable) {
        Log.e(TAG, "throwable = " + throwable, throwable);
      }
    });
  }
...

Nichts Spektakuläres… Eine neue Instanz der HttpAsyncDispatcher Klasse erzeugen und die GetVersion-Action abfeuern. Sobald der Server eine Antwort schickt, ruft der Dispatcher eine der beiden Methoden im AsyncCallback auf. Zu guter letzt die Implementierung des asynchronen Dispatchers:

public class HttpAsyncDispatcher implements AsyncDispatcher {
  private final BlockingQueue<Runnable> queue;
  private final ThreadPoolExecutor executor;
  private final HttpClient client;
  private final String url;
 
  public HttpAsyncDispatcher(String url, HttpParams params) {
    this(url, new DefaultHttpClient(params));
  }
 
  public HttpAsyncDispatcher(String url, HttpClient client) {
    /* The queue contains all tasks this dispatcher should handle */
    this.queue = new LinkedBlockingQueue<Runnable>();
    /* A thread pool for processing the tasks in the queue */
    this.executor = new ThreadPoolExecutor(2, 6, 10000, TimeUnit.MILLISECONDS, queue);
    /* Server target url */
    this.url = url;
    /* An instance of org.apache.http.client.HttpClient */
    this.client = client;
  }
 
  public <R extends Result, A extends Action<R>> void execute(final A action, final AsyncCallback<R> callback) {
    /* Creates a new Task and add it to the queue */
    executor.execute(new Runnable() {
      public void run() {
        try {
          /* The action and data parameters that will be processed by the server */
          final List<NameValuePair> params = new ArrayList<NameValuePair>();
          params.add(new BasicNameValuePair("a", action.getClass().getName()));
          params.add(new BasicNameValuePair("d", new Gson().toJson(action)));
          final UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
 
          /* Create the post request an set the message payload */
          final HttpPost post = new HttpPost(url);
          post.setEntity(entity);
 
          /* Executes the request and wait for the response */
          final HttpResponse httpResponse = client.execute(post);
          final String content = EntityUtils.toString(httpResponse.getEntity());
          /* Decode the json request and execute the callback */
          final R result = new Gson().fromJson(content, action.getResultClass());
          callback.onSuccess(result);
        }
        /* In case something went wrong */
        catch (Exception e) {
          callback.onError(e);
        }
      }
    });
  }
}

Da der Aufruf auf dem HttpClient synchron abläuft, habe ich mir überlegt einen Thread Pool zu nutzen und die auszuführenden Aufgaben in eine Queue zu schreiben, die vom Thread Pool abgearbeitet wird. (Was genau in dem Service passiert, bitte ich den Kommentaren im Quelltext zu entnehmen.)

Fazit: Allgemein bin ich mit meiner Lösung zufrieden. Einmal geschrieben, kann sie schnell erweitert werden. Auch ist sie nicht besonders komplex. Was einwenig Disziplin erfordert ist das Design der Beans, die zwischen Client und Server ausgetauscht werden. Es gibt keine Saubere Möglichkeit zyklische Beziehung zwischen einzelnen Objekten in JSON abzubilden. Deshalb würde ich persönlich mit eher flachen Strukturen arbeiten und keinen komplexen Objektgraphen übertragen. Das ist natürlich von Fall zu Fall unterschiedlich, aber ich muss bedenken, dass ich mit JSON keine Referenzen auf Objekte übertragen kann und ein doppelt referenziertes Objekten u.U. zwei mal erzeugt wird. Damit kann das Objekt auf der Clientseite 2x existieren.

2 thoughts on “Android und Grails über JSON miteinander sprechen lassen

  1. Thomas

    Hallo, wie wurde den der AsyncCallback entworfen? Oder ist der AsynchTask gemeint – der allerdings drei generische Parameter erwartet?

  2. Sergej Post author

    Hallo Thomas, die Definition des AsyncCallbacks habe ich versäumt zu veröffentlichen. Aber es ist eine eigene Schnittstellendefinition nach dem Vorbild aus GWT. Frei aus dem Kopf:

    public interface AsyncCallback() {
      public void onSuccess(T result);
      public void onError(Throwable throwable);
    }
    

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.