02.02.2013

InvocationCallback, Async und die Cient-API bei JAX-RS 2.0

In diesem Teil des Tutorials geht es darum, wie auf Client-Seite ein Aufruf asynchron an einen REST-Service abgesetzt werden kann. D.h., durch die Verwendung von async() und dem InvocationCallback wird dafür gesorgt, dass der Aufruf eines REST-Services asynchron abläuft und der Client nicht blockiert.

Auf der Abbildung sieht man, wie der Client einen Request an den Service /itemService/itemstring des Servers absetzt. Der Aufruf kehrt direkt zurück. Die eigentliche Response mit dem Ergebnis Item 1 wird erst dann geliefert, wenn sie vorliegt.

Dazu stelle man sich einfach vor, dass der REST-Service eine Methode calculateAnswerWait anbietet, die 3 Sekunden zur Berechnung des Wertes 42 benötigt.

Der Quellcode dazu sieht folgendermaßen aus:

package org.hameister.itemservice;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;

@Path("/itemservice")
public class ItemService {

...

  @GET
  @Path("calculateAnswerWait")
  @Produces("text/plain")
  public String calculateAnswerWait() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException ex) {
    }
    return "42 was calculated by calculateAnswerWait at "+System.currentTimeMillis();
  }

}

Wenn diese Methode vom Client auf herkömmlichen Wege aufgerufen wird, dann ist die Client mindestens 3 Sekunden lang blockiert. Um das zu verhindert, gibt es die Möglichkeit ein InvocationCallback für den Rückgabewert des Methoden-Aufrufs anzulegen und diesem beim Aufruf in der get-Methode mitzugeben.

Der InvocationCallback sieht folgendermaßen aus:

        InvocationCallback<String> resultCalculatedAnswer = new InvocationCallback<String>() {
            @Override
            public void completed(String rspns) {
                System.out.println("==== Calculated answer: " + rspns);
            }

            @Override
            public void failed(Throwable thrwbl) {
                System.out.println("== Something wet wrong ==");
            }
        };

Der InvocationCallback ist ein Interface, welches die Methoden completed und failed anbietet und die beim Erstellen einer Instanz implementiert werden müssen. Außerdem muss ein Rückgabewert (RESPONSE) festgelegt werden. In dem Beispiel wird ein String als erwartet.

Dieser InvocationCallback kann nun einfach beim Aufruf des REST-Service als Parameter der get-Methode mitgegeben werden.

client.target(SERVER +"/itemservice/calculateAnswerWait").
       request("text/plain").async().get(resultCalculatedAnswer);

Wichtig ist auch, dass die Methode async() an den InvocationBuilder übergeben wird.

Wenn der Aufruf in der Form abgesetzt wird, dann kehrt die Methode sofort zurück und der Programmfluss wird fortgesetzt. Sobald das Ergebnis von Server zurückkommt, wird der InvocationCallback aufgerufen und das Ergebnis wird ausgegeben.

Ein JUnit-Test dazu sieht folgendermaßen aus:

package org.hameister.itemservice.test;

import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientFactory;
import javax.ws.rs.client.InvocationCallback;
import junit.framework.Assert;
import org.hameister.itemservice.Item;
import org.hameister.itemservice.ItemListMessageBodyReader;
import org.hameister.itemservice.ItemListMessageBodyWriter;
import org.hameister.itemservice.ItemMessageBodyReader;
import org.hameister.itemservice.ItemMessageBodyWriter;
import org.junit.After;
import org.junit.AfterClass;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class JAXRESTClientTest {

    Client client;
    private static final String SERVER = "http://localhost:8080/JAXRSServer/webresources";

    public JAXRESTClientTest() {
    }

    @Before
    public void setUp() {
        client = ClientFactory.newClient();
    }

    @After
    public void tearDown() {
        client.close();
    }
    
...    
    
    @Test
    public void calculatAnswerAsyncCallback() {
        long startTime = System.currentTimeMillis();
        InvocationCallback<String> resultCalculatedAnswer = new InvocationCallback<String>() {
            @Override
            public void completed(String rspns) {
                Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "==== Calculated answer: {0}", rspns);
                Assert.assertTrue("Result must start with 42", rspns.startsWith("42"));
             }

            @Override
            public void failed(Throwable thrwbl) {
                Logger.getLogger(JAXRESTClientTest.class.getName()).log(Level.INFO, "== Something wet wrong ==");
                fail("Async call failed.");
            }
        };

        client.target(SERVER + "/itemservice/calculateAnswerWait").request("text/plain").async().get(resultCalculatedAnswer);
        long endTime = System.currentTimeMillis();
        
        Assert.assertTrue("Call must return immediately", (endTime - startTime) < 1000);
    }
}

In dem Test wird überprüft, ob der Aufruf "sofort" zurückgekehrt und ob die Response das gewünschte Ergebnis enthält.

Dieser Teil des Tutorial behandelte ausschließlich die Client-Seite. Allerdings gibt es auch die Möglichkeit auf Server-Seite Anpassungen zu machen, die die asynchrone Ausführung von Methoden beeinflussen. Darauf wird im nächsten Teil eingegangen.