Skip to content

2. Artikel 1 – Spring IoC

Ziele dieses Dokuments:

  • die Konfigurations- und Integrationsmöglichkeiten des Spring-Frameworks (http://www.springframework.org) kennenzulernen
  • das Konzept von IoC (Inversion of Control), auch bekannt als Dependency Injection, zu definieren und anzuwenden

2.1. Konfiguration einer 3-Tier-Anwendung mit Spring

Betrachten wir eine klassische 3-Tier-Anwendung:

Wir gehen davon aus, dass der Zugriff auf die Geschäftsschicht und DAO über Java-Schnittstellen gesteuert wird:

  1. die Schnittstelle [IArticlesDao] für die Datenzugriffsschicht
  2. die Schnittstelle [IArticlesManager] für die Geschäftslogikschicht

In der Datenzugriffsebene oder DAO-Ebene (Data Access Object) wird häufig mit einem SGBD und somit mit einem Treiber JDBC gearbeitet. Betrachten wir das Grundgerüst einer Klasse, die auf eine Artikeltabelle in einem SGBD zugreift:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // Verbindung zur Datenquelle
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // Abfrage der Artikelliste
        try {
             // Laden des Treibers JDBC
            Class.forName(driverClassName);
            // Verbindung zur BD wird hergestellt
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

Um eine Operation am SGBD durchzuführen, benötigt jede Methode ein [Connection]-Objekt, das die Verbindung zur Datenbank darstellt, über die der Datenaustausch zwischen dieser und dem Java-Code erfolgt. Um dieses Objekt zu erstellen, benötigt man vier Informationen:

String driverClassName
den Klassennamen des Treibers JDBC des SGBD
String url
die URL JDBC der zu verwendenden Datenbank
String user
die Identität, unter der die Verbindung hergestellt wird
String pwd
das Passwort für diese Identität

Wie kann unsere zuvor erwähnte Klasse [ArticlesDaoPlainJdbc] diese Informationen abrufen? Es gibt verschiedene Möglichkeiten:

Lösung 1 – Die Informationen sind fest in der Klasse hinterlegt:

1
2
3
4
5
6
7
8
public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // Verbindung zur Datenquelle
    private final String driverClassName = "org.firebirdsql.jdbc.FBDriver";
    private String url = "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb";
    private String user = "someone";
    private String pwd = "somepassword";
 ....

Der Nachteil dieser Lösung besteht darin, dass der Java-Code geändert werden muss, sobald sich diese Informationen ändern, beispielsweise bei einer Passwortänderung.

Lösung 2 – Die Informationen werden dem Objekt bei seiner Erstellung übergeben:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // Verbindung zur Datenquelle
    private final String driverClassName;
    private String url;
    private String user;
    private String pwd;
 ....
    public ArticlesDaoPlainJdbc(String driverClassName,String url,String user,String pwd) {
      this.driverClassName=driverClassName;
    this.url=url;
    this.user=user;
    this.pwd=pwd;
    ...
    }

Hier erhält das Objekt bei seiner Erstellung die Informationen, die es für seine Arbeit benötigt. Das Problem verlagert sich somit auf den Code, der ihm die vier Informationen übermittelt hat. Wie hat er diese erhalten? Die folgende Klasse [ArticlesManagerWithDataBase] der Geschäftssschicht könnte ein Objekt [ArticlesDaoPlainJdbc] der Datenzugriffsschicht erstellen:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // eine Instanz für den Datenzugriff
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // Erstellung des Datenzugriffsdienstes
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

Man sieht, dass auch hier die für die Erstellung des Objekts [ArticlesDaoPlainJdbc] erforderlichen Informationen dem Konstruktor des Objekts [ArticlesManagerWithDataBase] bereitgestellt werden. Man kann sich vorstellen, dass sie ihm von einer übergeordneten Schicht, wie beispielsweise der Benutzerschnittstellenschicht, übermittelt werden. So gelangt man Schritt für Schritt zur obersten Schicht der Anwendung. Aufgrund ihrer Position wird diese nicht von einer Schicht aufgerufen, die ihr die benötigten Konfigurationsinformationen übermitteln könnte. Es muss daher eine andere Lösung als die Konfiguration über den Konstruktor gefunden werden. Die übliche Lösung zur Konfiguration einer Anwendung auf der Ebene ihrer obersten Schicht ist die Verwendung einer Datei, in der alle Informationen enthalten sind, die sich im Laufe der Zeit ändern können. Es kann mehrere solcher Dateien geben. Beim Start der Anwendung erstellt dann eine Initialisierungsschicht alle oder einen Teil der Objekte, die für die verschiedenen Schichten der Anwendung erforderlich sind.

Es gibt eine große Vielfalt an Konfigurationsdateien. Der aktuelle Trend geht zur Verwendung von XML-Dateien. Diese Option wird von Spring genutzt. Die Datei zur Konfiguration eines [ArticlesDaoPlainJdbc]-Objekts könnte wie folgt aussehen:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- die Datenzugriffsklasse -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
</beans>

Eine Anwendung ist eine Sammlung von Objekten, die Spring als „Beans“ bezeichnet, da sie dem Standard JavaBean zur Benennung von Gettern und Settern für private Felder eines Objekts folgen. Objekte, deren Aufgabe es in einer Anwendung ist, einen Dienst bereitzustellen, werden oft nur in einer einzigen Instanz erstellt. Man bezeichnet sie als Singletons. So wird in unserem hier untersuchten Beispiel einer mehrschichtigen Anwendung der Zugriff auf die Artikel-Datenbank durch ein einziges Exemplar der Klasse [ArticlesDaoPlainJdbc] gewährleistet. Bei einer Webanwendung bedienen diese Serviceobjekte mehrere Clients gleichzeitig. Es wird nicht für jeden Client ein eigenes Serviceobjekt erstellt.

Die oben gezeigte Spring-Konfigurationsdatei ermöglicht die Erstellung eines einzigen Service-Objekts vom Typ [ArticlesDaoPlainJdbc] in einem Paket namens [istia.st.articles.dao]. Die vier für den Konstruktor dieses Objekts erforderlichen Informationen werden innerhalb eines <bean>...</bean>-Tags definiert. Es gibt so viele solcher <bean>-Tags, wie Singletons erstellt werden sollen.

Zu welchem Zeitpunkt erfolgt die Erstellung der in der Spring-Datei definierten Objekte? Die Initialisierung einer Anwendung kann der Methode main dieser Anwendung übertragen werden, sofern sie über eine solche verfügt. Bei einer Webanwendung kann dies die Methode [init] des Haupt-Servlets sein. In jeder Anwendung gibt es eine Methode, die garantiert als erste ausgeführt wird. In der Regel erfolgt in dieser Methode die Erstellung der Singletons.

Nehmen wir ein Beispiel. Angenommen, wir möchten die oben genannte Klasse [ArticlesDaoPlainJdbc] mithilfe eines Tests JUnit testen. Eine Testklasse JUnit verfügt über eine Methode [setUp], die vor allen anderen Methoden ausgeführt wird. Dort wird das Singleton [ArticlesDaoPlainJdbc] erstellt.

Wenn man die Lösung wählt, bei der die Konfigurationsinformationen über den Konstruktor übergeben werden, ergibt sich folgende Testklasse:

public class TestArticlesPlainJdbc extends TestCase {
    // Testet die Materialzugriffsklasse ArticlesDaoPlainJdbc
     // Die Datenquelle ist in Sprintest definiert

     // eine Instanz der getesteten Klasse
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // ruft eine Instanz des Datenzugriffs ab
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

Die aufrufende Klasse [TestArticlesPlainJdbc] muss die vier Informationen kennen, die für die Initialisierung des zu erstellenden Singletons [ArticlesDaoPlainJdbc] erforderlich sind.

Folgt man der Lösung, bei der die Konfigurationsinformationen über eine Konfigurationsdatei übergeben werden, könnte man unter Verwendung der oben beschriebenen Spring-Datei die folgende Testklasse erhalten.

public class TestSpringArticlesPlainJdbc extends TestCase {
    // testet die Artikelzugriffsklasse ArticlesDaoJdbc
     // Die Datenquelle ist in Sprintest definiert

     // eine Instanz der getesteten Klasse
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // ruft eine Instanz des Datenzugriffs ab
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

Hier muss die aufrufende Klasse [TestSpringArticlesPlainJdbc] die für die Initialisierung des zu erstellenden Singletons erforderlichen Informationen nicht kennen. Sie muss lediglich Folgendes wissen:

  1. [springArticlesPlainJdbc.xml]: den Namen der oben beschriebenen Spring-Konfigurationsdatei
  2. [articlesDao]: den Namen des zu erstellenden Singletons

Eine Änderung der Konfigurationsdatei, die über diese beiden Elemente hinausgeht, hat keinerlei Auswirkungen auf den Java-Code. Diese Methode zur Konfiguration der Objekte einer Anwendung ist sehr flexibel. Zur Konfiguration muss die Anwendung lediglich zwei Dinge kennen:

  • den Namen der Spring-Datei, die die Definitionen der zu erstellenden Singletons enthält
  • die Namen dieser Singletons, die dem Java-Code dienen, um über die Konfigurationsdatei eine Referenz auf die Objekte zu erhalten, denen sie zugeordnet wurden

2.2. Abhängigkeitsinjektion und Kontrollumkehr

Lassen Sie uns nun den Begriff der Abhängigkeitsinjektion (Dependency Injection) einführen, der von Spring zur Konfiguration von Anwendungen verwendet wird. Man verwendet auch den Begriff Kontrollumkehr (IoC, Inversion of Control). Betrachten wir die Erstellung des Singletons [ArticlesManagerWithDataBase] aus der Geschäftslogikschicht unserer Anwendung:

Um auf die Daten des SGBD zuzugreifen, muss die Geschäftslogik die Dienste eines Objekts nutzen, das die Schnittstelle [IArticlesDao] implementiert, beispielsweise ein Objekt vom Typ [ArticlesDaoPlainJdbc]. Der Code der Klasse [ArticlesManagerWithDataBase] könnte wie folgt aussehen:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // eine Instanz des Datenzugriffs
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // Erstellung des Datenzugriffsdienstes
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

Die Klasse [ArticlesDaoPlainJdbc] soll hier eine Schnittstelle [IArticlesDao] implementieren:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

Um das für den Betrieb der Klasse erforderliche Singleton vom Typ [IArticlesDao] zu erstellen, verwendet der Konstruktor dieser Klasse explizit den Namen der Implementierungsklasse der Schnittstelle [IArticlesDao]:

articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);

Es besteht also eine harte Abhängigkeit im Code vom Klassennamen. Sollte sich die Implementierungsklasse der Schnittstelle [IArticlesDao] ändern, müsste der Code des oben genannten Konstruktors angepasst werden. Es bestehen folgende Beziehungen zwischen den Objekten:

Die Klasse [ArticlesManagerWithDataBase] übernimmt selbst die Initiative zur Erstellung des Objekts [ArticlesDaoPlainJdbc], das sie benötigt. Um auf den Begriff „Kontrollumkehr“ zurückzukommen: Man könnte sagen, dass sie die „Kontrolle“ darüber hat, das Objekt zu erstellen, das sie benötigt.

Wenn man eine Testklasse JUnit für die Klasse [ArticlesManagerWithDataBase] schreiben müsste, könnte das etwa wie folgt aussehen:

public class TestArticlesManagerWithDataBase extends TestCase {
    // eine Instanz der getesteten Geschäftsklasse
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // erstellt eine Instanz der getesteten Geschäftsklasse
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

Die Testklasse erstellt eine Instanz der Geschäftsklasse [ArticlesManagerWithDataBase], die wiederum in ihrem Konstruktor eine Instanz der Datenzugriffsklasse [ArticlesDaoPlainJdbc] erstellt.

Die Lösung mit Spring macht es überflüssig, dass die Geschäftsklasse [ArticlesManagerWithDataBase] den Namen [ArticlesDaoPlainJdbc] der benötigten Datenzugriffsklasse kennt. Dadurch kann diese geändert werden, ohne den Java-Code der Geschäftsklasse zu verändern. Mit Spring lassen sich beide Singletons – das der Datenzugriffsebene und das der Geschäftsebene – gleichzeitig erstellen. Die Spring-Konfigurationsdatei definiert ein neues Bean:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- die Datenzugriffsklasse -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
</beans>

Die Neuerung besteht in dem Bean, das das zu erstellende Singleton der Geschäftsklasse definiert:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. Die Klasse, die die Bean [articlesManager] implementiert, ist definiert: [ArticlesManagerWithDataBase]
  2. Das Feld [articlesDao] der Bean erhält über das Tag <property name="articlesDao"> einen Wert. Es handelt sich dabei um das in der Klasse [ArticlesManagerWithDataBase] definierte Feld:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // Datenzugriffsschnittstelle
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

Damit das Feld [articlesDao] von Spring und dessen Tag <property> initialisiert werden kann, muss das Feld dem Standard JavaBean entsprechen und es muss eine Methode [setArticlesDao] zur Initialisierung des Feldes [articlesDao] vorhanden sein. Man beachte den Namen der Methode, der sich ganz genau aus dem Namen des Feldes ableitet. Parallel dazu gibt es oft eine Methode [get...], um den Wert des Feldes abzurufen. Hier ist es die Methode [getArticlesDao]. In dieser neuen Version hat die Klasse [ArticlesManagerWithDataBase] keinen Konstruktor mehr. Sie benötigt ihn nicht mehr.

  • Der Wert, der dem Feld [articlesDao] von Spring zugewiesen wird, ist der Wert des Beans [articlesDao], der in seiner Konfigurationsdatei definiert ist:
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
    .............
    </bean>
  • Wenn Spring das Singleton [ArticlesManagerWithDataBase] erstellt, wird es auch das Singleton [ArticlesDaoPlainJdbc] erstellen:
    • Spring erstellt einen Abhängigkeitsgraphen der Beans und stellt fest, dass die Bean [articlesManager] von der Bean [articlesDao] abhängt
    • Es wird die Bean [articlesDao] erstellen, also ein Objekt vom Typ [ArticlesDaoPlainJdbc]
    • anschließend erstellt es die Bean [articlesManager] vom Typ [ArticlesManagerWithDataBase]

Stellen wir uns nun einen Test JUnit für die Klasse [ArticlesManagerWithDataBase] vor. Dieser könnte wie folgt aussehen:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // testet die Geschäftsklasse [ArticlesManagerWithDataBase]

    // eine Instanz der getesteten Geschäftsklasse
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // ruft eine Instanz des Datenzugriffs ab
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

Verfolgen wir den Ablauf der Erstellung der beiden Singletons, die in der Spring-Datei mit dem Namen [springArticlesManagerWithDataBase.xml] definiert sind.

  • Die oben genannte Methode [setUp] fordert eine Referenz auf das Bean mit dem Namen [articlesManager] an
  • Spring durchsucht seine Konfigurationsdatei und findet die Bean „[articlesManager]“. Ist diese bereits erstellt, gibt Spring lediglich eine Referenz auf das Objekt (Singleton) zurück; andernfalls erstellt Spring die Bean.
  • Spring erkennt die Abhängigkeit der Bean „[articlesManager]“ von der Bean „[articlesDao]“. Daher erstellt es das Singleton [articlesDao] vom Typ [ArticlesDaoPlainJdbc], sofern dieses noch nicht erstellt wurde (Singleton).
  • Es erstellt das Singleton [articlesManager] vom Typ [ArticlesManagerWithDataBase]

Dieser Mechanismus lässt sich wie folgt schematisch darstellen:

Zur Erinnerung: Das Grundgerüst der Klasse [ArticlesManagerWithDataBase] lautet:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // Schnittstelle für den Datenzugriff
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

Am Ende der Erstellung der Singletons durch Spring haben wir ein Objekt vom Typ [ArticlesManagerWithDataBase], dessen Feld [articlesDao] initialisiert wurde, ohne dass es weiß, wie. Man sagt, dass eine Abhängigkeit in das Objekt [ArticlesManagerWithDataBase] injiziert wurde. Man spricht auch von einer Umkehrung der Kontrolle: Es ist nicht mehr das Objekt [ArticlesManagerWithDataBase], das die Initiative ergreift, um selbst das Objekt zu erstellen, das die von ihm benötigte Schnittstelle [IArticlesDao] implementiert, sondern die Anwendung auf der obersten Ebene (bei ihrer Initialisierung) sorgt dafür, alle Objekte zu erstellen, die sie benötigt, und verwaltet dabei deren gegenseitige Abhängigkeiten.

Der Hauptvorteil der Konfiguration des Singletons [ArticlesManagerWithDataBase] über eine Spring-Datei besteht darin, dass nun die Implementierungsklasse für das Feld [articlesDao] der Klasse [ArticlesManagerWithDataBase] geändert werden kann, ohne dass deren Code geändert werden muss. Es reicht aus, den Namen der Klasse in der Definition des Beans [articlesDao] in der Spring-Datei zu ändern:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
...
    </bean>

wird beispielsweise zu:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap">
...
    </bean>

Die Bean [ArticlesManagerWithDataBase] wird mit dieser neuen Datenzugriffsklasse arbeiten, ohne es überhaupt zu merken.

2.3. Spring IoC in der Praxis

2.3.1. Beispiel 1

Betrachten wir die folgende Klasse:

package istia.st.springioc.domain;

public class Personne {
  private String nom;
  private int age;

   // Anzeige „Person“
  public String toString() {
    return "nom=[" + this.nom + "], age=[" + this.age + "]";
  }

   // init-close
  public void init() {
    System.out.println("init personne [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy personne [" + this.toString() + "]");
  }

   // Getter-Setter
  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }
}

Die Klasse verfügt über:

  • zwei private Felder „name“ und „alter“
  • die Lese- (get) und Schreibmethoden (set) für diese beiden Felder
  • eine Methode „toString“, um den Wert des Objekts „[Personne]“ in Form einer Zeichenkette abzurufen
  • eine Methode „init“, die von Spring bei der Erstellung des Objekts aufgerufen wird, sowie eine Methode „close“, die bei der Löschung des Objekts aufgerufen wird

Um Objekte vom Typ [Personne] zu erstellen, verwenden wir die folgende Spring-Datei:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

Diese Datei wird den Namen config.xml tragen.

  • Sie definiert zwei Beans mit den Schlüsseln „personne1“ und „personne2“ vom Typ [Personne]
  • Sie initialisiert die Felder [nom, age] jeder Person
  • Es definiert die Methoden, die beim erstmaligen Anlegen des Objekts [init-method] und beim Löschen des Objekts [destroy-method] aufgerufen werden sollen

Für unsere Tests verwenden wir eine einzige Testklasse JUnit, der wir nach und nach Methoden hinzufügen werden. Die erste Version dieser Klasse sieht wie folgt aus:

package istia.st.springioc.tests;

import istia.st.springioc.domain.Personne;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

public class Tests extends TestCase {

   // Bean-Factory
  private ListableBeanFactory bf;

   // Tests initialisieren
  public void setUp() {
    bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
  }

  public void test1() {
    // Abruf der Beans [Personne] anhand ihres Schlüssels aus der Spring-Datei
    Personne personne1 = (Personne) bf.getBean("personne1");
    System.out.println("personne1=" + personne1.toString());
    Personne personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
  }
}

Anmerkungen:

  • Um die in der Datei [config.xml] definierten Beans abzurufen, verwenden wir ein Objekt vom Typ [ListableBeanFactory]. Es gibt weitere Objekttypen, die den Zugriff auf die Beans ermöglichen. Das Objekt [ListableBeanFactory] wird in der Methode [setUp] der Testklasse abgerufen und in einer privaten Variablen gespeichert. Es steht somit für alle Testmethoden zur Verfügung.
  • Die Datei [config.xml] wird im [ClassPath] der Anwendung, c.a.d, in einem der Verzeichnisse abgelegt, die von der Java-Virtual-Machine durchsucht werden, wenn sie nach einer von der Anwendung referenzierten Klasse sucht. Das Objekt [ClassPathResource] dient dazu, eine Ressource im [ClassPath] einer Anwendung zu suchen, in diesem Fall die Datei [config.xml].
  • Spring kann Konfigurationsdateien in verschiedenen Formaten verwenden. Das Objekt [XmlBeanFactory] ermöglicht die Analyse einer Konfigurationsdatei im Format XML.
  • Die Auswertung einer Spring-Datei liefert ein Objekt vom Typ [ListableBeanFactory], hier das Objekt „bf“. Mit diesem Objekt lässt sich ein Bean, der durch den Schlüssel „C“ identifiziert wird, über bf.getBean(C) abrufen.
  • Die Methode [test1] fragt den Wert der Beans mit den Schlüsseln „personne1“ und „personne2“ ab und gibt ihn aus.

Die Struktur des Eclipse-Projekts unserer Anwendung sieht wie folgt aus:

Image

Anmerkungen:

  • Der Ordner [src] enthält den Quellcode. Der kompilierte Code wird in einen Ordner [bin] abgelegt, der hier nicht dargestellt ist.
  • Die Datei [config.xml] befindet sich im Stammverzeichnis des Ordners [src]. Beim Erstellen des Projekts wird sie automatisch in den Ordner „[bin]“ kopiert, der Teil des Anwendungsordners „[ClassPath]“ ist. Dort wird sie vom Objekt „[ClassPathResource]“ gesucht.
  • Der Ordner „[lib]“ enthält drei für die Anwendung erforderliche Java-Bibliotheken:
      • commons-logging.jar und spring-core.jar für die Spring-Klassen
      • junit.jar für die Klassen JUnit
  • Der Ordner [lib] ist ebenfalls Teil des Ordners [ClassPath] der Anwendung

Die Ausführung der Methode [test1] des Tests JUnit liefert folgende Ergebnisse:

18 sept. 2004 11:28:53 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
personne1=nom=[Simon], age=[40]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
personne2=nom=[Brigitte], age=[20]
personne2=nom=[Brigitte], age=[20]

Anmerkungen:

  • Spring protokolliert mithilfe der Bibliothek [commons-logging.jar] eine Reihe von Ereignissen. Diese Protokolle ermöglichen es uns, die Funktionsweise von Spring besser zu verstehen.
  • Die Datei [config.xml] wurde geladen und anschließend
  • Der Vorgang*
Personne personne1 = (Personne) bf.getBean("personne1");

die Erstellung des Beans [personne1] ausgelöst. Das entsprechende Spring-Protokoll ist hier zu sehen. Da in der Definition des Beans [personne1] versehentlich [init-method="init"] angegeben wurde, wurde die Methode [init] des erstellten Objekts [Personne] ausgeführt. Die entsprechende Meldung wird angezeigt.

  • Der Vorgang
System.out.println("personne1=" + personne1.toString());

hat den Wert des angelegten Objekts [Personne] angezeigt.

  • Das gleiche Phänomen wiederholt sich für die Schlüssel-Bean [personne2].
  • Die letzte Operation
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

hat nicht zur Erstellung eines neuen Objekts vom Typ [Personne] geführt. Wäre dies der Fall gewesen, wäre die Methode [init] angezeigt worden, was hier jedoch nicht der Fall ist. Dies ist das Prinzip des Singletons. Spring erstellt standardmäßig nur eine einzige Instanz der Beans aus seiner Konfigurationsdatei. Es handelt sich um einen Objektreferenzdienst. Wenn man ihn nach der Referenz eines noch nicht erstellten Objekts fragt, erstellt er dieses und gibt eine Referenz darauf zurück. Wenn das Objekt bereits erstellt wurde, gibt Spring lediglich eine Referenz darauf zurück.

  • Man kann feststellen, dass von der Methode [close] des Objekts [Personne] keine Spur zu finden ist, obwohl wir sie in der Definition der Beans [destroy-method=close] geschrieben hatten. Möglicherweise wird diese Methode erst ausgeführt, wenn der vom Objekt belegte Speicher vom Garbage Collector freigegeben wird. Zu diesem Zeitpunkt ist die Anwendung bereits beendet, sodass die Ausgabe auf dem Bildschirm keine Wirkung mehr hat. Dies muss noch überprüft werden.

Da wir nun die Grundlagen einer Spring-Konfiguration verstanden haben, werden wir unsere Erklärungen von nun an etwas zügiger vorantreiben.

2.3.2. Beispiel 2

Betrachten wir die folgende neue Klasse [Voiture]:

package istia.st.springioc.domain;

public class Voiture {
  private String marque;
  private String type;
  private Personne propriétaire;

   // Konstruktoren

  public Voiture() {
  }

  public Voiture(String marque, String type, Personne propriétaire) {
    this.marque = marque;
    this.type = type;
    this.propriétaire = propriétaire;
  }

   // toString
  public String toString() {
    return "Voiture : marque=[" + this.marque + "] type=[" + this.type
        + "] propriétaire=[" + this.propriétaire + "]";
  }

     // Getter-Setter
  public String getMarque() {
    return marque;
  }

  public void setMarque(String marque) {
    this.marque = marque;
  }

  public Personne getPropriétaire() {
    return propriétaire;
  }

  public void setPropriétaire(Personne propriétaire) {
    this.propriétaire = propriétaire;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

   // init-close
  public void init() {
    System.out.println("init voiture [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy voiture [" + this.toString() + "]");
  }

}

Die Klasse verfügt über:

  • drei private Felder: „Typ“, „Marke“ und „Eigentümer“. Diese Felder können über öffentliche get- und set-Methoden der Bean initialisiert und ausgelesen werden. Sie können ebenfalls mithilfe des Konstruktors „Auto(String, String, Person)“ initialisiert werden. Die Klasse verfügt zudem über einen Konstruktor ohne Argumente, um dem Standard JavaBean zu entsprechen.
  • eine Methode „toString“, um den Wert des Objekts „[Voiture]“ in Form einer Zeichenkette abzurufen
  • eine Methode „init“, die von Spring unmittelbar nach der Erstellung des Objekts aufgerufen wird, sowie eine Methode „close“, die bei der Löschung des Objekts aufgerufen wird

Um Objekte vom Typ [Voiture] zu erstellen, verwenden wir die folgende Spring-Datei [config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <constructor-arg index="0">
            <value>Peugeot</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>307</value>
        </constructor-arg>
        <constructor-arg index="2">
            <ref bean="personne2"></ref>
        </constructor-arg>
    </bean>
</beans>

Diese Datei ergänzt die bisherigen Definitionen um eine Bean mit dem Schlüssel „voiture1“ vom Typ [Voiture]. Um diese Bean zu initialisieren, hätte man schreiben können:

    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <property name="marque">
            <value>Peugeot</value>
        </property>
        <property name="type">
            <value>307</value>
        </property>
        <property name="propriétaire">
            <ref bean="personne2"/>
        </property>
    </bean>

Anstatt diese bereits vorgestellte Methode zu wählen, haben wir uns hier dafür entschieden, den Konstruktor „Auto(String, String, Person)“ der Klasse zu verwenden. Außerdem definiert die Bean [voiture1] die Methode, die beim ersten Erstellen des Objekts [init-method] aufgerufen werden soll, sowie die Methode, die beim Löschen des Objekts [destroy-method] aufgerufen werden soll.

Für unsere Tests verwenden wir die bereits vorgestellte Testklasse JUnit und fügen ihr die folgende Methode [test2] hinzu:

1
2
3
4
5
  public void test2() {
     // Bean abrufen [voiture1]
    Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
    System.out.println("Voiture1=" + Voiture1.toString());
  }

Die Methode [test2] ruft die Bean [voiture1] ab und zeigt sie an.

Die Struktur des Eclipse-Projekts bleibt gegenüber dem vorherigen Test unverändert. Die Ausführung der Methode [test2] aus dem Test JUnit liefert folgende Ergebnisse:

18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'voiture1'
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture(java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]

Anmerkungen:

  1. Die Methode [test2] fordert eine Referenz auf die Bean [voiture1] an
  2. Zeile 4: Spring beginnt mit der Erstellung der Bean [voiture1], da diese Bean noch nicht erstellt wurde (Singleton)
  3. Zeile 6: Da die Bean [voiture1] auf die Bean [personne2] verweist, wird diese wiederum erstellt
  4. Zeile 7: Die Bean [personne2] wurde erstellt. Anschließend wird ihre Methode [init] ausgeführt.
  5. Zeile 9: Spring gibt an, dass es einen Konstruktor verwenden wird, um die Bean [voiture1] zu erstellen
  6. Zeile 10: Die Bean [voiture1] wurde erstellt. Anschließend wird ihre Methode [init] ausgeführt.
  7. Zeile 11: Die Methode [test2] gibt den Wert der Bean [voiture1] aus

2.3.3. Beispiel 3

Wir führen die folgende neue Klasse [GroupePersonnes] ein:

package istia.st.springioc.domain;

import java.util.Map;

public class GroupePersonnes {
  private Personne[] membres;
  private Map groupesDeTravail;

   // Getter – Setter
  public Personne[] getMembres() {
    return membres;
  }

  public void setMembres(Personne[] membres) {
    this.membres = membres;
  }

  public Map getGroupesDeTravail() {
    return groupesDeTravail;
  }

  public void setGroupesDeTravail(Map groupesDeTravail) {
    this.groupesDeTravail = groupesDeTravail;
  }

   // Anzeige
  public String toString() {
    String liste = "membres : ";
    for (int i = 0; i < this.membres.length; i++) {
      liste += "[" + this.membres[i].toString() + "]";
    }
    return liste + ", groupes de travail = " + this.groupesDeTravail.toString();
  }

   // Initialisierung und Schließung
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy GroupePersonnes [" + this.toString() + "]");
  }
}

Ihre beiden privaten Mitglieder sind:

Mitglieder: ein Array mit den Mitgliedern der Gruppe

groupesDeTravail: ein Dictionary, das eine Person einer Arbeitsgruppe zuordnet

Hier ist zu beachten, dass die Klasse [GroupePersonnes] keinen Konstruktor ohne Argumente definiert, um dem Standard JavaBean zu entsprechen. Wir erinnern daran, dass es bei Fehlen jeglichen Konstruktors einen „Standard“-Konstruktor gibt, nämlich den Konstruktor ohne Argumente, der keine Aktion ausführt.

Hier soll gezeigt werden, wie Spring die Initialisierung komplexer Objekte ermöglicht, beispielsweise Objekte mit Feldern vom Typ Array oder Dictionary. Wir fügen der vorherigen Spring-Datei [config.xml] eine neue Bean hinzu:

    <bean id="groupe1" class="istia.st.springioc.domain.GroupePersonnes" 
        init-method="init" destroy-method="close">
        <property name="membres">
            <list>
                <ref bean="personne1"/>
                <ref bean="personne2"/>
            </list>
        </property>
        <property name="groupesDeTravail">
            <map>
                <entry key="Brigitte">
                    <value>Marketing</value>
                </entry>
                <entry key="Simon">
                    <value>Ressources humaines</value>
                </entry>
            </map>
        </property>
    </bean>
  1. Mit dem Tag <list> lässt sich ein Feld vom Typ Array oder ein Feld, das die Schnittstelle List implementiert, mit verschiedenen Werten initialisieren.
  2. Mit dem Tag <map> lässt sich dasselbe mit einem Feld erreichen, das die Schnittstelle `Map` implementiert.

Für unsere Tests verwenden wir die bereits vorgestellte Testklasse JUnit und fügen ihr die folgende Methode [test3] hinzu:

1
2
3
4
5
  public void test3() {
    // Abruf der Bean [groupe1]
    GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
    System.out.println("groupe1=" + groupe1.toString());
  }

Die Methode [test3] ruft die Bean [groupe1] ab und zeigt sie an.

Die Struktur des Eclipse-Projekts bleibt unverändert gegenüber dem vorherigen Test. Die Ausführung der Methode [test3] aus dem Test JUnit liefert folgende Ergebnisse:

18 sept. 2004 15:51:45 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'groupe1'
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
init GroupePersonnes [membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}]
groupe1=membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}

Anmerkungen:

  • Die Methode [test3] fordert eine Referenz auf die Bean [groupe1] an
  • Zeile 4: Spring beginnt mit der Erstellung dieser Bean
  • Da die Bean [groupe1] auf die Beans [personne1] und [personne2] verweist, werden diese beiden Beans erstellt (Zeilen 6 und 9) und ihre init-Methode ausgeführt (Zeilen 7 und 10)
  • Zeile 11: Die Bean [groupe1] wurde erstellt. Ihre Methode [init] wird nun ausgeführt.
  • Zeile 12: Anzeige, die von der Methode [test3] angefordert wurde.

2.4. Spring zur Konfiguration von dreischichtigen Webanwendungen

2.4.1. Allgemeine Architektur der Anwendung

Wir möchten eine 3-Tier-Anwendung mit folgender Struktur erstellen:

  • Die drei Schichten werden durch die Verwendung von Java-Schnittstellen voneinander unabhängig gemacht
  • Die Integration der drei Schichten erfolgt über Spring
  • Für jede der drei Schichten werden separate Pakete erstellt, die „Control“, „Domain“ und „Dao“ heißen. Ein zusätzliches Paket enthält die Testanwendungen.

Die Struktur der Anwendung unter Eclipse könnte wie folgt aussehen:

Image

2.4.2. Die Datenzugriffsschicht DAO

Die Schicht DAO wird die folgende Schnittstelle implementieren:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • Schreiben Sie zwei Klassen Dao1Impl1 und Dao1Impl2, die die Schnittstelle IDao1 implementieren. Die Methode Dao1Impl1.doSomethingInDaoLayer gibt a+b zurück, und die Methode Dao1Impl2.doSomethingInDaoLayer gibt a-b zurück.
  • Schreiben Sie eine Testklasse JUnit, die die beiden vorherigen Klassen testet

2.4.3. Die Geschäftslogik-Schicht

Die Geschäftsebene implementiert die folgende Schnittstelle:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • Schreiben Sie zwei Klassen Domain1Impl1 und Domain1Impl2, die die Schnittstelle IDomain1 implementieren. Diese Klassen verfügen über einen Konstruktor, der einen Parameter vom Typ IDao1 entgegennimmt. Die Methode Domain1Impl1.doSomethingInDomainLayer erhöht a und b um eins und übergibt diese beiden Parameter anschließend an die Methode doSomethingInDaoLayer des empfangenen Objekts vom Typ IDao1. Die Methode Domain1Impl2.doSomethingInDomainLayer hingegen verringert a und b um jeweils eine Einheit, bevor sie dasselbe tut.
  • Eine Testklasse JUnit schreiben, die die beiden vorherigen Klassen testet

2.4.4. Die Benutzeroberflächenschicht

Die Benutzeroberflächenschicht implementiert die folgende Schnittstelle:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • Schreiben Sie zwei Klassen Control1Impl1 und Control1Impl2, die die Schnittstelle IControl1 implementieren. Diese Klassen verfügen über einen Konstruktor, der einen Parameter vom Typ IDomain1 entgegennimmt. Die Methode Control1Impl1.doSomethingInControlLayer erhöht a und b um eins und übergibt diese beiden Parameter anschließend an die Methode doSomethingInDomainLayer des empfangenen Objekts vom Typ IDomain1. Die Methode Control11Impl2.doSomethingInControlLayer hingegen verringert a und b um jeweils eine Einheit, bevor sie dasselbe tut.
  • Eine Testklasse JUnit schreiben, die die beiden vorherigen Klassen testet

2.4.5. Integration mit Spring

  • Erstellen Sie eine Spring-Konfigurationsdatei, die festlegt, welche Klassen jede der drei vorangegangenen Schichten verwenden soll
  • Schreiben Sie eine Testklasse JUnit, die verschiedene Spring-Konfigurationen verwendet, um die Flexibilität der geschriebenen Anwendung hervorzuheben
  • Schreiben Sie eine eigenständige Anwendung (Methode „main“), die der Schnittstelle IControl1 zwei Parameter übergibt und das von der Schnittstelle gerenderte Ergebnis anzeigt.

2.4.6. Eine Lösung

2.4.6.1. Das Eclipse-Projekt

Image

Die Archive des Ordners [lib] wurden dem Ordner [ClassPath] des Projekts hinzugefügt.

2.4.6.2. Das Paket [istia.st.demo.dao]

Die Schnittstelle:

1
2
3
4
5
6
7
8
9
package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}

Eine erste Implementierungsklasse:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public class Dao1Impl1 implements IDao1 {

     // Es wird etwas in der Schicht [dao] ausgeführt
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

Eine zweite Implementierungsklasse:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *
 */
public class Dao1Impl2 implements IDao1 {

     // Es wird etwas in der Schicht [dao] ausgeführt
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. Das Paket [istia.st.demo.domain]

Die Schnittstelle:

package istia.st.demo.domain;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDomain1 {

     // Wir machen etwas in der Schicht [domain]
  public int doSomethingInDomainLayer(int a, int b);
}

Eine erste Implementierungsklasse:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl1 implements IDomain1 {

     // Der Zugriffsdienst auf die Schicht [dao]
  private IDao1 dao1;

  public Domain1Impl1() {
     // Konstruktor ohne Argumente
  }

     // speichert den Zugriffsdienst auf die Schicht [dao]
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // Es wird etwas in der Schicht [domain] ausgeführt
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

Eine zweite Implementierungsklasse:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl2 implements IDomain1 {

     // der Zugriffsdienst auf die Schicht [dao]
  private IDao1 dao1;

  public Domain1Impl2() {
     // Konstruktor ohne Argumente
  }

     // speichert den Zugriffsdienst auf die Schicht [dao]
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // Es wird etwas in der Schicht [domain] ausgeführt
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. Das Paket [istia.st.demo.control]

Die Schnittstelle

1
2
3
4
5
6
7
8
9
package istia.st.demo.control;

/**
 * @author ST-ISTIA
 *  
 */
public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}

Eine erste Implementierungsklasse:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // Geschäftsklasse in der Schicht [domain]
    private IDomain1 domain1;

  public Control1Impl1() {
     // Konstruktor ohne Argumente
  }

     // Speicherung des Zugriffsdienstes auf die Schicht [domain]
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // Es wird etwas ausgeführt
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

Eine zweite Implementierungsklasse:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl2 implements IControl1 {

     // die Zugriffsklasse auf die Schicht [domain]
    private IDomain1 domain1;

  public Control1Impl2() {
     // Konstruktor ohne Argumente
  }

     // speichert die Zugriffsklasse auf die Schicht [domain]
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // es wird etwas ausgeführt
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. Die Konfigurationsdateien [Spring]

Eine erste: [springMainTest1.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- die DAO-Klasse -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
    </bean>
     <!-- die Fachklasse -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- die Kontrollklasse -->
    <bean id="control" class="istia.st.demo.control.Control1Impl1">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

Ein zweites [springMainTest2.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- die DAO-Klasse -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
    </bean>
     <!-- die Fachklasse -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- die Kontrollklasse -->
    <bean id="control" class="istia.st.demo.control.Control1Impl2">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

2.4.6.6. Das Testpaket [istia.st.demo.tests]

Ein Test vom Typ [main]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest1 {
  public static void main(String[] arguments) {
     // wird eine Implementierung der Schnittstelle abgerufen IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // die Klasse wird verwendet
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // Das Ergebnis wird angezeigt
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Die in der Eclipse-Konsole angezeigten Ergebnisse:

11 mars 2005 11:25:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest1.xml]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl1(istia.st.demo.dao.IDao1)]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl1(istia.st.demo.domain.IDomain1)]
control(10,20)=34

Ein weiterer Test unter Verwendung der zweiten Konfigurationsdatei [Spring]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest2 {
  public static void main(String[] arguments) {
     // Man ruft eine Implementierung der Schnittstelle IControl1 ab
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // man verwendet die Klasse
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // das Ergebnis wird angezeigt
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Die in der Eclipse-Konsole angezeigten Ergebnisse:

11 mars 2005 11:28:52 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest2.xml]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl2(istia.st.demo.dao.IDao1)]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl2(istia.st.demo.domain.IDomain1)]
control(10,20)=-10

Und schließlich ein JUnit-Test:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

/**
 * @author ST-ISTIA
 *  
 */
public class JunitTest2Control1 extends TestCase {
  public void testControl1() {
     // wird eine Implementierung der Schnittstelle abgerufen IControl1
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // die Klasse wird verwendet
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // wird eine weitere Implementierung der Schnittstelle abgerufen: IControl1
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // Man verwendet die Klasse
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusion

Das Spring-Framework bietet sowohl bei der Anwendungsarchitektur als auch bei der Konfiguration echte Flexibilität. Wir haben das Konzept IoC verwendet, eine der beiden Säulen von Spring. Die andere Säule ist AOP (Aspect Oriented Programming), die wir nicht vorgestellt haben. Sie ermöglicht es, einer Klassenmethode per Konfiguration „Verhalten“ hinzuzufügen, ohne deren Code zu ändern. Vereinfacht gesagt ermöglicht AOP das Filtern von Aufrufen bestimmter Methoden:

  • Der Filter kann vor oder nach der Zielmethode M oder in beiden Fällen ausgeführt werden.
  • Die M-Methode ignoriert die Existenz dieser Filter. Diese werden in der Spring-Konfigurationsdatei definiert.
  • Der Code der M-Methode wird nicht verändert. Filter sind Java-Klassen, die instanziiert werden müssen. Spring stellt vordefinierte Filter bereit, insbesondere zur Verwaltung von Transaktionen von SGBD.
  • Filter sind Beans und werden als solche in der Spring-Konfigurationsdatei als Beans definiert.

Ein gängiger Filter ist der Transaktionsfilter. Nehmen wir eine Methode M der Geschäftslogikschicht, die zwei untrennbare Operationen an Daten durchführt (Arbeitseinheit). Sie ruft zwei Methoden M1 und M2 der Schicht DAO auf, um diese beiden Operationen durchzuführen.

Da sich die Methode M in der Fachschicht befindet, abstrahiert sie vom Datenträger dieser Daten. Sie muss beispielsweise nicht davon ausgehen, dass sich die Daten in einem SGBD befinden und dass sie die beiden Aufrufe der Methoden M1 und M2 in eine Transaktion von SGBD einbetten muss. Es ist Aufgabe der Schicht DAO, sich um diese Details zu kümmern. Eine Lösung für das vorgenannte Problem besteht daher darin, in der Schicht DAO eine Methode anzulegen, die ihrerseits die Methoden M1 und M2 aufruft, die sie in eine Transaktion von SGBD einbinden würde.

Die Filterlösung AOP ist flexibler. Sie ermöglicht es, einen Filter zu definieren, der vor dem Aufruf von M eine Transaktion startet und nach dem Aufruf je nach Fall ein Commit oder ein Rollback durchführt.

Dieser Ansatz bietet mehrere Vorteile:

  • Sobald der Filter definiert ist, kann er auf mehrere Methoden angewendet werden, beispielsweise auf alle, die eine Transaktion benötigen
  • die so gefilterten Methoden müssen nicht neu geschrieben werden
  • Da die zu verwendenden Filter über die Konfiguration definiert werden, können sie geändert werden

Zusätzlich zu den Konzepten IoC und AOP bietet Spring zahlreiche Hilfsklassen für dreischichtige Anwendungen:

  • für JDBC, SqlMap (iBatis), Hibernate, JDO (Java Data Object) in der Schicht DAO
  • für das Modell MVC in der Benutzeroberflächenschicht

Weitere Informationen: http://www.springframework.org.