Spring 3, Hibernate 4 und Wicket 6: ein starkes Gespann

Print Friendly, PDF & Email

USA-1328

Ohne Vorkenntnisse ist es gar nicht so einfach, eine Web-Anwendung mit Spring, Hibernate und Wicket aufzusetzen. Dabei reichen dafür nur wenige Zeilen Java-Code.

Im Laufe des Jahre sind im Spring-Framework verschiedene Varianten entstanden, wie man eine Web-Anwendung aufsetzt. Inzwischen ist es möglich, auf externe XML-Konfigurationsdateien vollständig zu verzichten. Wie man konkret vorgeht, eine Spring-Anwendung mit Hibernate und Wicket aufzusetzen, zeigt dieser Artikel.

Im Gegensatz zur JEE 6-Welt muss man sich in der Spring-Welt gleich zu Beginn mit vielen Details beschäftigen, bis die erste Hallo-Welt-Anwendung steht. Dieser Nachteil wird zwar meiner Meinung nach durch die große Flexibilität und die Freiheit, auf einen Applikationsserver verzichten zu können, mehr als wett gemacht. Jedoch muss man sich erst einmal explizit für einen Technologie-Mix entscheiden und dann wissen, wie sich die ausgesuchten Produkte am besten integrieren lassen.

Gerade in der Spring-Welt führen meistens viele Wege zum Ziel, vor allem in der Konfiguration einer Anwendung. War es in den ersten Spring-basierten Anwendungen noch nötig, umfangreiche XML-Dateien zu pflegen, konnten diese durch den Einsatz von Annotationen und besonderen Namespaces in der zweiten Generation deutlich reduziert werden. Seit Spring 3.1 kommt eine Spring-Anwendung nun auch komplett ohne XML aus: Besondere, als @Configuration annotierte Klassen übernehmen die Initialisierung einer Anwendung.

Doch zuerst zur Maven pom.xm-Datei, die alle notwendigen Abhängigkeiten deklariert:

<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>
  <name>Spring-Showcase</name>
  <groupId>test</groupId>
  <artifactId>Spring-Showcase</artifactId>
  <version>1.0</version>
  <packaging>war</packaging>

  <properties>
    <version.spring>3.2.5.RELEASE</version.spring>
    <version.hibernate>4.2.7.Final</version.hibernate>
    <version.wicket>6.12.0</version.wicket>
  </properties>

  <repositories>
    <repository>
      <id>sun-repo-2</id>
      <url>http://download.java.net/maven/2/</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>jboss</id>
      <url>http://repository.jboss.org/nexus/content/groups/public-jboss/</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>SpringSource Enterprise Bundle Repository - SpringSource Bundle Releases</id>
      <url>http://repository.springsource.com/maven/bundles/release</url>
    </repository>
  </repositories>

  <dependencies>

    <!-- Core Spring libraries for setup and dependency injection -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${version.spring}</version>
    </dependency>    
    <dependency>
      <groupId>javax.inject</groupId>
      <artifactId>javax.inject</artifactId>
      <version>1</version>
    </dependency>

    <!-- In-memory database for testing -->
    <dependency>
      <groupId>org.hsqldb</groupId>
      <artifactId>hsqldb</artifactId>
      <version>2.3.1</version>
    </dependency>

    <!-- Spring support for database access -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${version.spring}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${version.spring}</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>${version.hibernate}</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>5.0.1.Final</version>
    </dependency>

    <!-- DB connection pool -->
    <dependency>
        <groupId>commons-dbcp</groupId>
        <artifactId>commons-dbcp</artifactId>
        <version>1.4</version>
    </dependency>
    <dependency>
        <groupId>commons-pool</groupId>
        <artifactId>commons-pool</artifactId>
        <version>1.6</version>
    </dependency>

    <!-- Spring support for web development -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${version.spring}</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.0.1</version>
      <scope>provided</scope>
    </dependency>

    <!-- Wicket with Spring support -->
    <dependency>
      <groupId>org.apache.wicket</groupId>
      <artifactId>wicket-core</artifactId>
      <version>${version.wicket}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.wicket</groupId>
      <artifactId>wicket-spring</artifactId>
      <version>${version.wicket}</version>
    </dependency>

  </dependencies>
</project>

Die Konfiguration einer Spring-Anwendung findet in Klassen statt, die mit der @Configuration-Annotation versehen sind. Die Konfiguration einer Anwendung kann auf mehrere solcher Klassen verteilt werden. Fangen wir mit der Konfiguration von Properties an:

/**
 * Property configuration needs to be done in a separate configuration class so that the 
 * properties themselves are available in the main configuration.
 */
@Configuration
@PropertySource("classpath:application.properties")
public class PropertyConfiguration {

  /**
   * Enable property placeholder functionality, i.e. resolution of ${} expressions.
   */
  @Bean
  public PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
  }
}

Diese Klasse sucht im Klassenpfad nach einer Datei namens application.properties und stellt alle dort definierten Properties der Anwendung zur Verfügung. Dahinter verbirgt sich das relativ neue Environment-Konzept, das es erlaubt, eine Anwendung relativ leicht und überschaubar für unterschiedliche Umgebungen zu konfigurieren.

Hier ist als Beispiel die referenzierte Konfigurationsdatei mit Einstellungen für eine In-Memory-Datenbank:

db.driverClassName=
db.url=jdbc:hsqldb:mem:ShowCase
db.username=sa
db.password=
db.showSql=true
db.generateDDL=true

Kommen wir zur nächsten Konfigurationdatei, die nun die Dependency-Injection starten und den Zugriff auf die Datenbank konfigurieren soll:

/**
 * Java-based Spring configuration of the application - mainly setting up the persistence layer.
 */
@Configuration
@Import(PropertyConfiguration.class)      // (1)
@ComponentScan(basePackages={"backend"})  // (2)
@EnableTransactionManagement              // (3)
public class ApplicationConfiguration {

  @Value("${db.driverClassName}") private String dbDriverClassName;   // (4)
  @Value("${db.url}")             private String dbUrl;            
  @Value("${db.username}")        private String dbUsername;
  @Value("${db.password}")        private String dbPassword;
  @Value("${db.showSql}")         private boolean dbShowSQl;
  @Value("${db.generateDDL}")     private boolean dbGenerateDdl;

  /**
   * Configures and returns a connection pool as JDBC DataSource
   */
  @Bean
  public DataSource dataSource() {                                   // (5)

    BasicDataSource dataSource = new BasicDataSource();
    if (!StringUtils.isEmpty(dbDriverClassName)) {
      dataSource.setDriverClassName(dbDriverClassName);
    }
    dataSource.setUrl(dbUrl);
    dataSource.setUsername(dbUsername);
    dataSource.setPassword(dbPassword);

    return dataSource;
  }

  /**
   * Configures and returns the entity manager factory, using an adaptor for Hibernate.
   */
  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {     // (6)

    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setDataSource(dataSource());
    factory.setPersistenceUnitName("ShowCase");
    factory.setPackagesToScan("common.domain"); // Because of this, no persistence.xml is needed!

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setDatabase(Database.HSQL);
    vendorAdapter.setShowSql(dbShowSQl);
    vendorAdapter.setGenerateDdl(dbGenerateDdl);
    factory.setJpaVendorAdapter(vendorAdapter);

    return factory;
  }

  /**
   * Configures and returns the transaction manager
   */
  @Bean
  public JpaTransactionManager transactionManager() {    // (7)
    return new JpaTransactionManager(entityManagerFactory().getObject());
  }
}

Hier passieren viele Dinge:

  1. Die Konfigurationsklasse für die Properties wird importiert. Anmerkung: Eine getrennte Konfigurationsklasse ist notwendig, damit in dieser Klasse schon das Injection von @Values funktioniert.
  2. Spring wird angewiesen, alle Klassen in den angegebenen Paketen auf Annotationen zu untersuchen. Hier wird festgelegt, dass alle Klassen, die in „backend.*“-Paketen liegen, als Services in Frage kommen.
  3. Spring stellt mehrere „EnableXY“-Annotationen zur Verfügung, um bestimmte Features zu aktivieren. Wenn in einer Anwendung Transaktionsmanagement gewünscht ist, muss es durch @EnableTransactionManagement angestellt werden.
  4. Über die @Value-Annotation erhält man Zugriff auf die Properties der Anwendung.
  5. Hier wird ein einfacher Pool für die Datenbankverbindungen eingerichtet.
  6. In dieser Methode wird Hibernate als JPA-Implementierung konfiguriert, sodass überall in der Anwendung über die Annotation @PersistenceContext eine EntityManager-Instanz erreichbar ist. Spring ermöglicht es, auf die persistence.xml zu verzichten, wenn der Spring-eigenen Klassen-Scanner verwendet wird.
  7. Zusätzlich zur Annotation @EnableTransactionManagement muss ein Transaction-Manager erzeugt werden. Für eine reine Web-Anwendung reicht dazu der JpaTransactionManager von Spring.

Die beiden Konfigurationsklassen alleine reichen noch nicht aus, dass die Spring-Anwendung wirklich hochfährt. Früher war es notwendig, in einer web.xml-Datei z.B. einen Listener zu installieren. Dank der Servlet 3-Spezifikation geht auch dies inzwischen in reinem Java-Code. Die folgende Klasse reicht dazu aus:

/**
 * Bootstrapping Spring application context for a web application. This is an alternative to 
 * explicitly integrating Spring in the web.xml file.
 */
public class TestWebApplicationInitializer implements WebApplicationInitializer {

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
    rootContext.register(ApplicationConfiguration.class); // (1)
    servletContext.addListener(new ContextLoaderListener(rootContext));
  }
}

Hier kommt viel Spring-Magie zum Tragen. Die wichtige Zeile ist mit (1) markiert; dort wird angegeben, welche Konfigurationsklasse Spring auswerten soll.

Die Konfiguration von Spring und Hibernate ist damit abgeschlossen. Bleibt noch Wicket übrig.

Leider wurde in Wicket 6 der Code auskommentiert, der es erlaubt, auch hier vollständig ohne die web.xml-Datei auszukommen (siehe in der Klasse WicketFilter die Methode getFilterPathFromAnnotation). Dieser Code wird erst wieder in Wicket 7 aktiviert, weil Wicket ansonsten nicht mehr in Java 5 funktionieren würde. Es spricht aber nichts dagegen, den Code in einer abgeleiteten Klasse selbst zu nutzen.

Ansonsten ist es erst einmal noch nötig, Wicket in einer web.xml zu initialisieren:

<?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">

  <description>Spring Showcase</description>
  <display-name>Spring Showcase</display-name>

  <filter>
    <description>Filter to catch all requests that come in within this context and route them to Wicket.</description>
    <filter-name>WicketAppFilter</filter-name>
    <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <init-param>
      <description>All requests shall be handled by the TestWebApplication class</description>
      <param-name>applicationClassName</param-name>
      <param-value>frontend.setup.TestWebApplication</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>WicketAppFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ERROR</dispatcher>
  </filter-mapping>

</web-app>

Hier passiert nicht viel mehr, als dass Wicket angewiesen wird, die Klasse TestWebApplication zu starten. Diese Klasse schaut nun wie folgt aus:

/**
 * Main starting point of the Wicket web application.
 */
public class TestWebApplication extends WebApplication {

  @Override
  public Class<? extends Page> getHomePage() {
    return HomePage.class;
  }

  @Override
  protected void init() {
    super.init();
    getComponentInstantiationListeners().add(new SpringComponentInjector(this));
  }
}

In dieser minimalen Version wird nur die Homepage-Klasse zurückgeliefert und der Adapter zwischen Wicket und Spring eingerichtet. Durch diesen Adapter ist es möglich, in allen Klassen, die von Wickets Component erben, mit der Annotation @SpringBean Zugriff auf alle durch Spring verwaltete Beans zu erhalten.

Damit ist die Web-Anwendung vollständig aufgesetzt und ein Rahmen geschaffen worden, der sich leicht durch weitere Bibliotheken aus dem Spring-Ökosystem erweitern lässt.

Zum Weiterlesen ein paar Tipps:

1 Gedanke zu „Spring 3, Hibernate 4 und Wicket 6: ein starkes Gespann“

  1. Artikel war sehr hilfreich, um ein erstes Projekt mit Wicket und Spring aufzusetzen.

    Zwei Ergänzungen, die vielleicht für andere hilfreich sein könnten:
    1. Will man @SpringBean in z.B. IDataProvidern verwenden, muss in deren Konstruktor folgendes aufgerufen werden:

    SpringComponentInjector.get().inject(this);

    2. Will man z.B. Jetty auch programmatisch starten (wie in Wicket Quickstarts Start.java – http://wicket.apache.org/start/quickstart.html), hilft es, TestWebApplicationInitializer zu entfernen und das Initialisieren des Spring Contextes in die init() Methode von TestWebApplication zu verlegen:

    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(ApplicationConfiguration.class);
    ctx.refresh();
    getComponentInstantiationListeners().add(new SpringComponentInjector(this, ctx));

    Antworten

Schreibe einen Kommentar zu Phil Antworten abbrechen

Fügen Sie die notwendige Nummer ins freie Feld ein *