Andreas Bruns

Softwareentwicklung für Oldenburg und Bremen

Leichtgewichtige Web-Apps mit dem Play-Framework

James Gosling gilt als Urvater von Java, eine der am weitesten verbreiteten Programmiersprachen (TIOBE-Index). Martin Odersky veröffentlicht 2004 die auf der Java-Plattform beruhende Programmiersprache Scala, die funktionale und objektorientierte Programmierung mit statischer Typisierung vereint. Wenn Martin Odersky ein Unternehmen gründet und sich von James Gosling beraten lässt, dann sind die Erwartungen hoch.

Das 2011 gegründete Unternehmen Typesafe entwickelt moderne Frameworks und Werkzeuge im Scala-Umfeld, wie Akka (Event-Driven Middleware), Play Framework (Web), sbt (Scala Build Tool). Inzwischen hat sich das Unternehmen zu LightBend umbenannt, entwickelt neue Produkte (z.B. Lagom Reactive Microservices Framework) und treibt die Standardisierung von Reactive Streams voran.

In einem Ecommerce-Projekt konnte ich das Play Framework in der Version 2.5 einsetzen, das sehr viel Spaß machte und leider manchmal nervte:

+ Nutzung von Java oder Scala als Programmiersprache möglich
+ Web-Framework nach MVC-Pattern (Model, View, Controller), ähnlich „Ruby on Rails“
+ sofortiges Nachladen von geänderten Programmdateien
+ Web-Templates beruhen auf Scala-Code und sind daher typsicher
+ alle Routen werden in einer Datei definiert
+ Routen werden kompiliert, sodass keine fehlerhaften Links möglich sind
+ Code-Completion auch in Web-Templates
+ durchgehende Verwendung von Non-Blocking-IO
+ baut nicht auf der Servlet-API von Java auf
+ Kommandozeilen-Befehle für Starten, Bauen, Testen, Deploy-Artefakte
+ Unterstützung für Filter, Mail, Persistierung
+ schlanke, klare, übersichtliche Architektur
+ einfache Dokumentation, ähnlich eines langen Tutorials
+ Beispiele für Unit- und Integrationstests
+ Unterstützung von Datenbank-Migrationen (Evolutions-Skript)

Scala-IDE (als Standalone oder Eclipse-Plugin) reagiert (teilweise) wegen IDE-Fehler nicht
– inkompatible API-Änderungen auch in Minor-Releases
– Dokumentation ist für vertiefte Recherche nicht ausreichend
– Lösungen aus Foren- und Stackoverflow-Recherchen sind schnell veraltet
– komplizierte Scala-Schnittstellen für Module (z.B. für Filter in Version 2.4)

Setup eines Play-Projekts

  • Play (Activator herunterladen und installieren)
  • Play-Projekt erstellen: activator new example-play-framework
    bruno$ activator new example-play-framework
    
    Fetching the latest list of templates...
    
    Browse the list of templates: http://typesafe.com/activator/templates
    Choose from these featured templates or enter a template name:
      1) minimal-akka-java-seed
      2) minimal-akka-scala-seed
      3) minimal-java
      4) minimal-scala
      5) play-java
      6) play-scala
    (hit tab to see a list of all templates)
    > play-java
    OK, application "example-play-framework" is being created using the "play-java" template.
    ....
    bruno$ cd example-play-framework
    
  • Aufbau eines Play-Projekts
  • bruno$ tree
    .
    ├── LICENSE
    ├── README
    ├── activator
    ├── activator-launch-1.3.7.jar
    ├── activator.bat
    ├── app
    │   ├── Filters.java
    │   ├── Module.java
    │   ├── controllers
    │   │   ├── AsyncController.java
    │   │   ├── CountController.java
    │   │   └── HomeController.java
    │   ├── filters
    │   │   └── ExampleFilter.java
    │   ├── services
    │   │   ├── ApplicationTimer.java
    │   │   ├── AtomicCounter.java
    │   │   └── Counter.java
    │   └── views
    │       ├── index.scala.html
    │       └── main.scala.html
    ├── build.sbt
    ├── conf
    │   ├── application.conf
    │   ├── logback.xml
    │   └── routes
    ├── project
    │   ├── build.properties
    │   └── plugins.sbt
    ├── public
    │   ├── images
    │   │   └── favicon.png
    │   ├── javascripts
    │   │   └── hello.js
    │   └── stylesheets
    │       └── main.css
    └── test
        ├── ApplicationTest.java
        └── IntegrationTest.java
    
  • Play-Projekt starten: activator run
  • => viele Java-Bibliotheken werden aus dem Internet geladen
  • Browser: http://localhost:9000

Entwicklungsumgebung aufsetzen

  • Play-UI im Browser starten: activator ui
  • IDEs können auf dem verwendeten SBT (Scala Buil Tool) aufbauen
  • Alternativ: IntelliJ- und Eclipse-Integration
  • => bei neuen Bibliotheken in Eclipse: activator eclipse
Play-Framework: Activator UI

Play-Framework: Activator UI

Play-Framework: IntelliJ

Play-Framework: IntelliJ

Datenbank anbinden

  • Ebean aktivieren:
  • project/plugins.sbt, build.sbt wie beschrieben anpassen
  • conf/appplication.conf erweitern um H2-Datenbank:
    ebean.default = ["models.*"]
    db {
      default.driver = org.h2.Driver
      default.url = "jdbc:h2:mem:play"
      default.username = sa
      default.password = ""
    }
    
  • Play starten activator run
  • Browser: http://localhost:9000
  • Datenbank-Tabellen anlegen: Button „Apply this script now !“
Play-Framework: DB-Migration mit Evolutions

Play-Framework: DB-Migration mit Evolutions

Model im Play-MVC User.java: Model, Validierung, Persistierung

  • Model-Klasse beinhaltet auch Validierung und Persistierung
  • Validierung durch Annotionen und validate()-Methode
  • Persistierung anhand JPA-Annotationen
  • Suchen, Erstellen und Löschen mit Ebean-Model
  • package models;
    
    import java.sql.Timestamp;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    import javax.persistence.Entity;
    import javax.persistence.Id;
    import javax.persistence.Table;
    import javax.persistence.Version;
    
    import play.Logger;
    import play.data.validation.Constraints.Required;
    import play.data.validation.ValidationError;
    
    import com.avaje.ebean.Model;
    import com.avaje.ebean.annotation.CreatedTimestamp;
    import com.avaje.ebean.annotation.UpdatedTimestamp;
    
    @Entity
    @Table(name = "Users")
    public class User extends Model {
    
    	private static final Logger.ALogger logger = Logger.of(User.class);
    
    	private static Find<Long, User> find = new Find<Long, User>() {
    	};
    
    	public static List<User> findAll() {
    		List<User> users = find.all();
    		return users;
    	}
    
    	public static User findByEmail(String email) {
    		User user = find.where().eq("email", email).findUnique();
    		return user;
    	}
    
    	public List<ValidationError> validate() {
    		List<ValidationError> errors = new ArrayList<ValidationError>();
    
    		User userWithEmail = findByEmail(email);
    		if (userWithEmail != null) {
    			logger.info("Email bereits vorhanden: " + email);
    			errors.add(new ValidationError("email", "Email bereits vorhanden."));
    		}
    
    		return errors.isEmpty() ? null : errors;
    	}
    
    	@Id
    	Long id;
    
    	@CreatedTimestamp
    	private Date createdAt;
    
    	@UpdatedTimestamp
    	private Date updatedAt;
    
    	@Version
    	private Timestamp version;
    
    	@Required
    	String email;
    
    	@Required
    	String name;
    
    	public User(String name) {
    		this.name = name;
    	}
    
    	public String getEmail() {
    		return email;
    	}
    
    	public void setEmail(String email) {
    		this.email = email;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    }
    

View im Play-MVC user.scala.html: Template

  • Template besteht aus Scala-Code und wird kompiliert
  • innerhalb des Templates können Helper-Klassen genutzt werden
  • @(users: List[User], userForm: Form[User])
    
    @main("Benutzer-Beispiel") {
    
    	<style>
    		dt, dd {
    		    display: inline;
    		}
    	</style>
    	
       	@if(flash.containsKey("success")){
    		<div id="globalMessageSuccess" class="alert alert-success">
    			@flash.get("success")
    		</div>
    	}
    
    	<h2>Benutzer</h2>
    	<table class="table table-striped">
    		<thead>
    			<tr>
    				<th>Name</th>
    				<th>Email</th>
    			</tr>
    		</thead>
    		<tbody>
    			@for(user <- users) {
    				<tr>
    					<td>@user.getName()</td>
    					<td>@user.getEmail()</td>
    				</tr>
    			}
    		</tbody>
    	</table>
    
    	<h2>Benutzer anlegen</h2>
    	
    	@if(userForm.hasGlobalErrors) {
    		<div class="form-group">
    			<div id="globalFormError" role="alert">@userForm.globalError.message</div>
    		</div>
    	}
    	
    	@helper.form(action = controllers.routes.UserController.create()) {
    		@helper.inputText(userForm("name"), '_label -> "Name", '_showConstraints -> false)
       		@helper.inputText(userForm("email"), '_label -> "Email", '_showConstraints -> false)  
    		<input type="submit" value="Benutzer erstellen">
    	}	
    }
    

Controller im Play-MVC UserController.java und routes: Controller, Form, Route

  • Routen werden in der Datei routes definiert
  • GET     /users      controllers.UserController.listUsers()
    POST    /users      controllers.UserController.create()
    
  • Controller befüllt Template mit den benötigten Daten
  • Controller erstellt dynamisch ein Form für Fomulare und validiert die Daten
  • package controllers;
    
    import java.util.List;
    
    import javax.inject.Inject;
    
    import models.User;
    import play.data.Form;
    import play.data.FormFactory;
    import play.mvc.Controller;
    import play.mvc.Result;
    
    public class UserController extends Controller {
    
    	@Inject
    	FormFactory formFactory;
    
    	public Result listUsers() {
    		List<User> allUsers = User.findAll();
    		Form<User> userForm = formFactory.form(User.class);
    		return ok(views.html.user.render(allUsers, userForm));
    	}
    
    	public Result create() {
    		Form<User> userForm = formFactory.form(User.class).bindFromRequest();
    		if (userForm.hasErrors()) {
    			List<User> allUsers = User.findAll();
    			return ok(views.html.user.render(allUsers, userForm));
    		}
    		User user = userForm.get();
    		user.save();
    		flash("success", String.format("User %s erfolgreich erstellt.", user.getName()));
    		return redirect(routes.UserController.listUsers());
    	}
    }
    
Play-Framework: Beispiel-Anwendung

Play-Framework: Beispiel-Anwendung

Entwicklung und Produktion

Das Play-Framework bietet noch viele hilfreiche Funktionen für die Entwicklung und den Betrieb einer Play-Anwendung:

  • Nutzung einer Entwicklung-, Test- und Produktiv-Umgebung
  • einfache Anbindung von anderen Datenbanken
  • Verwendung von Evolutions-Skripte für Datenbank-Migration
  • diverse Security-Einstellungen
  • Erstellung einer Produktiv-Distribution mit activator dist
  • vieles, vieles mehr …

Fazit

Bei Github kann man das Beispiel herunterladen: https://github.com/me4bruno/blog-examples -> example-play-framework. Das Arbeiten mit dem Play-Framework macht meistens Spaß. Falls allerdings ein Fehler auftritt, dann ist die Fehlermeldung oftmals nicht besonders sprechend. Ein Trial und Error-Vorgehen in kleinen Schritten ist dann nötig und manchmal fühlt es sich auch nach SODD (Stack Overflow Driven Development) an 😉

Kommentare sind geschlossen.