Über unsMediaKontaktImpressum
Dirk Weil 23. Februar 2016

Java EE – Anwendungsentwicklung ohne Ballast

Wie man mit Java EE schnell und effizient Software entwickeln kann

Unternehmensanwendungen sollen das "Business" eines Unternehmens möglichst gut unterstützen, wozu neben Integrationsmöglichkeiten mit anderen Anwendungen und der Ressourcenverfügbarkeit auch eine hohe Entwicklungs- und Anpassungsgeschwindigkeit notwendig sind. Durch die Standardisierung und die technische Breite der Plattform Java EE ist ersteres sicher gegeben. Aber kann man im Java Full Stack auch schnell und effizient Software entwickeln und anpassen? Vor Jahren hätte man diese Frage sicher verneint, war doch J2EE einfach an vielen Stellen sehr technisch und zu komplex. Spätestens seit der Version 5 der Plattform und damit erst recht in Java EE 7 ist die technische Komplexität aber in den Hintergrund getreten. Die Plattform kommt den Softwareentwicklern deutlich entgegen, ist einfacher und eleganter geworden.

Ich möchte in diesem Artikel zeigen, dass man Java-EE-Anwendungen schnell und einfach entwickeln kann – ohne den technischen Ballast der früheren Versionen.

Als Arbeitsbeispiel nutze ich einen imaginären Teil einer Anwendung, die ein Veranstalter z. B. für die Verwaltung von Konferenzen nutzen könnte: Da werden Vorträge – Talks – gehalten, zu denen es Stammdaten wie Titel, Datum, Typ, Referenten etc. gibt. Diese einfachen Geschäftsobjekte sollen für die weitere Betrachtung genügen. Eine entsprechende Klasse könnte so aussehen:

public class Talk {
  Private Integer      id;
  private String       title;
  private List<String> speakers;
  private TalkType     talkType;
  private Date         start;
  …

TalkType ist dabei ein Aufzählungstyp für die verschiedenen Vortragsarten:

public enum TalkType {
  KEYNOTE, SESSION, WORKSHOP;

Persistente Daten

Diese Klassen sind zunächst rein fachlich entwickelt worden, also ohne Berücksichtigung der Tatsache, dass die Daten persistent gemacht werden sollen. Die Persistenz-Metadaten lassen sich nun in Form einiger Annotationen aus der JPA-Spezifikation ergänzen:

@Entity
public class Talk {
  @Id @GeneratedValue
  Private Integer      id;
  private String       title;
  @ElementCollection
  private List<String> speakers;
  @Enumerated(EnumType.STRING);
  private TalkType     talkType;
  @Temporal(TemporalType.TIMESTAMP)
  private Date         start;
  …

@Entity markiert die Klasse als persistente Klasse. @Id bestimmt das identifizierende Attribut, das auf den Primärschlüssel in der Datenbank abgebildet wird, wobei @GeneratedValue dafür sorgt, dass für neue Einträge automatisch eindeutige Ids erzeugt werden. @ElementCollection dient der Abbildung einer Collection auf eine Zusatztabelle. @Enumerated wählt die Abbildung einer Aufzählung – interne Ordnungszahl oder ausgeschriebener Wertename. @Temporal schließlich bestimmt, ob von einem Date-Wert nur das Datum, nur die Uhrzeit oder der komplette Wert in der Datenbank abgelegt wird.
Generell gilt das Prinzip Configuration by Exception, d. h. es müssen nur dort Metadaten explizit angegeben werden, für die die Vorgabewerte nicht gewünscht sind. So ergeben sich Tabellen- und Spaltennamen aus den einfachen Klassennamen und Attributnamen. Abweichende Namen können mit @Table auf der Klasse bzw. @Column auf den Attributen angegeben werden.

Die Verbindung zur Datenbank erfolgt in einem Deskriptor namens persistence.xml im META-INF-Verzeichnis der Anwendung:

<persistence … version="2.1">
  <persistence-unit name="default">
    <jta-data-source>datasource/conference</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>

Bei den drei Pünktchen habe ich ein wenig geschummelt: Dort steht eine recht lange Schema-Angabe, die man aber nicht wirklich tippen muss, da der Deskriptor von allen gängigen IDEs inklusive Schema und Namespaces generiert werden kann.

Die letzte oben gezeigte Zeile sorgt dafür, dass der Container zur Laufzeit automatisch alle mit @Entity annotierten Klassen berücksichtigt. Alternativ könnte man die gewünschten Klassen auch einzeln angeben.

In der Zeile davor wird die Zieldatenbank referenziert, und zwar indirekt durch den Namen einer Datasource, die wiederum im Application Server mit den konkreten Verbindungsparametern der Datenbank verknüpft ist. Durch diese Indirektion wird die Anwendung frei von konkreten Datenbank-URLs, Usernamen und Passwörtern gehalten. Sie "weiß" also nicht, ob sie bspw. mit einer Oracle-, DB2- oder Postgres-DB arbeitet, und ob dies eine Entwicklungs-, Test- oder Produktions-DB ist! Die Konfiguration der Datasource im Server ist nicht standardisiert und findet im Allgemeinen mit Hilfe der jeweiligen Administrationswerkzeuge statt.

Der Deskriptor enthält die Angaben in einem Element namens persistence-unit. Solche Persistence Units darf es mehrere geben, natürlich mit eindeutigen Namen versehen.

Datenbank-Zugriff mit Java EE

Für die Arbeit mit den persistenten Daten wird nun ein Entity Manager benötigt. Er bietet u. a. die CRUD-Methoden an sowie Möglichkeiten, komplexere Queries zu formulieren. Seine Verwendung lässt sich recht schön in DB-Zugriffsklassen verkapseln, z. B. so:

@Transactional
public class TalkRepository {
  @PersistenceContext(unitName="default")
  EntityManager    entityManager;

  public void persist(Talk entity){
    this.entityManager.persist(entity);
  }

  public void remove(Talk entity) {
    this.entityManager.remove(entity);
  }

  public Talk findById(Integer id) {
    return this.entityManager.find(Talk.class, id);
  }

  public List<E> findAll() {
    return this.entityManager
     .createQuery("select x from Talk x", Talk.class)
     .getResultList();
  }
      …

Die ersten drei gezeigten Methoden sind trivial durch Delegation an Methoden aus EntityManager realisiert. Ich erspare Ihnen hier die Erklärung dieser Methoden – Sie können JavaDoc vermutlich genauso gut lesen wie ich.

Ich möchte Ihr Augemerk aber auf die Quelle des genutzten Entity Managers lenken: @PersistenceContext ist eine Injektions-Annotation, die dafür sorgt, dass in die damit annotierte Instanzvariable eine EntityManager-Instanz geschrieben wird, die auf der Persistence Unit arbeitet, deren Name als Parameter übergeben wurde. Der Entity Manager ist zudem transaktionsgebunden. Dies bedeutet einerseits, dass ein Commit oder Rollback im Transaktionssystem des Servers zum entsprechenden Abschluss der Transaktion im Entity Manager führt. Andererseits referenzieren alle mit @PersistenceContext annotierten Variablen innerhalb einer Transaktion denselben Entity Manager, während für eine andere Transaktion ein separater Entity Manager genutzt wird. Anders ausgedrückt: jeder User-Request arbeitet i. d. R. mit einer dedizierten Transaktion und hat dadurch auch einen "eigenen" Entity Manager.

Und wo erfolgt die Transaktionssteuerung? Sie ahnen es mittlerweile schon: die Annotation @Transactional ist dafür zuständig. Sie kann für Methoden oder ganze Klassen genutzt werden. Wird eine derart annotierte Methode später aufgerufen und ist noch keine Transaktion aktiv, so wird vor der Methodenausführung eine neue Transaktion begonnen. Nach dem Verlassen der Methode mit return oder Exception erfolgt dann ein Commit bzw. Rollback. Ist beim Aufruf der Methode schon eine aktive Transaktion vorhanden, wird diese einfach genutzt. So können sich mehrere transaktionale Methoden aufrufen und in einem gemeinsamen Transaktionskontext ablaufen. Mit Hilfe von Parametern von @Transactional kann das beschriebene Verfahren abgewandelt werden, um bspw. stets eine neue Transaktion zu beginnen oder bestimmte Exceptions nicht ein Rollback auslösen zu lassen.

CDI – das Schweizer Taschenmesser für Anwendungskomponenten

Bei aller Begeisterung für die deklarative Transaktionssteuerung und Injektionen muss man sich aber vor Augen halten, dass Annotationen zunächst mal nichts tun: sie sind wie gelbe Klebezettel am jeweiligen Programmcode – und der Zettel "Milch kaufen" an Ihrem Kühlschrank bewirkt auch nicht auf magische Weise, dass die Grundsubstanz für den Kakao am Morgen auf Sie wartet. Es muss also etwas im Server geben, das die Annotationen liest und danach handelt. Für die zuletzt beschriebenen Dinge ist dieses Etwas der CDI-Container. CDI ist seit Java EE 6 Teil der Plattform und greift die Idee der komponentenbasierten Softwareentwicklung auf, die schon seit Anbeginn mit EJB adressiert wurde: Fachliche und technische Module werden mit definierten Schnittstellen bereitgestellt, so dass der Container sie instanzieren und miteinander verknüpfen kann. CDI ist im Vergleich zu EJB der Versionen bis 2.1 deutlich aufgeräumter, weniger technisch und damit erheblich einfacher zu beherrschen. In der Weiterentwicklung der Plattform kann man nun beobachten, dass CDI immer weiter Raum greift und als Injektionscontainer für andere Teile der Gesamtspezifikation genutzt wird.

Wird eine Klasse wie TalkRepository vom CDI-Container instanziiert, so sorgt er für die Befüllung der Instanzvariablen, die mit Injections-Annotationen versehen sind. Zudem umhüllt der Container transaktionale Methoden mit dem zur Transaktionssteuerung notwendigen Programmcode. Technisch geschieht dies durch Interzeptoren, deren Beschreibung hier aber den Rahmen sprengen würde.

Damit eine Klasse vom CDI-Container instanziiert werden kann, muss sie eine CDI-Bean sein. Und das wird sie einfach dadurch, dass die Anwendung einen Deskriptor namens beans.xml enthält. Der Deskriptor wird im META-INF-Verzeichnis im Classpath der Anwendung platziert. Bei Webanwendungen darf er auch im WEB-INF-Verzeichnis stehen:

<beans … bean-discovery-mode="all" version="1.1">
      …

Würde eine CDI-Bean direkt instanziert werden, geschehen die beschriebenen Komfortfunktionen nicht: Injektionen werden nicht ausgeführt und deklarative Transaktionssteuerung steht nicht zur Verfügung. Für eine CDI-Bean darf also nie new verwendet werden. Stattdessen muss der Container mit der Bereitstellung der Instanz beauftragt werden, was durch Injektion in eine andere CDI-Bean geschieht:

@Named
@RequestScoped
public class TalkPresenter {
  @Inject
  TalkRepository talkRepository;

  List<Talk>     talks;

  @PostConstruct
  void postConstruct() {
    this.talks = this.talkRepository.findAll();
  }

  public List<Talk> getTalks() {
    return this.talks;
  }
      …

@Inject ist das Mittel zur Verknüpfung von CDI-Beans untereinander. Bei der Instanziierung eines TalkPresenter-Objektes durch den Container wird die Instanzvariable mit einem TalkRepository-Objekt gefüllt. Die mit @PostConstruct annotierte Methode dient der Initialisierung des neuen Objektes, nachdem alle Injektionen ausgeführt sind. Im Beispiel wird das dann bereits vorhandene Repository-Objekt zum Einlesen der Talks verwendet.

Serverseitige Präsentation mit JSF

Die gezeigte Klasse ist schon Teil der serverseitigen Präsentationsschicht, die im Beispiel mit JavaServer Faces realisiert wird. @Named gibt der CDI-Bean einen Namen, der als Referenz aus Umgebungen genutzt werden kann, die keine Java-Typen kennen. Das gilt bspw. für JSF, wo die Webseiten mit Hilfe sog. Facelets beschrieben werden. Wie weiter unten gezeigt wird, bestehen diese aus HTML-ähnlichem Text, in dem Klassennamen nicht bekannt sind, der aber über eine Expression Language Zugriff zu Bean-Namen erlaubt. Der Bean-Name ist im Default der einfache Klassenname mit kleinem Anfangsbuchstaben, kann aber auch als Parameter von @Named angegeben werden.

Ein Facelet zur Anzeige aller Talks könnte nun so aussehen:

<!DOCTYPE html>
<html xmlns=http://www.w3.org/1999/xhtml
          xmlns:f="http://java.sun.com/jsf/core" 
      xmlns:h="http://java.sun.com/jsf/html">

<body>
  <h2>Talks</h2>

  <h:dataTable var="t" value="#{talkPresenter.talks}">
    <h:column>
      <h:outputText value="#{t.title}" />
    </h:column>
    <h:column>
      <h:outputText value="#{t.start}" />
    </h:column>
      …
  </h:dataTable>
</body>
</html>

Auf den ersten Blick sieht das aus wie eine HTML-Datei. Man bemerkt aber schnell, dass der Sprachumfang um weitere Tags ergänzt wurde, die mit Hilfe der Namespace-Deklarationen im Start-Tag definiert werden.

Zum einen fallen die Ausdrücke der Form #{name.property} auf. Hiermit werden CDI-Beans und ihre Properties verwendet. Der erste Teil des Ausdrucks referenziert eine CDI-Bean über ihren mit @Named zugewiesenen Namen. Die durch Punkte abgetrennten weiteren Ausdrucksbestandteile zeigen auf Properties der Beans. Properties sind im Sinne der Java-Beans-Spezifikation öffentliche Getter- und Setter-Methoden, wobei das aufgeführte Beispiel nur lesend agiert, also nur die Getter verwendet. So führt #{talkPresenter.talks} zur Laufzeit zum Aufruf der Methode getTalks in einer TalkPresenter-Instanz.

Zum anderen ist das Konstrukt h:dataTable/h:column erklärungsbedürftig: h:dataTable ähnelt einer Schleife, in der die mittels var angegebene Laufvariable in jedem Schleifendurchlauf einen Wert aus der mit value angegebenen Menge annimmt. Der Inhalt des h:dataTable-Tags erzeugt für jeden Schleifendurchlauf eine Zeile einer HTML-Tabelle, wobei jedes h:column-Tag eine Spalte darin beschreibt. So werden also mit dem o. a. Code alle Talks tabellarisch angezeigt.

Wird dieser Text in der Datei talk.xhtml im Wurzelverzeichnis der Webanwendung abgelegt, so kann die Seite im Browser unter http:/ /localhost:8080/javaee-workshop/talk.xhtml angezeigt werden. Damit der Dateiinhalt nicht unverändert zum Browser geliefert wird, sondern die beschriebenen Zusatztags und Ausdrücke ausgewertet werden, muss schließlich noch dafür gesorgt werden, dass alle Requests für xhtml-Files durch den JSF-Teil des Servers verarbeitet werden. Dazu muss lediglich ein vordefiniertes Servlet registriert werden, und zwar im Deskriptor web.xml im WEB-INF-Verzeichnis der Anwendung:

<?xml version="1.0" encoding="UTF-8"?>
<web-app … version="3.1">
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>
  …

Zusammenfassung und Fazit

In diesem Artikel habe ich Ihnen einen "Durchstich" von der Web-Oberfläche über die Service-Schicht bis in die Datenbank gezeigt. Das gelang mit gerade mal vier Java-Klassen, einem Facelet und drei Deskriptoren. Natürlich ist die Demo-Anwendung bewusst sehr einfach gehalten. Zudem nutzt sie bei Weitem nicht alle verfügbaren Möglichkeiten. Im "richtigen Leben" kommt also schon noch einiges an Umfang und benötigtem Wissen hinzu. Dennoch denke ich, dass man den Zusatz "ohne Ballast" im Titel durchaus vertreten kann – insbesondere im Vergleich zu den früheren Versionen der J2EE, die dieses Attribut sicher nicht verdienten. Heute ermöglicht Java EE eine effiziente und schnelle Entwicklung von Software. Durch die umfangreiche Standardisierung lassen sich Vorgehensweisen und Know-how projekt- und teamübergreifend nutzen. Und nicht zuletzt findet man deshalb auch eine sehr aktive Community vor mit Beiträgen in Blogs, User Groups oder Konferenzen, die man in den eigenen Projekten nutzen kann.

Das Beispielprojekt können Sie von [1] beziehen. Dort finden Sie auch eine Anleitung zum Bauen der Software und zum Deployen auf einen Application Server. Und bei Fragen wenden Sie sich gerne an mich!

Autor

Dirk Weil

Dirk Weil ist seit 1998 als Berater im Bereich Java tätig. Als Geschäftsführer der GEDOPLAN GmbH ist er für die Konzeption und Realisierung von Informationssystemen auf Basis von Java EE verantwortlich.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben