How to create a Jersey-REST-Client with ProgressIndicator and asynchronous tasks in JavaFx

Dieses kleine Beispiel soll zeigen, wie einfach ein JavaFx Client mit dem Jersey implementiert werden kann.

Das Ziel des Tutorials wird sein, zu zeigen wie man REST-Aufrufe an den Server schicken kann, ohne dabei die Oberfläche zu blockieren. Denn nicht immer ist klar wie lange ein Aufruf dauert.

Im Laufe des Tutorials werden drei verschiedene Implementierungen gezeigt und die Auswirkungen anhand eines Login-Fensters demonstriert.

Server

Was für den Server benötigt wird:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

 

Der Server ist eine kleine SpringBoot-Anwendung, die einen Request vom Client erhält und überprüft ob der Benutzername und das Passwort korrekt ist. Damit der Client nicht sofort einen Response erhält, wartet der Server 5 Sekunden, bevor er dem Client eine Antwort zurückschickt.

@SpringBootApplication
public class Application {

    public static  void main(String... args){
        SpringApplication.run(Application.class, args);
    }

}

@RestController
@RequestMapping(value = "/api/rest", produces = MediaType.APPLICATION_JSON_VALUE)
class LoginController {

    private String loginUsername = "admin";
    private String loginPassword = "admin123";

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(@RequestBody Login login) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return login.getUsername().equalsIgnoreCase(loginUsername) && login.getPassword().equalsIgnoreCase(loginPassword)
                ? "You have successfully logged in"
                : "Wrong login credentials";
    }
}

@JsonIgnoreProperties(ignoreUnknown = true)
class Login {
    private String password;

    private String username;

    public Login(){
    }

    public String getPassword() {
        return password;
    }
    public String getUsername() {
        return username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

 

RESTServer

Das soll es für den Server auch schon gewesen sein.

Client

Der Client wird eine JavaFx Anwendung sein, an der sich der Nutzer mit Benutzername und Passwort einloggen kann.

Was für den Client benötigt wird:

   <dependencies>
        <dependency>
            <groupId>com.airhacks</groupId>
            <artifactId>afterburner.fx</artifactId>
            <version>1.6.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-client</artifactId>
            <version>2.22.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>2.22.1</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

Für den Client kommt das schlanke JavaFx Framework afterburner.fx zum Einsatz. Wer dazu weitere Informationen benötigt, findet diese auf der Seite des Entwicklers Adam Bien.

Als REST-Client dient in diesem Fall Jersey 2. 

In einem anderen Artikel habe ich bereits beschrieben, wie man einen ProgressIndicator auf einer Swing-GlassPane darstellt. Dies habe ich mir als Vorlage genommen und in JavaFx umgesetzt.

RESTClientLoginComboBox

Mit Hilfe der ComboBox kann man die verschiedenen Verhaltensweisen der folgenden Implementierungen recht schnell ausprobieren.

Implementierung #1 FX-Thread

In dem ersten Beispiel wird ein REST-Aufruf direkt im FX-Thread ausgeführt.

LoginPresenter.java

private void uiFreezeLogin(){
        final Response loginResponse = restService.uiFreezeLogin(new Login(username.getText(), password.getText()));
        if(loginResponse != null){
            final String readEntity = loginResponse.readEntity(String.class);
            if(readEntity.contains("You have successfully logged in")){
                loginCompleted();
            } else {
                loginFailed(readEntity);
            }
        } else {
            loginFailed("Failed to login");
        }
    }

RestService.java

public Response uiFreezeLogin(Login login){
        Response response = null;
        try{
            response = target.path("/login")
                        .request(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
                        .post(Entity.entity(login, MediaType.APPLICATION_JSON));
        } catch (Exception e){
        }
        return response;
    }

Auswirkungen: Die Oberfläche des Clients friert komplett ein, bis der Client eine Antwort erhält oder in einen Timeout läuft.

Implementierung #2 InvocationCallback

Die nächste Implementierung zeigt, wie ein Request mit einem InvocationCallback aussieht.

LoginPresenter.java

private void invocationCallbackLogin(){
        restService.invocationCallbackLogin(new InvocationCallback<Response>() {
            @Override
            public void completed(Response response) {
                if(response != null){
                    String readEntity = response.readEntity(String.class);
                    if (readEntity.contains("You have successfully logged in")) {
                        loginCompletedPlatformRunLater();
                    } else {
                        loginFailedPlatformRunLater(readEntity);
                    }
                } else {
                    loginFailedPlatformRunLater("Failed to login");
                }
            }

            @Override
            public void failed(Throwable throwable) {
                loginFailedPlatformRunLater("Failed to login");
            }
        }, new Login(username.getText(), password.getText()));
    }
//Die Folgenden zwei Methoden werden benötigt, um wieder zurück in den FX-Thread zu gelangen.
    //Dies ist für den InvocationCallbackLogin notwendig. Beim Task-Login wird dies durch die Klasse 'Task' für uns erledigt.

    private void loginCompletedPlatformRunLater(){
        Platform.runLater(() -> {
            loginCompleted();
        });
    }

    private void loginFailedPlatformRunLater(String msg){
        Platform.runLater(() -> {
            loginFailed(msg);
        });
    }

RestService.java

 public void invocationCallbackLogin(InvocationCallback<Response> callback,Login login){
        threadPool.executor().execute(()-> createLoginFuture(callback, login));
    }

    private Future<Response> createLoginFuture(InvocationCallback<Response> callback, Login login){
        return target.path("/login")
                .request(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .async()
                .post(Entity.entity(login, MediaType.APPLICATION_JSON), callback);
    }

Auswirkung: Die Oberfläche friert nicht ein und der ProgressIndicator wird für die Dauer des Requests angezeigt.

Implementierung #3 Task

Die letzte Implementierung verwendet die seit JavaFx 2.0 existierende Klasse Task. Die Klasse Task ist eine Implementierung der Klasse FutureTask und ist optimal für die Bearbeitung von asynchronen Aufgaben in JavaFx geeignet.

LoginPresenter.java

private void taskLogin(){
        restService.taskLogin(response -> {
            if(response != null){
                final String readEntity = response.readEntity(String.class);
                if(readEntity.contains("You have successfully logged in")){
                    loginCompleted();
                } else {
                    loginFailed(readEntity);
                }
            } else {
                loginFailed("Failed to login");
            }

        },new Login(username.getText(), password.getText()));
    }

RestService.java

 

 public void taskLogin(Consumer<Response> consumer, Login login){
        threadPool.taskLogin(createLoginCallable(login), consumer);
    }

    private Callable<?> createLoginCallable(Login login){
        return () -> {
            return target.path("/login")
                    .request(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .async()
                    .post(Entity.entity(login, MediaType.APPLICATION_JSON))
                    .get(10, TimeUnit.SECONDS); //Future get() call
        };
    }

ThreadPool.java

public class ThreadPool<T>{
    
    private final Executor pool = Executors.newFixedThreadPool(2);

    public Executor executor() {
        return pool;
    }

    public void taskLogin(Callable<T> callable, Consumer<T> consumer){
        pool.execute(createTask(callable, consumer));
    }

    private Task<T> createTask(Callable<T> callable, Consumer<T> consumer){
        return new Task<T>() {
            @Override
            protected T call() throws Exception {
                return callable.call();
            }

            @Override
            protected void succeeded() {
                super.succeeded();
                consumer.accept(getValue());
            }

            @Override
            protected void failed() {
                super.failed();
                consumer.accept(getValue());
            }
        };
    }
}

Mit dieser Implementierung kann man sich den Aufruf der Platform.runLater() Funktion sparen, denn bereits in der Methode succeeded() oder failed() befindet man sich wieder im Fx-Thread.

Beim Aufruf der Methode getValue() wird intern nochmal überprüft, ob man nun im Fx-Thread ist oder nicht. Sollte das an dieser Stelle nicht der Fall sein, wird eine IllegalStateException(„Task must only be used from the FX Application Thread“) geworfen.

Auswirkungen: Die Auswirkungen auf die Oberfläche sind die gleichen wie bei der Verwendung von InvocationCallbacks. Im Code sind die Auswirkungen durchaus größer, denn der Aufruf von Platform.runLater() kann so gut wie vernachlässigt werden.

Fazit

Persönlich würde ich mich für die Umsetzung mit Tasks entscheiden, denn ich kann dieses Konstrukt nicht nur für REST-Aufrufe, sondern auch für alle anderen Aufgaben benutzen, bei denen Nebenläufigkeit erforderlich ist. (z.B. Datenbankaufrufe, Berechnungen etc…)

Download

Das gesamte Projekt steht bei Bitbucket zur Verfügung.

Screenshots

RESTClientLogin

RESTClientExample

How to create a Jersey-REST-Client with ProgressIndicator and asynchronous tasks in JavaFx
Markiert in:                             

Ein Gedanke zu „How to create a Jersey-REST-Client with ProgressIndicator and asynchronous tasks in JavaFx

Schreibe einen Kommentar

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