Über unsMediaKontaktImpressum
Guido Oelmann 20. September 2016

Das neue MVC-Webframework in Java EE 8

Mit der für das im kommenden Jahr angekündigten Java EE 8-Version wird ein neues Webframework Einzug erhalten. Im nachfolgenden wird dieses Framework vorgestellt und die konkrete Verwendung anhand eines Beispiels gezeigt. Zudem wird der Unterschied zum JSF-Standard erklärt und warum es dieses Framework nicht ersetzt, sondern eine sinnvolle Alternative darstellt.

Das neue Framework ist unter der MVC 1.0 (JSR-371) Spezifikation zu finden [1]. Es handelt sich um ein actionbasiertes Framework, welches auf der JAX-RS-API aufsetzt und CDI sowie BeanValidation unterstützt. Damit steht neben dem komponentenbasierten JSF-Framework ein weiterer Ansatz zur Verfügung, um Webapplikationen zu bauen. Moderne Webapplikationen, z. B. im Gewand von Single-Page-Applications, halten zunehmend mehr Logik auf der Client-Seite und kommunizieren mit dem Server per REST zum Austausch kleiner Datenobjekte. Das Rendern neuer Seiten übernimmt dabei der Client gleich selber. Gerade für solche Ansätze bietet das neue Framework gute Dienste.

MVC

Der Name des MVC-Frameworks steht für Model-View-Controller. MVC ist ein schon seit den späten 70ern bekanntes Entwurfsmuster (bzw. Architekturmuster) und dient der Aufteilung einer Anwendung in drei Komponenten:

  • model: Komponente zum Halten des Datenmodells
  • view: Komponente für die Benutzeroberfläche
  • controller: Komponente für die Programmsteuerung

Wir betrachten im Folgenden das Muster ausschließlich im Kontext des neuen Java EE 8-Frameworks. Daher beinhaltet z. B. der Model-Teil auch nur das Datenmodell und nicht noch weitergehende Geschäftslogik.

Eine Anfrage vom Webbrowser wird immer zuerst an die Controller-Komponente geleitet (Schritt 1). Der Controller ist dann für die Bearbeitung der Anfrage zuständig. In Abhängigkeit der angeforderten URL und der übermittelten Request-Parameter entscheidet dieser, welche Daten angefordert werden. Dementsprechend wird das Model aktualisiert (Schritt 2). In dem Model werden die eigentlichen Daten gehalten, die von der Anwendung angefragt wurden. Beispielsweise Informationen über den Artikel eines Webshops oder die Adresse einer Person. Ist dies geschehen, wählt der Controller eine View (Schritt 3) aus, die für die Anzeige der Daten verantwortlich ist. Die View-Komponente liest die Daten im Model (Schritt 4) und generiert eine Antwort, welche an den Webbrowser gesendet wird (Schritt 5).

Der erfahrene JSF-Entwickler wird sich nun fragen, ob das nicht so im Grunde auch bei JSF abläuft. Also ob JSF nicht auch nach diesem MVC-Muster funktioniert? Die Antwort ist: Ja, aber JSF ist komponentenbasiert und das neue MVC-Framework ist actionbasiert. Was genau der Unterschied ist, klärt der folgende Abschnitt.

Actionbasiertes Framework vs. komponentenorientiertes Framework

Das Java-Universum hat in der Vergangenheit eine Menge actionbasierter Frameworks hervorgebracht. Zum Beispiel das schon stark angegraute Apache Struts und Spring MVC, um nur zwei zu nennen. Die Verwendung dieser Frameworks funktioniert dabei wie zuvor beschrieben. Vom Browser ausgehend, trifft ein Request am Webserver ein. Man spricht auch von einer Action, die im Browser ausgelöst wurde und nun vom Server zu verarbeiten ist. Diese Action wird vom Controller (bei Struts spricht man auch von Action-Klassen) verarbeitet und nach dem Senden der Antwort verbleiben keinerlei Informationen dieses Vorgangs auf dem Server. Das bedeutet, dass serverseitig nirgends festgehalten ist, was dem Benutzer vor dem Browser aktuell angezeigt wird. Gemeint ist die Kenntnis aller Anzeigeelemente und nicht nur welche Seite der Benutzer sich gerade anschaut. Man spricht hier auch von einem zustandslosen Programmiermodell. Eine vorhandene serverseitige Session kann dabei durchaus nutzerbezogene Daten speichern, ist aber getrennt vom Framework zu sehen.

Hier werden bereits zwei ganz wesentliche Dinge ersichtlich, die gänzlich unterschiedlich zu JSF sind. Zum einen hat der Entwickler volle Kontrolle über die Verarbeitung des Requests und zum anderen ist das Framework grundsätzlich zustandslos.

Komponentenbasierte Frameworks wie JSF hingegen machen die Verarbeitung des Requests für den Entwickler nicht sichtbar. Der komponentenorientierte Ansatz von JSF abstrahiert vom Request/Response-Konzept und legt – wenn man so will – eine zusätzliche Schicht darüber. Komponenten sind dabei die sichtbaren GUI-Elemente, wie z. B. eine Auswahlliste, einschließlich ihrer auf dem Server liegenden Logik. Wenn auf der Komponente eine Aktion ausgelöst wird, z. B. die Auswahl eines Elements einer Auswahlliste, wird ein Request an den Server geschickt. In diesem Zusammenhang wird jedoch von einem Event der Komponente gesprochen. Das Framework identifiziert zunächst die Komponente, ein entsprechendes UI-Event wird ausgelöst und das Datenmodell der Komponente wird dem Event gemäß aktualisiert. Aus der aktualisierten Komponente stellt der Server dann ein passendes Response zusammen und sendet diesen zum Client. Dies soll als grobe Erklärung des Ablaufs genügen. Konkret bedeutet das, dass der Entwickler sich hier nicht mehr auf der Ebene der Requests und Responses bewegt, sondern auf einer Frameworkebene darüber, die diese Vorgänge unsichtbar im Hintergrund ausführt. Trotzdem lässt sich hier leicht der MVC-Ansatz erkennen.

Beim MVC 1.0 kann der Entwickler frontendseitig jede beliebige Technologie wählen, egal ob reines HTML5, AngularJS oder was auch immer. Der nackte Request kommt beim Controller an und kann beliebig verarbeitet werden. Ebenso fallen aber auch Validierungen und Konvertierungen in die Verantwortung des Entwicklers. Die ViewEngine zum Rendern der Seite und der Erstellung des Responses kann frei gewählt oder selber erstellt werden, wie später noch gezeigt wird.

JSF hingegen kümmert sich selbst um Validierungen und Konvertierungen. Ebenso um das Rendern der Komponente, die frontendseitig meistens aus einem Bündel von HTML, CSS und JS bestehen. Requests werden bei JSF von den Komponenten automatisch gesendet und serverseitig verarbeitet. Der Entwickler muss lediglich noch seine Programmlogik hinzufügen und hat sonst mit den eigentlichen Requests und Responses nichts zu tun.
Als kleine Einführung theoretischer Natur soll dies schon reichen und es kommt nun zum spannenden Teil. Wie wird das neue Framework nun verwendet? Dafür wird ein kleines Beispielprogramm erstellt, anhand dessen die Verwendung erläutert wird.

Die erste Java EE 8 MVC-Anwendung

Gebaut wird eine Anwendung, mit der jeder unter Hinzunahme des entsprechenden YouTube-Links, seine Lieblingsvideos in einer Liste speichern kann. Aus dem Link wird ein Vorschaubild für die Liste genommen und es kann direkt zum Video oder dem Trailer gesprungen werden. Abb.3 zeigt das fertige Programm.

Die Referenzimplementierung von MVC trägt den Namen Ozark [2] und liegt zum Zeitpunkt der Erstellung dieses Artikels in einer "Early Draft Review 2"-Version vor. Wie in Abb.2 zu sehen war, wird MVC 1.0 als Aufsatz auf JAX-RS realisiert. Die aktuellen Bibliotheken der Referenzimplementierung nutzen derzeit zwingend die Jersey-Implementierung von JAX-RS. Dies ist beim Deployen auf Applicationservern wie WildFly, die eine andere Implementierung mitbringen, zu berücksichtigen. Details folgen im weiteren Verlauf des Textes.

Für das Einbinden der entsprechenden Bibliotheken sind folgende Dependencies der pom.xml hinzuzufügen:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.mvc</groupId>
    <artifactId>javax.mvc-api</artifactId>
    <version>1.0-edr2</version>
</dependency>
<dependency>
    <groupId>org.glassfish.ozark</groupId>
    <artifactId>ozark</artifactId>
    <version>1.0.0-m02</version>
</dependency>  

Und für die CDI-Unterstützung ist eine beans.xml im /WEB-INF- oder /META-INF-Verzeichnis notwendig:

<?xml version="1.0"?>
<beans xmlns="http: //xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http: //xmlns.jcp.org/xml/ns/javaee xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       version="1.1" bean-discovery-mode="all">
</beans>

Um eine MVC-Anwendung zu schreiben wird zunächst eine JAX-RS-Application class benötigt.

package de.javaakademie.moviestore;

import java.util.HashMap;
import java.util.Map;

import javax.mvc.security.Csrf;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("mvc")
public class MovieStoreApp extends Application {
   ...
}

Mit @ApplicationPath wird der Pfad zu der Anwendung definiert. Der Rest der Klasse kann für die Beispielanwendung leer bleiben. Als nächstes wird ein Controller für die Anwendung benötigt.

Controller

Der MVC-Controller ist eine JAX-RS-Ressource. Er ist verantwortlich für die Verarbeitung der hereinkommenden Requests. Ein simpler Controller kann so aussehen:

import javax.mvc.annotation.Controller;
import javax.ws.rs.Path;
import javax.inject.Inject;
import javax.mvc.Models;
import javax.ws.rs.GET;

@Controller
@Path("hallo")
public class HalloController {
 
  @Inject
  Models models;
 
  @GET
  public String sagHallo(@QueryParam("name") String name) {
    String message = "Hallo " + name;
    models.put("message", message);
    return "/WEB-INF/jsp/hallo.jsp";
  }
}

Die Controller-Klasse wird mit @Controller und @Path annotiert, um die Klasse als MVC-Controller zu identifizieren und über einen Pfad in der URL aufrufbar zu machen (/mvc/hallo). Mittels CDI wird die Model-Klasse instanziiert.

Models

Models repräsentiert einfach nur eine HashMap, in welcher Objekte abgelegt werden können. Auf diese können dann in der View zugegriffen werden. Das Interface sieht wie folgt aus:

public interface Models extends Map<String, Object>, Iterable<String> { }

Statt Models kann auch jede andere CDI-Bean zur Datenhaltung für die View verwendet werden. Die sagHallo-Methode wird bei einem HTTP GET-Request ausgeführt (z. B. /mvc/hallo?name=Hase). Mit @QueryParam wird der Request-Parameter an den Methodenparameter name gebunden und in der Methode ausgelesen. Der auszugebene Text wird dem Model hinzugefügt und es wird auf hallo.jsp verzweigt. Als Template-Engine wird hier also JSP verwendet. Die sagHallo-Methode liefert standardmäßig text bzw. html zurück, was der Annotation @Produces(“text/html”) entsprechen würde. Dies kann natürlich geändert werden. In der View kann dann mittels EL einfach auf das Attribut message im Model zugegriffen werden.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Test</title>
  </head>
  <body>
    <h1>${message}</h1>
  </body>
</html>

ViewEngine

Die Aufgabe der ViewEngine ist die Zusammenführung der Model-Daten mit der View. Als View-Technologie kann jede beliebige verwendet werden. MVC 1.0 unterstützt standardmäßig JSP (JspViewEngine) und Facelets (FaceletsViewEngine). Leicht können aber auch andere wie z. B. Thymeleaf, Handlebars oder etwas Eigenes hinzugefügt werden. Für die Erstellung einer eigenen ViewEnginge ist folgendes Interface zu implementieren:

public interface ViewEngine {
    boolean supports(String view);  
    void processView(ViewEngineContext context) throws ViewEngineException;  
}

Die supports-Methode gibt zurück, ob die View bzw. das Template unterstützt wird oder nicht und die processView-Methode rendert die View. Die ViewEngine für JSP sieht z. B. wie folgt aus:

@Priority(1000)
public class JspViewEngine implements ViewEngine {

    private static final String VIEW_BASE = "/WEB-INF/"; 

    @Inject
    private ServletContext servletContext;

    public boolean supports(String view) {
        return view.endsWith("jsp") || view.endsWith("jspx"); 
    }

    public void processView(ViewEngineContext context) throws ViewEngineException {
        Models models = context.getModels(); 
        HttpServletRequest request = context.getRequest();
        HttpServletResponse response = context.getResponse();
        Iterator rd = models.iterator();

        while(rd.hasNext()) {
            String e = (String)rd.next();
            request.setAttribute(e, models.get(e)); 
        }

        RequestDispatcher rd1 = this.servletContext.getRequestDispatcher("/WEB-INF/" + context.getView());

        try {
            rd1.forward(request, response);  
        } catch (IOException | ServletException var7) {
            throw new ViewEngineException(var7);
        }
    }
}

Für die MovieStore-Anwendung wird die JSP-Technologie verwendet. Der Controller dieser Anwendung muss natürlich mehr können, als nur einen Begrüßungstext auszugeben. Die Anwendung soll auf einer Webseite alle Filme auflisten. Neue Filme sollen hinzugefügt und vorhandene gelöscht werden können.

Formulare

Abb.4 zeigt den Vorgang beim Hinzufügen eines neuen Filmes. Auf der Webseite wird ein Formular ausgefüllt und an den Server gesendet. Der Controller nimmt dann dort die Daten entgegen. Bei der Entgegennahme von Formulardaten hilft das MVC-Framework.

Angenommen, es gibt ein einfaches Formular zur Übertragung eines Namens:

<form action="/mvc/hallo" method="POST">
    <input type="text" name="name"/>
    <input type="submit" value="Absenden"/>
</form>

Dann kann auf den Formularwert mit der Annotation @FormParam zugegriffen werden.
Ein entsprechender Controller könnte wie folgt aussehen:

import javax.mvc.annotation.Controller;
import javax.ws.rs.Path;
import javax.inject.Inject;
import javax.mvc.Models;
import javax.ws.rs.GET;
import javax.ws.rs.FormParam;

@Controller
@Path("/hallo")
public class FormController {

    @Inject
    private Models models?

    @POST
    public String post( @FormParam("name") String name ) {
    models.put( "message", "Hallo " + name )?
    return "hallo.jsp"?
}

Das Formular für den MovieStore ist komplexer. Hier soll der Filmtitel, das Erscheinungsdatum, die Spielzeit, das Land, das Genre, eine Liste der Darsteller und ein Link zum Trailer auf YouTube, von wo aus auch direkt das Vorschaubild geholt wird, übermittelt werden. Die Action unseres Formulars sieht wie folgt aus:

<form action="${mvc.basePath}/movies/create" method="POST">
...
</form>

Mit ${mvc…} wird auf den MVC-Context zugegriffen. Details folgen später im Text. Neben der einfachen Möglichkeit der Entgegennahme von Formulardaten mit @FormParam, gibt es die zweite Möglichkeit, ein komplettes Formular automatisch auf ein Objekt, dessen Attribute mit @FormParam annotiert sind, zu mappen. Für den MovieStore wird dafür folgende Klasse erstellt:

public class CreateMovieForm {

    @FormParam("title")
    private String title;

    @FormParam("releaseDate")
    private LocalDate releaseDate;

    @FormParam("runningTime")
    private Double runningTime;

    @FormParam("country")
    private String country;

    @FormParam("genre")
    private Genre genre;

    @FormParam("actors")
    private List<Actor> actors;

    @FormParam("trailerUrl")
    private String trailerUrl;

    ...

}

Die entsprechende Methode im Controller muss dann so aussehen:

@Path("/movies")
@Controller
public class MovieController {

    @POST
    @Path("/create")
    public String createItem(@BeanParam CreateMovieForm form) {
        ...
    }

    ...
}

Anstatt einzelne Fomulardaten mittels @FormParam zu mappen, wird hier die Annotation @BeanParam verwendet, die alle gesendeten Formulardaten aggregiert und auf das annotierte Objekt mappt. Damit keine Unsinnswerte übernommen werde, soll BeanValidation zum Validieren der Eingabedaten genutzt werden.

Validierung

Mit dem neuen MVC-Framework wird auch BeanValidation unterstützt. Dafür wird die obige Klasse einfach um entsprechende Annotations erweitert.

import javax.validation.constraints.Digits;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.ws.rs.FormParam;

import de.javaakademie.moviestore.model.Actor;
import de.javaakademie.moviestore.model.Genre;

public class CreateMovieForm {

    @NotNull
    @Size(min = 3, message = "The title must be at least 3 characters.")
    @FormParam("title")
    private String title;

    @FormParam("releaseDate")
    private LocalDate releaseDate;

    @Digits(fraction = 0, integer = 0, message = "Running time in minutes.")
    @FormParam("runningTime")
    private Double runningTime;

    @FormParam("country")
    private String country;

    @NotNull(message = "Please select the genre of the movie.")
    @FormParam("genre")
    private Genre genre;

    @FormParam("actors")
    private List<Actor> actors;

    @FormParam("trailerUrl")
    private String trailerUrl;

    ...

}

Die Controller-Methode zur Entgegennahme der Formularmethoden muss zum Ausführen der Validierung noch um die BeanValidation-Annotation @Valid ergänzt werden. Um Zugriff auf das Ergebnis der Prüfung zu bekommen wird das BindingResult-Objekt injiziert.

import javax.ws.rs.Path;
import javax.mvc.annotation.Controller;
import javax.mvc.binding.BindingResult;
import javax.ws.rs.POST;
import javax.ws.rs.BeanParam;
import javax.validation.Valid;

@Path("/movies")
@Controller
public class MovieController {

    @Inject
    private Models models;

    @Inject
    private Message message;
    
    @Inject
    private BindingResult bindingResult;

    @POST
    @Path("/create")
    public String createItem(@BeanParam @Valid CreateMovieForm form) {

        if (bindingResult.isFailed()) {
            message.addErrors(bindingResult.getAllMessages());
            models.put("form", form);
            return listMovies();

        }    
        ...
    }
    
    ...
}

Das Ergebnis der Validierung kann über die isFailed-Methode des BindingResult-Objekts abgefragt werden. In diesem Fall holt der obige Programmcode alle Fehlermeldungen aus diesem Objekt und packt diese in ein Message-Objekt. Die übertragenden Formulardaten werden in das Model gepackt, damit die View diese dem Benutzer später wieder anzeigen kann und dieser nur seine fehlerhaften Eingaben überarbeiten muss.

Das Message-Objekt wurde an dieser Stelle erstellt, um zu demonstrieren, wie neben der Nutzung der Model-Klasse auch andere CDI-Beans als Models verwendet werden können.

package de.javaakademie.moviestore.web;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.inject.Named;
import javax.mvc.annotation.RedirectScoped;

@Named
@RedirectScoped
public class Message implements Serializable {

    private static final long serialVersionUID = 6012270416224546642L;

    private String info;

    private final List<String> errors = new ArrayList<>();

    public Message addError(String error) {
        errors.add(error);
        return this;
    }

    public Message addErrors(Collection<String> errors) {
        this.errors.addAll(errors);
        return this;
    }

    public List<String> getErrors() {
        return Collections.unmodifiableList(errors);
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }

}

In dieser Klasse werden alle Validierungsfehler bzw. generell alle Meldungen, die in der View angezeigt werden sollen, gespeichert. Durch die @Named-Annotation können wir von der View aus bequem per EL auf die Bean zugreifen. Neu ist an dieser Stelle der Scope.

@RedirectScoped

Neben den üblichen und weiterhin verwendbaren CDI-Scopes, liefert das neue MVC-Framework einen neuen Scope als CDI-Erweiterung mit. Dabei handelt es sich um den Redirect-Scope. Wie gleich gezeigt wird, können auch Redirects durchgeführt werden. Damit die View, auf die dadurch umgeleitet wird, auch weiterhin Zugriff auf die CDI-Bean hat und die Daten erhalten bleiben, wird die Bean mit dem neuen Scope annotiert.

Die Controller-Methode zum Anlegen eines neuen Films soll bei Erfolg einen solchen Redirect durchführen, damit der Nutzer nicht durch einen Browser-Reload die Daten erneut senden kann. Die Methode sieht dann wie folgt aus:

@Path("/movies")
@Controller
public class MovieController {

    ...
    
    @POST
    @Path("/create")
    public String createItem(@BeanParam @Valid CreateMovieForm form) {

        if (bindingResult.isFailed()) {
            message.addErrors(bindingResult.getAllMessages());
            models.put("form", form);
            return listMovies();
        }

        Movie newMovie = movieService.createMovie(form.getTitle(), form.getReleaseDate(), form.getRunningTime(), form.getCountry(), form.getGenre(), form.getActors(), form.getTrailerUrl());

        message.setInfo("Movie created: " + newMovie.getTitle());
        return "redirect:/movies";
    }
    
}

Die Methode liefert den String redirect:/movies zurück, wobei das redirect: das Framework dazu veranlasst, einen Redirect auf die URL /movies auszuführen. Da das MVC-Framework auf JAX-RS aufsetzt, kann natürlich auch der Redirect-Mechanismus von JAX-RS verwendet werden. Bisher wurden immer Strings als Antwort zurückgegeben. Controller können aber verschiedene Rückgabetypen aufweisen.

Return Types

Ein MVC-Controller kann vier verschiedene Rückgabetypen haben: String, Void, Viewable und Response. String wurde bereits gezeigt.

Void

Wenn eine Controller-Methode den Rückgabetyp Void haben soll, muss diese mit @View annotiert werden, um dem Framework mitzuteilen, wo dieses die View finden kann. Das folgende Listing zeigt dies.

import javax.mvc.annotation.View;

@Path("hallo")
public class HalloController {

  @GET
  @Controller
  @View("hallo.jsp")
  public String sagHallo() {
  }
  
}

Weiter ist hier zu sehen, dass auch nur die Methode als Controller annotiert werden kann.

Viewable

Das Viewable-Objekt kapselt Informationen über eine View und optional über das zugehörige Model und der ViewEngine, die beim Rendern der View Verwendung finden soll. Letzteres kann genutzt werden, wenn für diese spezielle View eine andere Art des Renderns genutzt werden soll. Das folgende Listing zeigt die einfachste Nutzung einer Viewable. Der Viewable-Instanz wird lediglich die View übergeben.

import javax.mvc.Viewable;

@Path("hallo")
public class HalloController {

  @GET
  @Controller
  public Viewable sagHallo() {
    return new Viewable("/hallo.jsp");
  }
  
}

Response

Response als Rückgabetyp ist von JAX-RS bereits bekannt. Es bietet unter anderem den zusätzlichen Vorteil, Zugriff auf den HTTP-Antwort-Header zu haben, wie folgendes Listing zeigt.

import javax.ws.rs.core.Response;

@Path("hallo")
public class HalloController {

  @GET
  @Controller
  public Response sagHallo() {
    return Response.status(Response.Status.OK).entity("/hallo.jsp").build();
  }
  
}

Der fertige Controller für die MovieStore-Anwendung sieht wie folgt aus:

package de.javaakademie.moviestore;

import javax.inject.Inject;
import javax.mvc.Models;
import javax.mvc.annotation.Controller;
import javax.mvc.binding.BindingResult;
import javax.validation.Valid;
import javax.ws.rs.BeanParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

import de.javaakademie.moviestore.model.Movie;
import de.javaakademie.moviestore.service.MovieService;
import de.javaakademie.moviestore.web.CreateMovieForm;
import de.javaakademie.moviestore.web.Message;

@Path("/movies")
@Controller
public class MovieController {

    @Inject
    private Models models;

    @Inject
    private BindingResult bindingResult;

    @Inject
    private Message message;

    @Inject
    private MovieService movieService;

    @GET
    public String listMovies() {
        models.put("movies", movieService.getMovies());
        return "/movies.jsp";
    }

    @POST
    @Path("/create")
    public String createItem(@BeanParam @Valid CreateMovieForm form) {

        if (bindingResult.isFailed()) {
            message.addErrors(bindingResult.getAllMessages());
            models.put("form", form);
            return listMovies();
        }

        Movie newMovie = movieService.createMovie(form.getTitle(), form.getReleaseDate(), form.getRunningTime(), form.getCountry(), form.getGenre(), form.getActors(),
                form.getTrailerUrl());

        message.setInfo("Movie created: " + newMovie.getTitle());
        return "redirect:/movies";
    }

    @POST
    @Path("/delete")
    public String deleteMovie(@FormParam("id") Integer id) {
        movieService.deleteMovie(id);
        return "redirect:/movies";
    }
}

Die MovieService-Klasse enthält die Methoden zur eigentlichen Persistierung der Daten und die Model-Daten sind in simplen Beans abgelegt.

Abb.5 zeigt das Model und der nachfolgende Code die entsprechende Implementierung.

public class Movie {

    private Integer id;
    private String title;
    private LocalDate releaseDate;
    private Double runningTime;
    private String country;
    private Genre genre;
    private List<Actor> actors;
    private String trailerUrl;
    
    ...
}

public class Actor {

    private Integer id;
    private String name;
    private String surname;
    private LocalDate birthday;
    private String wikipediaUrl;
    
    ...
}

public enum Genre {
    ACTION, 
    ADVENTURE, 
    COMEDY, 
    ...
}

Der MovieStore kommt mit einer einzigen View aus. Ein Auszug ist im Folgenden zu sehen:

<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html lang="en">
<body>

    ...
    
    <c:if test="${not empty message.errors}">
        <c:forEach var="error" items="${message.errors}">
            <li>${mvc.encoders.html(error)}</li>
        </c:forEach>
    </c:if>


    <c:forEach var="movie" items="${movies}">
        <tr>
            <td class="text-left">
                <!-- For security reasons text should be escaped -->
                ${mvc.encoders.html(movie.title)}
            </td>
            <td class="text-center">
                ${mvc.encoders.html(movie.releaseDate)}</td>
            <td class="text-center">
                ${mvc.encoders.html(movie.runningTime)}</td>
            <td class="text-center">${mvc.encoders.html(movie.country)}</td>
            <td class="text-center">${mvc.encoders.html(movie.genre)}</td>
            <td class="text-left">
                <c:forEach var="actor" items="${movie.actors}">
                    ${actor.name} ${actor.surname} (${actor.age})<br />
                </c:forEach>
            </td>
            <td class="text-center">
                <form action="${mvc.basePath}/movies/delete" method="POST">
                    <input type="hidden" name="id" value="${movie.id}" /> <input
                        type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}" />
                    <button type="submit" class="btn btn-danger">Delete</button>
                </form>
            </td>
        </tr>
    </c:forEach>

    <form action="${mvc.basePath}/movies/create" method="POST">
         <input type="text" id="title" name="title" value="${form.title}" autofocus>
         ...
        <input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}" />
    </form>
    
    ...
            
</html>

Da als ViewEngine JSP gewählt wurde, wird die View durch eine JSP-Seite repräsentiert. Mittels Expression Language (EL) läßt sich komfortabel auf alle CDI-Beans zugreifen bzw. auf die Beans, die dem Model hinzugefügt wurden. Zum Beispiel kann auf alle Filme per ${movies} zugegriffen werden, wobei movies im Controller durch models.put("movies", movieService.getMovies()) dem Model hinzugefügt wurde. Zugriff auf alle aufgetretenen Fehler beim Abschicken des Formulars geschieht mit ${message.errors}, um direkt auf die CDI-Bean-Message zuzugreifen.

Eine Besonderheit fällt auf: Der Zugriff auf das mvc-Objekt (z. B. ${mvc.encoders.html(error)} und ${mvc.csrf.name}). Mittels mvc kann auf den MvcContext zugegriffen werden.

MvcContext

Der MvcContext liefert den Zugriff auf Encoders, Sicherheit- und Pfad-relevante Informationen. Folgendes Listing zeigt das Interface:

public interface MvcContext {
   
   Configuration getConfig();

   String getContextPath();

   String getApplicationPath();

   String getBasePath();

   Csrf getCsrf();

   Encoders getEncoders();
}

Die Instanz von MvcContext kann injiziert werden oder ist über die EL erreichbar.

@Inject
private MvcContext mvcContext;

Oder Zugriff über die EL:

<form action="${mvc.basePath}/movies/create" method="POST" class="form-inline">
<link rel="stylesheet" type="text/css" href="${mvc.contextPath}/moviestore.css">

Sicherheit

Der MvcContext liefert sicherheitsrelevante Möglichkeiten gegen die gängigsten Hackerangriffe. Zum Schutz gegen Cross-Site Scripting (XSS)-Attacken hilft das Encodieren von Daten.

${mvc.encoders.html(movie.country)}

Die andere große potentielle Bedrohung sind Cross-Site Request Forgery (CSRF)-Attacken. Ohne jetzt im Detail auf diese Art der Angriffe einzugehen, soll an dieser Stelle nur kurz der Lösungsansatz zur Gegenwehr gezeigt werden. Einzelheiten können der Beispielanwendung und der Spezifikation entnommen werden. Um den CSRF-Schutz zu aktivieren, muss zunächst die Application-Klasse erweitert werden:

@ApplicationPath("mvc")
public class MovieStoreApp extends Application {

    @Override
    public Map<String, Object> getProperties() {
        Map<String, Object> properties = new HashMap<>();

        properties.put(Csrf.CSRF_PROTECTION, Csrf.CsrfOptions.IMPLICIT);

        return properties;
    }

}

Die View bekommt ein Hidden-Input mit dem von der Applikation erzeugten Token:

<input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}" />

Die Methode des Controllers, welches die Formulardaten entgegennimmt, muss um die Annotation @CsrfValid ergänzt werden. Dies stellt sicher, dass die Methode nur bei korrektem Tokenwert ausgeführt wird.

@POST
@CsrfValid
@Path("/create")
public String createItem(@BeanParam @Valid CreateMovieForm form) {
    ...
}

Der Programmcode der kompletten Beispielanwendung steht unter GitHub zur Verfügung [3].

Besonderheiten zur aktuellen Referenzimplementierung

Wie eingangs erwähnt, ist die Referenzimplementierung aktuell noch fest mit der Jersey JAX-RS-Implementierung verknüpft. Wenn die Anwendung auf Application-Servern ausgeführt werden soll, wo eine andere JAX-RS-Implementierung verwendet wird, muss diese deaktiviert werden. Bei der Nutzung des MVC-Frameworks auf Webservern wie z. B. Tomcat oder Jetty, ist darauf zu achten, dass auch die CDI-Bibliotheken mit deployed werden.

Die hier vorgestellte Beispielanwendung ist für das Deployen auf den WildFly ausgerichtet. Die entsprechend notwendigen Konfigurationen können dort in der pom.xml (Deaktivieren der RESTEasy-Implementierung im WildFly) und im /WEB-INF-Verzeichnis (jboss-all.xml, jboss-deployment-structure.xml) eingesehen werden [4].

Fazit

Mit MVC 1.0 kommt neben dem komponentenbasierten JSF-Framework ein zweites actionbasiertes Framework hinzu. Der JSF-Ansatz mit der weitgehenden Abstraktion von HTTP-Requests-Responses und HTML, vereinfacht viele Dinge und macht das Entwicklerleben an der ein oder anderen Stelle sicher einfacher und komfortabler. Der actionbasierte Ansatz hingegen gibt wieder viel Kontrolle an den Entwickler zurück und wird gerade den Bedürfnissen moderner Webanwendungen, die z. B. Frontendframeworks wie AngularJS oder nur HTML5 mit JavaScript kombinierend nutzen und über REST mit dem Backend kommunizieren, gerechter. Aber auch kleine, performante Webanwendungen sind ohne den Koloss JSF hier und da sicher besser unterwegs. Die klassische Enterprise-Anwendung in Großkonzernen hingegen wird aber sicherlich auch in Zukunft weiter mit dem JSF-Standard umgesetzt und das ist auch völlig in Ordnung. JSF und das neue MVC 1.0 sind zwei Frameworks, die sehr gut nebeneinander bestehen können und sich nicht in die Quere kommen müssen. Es ist aber gut, dass endlich ein actionbasiertes Framework den Weg in den Standard gefunden hat. Die Entscheidung, welcher Art von Framework der Vorzug gegeben wird, hängt letztendlich vom jeweiligen Projekt ab und sollte ohnehin nicht pauschal entschieden werden. Je nach Anforderung kann der eine oder der andere Ansatz die bessere Wahl sein.

Quellen
  1. JSR 371: Model-View-Controller (MVC 1.0) Specification
  2. Referenzimplementierung Ozark
  3. Moviestore-Code auf GitHub
  4. Java Akademie (Blog): Java EE 8 MVC (Ozark) mit WildFly und Tomcat
     

Autor

Guido Oelmann

Guido Oelmann arbeitet als freiberuflicher Softwarearchitekt, Berater und Trainer. Zu seinen Schwerpunkten gehören neben agilen Entwicklungsmethoden und Softwarearchitekturen, der Einsatz von Java-/Java-EE-Technologien in…
>> Weiterlesen
Kommentare (0)

Neuen Kommentar schreiben