Java Webprogrammierung mit Spring und Maven (Teil 2)

Ausgehend vom ersten Teil der Serie soll nun noch Persistenz, also die dauerhafte Speicherung von Daten, hinzukommen.

Das Spring-Framework bietet hierzu ein Modul (spring-orm) an. Spring-orm bietet die Integration von verschiedenen ORM Produkten, wie Hibernate, JPA, JDO und iBATIS SQL Maps, an. Dieses Beispiel verwendet Hibernate. Die Auswahl der unterstützten Datenbanksystemen ist bei Hibernate durchaus ansehnlich. Hier wird die weit verbreitete  MySQL in Version 5.5 eingesetzt.

Voraussetzung

Wie oben angesprochen, setzt dieses Beispiel eine existierende Installation von MySQL voraus. Wichtig ist ein leeres Schema, welches sonst ungenutzt ist, ein Nutzer samt Passwort, welcher in diesem Schema alle Berechtigungen hat. Dies ist wichtig, um während der Entwicklung die Struktur der Tabellen anpassen zu können.

pom.xml

In der pom.xml werden die Abhängigkeiten für die neuen Bibliotheken hinzugefügt.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.6.Final</version>
</dependency>
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.21</version>
</dependency>

Weitere Änderungen sind nicht erforderlich.

Java Entity Klasse Counter

In Entitäten werden die zu persistierenden Daten auf Java-Seite abgelegt. Es handelt sich dabei um POJOs, die über Annotations zu Entities werden.

package de.lusiardi.testprojekt.entity;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@NamedQueries({
    @NamedQuery(name="counter.findByPage",
        query="from Counter c where c.page=:page")
})
public class Counter implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false, unique = true)
    private String page;

    @Column(nullable = false, name = "counter")
    private int count;

    // notwendige Getter und Setter
}

Hier werden folgende Annotations verwendet:

  • @Entity: Legt fest, das diese Klasse eine Entität ist.
  • @Id: Legt den Primärschlüssel einer Entität fest, dieser dient auch für Zugriffe über die Id.
  • @GeneratedValue: Legt für die Id fest, dass sie automatisch erzeugt wird. Dies entspricht dem Zusatz AUTO_INCREMENT in SQL.
  • @Column: Erlaubt die verfeinerte Definition der Spalten in der resultierenden Datenbanktabelle. Hierbei legt nullable fest, ob zwingend ein Wert angeben sein muss oder nicht. unique legt fest, dass jeder Wert in dieser Spalte eindeutig sein muss. Über name lassen sich die Spalten umbenennen, beispielsweise wenn der Java-Name des Feldes mit einem HQL/SQL-Keyword kollidiert. Dies ist bei count der Fall.
  • @NamedQueries und @NamedQuery: Diese Annotationen erlauben es, benannte Queries direkt bei der Entität anzugeben. Dies hat den Vorteil, dass Daten und Abfragen in einer Datei zusammen liegen und nicht wild über den Code verteilt sind. Die Namen der Queries sind global eindeutig zu halten.
Weitere Informationen zu den einzelnen Annotationen findet man in den verlinkten APIs und unter der Dokumentation zu javax.persistence.

Java Dao Klasse CounterDao

Data Access Objects oder kurz DAOs übernehmen die eigentliche Kommunikation mit der Datenquelle. In diesem Fall über Hibernate. Auch hier werden mit Annotationen zusätzliche Informationen transportiert.

package de.lusiardi.testprojekt.dao;

import de.lusiardi.testprojekt.entity.Counter;
import org.hibernate.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class CounterDao {

    @Autowired
    SessionFactory sessionFactory;

    @Transactional(readOnly = true)
    public Counter findByPage(String page) {
        Query q = getCurrentSession().
            getNamedQuery("counter.findByPage");
        q.setString("page", page);
        return (Counter) q.uniqueResult();
    }

    @Transactional
    public void save(final Counter entity) {
        getCurrentSession().persist(entity);
    }

    @Transactional
    public void update(final Counter entity) {
        getCurrentSession().merge(entity);
    }

    @Transactional
    public void delete(final Counter entity) {
        getCurrentSession().delete(entity);
    }

    private Session getCurrentSession() throws HibernateException {
        return sessionFactory.getCurrentSession();
    }
}

Der Zugriff auf Datenbanken geschieht in Hibernate über Session-Objekte. Diese ermöglichen das Speichern (persist), Aktualisieren (merge) und Löschen (delete) von Entitäten. Über getNamedQuery können dieses abgerufen und eingesetzt werden.

Wichtigste Annotation in dieser Klasse ist @Autowired. Autowired dient der Dependeny Injection, einer wichtigen Technik im Spring Framework. Das Feld sessionFactory ist deshalb auch protected um Spring die Möglichkeit zu geben, die in der spring-servlet.xml definierte SessionFactory einzufügen. Dies geschieht in diesem Fall anhand des Typs des Feldes.

@Repository markiert das DAO für Spring als solches und legt fest, das es sich um eine Bean handelt, die über ihren Typ ansprechbar ist.

@Transactional legt fest, welche Methoden innerhalb einer Datenbanktransaktion ablaufen. Das Attribute readOnly gibt hier Hilfestellungen bei Optimierungen für rein lesenende Transaktionen.

Änderungen an der Controller Klasse

Nun wird der Controller aus Teil 1 so erweitert, das er für die angezeigte View zählt, wie oft diese aufgerufen wurde.

package de.lusiardi.testprojekt.controller;

import de.lusiardi.testprojekt.dao.CounterDao;
import de.lusiardi.testprojekt.entity.Counter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MainController {

    @Autowired
    CounterDao counterDao;

    @RequestMapping("view")
    public ModelAndView getTest() {
        Counter c = counterDao.findByPage("test");
        if (c == null) {
            c = new Counter();
            c.setPage("test");
            c.setCount(0);
            counterDao.save(c);
        }
        int count = c.getCount() + 1;
        c.setCount(count);
        counterDao.update(c);

        return new ModelAndView("view", "key", "Page view number: "
            + count);
    }
}

Auch hier wird über  @Autowired eine Abhängigkeit, hier das DAO, automatisch eingefügt. In der Methode getTest wird nun dieses DAO verwendet um die Anzahl der Pageviews zu zählen. Dabei muss der Fall abgedeckt sein, dass für die gewünschte Seite noch kein Datenbankeintrag besteht.

spring-servlet.xml

Damit Instanzen der SessionFactory und des CounterDao jeweils automatisch eingefügt werden können, muss die spring-servlet.xml entsprechend angepasst werden. Dazu fügt man folgende Definition hinzu:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location" 
        value="/WEB-INF/config/jdbc/jdbc.properties"></property>
</bean>

<bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" 
        value="${jdbc.driverClassName}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>  
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource"></property>
    <property name="packagesToScan" 
         value="de.lusiardi.testprojekt" ></property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${jdbc.dialect}</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.hbm2ddl.auto">create-drop</prop> 
        </props>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="dataSource" ref="myDataSource" />
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:annotation-driven transaction-manager="txManager" />

Zunächst wird eine Instanz von PropertyPlaceholderConfigurer definiert, die es ermöglicht Konfigurationsdaten für die Datenbank in einer Property-Datei abzulegen. Die Bean mit der Id myDataSource ist eine Instanz von DriverManagerDataSource und wird über Werte aus der Property-Datei passend konfiguriert. Die SessionFactory, welche  in das DOA injiziert wurde, wird ebenfalls hier definiert. Sie bekommt selbst die Bean namens myDataSource eigefügt und wird ebenfalls passend konfiguriert. Wichtig hier bei ist die Eigentschaft hibernate.hbm2ddl.auto und der Wert create-drop. Mit diesen Einstellungen wird bei jedem Start der Applikation die Datenbank neu angelegt und ist anschließend leer!  Diese Einstellung ist nur für die Entwicklung sinnvoll und sollte im Produktivbetrieb auf validate umgestellt werden.

web-inf/config/jdbc.properties

In dieser Properties-Datei werden die ensprechenden Schlüssel-Wert-Paare zur Konfiguration der Datenbank abgelegt.

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=URL wie jdbc:mysql://server:3306/schema eintragen
jdbc.username=Nutzernamen eintragen
jdbc.password=Passwort eintragen
jdbc.dialect=org.hibernate.dialect.MySQL5Dialect

Die Konfiguration ist für MySQL 5 ausgelegt und muss noch mit entsprechenden Werten für Verbindungs-URL, Nutzername und Passwort versehen werden.

Übersetzen und Deployen

Dieser Schritt verläuft analog zu Teil 1, nur wird die entstehende war-Datei durch die zusätzlichen Abhängigkeiten größer.

Résumé

Die Integration einer Datenbankanbindung in ein Demoprojekt setzt bereits einiges an Hintergrundwissen voraus und macht deutlich, das man mit anderen Techniken sicher schneller ans Ziel kommt. Allerdings sorgt die deutliche Trennung der Schichten und das Verwenden etablierter Techniken für wartbaren und übersichtlichen Code.

Update 26. Juli 2013

Die Quellen zu diesem Artikel kann man nun auch bei GitHub herunterladen oder forken.

Java Webprogrammierung mit Spring und Maven (Teil 1)

20. September 2012:
Update auf Spring 3.1.2-RELEASE

Dieser Artikel führt knapp in die Webprogrammierung mit Java ein. Wir setzen dabei auf einen Model-View-Controller-Ansatz unterstützt durch das Spring-Framework, speziell das Web MVC Framework.

Dabei baut dieser Artikel auf den Grundlagenartikeln zu Maven auf. Die ersten beiden Artikel der sind hier verlinkt: Teil 1, Teil 2. Alle Artikel zum Thema Maven finden sich in der entsprechenden Kategorie.

Nachfolgend sieht man die Verzeichnisstruktur und die Dateien eines Maven-Projektes, welches eine simple Webapplikation auf Spring-Basis darstellt.

projekt                                             ( 1)
 + pom.xml                                          ( 2)
 + src                                              (  )
 |  + main                                          (  )
 |  |  + java                                       (  )
 |  |  |  + de/lusiardi/testprojekt/controller/     (  )
 |  |  |     + MainController.java                  ( 3)
 |  |  + webapp                                     ( 4)
 |  |     + META-INF                                (  )
 |  |     |  + context.xml                          ( 5)
 |  |     + WEB-INF                                 (  )
 |  |     |  + web.xml                              ( 6)
 |  |     |  + spring-servlet.xml                   ( 7)
 |  |     + views                                   ( 8)
 |  |     |  + view.jsp                             ( 9)
 |  |     + index.jsp                               (10)
 |  + test                                          (  )
 |     + java                                       (  )
 + target                                           (  )

Nun sollen die einzelnen Teile des Projektes beschrieben werden.

1) Wurzelverzeichnis des Projekts

Hier beginnt der ganze Spass und alles, was man zum bauen benötigt liegt in diesem Verzeichnis.

2) pom.xml

Die aus Teil 1 und Teil 2 bekannte Verwaltungsdatei für Maven. Die klassische POM für Abhängigkeiten und Build-Prozess.

<project 
    xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
            http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>de.lusiardi</groupId>
    <artifactId>testprojekt</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>testprojekt</name>

    <properties>
        <project.build.sourceEncoding>
            UTF-8
        </project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>3.1.2.RELEASE</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Wichtig sind hier lediglich die beiden Dependencies für spring-webmvc und javax.servlet-api. provided bedeutet in diesem Zusammenhang, dass diese Dependency nicht mit in das entstehende Artefakt gepackt werden soll, da sie vom Servlet Container bereit gestellt wird. Über das maven-compiler-plugin legt man die verwendeten Java-Versionen fest, da sonst Annotationen nicht unterstützt werden.

3) Controller Java Klasse

Das eingesetzte Spring-Framework Web MVC ermöglicht durch einfache Annotationen das Ausweisen von Controllern.

package de.lusiardi.testprojekt.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MainController {

    @RequestMapping("view")
    public ModelAndView getTest() {
        return new ModelAndView("view", "key", "value");
    }
}

In diesem Beispiel-Controller (ausgezeichnet durch die @Controller Annotation) existiert ein einzelnes Mapping für Anfragen (@RequestMapping Annotation). Die Funktion getTest wird für alle Anfragen an die URL

http://localhost:8080/testprojekt-1.0-SNAPSHOT/main/view.html

aufgerufen. Über die Rückgabe der ModelAndView-Instanz wird die View mit Namen view aufgerufen und das Modell an die View. Das Modell ist in diesem Fall ein String, der unter „key“ zugreifbar ist.

4) Konfigurationen für Webapplikation

Webapplikationen werden in der Java-Welt über war-Dateien verteilt. In der Spezifikation ist die Struktur der Daten festgelegt. Der Inhalt des Verzeichnisses webapp wird in die war-Datei direkt eingefügt.

5) context.xml

Dies ist der Tomcat Context Descriptor und legt den ersten Teil der URL nach dem Server fest. Dieser muss eindeutig sein.

<?xml version="1.0" encoding="UTF-8"?>
<Context path="/HelloMaven"/>

6) web.xml

In der Datei web.xml können einige grundlegende Einstellungen der Web-Applikation getätigt werden.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <display-name>Spring3MVC</display-name>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>   

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>

</web-app>

In diesem Beispiel legen wir den Anzeigenamen (display-name) der Applikation fest. Die welcome-file-list gibt an welche Dateien verwendet werden sollen, wenn Anfragen direkt auf ein Verzeichnis gehen. Als nächstes wird ein servlet vom Spring-Typ DispatcherServlet und ein entsprechendes servlet-mapping auf alle html-Dateien.

7) spring-servlet.xml

Der Name der Konfigurationsdatei spring-servlet.xml ergibt sich aus dem Namen des Servlets in der web.xml. Es existiert also für jedes Servlet eine entsprechende Datei.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping" />

    <context:component-scan base-package="de.lusiardi.testprojekt" />

    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

    <mvc:annotation-driven />

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/views/" />
        <property name="suffix" value=".jsp" />
    </bean>

</beans>

Durch die Definition eine Bean vom Spring-Typ ControllerClassNameHandlerMapping wird festgelegt, das per Konvention aus MainController der Pfad /main/ wird.

Über context:component-scan aktiviert man die Unterstützung für die verwendeten Annotationen im angegeben Package. Die ermöglicht in Kombination mit mvc:annotation-driven die Einführung des Controllers rein über Annotationen.

Die Bean viewResolver vom Typ InternalResourceViewResolver ermöglicht es, Views wie im Controller getan, einfach nur über den Namen anzusprechen. Hier sieht man die Möglichkeit, Beans mit speziellen Eigenschaften (Properties) zu versorgen. Prefix und Suffix kompletieren die Namen der Views und ergeben den Dateinamen.

8) Ordner für die Views

In diesem Ordner werden die Views gespeichert. Der Name des Ordners ist wie in der spring-servlet.xml (7) beim ViewResolver als Prefix definiert.

9) view.jsp

Views dienen zur tatsächlichen Darstellung des Models in Model-View-Controler. Hier sind die VIews in JSP geschrieben.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <title>Lusiardi.de - Tutorial</title>
    </head>
    <body>
        <a href="../">${key}</a><br />
    </body>
</html>

Hier wird über ${key} auf das Model (den einzelnen String) zugegriffen, und dieser ausgegeben.

10) index.jsp

Die in der web.xml (6) festgelegte Welcome-Page

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <title>Lusiardi.de - Tutorial</title>
    </head>
    <body>
    <a href="main/view.html">Controller</a><br />
    </body>
</html>

Bauen

In diesem Fall ist das Ergebnis der Bemühungen keine JAR-Datei, sondern eine WAR-Datei. WAR steht für WebARchive. Aufgrund des in der pom.xml festegelegten packaging war, wird durch folgenden Aufruf die fertige WAR-Datei erzeugt.

mvn package

Dabei entsteht im target-Verzeichnis die Datei testprojekt-1.0-SNAPSHOT.war. Diese kann dann in einem Tomcat oder anderen Servlet-Container ausgeführt werden.

Résumé

Java und Webentwicklung haben höhere Einstiegshürden als andere Techniken. Das stimmt zweifelsfrei. Inzwischen kann aber über entsprechende Frameworks viel dieser Komplexität abgefangen werden, wenn man sich klar gemacht hat, was diese Frameworks exakt tun.

Dieser Artikel war lediglich eine kurze Einführung in dieses Thema und lässt auch vieles, was IDEs leisten können, bewusst aussen vor, um ein wenig Licht in diesen Bereich zu bringen.

Update 26. Juli 2013

Die Quellen zu diesem Artikel kann man nun auch bei GitHub herunterladen oder forken.