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.
${ ... } | Allgemeine Syntax für Property Placeholder. |
${ jdbc.url } | Beispiel für die Property jdbc.url. |
meineMethode( @Value( "${jdbc.url}" ) String arg ) { ... } | Property Placeholder per @Value()-Annotation als Methodenparameter. |
@Configuration @PropertySource( "classpath:/conf/app.properties" ) public class MeineConfiguration { ... } |
Hinzufügen einer weiteren Quelle für Properties per @PropertySource. |
#{ ... } | Allgemeine Syntax für SpEL-Ausdrücke. |
#{ systemProperties['meine.prop'] } | Zugriff auf System-Properties. |
meineMethode( @Value( "#{ ... }" ) String arg ) { ... } | Ähnlich wie oben für Property Placeholder beschrieben, können SpEL-Ausdrücke per @Value()-Annotation als Methodenparameter übergeben werden. |
#{ meineBean.meinFeld } | Wert des Feldes meinFeld der Bean meineBean. |
#{ meineBean.meineMethode() } | Rückgabewert der Methode meineMethode() der Bean meineBean. |
#{ meineBean.meineMethode().weitereMethode() } | Kaskadierung von Methoden. |
#{ meineBean.meineMethode()?.weitereMethode() } | Mit dem ?.-Operator wird weitereMethode() nur ausgeführt, wenn das Ergebnis von meineMethode() nicht null ist. |
#{ T(...)... } | Mit dem T()-Operator werden statische Methoden auf Klassen (= Typen) aufgerufen. |
#{ T(System).currentTimeMillis() } | aktueller Wert von java.lang.System.currentTimeMillis(). |
#{ 2 * T(java.lang.Math).PI * circle.radius } | Kreisdurchmesser. |
#{ T(java.lang.Math).PI * circle.radius ^ 2 } | Kreisfläche. |
#{ meineBean.meinText1 + ' und ' + meineBean.meinText2 } | String-Zusammensetzung (= Concatenation). |
#{ meineBean.meinValue > 100 ? "gut" : "schlecht" } | Ternärer Operator. |
#{ meineBean.meinText ?: 'Default-Ersatztext' } | Elvis-Operator: Falls meineBean.meinText != null ist wird dieser Wert genommen, ansonsten der Default-Ersatzwert dahinter. |
#{ meineBean.meinName matches '[a-zA-Z0-9._]' } | Reguläre Ausdrücke. |
#{ meineBean.meineListe[7] } | Das 8. Element einer Liste. |
#{ meineBean.meineListe[7].meineMethode() } | Methodenaufruf auf dem Element einer Liste. |
#{ meineBean.meineListe.?[groesse > 100] } | Selektions-Operator: Filtert die Liste zu einer neuen Liste, welche nur die Elemente enthält, welche die Filterbedingung erfüllen. |
#{ meineBean.meineListe.^[groesse > 100] } | Selektions-Operator: Findet das erste Element aus der Liste, welches die Filterbedingung erfüllt. |
#{ meineBean.meineListe.$[groesse > 100] } | Selektions-Operator: Findet das letzte Element aus der Liste, welches die Filterbedingung erfüllt. |
#{ meineBean.meineListe.![groesse] } | Projektions-Operator: Erstellt eine neue Liste, welche statt der Elemente nur das gewählte Attribut enthält. |
#{ meineBean.meineListe.?[groesse > 100].![groesse] } | Kaskadierung von Operatoren: zuerst Selektion auf Filterbedingung, anschließend Reduzierung auf das Projektions-Attribut. |
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:
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
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>
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.
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.
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
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
Sie erhalten:
---- Meine injizierte Component ---- ---- Meine injizierte Bean ----
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.
Sehen Sie sich die verwendeten Libs an:
mvn dependency:tree
Das folgende einfache Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
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
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>
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.
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.
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
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
Sie erhalten:
---- Meine injizierte Component ---- ---- Meine injizierte Bean ----
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.
Sehen Sie sich die verwendeten Libs an:
mvn dependency:tree
Das folgende einfache Beispiel
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
Wechseln Sie in das SpringDemo02-Di-Projektverzeichnis und erstellen Sie einen zweiten Sourcecodeverzeichnisbaum:
cd \MeinWorkspace\SpringDemo02-Di
md src\test\java\didemo
tree /F
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>
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.
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
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringDemo02-Di
mvn clean test
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 ----
Das folgende Beispiel demonstriert:
Beispiele für Scope-Definitionen:
@Scope( ConfigurableBeanFactory.SCOPE_SINGLETON )
Dies ist der Default für Spring-Beans und -Components (und braucht deshalb nicht angegeben zu werden). Die Bean-Komponente wird nur einmal als Singleton instanziiert und bei mehrmaliger Verwendung wieder verwendet. Um Multi-threading-Probleme zu vermeiden, können die Bean-Komponenten stateless angelegt werden. Diese Spring-Singleton-Beans sind keine echten GoF-Singletons, die es pro ClassLoader nur einmal gibt, sondern sie sind nur pro Spring-ApplicationContext einmalig. Falls mehrere Spring-Application-Kontexte verwendet werden, kann es mehrere Instanzen einer Spring-Singleton-Bean geben, siehe: DI-Demo mit mehreren Application-Kontexten und Singleton-Dubletten.
@Scope( ConfigurableBeanFactory.SCOPE_PROTOTYPE )
Wird eine Spring-Bean oder -Component so annotiert, wird bei jeder Verwendung jedesmal eine neue Bean instanziiert. Beispiele siehe: unten und Multi-threaded stateful Step-Bean.
@Scope( value=WebApplicationContext.SCOPE_REQUEST, proxyMode=ScopedProxyMode.INTERFACES )
Request-Scope für Webanwendungen: Bean-Komponente existiert für die Dauer eines Web-Requests.
@Scope( value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES )
Session-Scope für Webanwendungen: Bean-Komponente existiert für die Dauer einer Web-Session.
@Scope( value="step", proxyMode=ScopedProxyMode.INTERFACES )
Step-Scope für Spring-Batch-Anwendungen: Bean-Komponente existiert für die Dauer eines Spring-Batch-Steps.
@Scope( value="job", proxyMode=ScopedProxyMode.INTERFACES )
Job-Scope für Spring-Batch-Anwendungen: Bean-Komponente existiert für die Dauer eines Spring-Batch-Jobs. Beispiele siehe: Multi-threaded stateful Step-Bean mit Job-Scope am Step und Multi-threaded stateful Step-Bean mit Context-Holder mit Job-Scope.
@Scope( value="job", proxyMode=ScopedProxyMode.TARGET_CLASS )
Wie vorher, aber für Klassen, für die es kein Interface gibt. Siehe auch: @JobScope.
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:
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
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>
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.
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.
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.
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
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringDemo03-Scope
mvn clean test
Der JUnit-Modultest meldet Erfolg und Sie erhalten:
aa bbPro Komponente wird stets derselbe Buchstabe gespeichert. Die beiden Komponenten compA und compB sind korrekt getrennt.
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
Eine aufwändigere Demo zum Spring-Scope finden Sie unter: Spring-Batch-Demo mit multi-threaded stateful Step-Bean.
Das folgende Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
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
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>
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.
Die Projektstruktur sieht jetzt so aus (überprüfen Sie es mit tree /F):
[\MeinWorkspace\SpringDemo04-SingletonDublette] |- [src] | '- [main] | '- [java] | '- [singldubldemo] | '- MainApp.java '- pom.xml
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
Sie erhalten:
---- Die beiden Singleton-Instanzen sind verschieden.
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.
Das folgende Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
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
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>
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.
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.
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' );
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.
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
Führen Sie im Kommandozeilenfenster aus:
cd \MeinWorkspace\SpringDemo05-Ds
mvn clean test -Dspring.profiles.default=dev
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.
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.
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
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
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
Die Aktivierung von Spring-Profilen erfolgt über spring.profiles.active und spring.profiles.default, die über verschiedene Mechanismen gesetzt werden können, beispielsweise:
Kompliziertere Bedingungen können mit der @Conditional-Annotation formuliert werden.
Sehen Sie sich die vielen weiteren Möglichkeiten an, welche Spring-Profile bieten: Environment abstraction.
Wenn Sie nicht zur Laufzeit, sondern beim Compilieren aus mehreren Beans die richtige auswählen wollen, sehen Sie sich die @Qualifier-Annotation an.
Das folgende Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
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
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.
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.
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.
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.
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
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
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.
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.
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
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
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:
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
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>
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.
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
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 }
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.
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
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:
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.
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
Das folgende Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
Voraussetzung ist das letzte Beispiel Web-Demo mit Spring MVC und Thymeleaf im Projektverzeichnis SpringDemo07-MVC-Thymeleaf.
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
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>
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.
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
Führen Sie im Kommandozeilenfenster den JUnit-Modultest aus:
cd \MeinWorkspace\SpringDemo07-MVC-Thymeleaf
mvn test
Die Webanwendung funktioniert natürlich weiterhin wie vorher:
mvn jetty:run
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.
Das folgende Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
Voraussetzung ist das letzte Beispiel Web-Demo mit JUnit-Modultest mit MVC-Mock im Projektverzeichnis SpringDemo07-MVC-Thymeleaf.
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
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 ); }
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 ); } }
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"; } }
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() );
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() ) ) ); } }
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
Führen Sie im Kommandozeilenfenster die beiden JUnit-Modultests aus:
cd \MeinWorkspace\SpringDemo08-MVC-DataMock
mvn test
Die Webanwendung funktioniert natürlich weiterhin wie vorher:
mvn jetty:run
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>
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.
Das folgende Beispiel demonstriert:
Downloaden Sie das Beispiel oder führen Sie folgende Schritte aus:
Voraussetzung ist das letzte Beispiel Web-Demo mit JUnit-Modultest mit Data-Tier-Mock im Projektverzeichnis SpringDemo08-MVC-DataMock.
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
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>
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.
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"; } }
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>
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:
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() );.