Spring DI, AOP und MVC

+ andere TechDocs
+ Spring-Projekte
+ Spring Framework
+ Spring Boot
+ Spring Batch
+


Spring unterstützt die Softwareentwicklung von Enterprise-tauglichen JVM-basierenden Anwendungen durch Vereinfachungen, Effektivität, Flexibilität, Portabilität und Förderung guter Programmierpraktiken.

Wichtige Schlüsselstrategien von Spring sind:

Spring besteht aus mehreren Spring-Projekten.

Die Basis bildet das Spring Framework, welches Dependency Injection (DI), Aspect-Oriented Programming (AOP), Declarative Transaction Management, MVC Web Application, RESTful Web Services und vieles mehr bietet.



Inhalt

  1. Begriffe im Spring-Umfeld
  2. DI-Demo mit den JSR-330-Annotationen @Named und @Inject
  3. DI-Demo mit Spring-Annotationen
  4. DI-Demo mit JUnit-Modultest
  5. DI-Demo mit Prototype-Scope
  6. DI-Demo mit mehreren Application-Kontexten und Singleton-Dubletten
  7. DI-Demo mit DataSourcen und Profilen
  8. AOP-Demo mit Aspect, Pointcut und Advices
  9. Web-Demo mit Spring MVC und Thymeleaf
  10. Web-Demo mit JUnit-Modultest mit MVC-Mock
  11. Web-Demo mit JUnit-Modultest mit Data-Tier-Mock
  12. Web-Demo mit JSR-303-Validator



Begriffe im Spring-Umfeld



DI-Demo mit den JSR-330-Annotationen @Named und @Inject

Das folgende einfache Beispiel demonstriert:

Normalerweise werden in Nicht-Spring-Anwendungen die JSR-330-Annotationen verwendet, und in Spring-Anwendungen ausschließlich Spring-Annotationen. Wie das folgende Beispiel zeigt, kann auch beides gemischt werden.

Sie können das Beispiel wahlweise entweder downloaden oder wie beschrieben Schritt für Schritt aufbauen und ausführen.

Führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo01-Jsr330

    cd SpringDemo01-Jsr330

    md src\main\java\didemo

    tree /F

  3. Erstellen Sie im SpringDemo01-Jsr330-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>didemo</groupId>
      <artifactId>SpringDiDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
        <dependency>
          <groupId>javax.inject</groupId>
          <artifactId>javax.inject</artifactId>
          <version>1</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>4.3.6.RELEASE</version>
        </dependency>
      </dependencies>
    </project>
    
  4. Erzeugen Sie im src/main/java/didemo-Verzeichnis die Main- und Konfigurationsklasse: MainApp.java

    package didemo;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan
    public class MainApp
    {
       public static void main( String[] args )
       {
          ApplicationContext ctx = new AnnotationConfigApplicationContext( MainApp.class );
          MeineComponent meineInjizierteComponent = ctx.getBean( MeineComponent.class );
          meineInjizierteComponent.printInfo();
          meineInjizierteComponent.runInjizierteBean();
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @ComponentScan, ApplicationContext und AnnotationConfigApplicationContext.

  5. Erzeugen Sie im src/main/java/didemo-Verzeichnis eine einfache POJO-Klasse, die per @Named-Annotation als injizierbare managed Bean deklariert wird, und in deren Feldvariable eine andere managed Bean per @Inject injiziert wird: MeineComponent.java

    package didemo;
    
    import javax.inject.Inject;
    import javax.inject.Named;
    import org.springframework.context.annotation.Bean;
    
    @Named
    public class MeineComponent
    {
       @Inject
       private Runnable meineInjizierteBean;
    
       public void printInfo()
       {
          System.out.println( "---- Meine injizierte Component ----" );
       }
    
       public void runInjizierteBean()
       {
          meineInjizierteBean.run();
       }
    
       @Bean
       private Runnable meineBean()
       {
          return new Runnable() {
             public void run() {
                System.out.println( "---- Meine injizierte Bean ----" );
          } };
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Named, @Inject, @Bean und Runnable.

  6. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo01-Jsr330]
     |- [src]
     |   '- [main]
     |       '- [java]
     |           '- [didemo]
     |               |- MainApp.java
     |               '- MeineComponent.java
     '- pom.xml
    
  7. Führen Sie im Kommandozeilenfenster aus (passen Sie die Pfade an) (das lange java...-Kommando als eine Zeile):

    cd \MeinWorkspace\SpringDemo01-Jsr330

    mvn clean package

    set MVN_REPO=\Tools\Maven3-Repo

    set MVN_REPO_ORG_SPRFW=%MVN_REPO%\org\springframework

    set SPRFW_VERS=4.3.6.RELEASE

    java -cp target\SpringDiDemo-1.0-SNAPSHOT.jar;%MVN_REPO%\javax\inject\javax.inject\1\javax.inject-1.jar;%MVN_REPO_ORG_SPRFW%\spring-aop\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-beans\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-core\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-context\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-expression\%SPRFW_VERS%\*;%MVN_REPO%\commons-logging\commons-logging\1.2\* didemo.MainApp

  8. Sie erhalten:

    ---- Meine injizierte Component ----
    ---- Meine injizierte Bean ----
    
  9. Wenn Sie die umständliche lange java...-Kommandozeile vermeiden wollen, sehen Sie sich an: Spring-Boot, Maven Assembly Plugin oder Maven Shade Plugin.
    Oder importieren Sie das Maven-Projekt einfach in Ihre IDE, beispielsweise Eclipse oder IntelliJ Idea, und führen Sie darin die MainApp-Klasse aus.

  10. Sehen Sie sich die verwendeten Libs an:

    mvn dependency:tree



DI-Demo mit Spring-Annotationen

Das folgende einfache Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo02-Di

    cd SpringDemo02-Di

    md src\main\java\didemo

    tree /F

  3. Erstellen Sie im SpringDemo02-Di-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>didemo</groupId>
      <artifactId>SpringDiDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>4.3.6.RELEASE</version>
        </dependency>
      </dependencies>
    </project>
    
  4. Erzeugen Sie im src/main/java/didemo-Verzeichnis die Main- und Konfigurationsklasse: MainApp.java

    package didemo;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan
    public class MainApp
    {
       public static void main( String[] args )
       {
          ApplicationContext ctx = new AnnotationConfigApplicationContext( MainApp.class );
          MeineComponent meineInjizierteComponent = ctx.getBean( MeineComponent.class );
          meineInjizierteComponent.printInfo();
          meineInjizierteComponent.runInjizierteBean();
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @ComponentScan, ApplicationContext und AnnotationConfigApplicationContext.

  5. Erzeugen Sie im src/main/java/didemo-Verzeichnis eine einfache POJO-Klasse, die per @Component-Annotation als injizierbare managed Bean deklariert wird, und in deren Feldvariable eine andere managed Bean per @Autowired injiziert wird: MeineComponent.java

    package didemo;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MeineComponent
    {
       @Autowired
       private Runnable meineInjizierteBean;
    
       public void printInfo()
       {
          System.out.println( "---- Meine injizierte Component ----" );
       }
    
       public void runInjizierteBean()
       {
          meineInjizierteBean.run();
       }
    
       @Bean
       private Runnable meineBean()
       {
          return new Runnable() {
             public void run() {
                System.out.println( "---- Meine injizierte Bean ----" );
          } };
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Component, @Autowired, @Bean und Runnable.

  6. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo02-Di]
     |- [src]
     |   '- [main]
     |       '- [java]
     |           '- [didemo]
     |               |- MainApp.java
     |               '- MeineComponent.java
     '- pom.xml
    
  7. Führen Sie im Kommandozeilenfenster aus (passen Sie die Pfade an) (das lange java...-Kommando als eine Zeile):

    cd \MeinWorkspace\SpringDemo02-Di

    mvn clean package

    set MVN_REPO=\Tools\Maven3-Repo

    set MVN_REPO_ORG_SPRFW=%MVN_REPO%\org\springframework

    set SPRFW_VERS=4.3.6.RELEASE

    java -cp target\SpringDiDemo-1.0-SNAPSHOT.jar;%MVN_REPO_ORG_SPRFW%\spring-aop\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-beans\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-core\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-context\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-expression\%SPRFW_VERS%\*;%MVN_REPO%\commons-logging\commons-logging\1.2\* didemo.MainApp

  8. Sie erhalten:

    ---- Meine injizierte Component ----
    ---- Meine injizierte Bean ----
    
  9. Wenn Sie die umständliche lange java...-Kommandozeile vermeiden wollen, sehen Sie sich an: Spring-Boot, Maven Assembly Plugin oder Maven Shade Plugin.
    Oder importieren Sie das Maven-Projekt einfach in Ihre IDE, beispielsweise Eclipse oder IntelliJ Idea, und führen Sie darin die MainApp-Klasse aus.

  10. Sehen Sie sich die verwendeten Libs an:

    mvn dependency:tree



DI-Demo mit JUnit-Modultest

Das folgende einfache Beispiel

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. Wechseln Sie in das SpringDemo02-Di-Projektverzeichnis und erstellen Sie einen zweiten Sourcecodeverzeichnisbaum:

    cd \MeinWorkspace\SpringDemo02-Di

    md src\test\java\didemo

    tree /F

  2. Erweitern Sie im SpringDemo02-Di-Projektverzeichnis die pom.xml vor </dependencies> um:

        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>4.3.6.RELEASE</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
    
  3. Erzeugen Sie im src/test/java/didemo-Verzeichnis den JUnit-Modultest: MeineComponentTest.java

    package didemo;
    
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith( SpringJUnit4ClassRunner.class )
    @ContextConfiguration( classes = MainApp.class )
    public class MeineComponentTest
    {
       @Autowired private Runnable       meineBean;
       @Autowired private MeineComponent meineComponent;
    
       @Test
       public void testMeineComponent()
       {
          Assert.assertNotNull( meineBean );
          Assert.assertNotNull( meineComponent );
          meineComponent.printInfo();
          meineComponent.runInjizierteBean();
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @RunWith, SpringJUnit4ClassRunner, @ContextConfiguration, @Autowired, @Test.

  4. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo02-Di]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [didemo]
     |   |           |- MainApp.java
     |   |           '- MeineComponent.java
     |   '- [test]
     |       '- [java]
     |           '- [didemo]
     |               '- MeineComponentTest.java
     '- pom.xml
    
  5. Führen Sie im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringDemo02-Di

    mvn clean test

  6. Im JUnit-Modultest wird überprüft, dass die beiden Beans wirklich in die @Autowired-Felder injiziert werden. Anschließend werden Methoden der Beans aufgerufen und Sie erhalten wieder:

    ---- Meine injizierte Component ----
    ---- Meine injizierte Bean ----
    


DI-Demo mit Prototype-Scope

Das folgende Beispiel demonstriert:

Beispiele für Scope-Definitionen:

Der ScopedProxyMode INTERFACES oder TARGET_CLASS ist immer dann notwendig, wenn eine Bean mit zeitlich einschränkendem Scope in eine andere Bean mit Singleton-Scope injiziert werden soll. Zu bevorzugen ist proxyMode=ScopedProxyMode.INTERFACES. proxyMode=ScopedProxyMode.TARGET_CLASS sollte nur verwendet werden, wenn es kein Interface gibt.

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo03-Scope

    cd SpringDemo03-Scope

    md src\main\java\scopedemo

    md src\test\java\scopedemo

    tree /F

  3. Erstellen Sie im SpringDemo03-Scope-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>scopedemo</groupId>
      <artifactId>SpringScopeDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>4.3.6.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>4.3.6.RELEASE</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  4. Erzeugen Sie im src/main/java/scopedemo-Verzeichnis die Main- und Konfigurationsklasse: MainApp.java

    package scopedemo;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    public class MainApp
    {
       public static void main( String[] args )
       {
          new AnnotationConfigApplicationContext( MainApp.class );
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @ComponentScan und AnnotationConfigApplicationContext.

  5. Erzeugen Sie im src/main/java/scopedemo-Verzeichnis eine Spring-Component-Klasse: MeineComponent.java

    package scopedemo;
    
    import org.springframework.beans.factory.config.ConfigurableBeanFactory;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    @Component
    @Scope( value=ConfigurableBeanFactory.SCOPE_PROTOTYPE )
    public class MeineComponent
    {
       String text = "";
    
       public void addText( String txt ) {
          this.text += txt;
       }
    
       public String getText() {
          return text;
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Component, Scope, ConfigurableBeanFactory, SCOPE_PROTOTYPE.

  6. Erzeugen Sie im src/test/java/scopedemo-Verzeichnis den JUnit-Modultest: MeineComponentTest.java

    package scopedemo;
    
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith( SpringJUnit4ClassRunner.class )
    @ContextConfiguration( classes = MainApp.class )
    public class MeineComponentTest
    {
       @Autowired private MeineComponent compA;
       @Autowired private MeineComponent compB;
    
       @Test
       public void testScope()
       {
          compA.addText( "a" );
          compB.addText( "b" );
          compA.addText( "a" );
          compB.addText( "b" );
          System.out.println( compA.getText() );
          System.out.println( compB.getText() );
          Assert.assertEquals( "aa", compA.getText() );
          Assert.assertEquals( "bb", compB.getText() );
       }
    }
    

    Sehen Sie sich die Javadoc an zu: @RunWith, SpringJUnit4ClassRunner, @ContextConfiguration, @Autowired, @Test.

  7. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo03-Scope]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [scopedemo]
     |   |           |- MainApp.java
     |   |           '- MeineComponent.java
     |   '- [test]
     |       '- [java]
     |           '- [scopedemo]
     |               '- MeineComponentTest.java
     '- pom.xml
    
  8. Führen Sie im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringDemo03-Scope

    mvn clean test

  9. Der JUnit-Modultest meldet Erfolg und Sie erhalten:

    aa
    bb
    
    Pro Komponente wird stets derselbe Buchstabe gespeichert. Die beiden Komponenten compA und compB sind korrekt getrennt.
  10. Fügen Sie in MeineComponent.java vor der Zeile

    @Scope( value=ConfigurableBeanFactory.SCOPE_PROTOTYPE )

    zwei Schrägstriche hinzu, um sie auszukommentieren. Führen Sie erneut aus:

    mvn clean test

    Diesmal sind die beiden Komponenten nicht getrennte Instanzen. Stattdessen wird die Instanzvariable gegenseitig überschrieben:

    abab
    abab
    
  11. Eine aufwändigere Demo zum Spring-Scope finden Sie unter: Spring-Batch-Demo mit multi-threaded stateful Step-Bean.



DI-Demo mit mehreren Application-Kontexten und Singleton-Dubletten

Das folgende Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo04-SingletonDublette

    cd SpringDemo04-SingletonDublette

    md src\main\java\singldubldemo

    tree /F

  3. Erstellen Sie im SpringDemo04-SingletonDublette-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>singldubldemo</groupId>
      <artifactId>SpringSingeltonDubletteDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>4.3.6.RELEASE</version>
        </dependency>
      </dependencies>
    </project>
    
  4. Erzeugen Sie im src/main/java/singldubldemo-Verzeichnis die Main-Applikationsklasse: MainApp.java

    package singldubldemo;
    
    import java.util.UUID;
    import org.springframework.beans.factory.config.ConfigurableBeanFactory;
    import org.springframework.context.annotation.*;
    import org.springframework.stereotype.Component;
    
    /**
     * Diese Demo zeigt:
     * a) In Spring koennen mehrere Spring-Application-Kontexte parallel erzeugt werden.
     *    Bei Spring-MVC-Anwendungen ist dies sogar der Normalfall.
     * b) Spring-Singleton-Beans sind keine echten GoF-Singletons, die es pro ClassLoader nur einmal gibt,
     *    sondern sie sind nur pro Spring-ApplicationContext einmalig.
     *    Bei mehreren Spring-Application-Kontexten kann es mehrere Instanzen einer Spring-Singleton-Bean geben.
     */
    public class MainApp
    {
       public static void main( String[] args )
       {
          AnnotationConfigApplicationContext appRootContext = new AnnotationConfigApplicationContext();
          appRootContext.register( RootConfig.class );
          appRootContext.refresh();
    
          AnnotationConfigApplicationContext servletContext = new AnnotationConfigApplicationContext();
          servletContext.register( WebConfig.class );
          servletContext.refresh();
    
          MeinSingleton singletonRt = (MeinSingleton) appRootContext.getBean( "meinSingleton" );
          MeinSingleton singletonWb = (MeinSingleton) servletContext.getBean( "meinSingleton" );
    
          System.out.println( "\n---- Die beiden Singleton-Instanzen sind " +
                              (singletonRt.id.equals( singletonWb.id ) ? "identisch." : "verschieden.") );
       }
    }
    
    @Configuration
    @ComponentScan( basePackages = { "singldubldemo" } )
    class RootConfig
    { /* ... */
    }
    
    @Configuration
    @ComponentScan( basePackages = { "singldubldemo" } )
    class WebConfig
    { /* ... */
    }
    
    @Component
    @Scope( ConfigurableBeanFactory.SCOPE_SINGLETON )
    class MeinSingleton
    {
       public final String id = UUID.randomUUID().toString();
    }
    

    Die Annotation "@Scope( ConfigurableBeanFactory.SCOPE_SINGLETON )" bei MeinSingleton kann auch weggelassen werden, da dies ohnehin der Default ist.

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: ApplicationContext, AnnotationConfigApplicationContext, @Configuration, @ComponentScan, @Component, @Scope, ConfigurableBeanFactory.SCOPE_SINGLETON.

  5. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo04-SingletonDublette]
     |- [src]
     |   '- [main]
     |       '- [java]
     |           '- [singldubldemo]
     |               '- MainApp.java
     '- pom.xml
    
  6. Führen Sie im Kommandozeilenfenster aus (passen Sie die Pfade an) (das lange java...-Kommando als eine Zeile):

    cd \MeinWorkspace\SpringDemo04-SingletonDublette

    mvn clean package

    set MVN_REPO=\Tools\Maven3-Repo

    set MVN_REPO_ORG_SPRFW=%MVN_REPO%\org\springframework

    set SPRFW_VERS=4.3.6.RELEASE

    java -cp target\SpringSingeltonDubletteDemo-1.0-SNAPSHOT.jar;%MVN_REPO_ORG_SPRFW%\spring-aop\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-beans\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-core\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-context\%SPRFW_VERS%\*;%MVN_REPO_ORG_SPRFW%\spring-expression\%SPRFW_VERS%\*;%MVN_REPO%\commons-logging\commons-logging\1.2\* singldubldemo.MainApp

  7. Sie erhalten:

    ---- Die beiden Singleton-Instanzen sind verschieden.
    
  8. Wenn Sie die umständliche lange java...-Kommandozeile vermeiden wollen, sehen Sie sich an: Spring-Boot, Maven Assembly Plugin oder Maven Shade Plugin.
    Oder importieren Sie das Maven-Projekt einfach in Ihre IDE, beispielsweise Eclipse oder IntelliJ Idea, und führen Sie darin die MainApp-Klasse aus.



DI-Demo mit DataSourcen und Profilen

Das folgende Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo05-Ds

    cd SpringDemo05-Ds

    md src\main\resources

    md src\main\java\didemo

    md src\test\java\didemo

    tree /F

  3. Erstellen Sie im SpringDemo05-Ds-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>didemo</groupId>
      <artifactId>SpringDiDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.6.0</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>com.h2database</groupId>
          <artifactId>h2</artifactId>
          <version>1.4.193</version>
        </dependency>
        <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-dbcp2</artifactId>
          <version>2.1.1</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>4.3.6.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>4.3.6.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>4.3.6.RELEASE</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    
  4. Erzeugen Sie im src/main/java/didemo-Verzeichnis die Main- und Konfigurationsklasse: MainApp.java

    package didemo;
    
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    public class MainApp
    {
       public static void main( String[] args )
       {
          new AnnotationConfigApplicationContext( MainApp.class );
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @ComponentScan und AnnotationConfigApplicationContext.

  5. Erzeugen Sie im src/main/java/didemo-Verzeichnis eine Klasse mit mehreren DataSource-Beans: MeineDataSource.java

    package didemo;
    
    import javax.sql.DataSource;
    import org.apache.commons.dbcp2.BasicDataSource;
    import org.springframework.context.annotation.*;
    import org.springframework.jdbc.datasource.embedded.*;
    import org.springframework.jndi.JndiObjectFactoryBean;
    
    @Configuration
    public class MeineDataSource
    {
       // DataSource fuer "Entwicklung" und JUnit-Modultests:
       @Bean( destroyMethod="shutdown" )
       @Profile( "dev" )
       public DataSource dataSource1()
       {
          return new EmbeddedDatabaseBuilder()
                .setType( EmbeddedDatabaseType.H2 )
                .addScript( "classpath:schema.sql" )
                .addScript( "classpath:test-data.sql" )
                .build();
       }
    
       // DataSource fuer "Integrationstest" (URL-Pfad anpassen!):
       @Bean
       @Profile( "test" )
       public DataSource dataSource2()
       {
          BasicDataSource ds = new BasicDataSource();
          ds.setDriverClassName( "org.h2.Driver" );
          ds.setUrl( "jdbc:h2:./target/h2-db;DB_CLOSE_ON_EXIT=FALSE" );
          ds.setUsername( "sa" );
          ds.setPassword( "" );
          return ds;
       }
    
       // DataSource fuer "Produktion" (JNDI-Name appassen!):
       @Bean
       @Profile( "prod" )
       public DataSource dataSource3()
       {
          JndiObjectFactoryBean factBean = new JndiObjectFactoryBean();
          factBean.setJndiName( "jdbc/meineDataSource" );
          factBean.setResourceRef( true );
          factBean.setProxyInterface( javax.sql.DataSource.class );
          return (DataSource) factBean.getObject();
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @Bean, DataSource, EmbeddedDatabaseBuilder und EmbeddedDatabaseType.

    Falls Sie andere Datenbanken bevorzugen, sehen Sie sich beispielsweise die Infos an unter: andere Datenbanken und JpaTestUtil.

    Falls Sie die Datenbankparameter (URL, Username, Password etc.) nicht im Java-Code haben wollen, sondern per Properties-Datei übergeben wollen, sehen Sie sich beispielsweise an: Property Placeholder sowie Spring-Boot-Kommandozeilenanwendung mit JPA.

  6. Erzeugen Sie im src/main/resources-Verzeichnis zwei SQL-Skripte.

    schema.sql

    CREATE TABLE MeineEntity
    (
      id    INTEGER NOT NULL,
      datum DATE    NOT NULL,
      text  VARCHAR( 100 ),
      UNIQUE ( id ),
      PRIMARY KEY ( id )
    );
    

    test-data.sql

    INSERT INTO MeineEntity VALUES ( 1, '2017-01-01', 'Mein Test-Text' );
    
  7. Erzeugen Sie im src/test/java/didemo-Verzeichnis den JUnit-Modultest: MeineDataSourceTest.java

    package didemo;
    
    import java.sql.*;
    import javax.sql.DataSource;
    import org.junit.*;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.*;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith( SpringJUnit4ClassRunner.class )
    @ContextConfiguration( classes = MainApp.class )
    // @ActiveProfiles( "dev" )
    public class MeineDataSourceTest
    {
       @Autowired
       private DataSource ds;
    
       @Test
       public void test() throws SQLException
       {
          Assert.assertNotNull( "DataSource ist null", ds );
          DatabaseMetaData md = ds.getConnection().getMetaData();
          System.out.println(
                "\nDB:  " + md.getDatabaseProductName() + " " + md.getDatabaseProductVersion() + ", " + md.getDriverName() +
                ",\nURL: " + md.getURL() + ", User: " + md.getUserName() + "\n" );
          try( ResultSet rs = ds.getConnection().createStatement().executeQuery( "Select * from MeineEntity" ) ) {
             ResultSetMetaData rsmd = rs.getMetaData();
             int n = rsmd.getColumnCount();
             while( rs.next() ) {
                for( int i = 1; i <= n; i++ ) {
                   System.out.println( rsmd.getColumnName( i ) + ": " + rs.getString( i ) );
                }
             }
          } catch( org.h2.jdbc.JdbcSQLException ex ) {
             System.out.println( ex.getMessage() );
          }
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @RunWith, SpringJUnit4ClassRunner, @ContextConfiguration, @ActiveProfiles, @Autowired, @Test und DataSource.

  8. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo05-Ds]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   '- [didemo]
     |   |   |       |- MainApp.java
     |   |   |       '- MeineDataSource.java
     |   |   '- [resources]
     |   |       |- schema.sql
     |   |       '- test-data.sql
     |   '- [test]
     |       '- [java]
     |           '- [didemo]
     |               '- MeineDataSourceTest.java
     '- pom.xml
    
  9. Führen Sie im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringDemo05-Ds

    mvn clean test -Dspring.profiles.default=dev

  10. Sie erhalten:

    DB:  H2 1.4.193 (2016-10-31), H2 JDBC Driver,
    URL: jdbc:h2:mem:testdb, User: SA
    
    ID: 1
    DATUM: 2017-01-01
    TEXT: Mein Test-Text
    

    Da als Spring-Profil "dev" vorgegeben wurde, wurde aus der Klasse MeineDataSource die Bean mit der Methode dataSource1() verwendet, welche zwei SQL-Skripte ausführt.

    Die embedded H2-Datenbank wurde erfolgreich gestartet, die Tabelle MeineEntity wurde angelegt, der INSERT-Datensatz wurde geschrieben, und im JUnit-Modultest wurde der Datensatz erfolgreich gelesen.

  11. Wenn Sie die Angabe des Spring-Profils weglassen:

    mvn clean test

    erhalten Sie:

    org.springframework.beans.factory.NoSuchBeanDefinitionException:
    No qualifying bean of type 'javax.sql.DataSource' available:
    expected at least 1 bean which qualifies as autowire candidate.
    

    Die drei DataSource-Beans in MeineDataSource werden nur aktiv, wenn ein passendes Spring-Profil aktiviert ist.

  12. Wenn Sie in der MeineDataSource-Klasse die drei @Profile(...)-Annotationen entfernen oder alternativ alle drei Spring-Profile aktivieren:

    mvn clean test -Dspring.profiles.default=dev,test,prod

    erhalten Sie:

    org.springframework.beans.factory.NoUniqueBeanDefinitionException:
    No qualifying bean of type 'javax.sql.DataSource' available:
    expected single matching bean but found 3: dataSource1,dataSource2,dataSource3
    
  13. Die beiden Profile test und prod funktionieren nur, wenn es entsprechende Datenbanken bzw. JNDI-Konfigurationen gibt. Aber Sie können anhand der unterschiedlichen Fehlermeldung erkennen, dass die Umschaltung per Spring-Profil funktioniert:

    mvn clean test -Dspring.profiles.default=test

    mvn clean test -Dspring.profiles.default=prod

  14. Normalerweise soll der JUnit-Modultest immer im dev-Profil ausgeführt werden, unabhängig davon, welches andere Profil aktiviert ist. Um dies zu erreichen, entfernen Sie die Auskommentierung vor @ActiveProfiles( "dev" ) in der Testklasse MeineDataSourceTest.java. Dann funktioniert auch:

    mvn clean test

  15. Die Aktivierung von Spring-Profilen erfolgt über spring.profiles.active und spring.profiles.default, die über verschiedene Mechanismen gesetzt werden können, beispielsweise:

  16. Kompliziertere Bedingungen können mit der @Conditional-Annotation formuliert werden.

  17. Sehen Sie sich die vielen weiteren Möglichkeiten an, welche Spring-Profile bieten: Environment abstraction.

  18. Wenn Sie nicht zur Laufzeit, sondern beim Compilieren aus mehreren Beans die richtige auswählen wollen, sehen Sie sich die @Qualifier-Annotation an.



AOP-Demo mit Aspect, Pointcut und Advices

Das folgende Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo06-AOP

    cd SpringDemo06-AOP

    md src\main\java\aopdemo

    md src\test\java\aopdemo

    tree /F

  3. Erstellen Sie im SpringDemo06-AOP-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>aopdemo</groupId>
      <artifactId>SpringAopDemo</artifactId>
      <version>1.0-SNAPSHOT</version>
      <properties>
        <spring.version>4.3.6.RELEASE</spring.version>
        <aspectj.version>1.8.10</aspectj.version>
      </properties>
      <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>${spring.version}</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aop</artifactId>
          <version>${spring.version}</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>${aspectj.version}</version>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>${spring.version}</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
      <!-- Der folgende build-Block wird nur benoetigt, falls eine ausfuehrbare Fat-Jar
           inklusive aller benoetigten Dependencies erstellt werden soll: -->
      <build>
        <plugins>
          <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.0.0</version>
            <configuration>
              <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
              </descriptorRefs>
              <archive>
                <manifest>
                  <mainClass>aopdemo.MainApp</mainClass>
                </manifest>
              </archive>
            </configuration>
            <executions>
              <execution>
                <id>make-assembly</id>
                <phase>package</phase>
                <goals>
                  <goal>single</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </project>
    

    Durch das maven-assembly-plugin wird eine ausführbare Fat-Jar inklusive aller benötigten Dependencies erstellt. Siehe hierzu: Ausführbare Jar-Datei inklusive Abhängigkeiten mit dem Assembly Plugin.

  4. Erzeugen Sie im src/main/java/aopdemo-Verzeichnis die Main- und Konfigurationsklasse: MainApp.java

    package aopdemo;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.*;
    
    @Configuration
    @ComponentScan
    @EnableAspectJAutoProxy
    public class MainApp
    {
       public static void main( String[] args )
       {
          ApplicationContext ctx = new AnnotationConfigApplicationContext( MainApp.class );
          if( args.length > 0 ) {
             int n = Integer.parseInt( args[0] );
             Fibonacci fibonacci = ctx.getBean( Fibonacci.class );
             System.out.println( "fibonacci( " + n + " ) = " + fibonacci.calc( n ) );
          }
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @ComponentScan, @EnableAspectJAutoProxy, ApplicationContext und AnnotationConfigApplicationContext.

  5. Erzeugen Sie im src/main/java/aopdemo-Verzeichnis die Spring-Bean: Fibonacci.java

    package aopdemo;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class Fibonacci
    {
       public long calc( int n )
       {
          return ( n < 2 ) ? n : (calc( n - 1 ) + calc( n - 2 ));
       }
    }
    

    Sehen Sie sich die Javadoc an zu: @Component.

  6. Erzeugen Sie im src/test/java/aopdemo-Verzeichnis den JUnit-Modultest: FibonacciTest.java

    package aopdemo;
    
    import org.junit.*;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith( SpringJUnit4ClassRunner.class )
    @ContextConfiguration( classes = MainApp.class )
    public class FibonacciTest
    {
       @Autowired private Fibonacci fibonacci;
    
       @Test
       public void testFibonacci()
       {
          Assert.assertNotNull( fibonacci );
          Assert.assertEquals(  13, fibonacci.calc( 7 ) );
       }
    }
    

    Sehen Sie sich die Javadoc an zu: @RunWith, SpringJUnit4ClassRunner, @ContextConfiguration, @Autowired, @Test.

  7. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo06-AOP]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [aopdemo]
     |   |           |- Fibonacci.java
     |   |           '- MainApp.java
     |   '- [test]
     |       '- [java]
     |           '- [aopdemo]
     |               '- FibonacciTest.java
     '- pom.xml
    
  8. Durch das maven-assembly-plugin wurde eine ausführbare Fat-Jar inklusive aller benötigten Dependencies erstellt. Führen Sie im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringDemo06-AOP

    mvn clean package

    java -jar target\SpringAopDemo-1.0-SNAPSHOT-jar-with-dependencies.jar 8

    Sie erhalten:

    fibonacci( 8 ) = 21
    
  9. Bis jetzt haben wir ein lauffähiges normales Spring-DI-Projekt mit JUnit-Modultest erstellt (die einzige Besonderheit ist die @EnableAspectJAutoProxy-Annotation, die bislang noch keine Auswirkung hat). In den nächsten Schritten wird es um AOP erweitert.

  10. Erzeugen Sie im src/main/java/aopdemo-Verzeichnis die Aspect-Klasse mit einem Pointcut und drei Advices: MeinAspect.java

    package aopdemo;
    
    import java.util.Arrays;
    import org.aspectj.lang.*;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class MeinAspect
    {
       @Pointcut( "execution( * Fibonacci.*(..) )" )
       public void meinPointcut() {}
    
       @Before( "meinPointcut()" )
       public void logBefore( JoinPoint jp ) {
          System.out.println( "@Before:       " + jp );
       }
    
       @Around( "meinPointcut()" )
       public Object logAround( ProceedingJoinPoint jp ) throws Throwable {
          System.out.println( "\n@Around-Start, Argumente: " + Arrays.asList( jp.getArgs() ) );
          Object obj = jp.proceed();
          System.out.println( "@Around-Ende,  Ergebnis:  " + obj );
          return obj;
       }
    
       @After( "meinPointcut()" )
       public void logAfter( JoinPoint jp ) {
          System.out.println( "@After:        " + jp + "\n" );
       }
    }
    

    Sehen Sie sich die Javadoc an zu: @Component, @Aspect, @Before, @Around, @After, @Pointcut, JoinPoint, ProceedingJoinPoint.

    Bei den drei Advices hätte man statt der Verweise auf den Pointcut mit "meinPointcut()" auch jeweils direkt den Pointcut-Ausdruck "execution( * Fibonacci.*(..) )" verwenden können, also beispielsweise so:

    @Before( "execution( * Fibonacci.*(..) )" )

    Der Pointcut-Ausdruck "execution( * Fibonacci.*(..) )" ist in der "AspectJ Pointcut Expression Language" formuliert und besteht aus den fünf Teilen:
      execution: AspectJ-Designator für Method Join Point.
      *: Rückgabewert-Typ, * bedeutet beliebiger Typ.
      Fibonacci: Die zu überwachende Klasse, wahlweise inklusive Package.
      .*: Methodenname, .* bedeutet beliebiger Methodenname.
      (..): Methodenargumente, (..) bedeutet beliebige Argumente.
    Sehen Sie sich hierzu die Doku an: Declaring a pointcut und The AspectJ Programming Guide.
    Beachten Sie, dass zwar AspectJ-Annotationen und die AspectJ Pointcut Expression Language verwendet werden, aber trotzdem von Spring AOP nur eine Teilmenge der AspectJ-Funktionalität unterstützt wird.

  11. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo06-AOP]
     |- [src]
     |   |- [main]
     |   |   '- [java]
     |   |       '- [aopdemo]
     |   |           |- Fibonacci.java
     |   |           |- MainApp.java
     |   |           '- MeinAspect.java
     |   '- [test]
     |       '- [java]
     |           '- [aopdemo]
     |               '- FibonacciTest.java
     '- pom.xml
    
  12. Führen Sie wieder im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringDemo06-AOP

    mvn clean package

    java -jar target\SpringAopDemo-1.0-SNAPSHOT-jar-with-dependencies.jar 8

    Sie erhalten:

    Running aopdemo.FibonacciTest
    ...
    @Around-Start, Argumente: [7]
    @Before:       execution(long aopdemo.Fibonacci.calc(int))
    @Around-Ende,  Ergebnis:  13
    @After:        execution(long aopdemo.Fibonacci.calc(int))
    
    ...
    
    java -jar target\SpringAopDemo-1.0-SNAPSHOT-jar-with-dependencies.jar 8
    ...
    @Around-Start, Argumente: [8]
    @Before:       execution(long aopdemo.Fibonacci.calc(int))
    @Around-Ende,  Ergebnis:  21
    @After:        execution(long aopdemo.Fibonacci.calc(int))
    
    fibonacci( 8 ) = 21
    


Web-Demo mit Spring MVC und Thymeleaf

Das folgende Beispiel demonstriert:

Spring MVC ist ein Model-View-Controller-Webframework basierend auf einem DispatcherServlet, welches konfigurierbar Requests zu Handlern weiterleitet.

Thymeleaf ist eine Template-Engine und verwendet eine eigene DOM-Implementierung, um das eingelesene Template als DOM-Tree zu laden und dynamische Inhalte zu ersetzen. Thymeleaf verwendet keine eigenen Tags und keinen Inline-Code, sondern stattdessen spezielle Thymeleaf-Attribute mit eigenem Namespace in den HTML-Tags. Dadurch kann das Design der HTML-Seite unabhängig vom Programmcode erstellt werden, Designer und Programmierer können unabhängig voneinander arbeiten. Die Expression Language von Thymeleaf basiert normalerweise auf der Object-Graph-Navigation Language (OGNL). Aber in Spring-Anwendungen wird SpEL verwendet. Es gibt vier Typen von Expressions:

Um die Spring-Basics explizit zeigen zu können, wird in diesem Beispiel nicht Spring Boot verwendet. Damit das Beispiel trotzdem einfach bleibt, enthält es keine Datenbankanbindung. Bevorzugen sollten Sie allerdings die Spring-Boot-Variante. Ein Beispiel hierzu inklusive Datenbankanbindung finden Sie unter Spring-Boot-Webanwendung mit MVC und Thymeleaf.

Downloaden Sie das folgende Beispiel oder führen Sie folgende Schritte aus:

  1. JDK und Maven müssen installiert sein.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und führen Sie folgende Kommandos aus:

    cd \MeinWorkspace

    md SpringDemo07-MVC-Thymeleaf

    cd SpringDemo07-MVC-Thymeleaf

    md src\main\java\mvcdemo

    md src\main\resources

    md src\main\webapp\templates

    tree /F

  3. Erstellen Sie im SpringDemo07-MVC-Thymeleaf-Projektverzeichnis die Maven-Projektkonfigurationsdatei: pom.xml

    <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>mvcdemo</groupId>
      <artifactId>SpringMvcDemo</artifactId>
      <packaging>war</packaging>
      <version>1.0-SNAPSHOT</version>
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <springframework-version>4.3.8.RELEASE</springframework-version>
      </properties>
      <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>${springframework-version}</version>
          <type>jar</type>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
          <version>${springframework-version}</version>
          <type>jar</type>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
          <version>${springframework-version}</version>
          <type>jar</type>
        </dependency>
        <dependency>
          <groupId>org.thymeleaf</groupId>
          <artifactId>thymeleaf-spring3</artifactId>
          <version>2.1.5.RELEASE</version>
          <type>jar</type>
        </dependency>
        <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
          <scope>provided</scope>
        </dependency>
        <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.17</version>
          <scope>runtime</scope>
        </dependency>
      </dependencies>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.6.1</version>
            <configuration>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.1.0</version>
            <configuration>
              <failOnMissingWebXml>false</failOnMissingWebXml>
            </configuration>
          </plugin>
          <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>9.4.5.v20170502</version>
            <configuration>
              <webApp>
                <contextPath>/</contextPath>
              </webApp>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
  4. Erzeugen Sie im src/main/java/mvcdemo-Verzeichnis folgende vier Klassen:

    WebAppInitializer.java

    package mvcdemo;
    
    import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
    
    /**
     * DispatcherServlet-Konfiguration:
     * Jede Klasse, die AbstractAnnotationConfigDispatcherServletInitializer erweitert, wird automatisch verwendet,
     * um das DispatcherServlet zu konfigurieren, und um zwei Spring Application Contexte zu erstellen.
     */
    public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer
    {
       /** Vom ContextLoaderListener erzeugter Spring Application Context fuer die Beans im Backend, Middle-Tier und Data-Tier
           (wird in diesem simplen Beispiel nicht benoetigt) */
       @Override
       protected Class<?>[] getRootConfigClasses() {
          return null;
       }
    
       /** Konfiguration des DispatcherServlet Spring Application Servlet Context fuer die Webkomponenten wie
           Controller, View-Resolver und Handler-Mappings */
       @Override
       protected Class<?>[] getServletConfigClasses() {
          return new Class[] { WebMvcConfig.class };
       }
    
       /** Mapping vom DispatcherServlet auf den Kontextpfad "/" */
       @Override
       protected String[] getServletMappings() {
          return new String[] { "/" };
       }
    }
    

    Sehen Sie sich die Erläuterungen zum DispatcherServlet an, sowie die Javadoc zu: DispatcherServlet und AbstractAnnotationConfigDispatcherServletInitializer.

    WebMvcConfig.java

    package mvcdemo;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.ViewResolver;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    import org.thymeleaf.spring3.SpringTemplateEngine;
    import org.thymeleaf.spring3.view.ThymeleafViewResolver;
    import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
    
    /** Webkonfiguration fuer Spring MVC und Thymeleaf */
    @Configuration
    @ComponentScan
    @EnableWebMvc
    public class WebMvcConfig extends WebMvcConfigurerAdapter
    {
       @Override
       public void addResourceHandlers( ResourceHandlerRegistry registry )
       {
          registry.addResourceHandler( "/**" ).addResourceLocations( "/" );
       }
    
       @Bean
       public ViewResolver viewResolver()
       {
          ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
          templateResolver.setCacheable( false );
          templateResolver.setPrefix( "/templates/" );
          templateResolver.setSuffix( ".html" );
          templateResolver.setTemplateMode( "HTML5" );
    
          SpringTemplateEngine templateEngine = new SpringTemplateEngine();
          templateEngine.setTemplateResolver( templateResolver );
    
          ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
          viewResolver.setCharacterEncoding( "UTF-8" );
          viewResolver.setOrder( 1 );
          viewResolver.setTemplateEngine( templateEngine );
    
          return viewResolver;
       }
    }
    

    Sehen Sie sich obige Erläuterungen zu den Basis-Annotationen an, sowie die Javadoc zu: @Configuration, @ComponentScan, @EnableWebMvc, WebMvcConfigurerAdapter, ResourceHandlerRegistry, @Bean, ViewResolver, ServletContextTemplateResolver, SpringTemplateEngine, ThymeleafViewResolver.

    MeineEntity.java

    package mvcdemo;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /** Einzelnes Datenelement */
    public class MeineEntity
    {
       private Long   id;
       private Date   date;
       private String text;
    
       public MeineEntity() {}
    
       public MeineEntity( Long id, Date date, String text ) {
          this.id   = id;
          this.date = date;
          this.text = text;
       }
    
       public Long   getId()   { return id;   }
       public Date   getDate() { return date; }
       public String getText() { return text; }
       public void setId(   Long   id   ) { this.id   = id;   }
       public void setDate( Date   date ) { this.date = date; }
       public void setText( String text ) { this.text = text; }
    
       public String getDatumZeit() {
          return ( date == null ) ? "null" : (new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" )).format( date );
       }
    
       @Override
       public String toString() {
          return "[id=" + id + ", datum=" + getDatumZeit() + ", text=" + text + "]";
       }
    }
    

    MeineEntityController.java

    package mvcdemo;
    
    import java.util.*;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    /** Controller-Klasse fuer die Webseite MeineEntityWebseite.html */
    @Controller
    @RequestMapping( "/mvc-th" )
    public class MeineEntityController
    {
       private List<MeineEntity> entitiesListe = new ArrayList<>();
    
       @RequestMapping( method=RequestMethod.POST )
       public synchronized String addToListe( MeineEntity newEntity )
       {
          newEntity.setDate( new Date() );
          newEntity.setId( Long.valueOf( entitiesListe.size() ) );
          entitiesListe.add( newEntity );
          return "redirect:/mvc-th";
       }
    
       @RequestMapping( method=RequestMethod.GET )
       public String getListe( Model model )
       {
          model.addAttribute( "entitiesListe", Collections.unmodifiableList( entitiesListe ) );
          return "MeineEntityWebseite";
       }
    }
    

    Sehen Sie sich die Erläuterungen unter Implementing Controllers und GET / POST an, sowie die Javadoc zu: @Controller und @RequestMapping.

  5. Erzeugen Sie im src/main/resources-Verzeichnis die Log4j-Konfigurationsdatei: log4j.properties

    log4j.rootCategory=INFO, stdout
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2}:%L - %m%n
    log4j.category.org.springframework.beans.factory=DEBUG
    
  6. Erzeugen Sie im src/main/webapp-Verzeichnis die CSS-Style-Datei: style.css

    body          { font-family: arial,helvetica,sans-serif; }
    table, th, td { border: 1px solid black;
                    border-collapse: collapse;
                    padding: 5px;
                    text-align: left; }
    th            { background-color: #eeeeee }
    .errors       { background-color: #eeee00 }
    .error        { color: #ff0088 }
    
  7. Erzeugen Sie im src/main/webapp/templates-Verzeichnis das Thymeleaf-Template: MeineEntityWebseite.html

    <html xmlns:th="http://www.thymeleaf.org">
      <head>
        <title>Spring-MVC-Thymeleaf-Webanwendung</title>
        <link rel="stylesheet" th:href="@{/style.css}" />
      </head>
      <body onload='document.f.text.focus();'>
        <hr />
        <h2>Spring-MVC-Thymeleaf-Webanwendung</h2>
        <hr />
        <h3>Erstelle neues Datenelement</h3>
        <form name='f' method="POST">
          <input type="hidden" th:if="${_csrf}" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
          <label for="text">Text des neuen Datenelements:</label>
          <input type="text" name="text" size="30" />
          <input type="submit" />
        </form>
        <hr />
        <h3>Gespeicherte Datenelemente</h3>
        <div th:if="${#lists.isEmpty( entitiesListe )}"><p>Keine Datenelemente vorhanden.</p></div>
        <div th:unless="${#lists.isEmpty( entitiesListe )}">
          <table>
            <tr>
              <th>Id</th>
              <th>Datum</th>
              <th>Text</th>
            </tr>
            <tr th:each="me : ${entitiesListe}">
              <td th:text="${me.id}">Id</td>
              <td th:text="${me.datumZeit}">Datum</td>
              <td th:text="${me.text}">Text</td>
            </tr>
          </table>
        </div>
        <hr />
      </body>
    </html>
    

    Sehen Sie sich hierzu an: Tutorial: Using Thymeleaf, Tutorial: Thymeleaf + Spring, Conditionals: “if” and “unless”, Iteration, Using th:each.
    Beachten Sie folgende magischen Vereinfachungen:
    Das HTML-Formular enthält ein Textfeld, woraus automatisch ein MeineEntity-Objekt für die addToListe(MeineEntity)-REST-Methode erstellt wird, und die im Model gespeicherte entitiesListe kann direkt zur Darstellung in der Tabelle verwendet werden.
    Das CSRF-Hidden-Feld wird erst benötigt, wenn Sie zusätzlich Spring Security verwenden. Siehe hierzu: Test einer Webanwendung mit MVC-Model, MVC-View und Spring Security, Cross Site Request Forgery (CSRF) und Testing with CSRF Protection.

  8. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo07-MVC-Thymeleaf]
     |- [src]
     |   '- [main]
     |       |- [java]
     |       |   '- [mvcdemo]
     |       |       |- MeineEntity.java
     |       |       |- MeineEntityController.java
     |       |       |- WebAppInitializer.java
     |       |       '- WebMvcConfig.java
     |       |- [resources]
     |       |   '- log4j.properties
     |       '- [webapp]
     |           |- [templates]
     |           |   '- MeineEntityWebseite.html
     |           '- style.css
     '- pom.xml
    
  9. Führen Sie im Kommandozeilenfenster aus:

    cd \MeinWorkspace\SpringDemo07-MVC-Thymeleaf

    mvn jetty:run

    start http://localhost:8080/mvc-th

    Tragen Sie auf der Webseite mehrmals Text ein und speichern Sie. Sie erhalten:

  10. Vergleichen Sie die erstellte Anwendung mit der bereits oben erwähnten Spring-Boot-Webanwendung mit MVC und Thymeleaf, welche Spring Boot verwendet und die Datenelemente in einer Datenbank speichert.

  11. Da die Anwendung per REST-Schnittstellen funktioniert, können Sie auch direkt über die REST-Schnittstelle Datenelemente hinzufügen, beispielsweise mit curl:

    curl -X POST -d "text=Mein weiterer Test-Text" localhost:8080/mvc-th

    Das Ergebnis sehen Sie dann auf der Webseite, die Sie sich entweder im Webbrowser, oder auch per curl ansehen können:

    curl localhost:8080/mvc-th



Web-Demo mit JUnit-Modultest mit MVC-Mock

Das folgende Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. Voraussetzung ist das letzte Beispiel Web-Demo mit Spring MVC und Thymeleaf im Projektverzeichnis SpringDemo07-MVC-Thymeleaf.

  2. Wechseln Sie in das Projektverzeichnis SpringDemo07-MVC-Thymeleaf und erstellen Sie ein Verzeichnis für den JUnit-Modultest:

    cd \MeinWorkspace\SpringDemo07-MVC-Thymeleaf

    md src\test\java\mvcdemo

    tree /F

  3. Erweitern Sie im SpringDemo07-MVC-Thymeleaf-Projektverzeichnis die Maven-Projektkonfigurationsdatei pom.xml im <dependencies>-Block um folgende Dependencies:

        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>${springframework-version}</version>
          <scope>test</scope>
        </dependency>
        <!-- hamcrest muss vor mockito eingebunden werden: -->
        <dependency>
          <groupId>org.hamcrest</groupId>
          <artifactId>hamcrest-all</artifactId>
          <version>1.3</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.mockito</groupId>
          <artifactId>mockito-core</artifactId>
          <version>2.7.19</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
    
  4. Erzeugen Sie im src/test/java/mvcdemo-Verzeichnis folgenden JUnit-Modultest mit MVC-Mock: MeineEntityControllerTest.java

    package mvcdemo;
    
    import static org.hamcrest.Matchers.*;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
    import java.util.List;
    import org.junit.*;
    import org.springframework.test.web.servlet.*;
    
    public class MeineEntityControllerTest
    {
       @Test
       public void testMeineEntityController() throws Exception
       {
          MeineEntityController controller = new MeineEntityController();
          MockMvc mockMvc = standaloneSetup( controller ).build();
    
          mockMvc.perform( post( "/mvc-th" )
                .param( "text", "Mein Test-Text" ) )
                .andExpect( status().is3xxRedirection() )
                .andExpect( redirectedUrl( "/mvc-th" ) );
    
          MvcResult mvcResult = mockMvc.perform( get( "/mvc-th" ) )
                .andExpect( status().isOk() )
                .andExpect( view().name( "MeineEntityWebseite" ) )
                .andExpect( model().attributeExists( "entitiesListe" ) )
                .andExpect( model().attribute( "entitiesListe", instanceOf( List.class ) ) )
                .andExpect( model().attribute( "entitiesListe", hasSize( 1 ) ) )
                .andExpect( model().attribute( "entitiesListe", hasItem( hasProperty( "id",   equalTo( Long.valueOf( 0 ) ) ) ) ) )
                .andExpect( model().attribute( "entitiesListe", hasItem( hasProperty( "date", notNullValue() ) ) ) )
                .andExpect( model().attribute( "entitiesListe", hasItem( hasProperty( "text", equalTo( "Mein Test-Text" ) ) ) ) )
                .andReturn();
    
          // Der folgende Test ist unnoetig, weil der Text bereits oben ueberprueft wurde,
          // er soll nur eine alternative Testmoeglichkeit demonstrieren:
          Assert.assertEquals( "Mein Test-Text",
                ((List<MeineEntity>) mvcResult.getModelAndView().getModel().get( "entitiesListe" )).get( 0 ).getText() );
       }
    }
    

    Sehen Sie sich die Erläuterungen zum Spring MVC Test Framework an, sowie die Javadoc zu: @Test, MockMvc, MockMvcBuilders, MockMvcRequestBuilders, MvcResult, MockMvcResultMatchers und Matchers.

  5. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo07-MVC-Thymeleaf]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   '- [mvcdemo]
     |   |   |       |- MeineEntity.java
     |   |   |       |- MeineEntityController.java
     |   |   |       |- WebAppInitializer.java
     |   |   |       '- WebMvcConfig.java
     |   |   |- [resources]
     |   |   |   '- log4j.properties
     |   |   '- [webapp]
     |   |       |- [templates]
     |   |       |   '- MeineEntityWebseite.html
     |   |       '- style.css
     |   '- [test]
     |       '- [java]
     |           '- [mvcdemo]
     |               '- MeineEntityControllerTest.java
     '- pom.xml
    
  6. Führen Sie im Kommandozeilenfenster den JUnit-Modultest aus:

    cd \MeinWorkspace\SpringDemo07-MVC-Thymeleaf

    mvn test

  7. Die Webanwendung funktioniert natürlich weiterhin wie vorher:

    mvn jetty:run

    start http://localhost:8080/mvc-th

  8. Vergleichen Sie den erstellten JUnit-Modultest mit den beiden Tests in JUnit-Modultest mit Mock für Spring-Boot-Webanwendung, welche sehr ähnlich aussehen, aber Spring Boot verwenden.



Web-Demo mit JUnit-Modultest mit Data-Tier-Mock

Das folgende Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. Voraussetzung ist das letzte Beispiel Web-Demo mit JUnit-Modultest mit MVC-Mock im Projektverzeichnis SpringDemo07-MVC-Thymeleaf.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und kopieren Sie das SpringDemo07-MVC-Thymeleaf-Projekt:

    cd \MeinWorkspace

    xcopy SpringDemo07-MVC-Thymeleaf SpringDemo08-MVC-DataMock\ /S

    cd SpringDemo08-MVC-DataMock

  3. Um mit Mockito.verify() Ergebnisse überprüfen zu können, fügen Sie der Entity-Klasse MeineEntity.java im src/main/java/mvcdemo-Verzeichnis folgende drei Methoden hinzu:

       @Override
       public int hashCode() {
          return ("" + id).hashCode() + ("" + date).hashCode() + ("" + text).hashCode();
       }
    
       @Override
       public boolean equals( Object obj ) {
          if( obj == null || !(obj instanceof MeineEntity) ) { return false; }
          MeineEntity me = (MeineEntity) obj;
          return isEqual( me.getId(), id ) && isEqual( me.getDate(), date ) && isEqual( me.getText(), text );
       }
    
       private static boolean isEqual( Object obj1, Object obj2 ) {
          if( obj1 == obj2 ) { return true; }
          if( obj1 == null || obj2 == null ) { return false; }
          return obj1.equals( obj2 );
       }
    
  4. Um den Data-Tier zu abstrahieren und die Implementierung austauschen und mocken zu können, erzeugen Sie im src/main/java/mvcdemo-Verzeichnis folgendes Interface sowie folgende Implementierung:

    DatenRepository.java

    package mvcdemo;
    
    import java.util.List;
    
    public interface DatenRepository
    {
       public void addToRepository( MeineEntity newEntity );
    
       public List<MeineEntity> getAll();
    }
    

    DatenRepositoryImpl.java

    package mvcdemo;
    
    import java.util.*;
    import org.springframework.stereotype.Component;
    
    @Component
    public class DatenRepositoryImpl implements DatenRepository
    {
       private List<MeineEntity> entitiesListe = new ArrayList<>();
    
       @Override
       public synchronized void addToRepository( MeineEntity newEntity )
       {
          if( newEntity.getId()   == null ) { newEntity.setId( Long.valueOf( entitiesListe.size() ) ); }
          if( newEntity.getDate() == null ) { newEntity.setDate( new Date() ); }
          entitiesListe.add( newEntity );
       }
    
       @Override
       public List<MeineEntity> getAll()
       {
          return Collections.unmodifiableList( entitiesListe );
       }
    }
    
  5. Um die Data-Tier-Abstraktion zu verwenden, muss im src/main/java/mvcdemo-Verzeichnis die Controller-Klasse MeineEntityController.java ersetzt werden durch:

    package mvcdemo;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    /** Controller-Klasse fuer die Webseite MeineEntityWebseite.html */
    @Controller
    @RequestMapping( "/mvc-th" )
    public class MeineEntityController
    {
       private DatenRepository datenRepo;
    
       @Autowired
       public MeineEntityController( DatenRepository datenRepo )
       {
          this.datenRepo = datenRepo;
       }
    
       @RequestMapping( method=RequestMethod.POST )
       public String addToListe( MeineEntity newEntity )
       {
          datenRepo.addToRepository( newEntity );
          return "redirect:/mvc-th";
       }
    
       @RequestMapping( method=RequestMethod.GET )
       public String getListe( Model model )
       {
          model.addAttribute( "entitiesListe", datenRepo.getAll() );
          return "MeineEntityWebseite";
       }
    }
    
  6. Damit im src/test/java/mvcdemo-Verzeichnis der bisherige JUnit-Modultest MeineEntityControllerTest.java weiterhin funktioniert, muss die Zeile:

    MeineEntityController controller = new MeineEntityController();
    

    ersetzt werden durch:

    MeineEntityController controller = new MeineEntityController( new DatenRepositoryImpl() );
    
  7. Erzeugen Sie im src/test/java/mvcdemo-Verzeichnis folgenden neuen JUnit-Modultest mit Data-Tier-Mock: DatenMockControllerTest.java

    package mvcdemo;
    
    import static org.hamcrest.Matchers.*;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
    import java.util.*;
    import org.junit.Test;
    import org.mockito.ArgumentMatchers;
    import org.mockito.Mockito;
    import org.springframework.test.web.servlet.MockMvc;
    
    public class DatenMockControllerTest
    {
       @Test
       public void testPost() throws Exception
       {
          MeineEntity meineEntityMock = new MeineEntity( null, null, "Mein Test-Text" );
          DatenRepository datenRepositoryMock = Mockito.mock( DatenRepository.class );
    
          MeineEntityController controller = new MeineEntityController( datenRepositoryMock );
          MockMvc mockMvc = standaloneSetup( controller ).build();
    
          mockMvc.perform( post( "/mvc-th" )
                .param( "text", "Mein Test-Text" ) )
                .andExpect( status().is3xxRedirection() )
                .andExpect( redirectedUrl( "/mvc-th" ) );
    
          // Dieser Test ueberprueft die Datenfelder des Mock-Objekts, aber nicht das Mock-Objekt selbst:
          Mockito.verify( datenRepositoryMock ).addToRepository( ArgumentMatchers.refEq( meineEntityMock ) );
    
          // Dieser Test ueberprueft das Mock-Objekt auf Gleichheit, aber er funktioniert nur,
          // wenn MeineEntity equals(Object) geeignet implementiert ist: 
          Mockito.verify( datenRepositoryMock, Mockito.atLeastOnce() ).addToRepository( meineEntityMock );
       }
    
       @Test
       public void testGet() throws Exception
       {
          List<MeineEntity> erwarteteEntities = new ArrayList<>();
          erwarteteEntities.add( new MeineEntity( Long.valueOf(   42 ), new Date(), "Mein erster Text"  ) );
          erwarteteEntities.add( new MeineEntity( Long.valueOf( 4711 ), new Date(), "Mein zweiter Text" ) );
    
          DatenRepository datenRepositoryMock = Mockito.mock( DatenRepository.class );
          Mockito.when( datenRepositoryMock.getAll() ).thenReturn( erwarteteEntities );
    
          MeineEntityController controller = new MeineEntityController( datenRepositoryMock );
          MockMvc mockMvc = standaloneSetup( controller ).build();
    
          mockMvc.perform( get( "/mvc-th" ) )
                .andExpect( status().isOk() )
                .andExpect( view().name( "MeineEntityWebseite" ) )
                .andExpect( model().attributeExists( "entitiesListe" ) )
                .andExpect( model().attribute( "entitiesListe", instanceOf( erwarteteEntities.getClass() ) ) )
                .andExpect( model().attribute( "entitiesListe", hasSize(    erwarteteEntities.size() ) ) )
                .andExpect( model().attribute( "entitiesListe", hasItems(   erwarteteEntities.toArray() ) ) );
       }
    }
    
  8. Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):

    [\MeinWorkspace\SpringDemo08-MVC-DataMock]
     |- [src]
     |   |- [main]
     |   |   |- [java]
     |   |   |   '- [mvcdemo]
     |   |   |       |- DatenRepository.java
     |   |   |       |- DatenRepositoryImpl.java
     |   |   |       |- MeineEntity.java
     |   |   |       |- MeineEntityController.java
     |   |   |       |- WebAppInitializer.java
     |   |   |       '- WebMvcConfig.java
     |   |   |- [resources]
     |   |   |   '- log4j.properties
     |   |   '- [webapp]
     |   |       |- [templates]
     |   |       |   '- MeineEntityWebseite.html
     |   |       '- style.css
     |   '- [test]
     |       '- [java]
     |           '- [mvcdemo]
     |               |- DatenMockControllerTest.java
     |               '- MeineEntityControllerTest.java
     '- pom.xml
    
  9. Führen Sie im Kommandozeilenfenster die beiden JUnit-Modultests aus:

    cd \MeinWorkspace\SpringDemo08-MVC-DataMock

    mvn test

  10. Die Webanwendung funktioniert natürlich weiterhin wie vorher:

    mvn jetty:run

    start http://localhost:8080/mvc-th

  11. Falls Sie folgende Exception erhalten:

    java.lang.NoSuchMethodError: org.hamcrest.Matcher.describeMismatch(Ljava/lang/Object;Lorg/hamcrest/Description;)V

    Dann kollidieren gleichnamige Klassen (z.B. org.hamcrest.Matcher) in den hamcrest-, mockito- und junit-Libs.

    Folgendermaßen lösen Sie das Problem:
    - Verwenden Sie aktuelle Versionen der hamcrest-, mockito- und junit-Libs.
    - Tragen Sie die Dependencies zu diesen drei Libs in genau der Reihenfolge wie oben gezeigt in Ihre pom.xml ein.
    - Falls das nicht ausreicht, können Sie in der pom.xml Exclusions hinzufügen, beispielsweise so:

          <exclusions>
            <exclusion>
              <groupId>org.hamcrest</groupId>
              <artifactId>hamcrest-core</artifactId>
            </exclusion>
          </exclusions>
    
  12. Falls Sie folgende Fehlermeldung beim Mockito.verify( )-Test erhalten:

    Wanted but not invoked:
    datenRepository.addToRepository( [..., text=Mein Test-Text] );
    However, there was exactly 1 interaction with this mock:
    datenRepository.addToRepository( [..., text=Mein Test-Text] );

    Obwohl das gewünschte und das erhaltene Datenelement identisch aussehen, bemängelt Mockito.verify( ), dass sie unterschiedlich seien.

    Der Grund hierfür ist in der Regel eine ungeeignete Implementierung der equals()-Methode. Oben ist eine geeignete Implementierung gezeigt. Bessere Implementierungen können Sie mit EqualsBuilder und HashCodeBuilder implementieren.



Web-Demo mit JSR-303-Validator

Das folgende Beispiel demonstriert:

Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:

  1. Voraussetzung ist das letzte Beispiel Web-Demo mit JUnit-Modultest mit Data-Tier-Mock im Projektverzeichnis SpringDemo08-MVC-DataMock.

  2. Wechseln Sie in Ihr Workspace-Verzeichnis (z.B. \MeinWorkspace) und kopieren Sie das SpringDemo08-MVC-DataMock-Projekt:

    cd \MeinWorkspace

    xcopy SpringDemo08-MVC-DataMock SpringDemo09-MVC-Validator\ /S

    cd SpringDemo09-MVC-Validator

  3. Erweitern Sie im SpringDemo07-MVC-Thymeleaf-Projektverzeichnis die Maven-Projektkonfigurationsdatei pom.xml im <dependencies>-Block um eine Dependency zu einem beliebigen JSR-303-Validator, beispielsweise zum Hibernate-Validator:

        <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-validator</artifactId>
          <version>5.4.1.Final</version>
        </dependency>
    
  4. Fügen Sie der Entity-Klasse MeineEntity.java im src/main/java/mvcdemo-Verzeichnis Folgendes hinzu:

    Oben bei den Import-Anweisungen:

    import javax.validation.constraints.*;
    

    Bei den Instanzvariablen vor der Zeile private String text;:

    @NotNull @Size( min=1, max=25 )
    

    Sehen Sie sich auch die vielen weiteren Validierungs-Annotationen an im Package: javax.validation.constraints.

  5. Ersetzen Sie im src/main/java/mvcdemo-Verzeichnis den Inhalt der Controller-Klasse MeineEntityController.java durch:

    package mvcdemo;
    
    import javax.validation.Valid;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.validation.Errors;
    import org.springframework.web.bind.annotation.*;
    
    /** Controller-Klasse fuer die Webseite MeineEntityWebseite.html */
    @Controller
    @RequestMapping( "/mvc-th" )
    public class MeineEntityController
    {
       private DatenRepository datenRepo;
    
       @Autowired
       public MeineEntityController( DatenRepository datenRepo )
       {
          this.datenRepo = datenRepo;
       }
    
       @RequestMapping( method=RequestMethod.POST )
       public String addToListe( @Valid MeineEntity newEntity, Errors errors )
       {
          if( errors.hasErrors() ) {
             return "MeineEntityWebseite";
          }
          datenRepo.addToRepository( newEntity );
          return "redirect:/mvc-th";
       }
    
       @RequestMapping( method=RequestMethod.GET )
       public String getListe( Model model )
       {
          model.addAttribute( "meineEntity", new MeineEntity() );
          model.addAttribute( "entitiesListe", datenRepo.getAll() );
          return "MeineEntityWebseite";
       }
    }
    

    Sehen Sie sich die Javadoc an zu: @Valid und Errors.

  6. Ersetzen Sie im src/main/webapp/templates-Verzeichnis den Inhalt des Templates MeineEntityWebseite.html durch:

    <html xmlns:th="http://www.thymeleaf.org">
      <head>
        <title>Spring-MVC-Thymeleaf-Webanwendung</title>
        <link rel="stylesheet" th:href="@{/style.css}" />
      </head>
      <body onload='document.f.text.focus();'>
        <hr />
        <h2>Spring-MVC-Thymeleaf-Webanwendung</h2>
        <hr />
        <h3>Erstelle neues Datenelement</h3>
        <form name='f' th:object="${meineEntity}" method="POST">
          <div class="errors" th:if="${#fields.hasErrors('*')}">
            <ul>
              <li th:each="err : ${#fields.errors('*')}" th:text="'Eingabefehler: Länge ' + ${err}" />
            </ul>
          </div>
          <label th:class="${#fields.hasErrors('text')} ? 'error'">Text des neuen Datenelements:</label> 
          <input type="text" th:field="*{text}" size="30" />
          <input type="submit" />
          <input type="hidden" th:if="${_csrf}" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        </form>
        <hr />
        <h3>Gespeicherte Datenelemente</h3>
        <div th:if="${#lists.isEmpty( entitiesListe )}"><p>Keine Datenelemente vorhanden.</p></div>
        <div th:unless="${#lists.isEmpty( entitiesListe )}">
          <table>
            <tr>
              <th>Id</th>
              <th>Datum</th>
              <th>Text</th>
            </tr>
            <tr th:each="me : ${entitiesListe}">
              <td th:text="${me.id}">Id</td>
              <td th:text="${me.datumZeit}">Datum</td>
              <td th:text="${me.text}">Text</td>
            </tr>
          </table>
        </div>
        <hr />
      </body>
    </html>
    
  7. Die Webanwendung funktioniert weiterhin wie vorher:

    cd \MeinWorkspace\SpringDemo09-MVC-Validator

    mvn test jetty:run

    start http://localhost:8080/mvc-th

    Aber wenn Sie im Eingabefeld nichts eintragen oder gegen die Valdierungs-Annotation @NotNull @Size( min=1, max=25 ) verstoßen, erhalten Sie:

  8. Falls Sie folgende Exception erhalten:

    java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name '...' available as request attribute

    Dann haben Sie wahrscheinlich im MVC-Model noch nicht das zu validierende Objekt angelegt. Im Beispiel erfolgt dies in MeineEntityController.getListe() in der Zeile model.addAttribute( "meineEntity", new MeineEntity() );.





Weitere Themen: andere TechDocs | Spring Boot | Spring Batch
© 2017 Torsten Horn, Aachen