Skip to content

2. Artykuł 1 – Spring IoC

Cele niniejszego dokumentu:

  • zapoznanie się z możliwościami konfiguracji i integracji frameworka Spring (http://www.springframework.org)
  • zdefiniowanie i wykorzystanie pojęcia IoC (Inversion of Control), zwanego również wstrzykiwaniem zależności (Dependency Injection)

2.1. Skonfigurowanie aplikacji trójwarstwowej przy użyciu Spring

Rozważmy klasyczną aplikację trójwarstwową:

Załóżmy, że dostęp do warstwy biznesowej i DAO jest kontrolowany przez interfejsy Java:

  1. interfejs [IArticlesDao] dla warstwy dostępu do danych
  1. interfejs [IArticlesManager] dla warstwy biznesowej

W warstwie dostępu do danych lub warstwie DAO (Data Access Object) często pracuje się z obiektem SGBD, a tym samym z sterownikiem JDBC. Rozważmy szkielet klasy uzyskującej dostęp do tabeli artykułów w SGBD:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // połączenie ze źródłem danych
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // żądanie listy artykułów
        try {
             // ładowanie sterownika JDBC
            Class.forName(driverClassName);
            // tworzenie połączenia z BD
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

Aby wykonać operację na pliku SGBD, każda metoda wymaga obiektu [Connection], który reprezentuje połączenie z bazą danych, przez które będą przebiegać wymiany danych między bazą a kodem Java. Aby utworzyć ten obiekt, potrzebne są cztery informacje:

String driverClassName
nazwa klasy sterownika JDBC dla SGBD
String url
adres URL JDBC bazy, z której ma być korzystano
String user
identyfikator, pod którym tworzone jest połączenie
String pwd
hasło dla tego identyfikatora

W jaki sposób nasza poprzednia klasa [ArticlesDaoPlainJdbc] może uzyskać te informacje? Istnieje kilka możliwości:

rozwiązanie 1 – informacje są zakodowane na stałe w klasie:

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

     // połączenie ze źródłem danych
    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";
 ....

Wadą tego rozwiązania jest konieczność modyfikacji kodu Java za każdym razem, gdy nastąpi zmiana tych informacji, na przykład zmiana hasła.

rozwiązanie 2 – informacje są przekazywane do obiektu podczas jego tworzenia:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // połączenie ze źródłem danych
    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;
    ...
    }

W tym przypadku obiekt otrzymuje podczas tworzenia informacje niezbędne do działania. Problem przenosi się zatem na kod, który przekazał mu te cztery informacje. W jaki sposób je uzyskał? Następująca klasa [ArticlesManagerWithDataBase] z warstwy biznesowej mogłaby utworzyć obiekt [ArticlesDaoPlainJdbc] z warstwy dostępu do danych:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // instancja dostępu do danych
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // utworzenie usługi dostępu do danych
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

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

Widać, że również w tym przypadku informacje niezbędne do utworzenia obiektu [ArticlesDaoPlainJdbc] są przekazywane konstruktorowi obiektu [ArticlesManagerWithDataBase]. Można sobie wyobrazić, że są one przekazywane przez wyższą warstwę, taką jak warstwa interfejsu użytkownika. W ten sposób, krok po kroku, dochodzimy do najwyższej warstwy aplikacji. Ze względu na swoje położenie warstwa ta nie jest wywoływana przez żadną warstwę, która mogłaby przekazać jej potrzebne informacje konfiguracyjne. Należy zatem znaleźć inne rozwiązanie niż konfiguracja za pomocą konstruktora. Typowym rozwiązaniem służącym do konfiguracji aplikacji na poziomie jej najwyższej warstwy jest wykorzystanie pliku, w którym znajdują się wszystkie informacje mogące ulec zmianie w czasie. Plików takich może być kilka. Podczas uruchamiania aplikacji warstwa inicjalizacyjna utworzy wówczas wszystkie lub część obiektów niezbędnych dla poszczególnych warstw aplikacji.

Istnieje szeroka gama plików konfiguracyjnych. Obecnie dominuje tendencja do stosowania plików XML. Takie rozwiązanie wybrał Spring. Plik konfigurujący obiekt [ArticlesDaoPlainJdbc] mógłby wyglądać następująco:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- klasa dostępu do danych -->
    <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>

Aplikacja to zbiór obiektów, które Spring nazywa „beanami”, ponieważ są one zgodne ze standardem JavaBean dotyczącym nazewnictwa metod dostępu (getterów/setterów) do pól prywatnych obiektu. Obiekty, których rolą w aplikacji jest świadczenie usługi, są często tworzone w jednym egzemplarzu. Nazywa się je singletonami. Tak więc w naszym przykładzie aplikacji wielowarstwowej, którą tutaj omawiamy, dostęp do bazy artykułów będzie zapewniony przez jedyny egzemplarz klasy [ArticlesDaoPlainJdbc]. W przypadku aplikacji internetowej obiekty usługowe obsługują wielu klientów jednocześnie. Nie tworzy się osobnego obiektu usługowego dla każdego klienta.

Powyższy plik konfiguracyjny Springa pozwala na utworzenie pojedynczego obiektu usługowego typu [ArticlesDaoPlainJdbc] w pakiecie o nazwie [istia.st.articles.dao]. Cztery informacje niezbędne dla konstruktora tego obiektu są zdefiniowane wewnątrz tagu <bean>...</bean>. Liczba takich tagów <bean> będzie równa liczbie singletonów do utworzenia.

W którym momencie nastąpi tworzenie obiektów zdefiniowanych w pliku Spring? Inicjalizacja aplikacji może zostać powierzona metodzie main tej samej aplikacji, o ile taka istnieje. W przypadku aplikacji internetowej może to być metoda [init] głównego serwletu. W każdej aplikacji znajduje się metoda, która z pewnością zostanie wykonana jako pierwsza. Zazwyczaj to właśnie w niej odbywa się tworzenie singletonów.

Weźmy przykład. Załóżmy, że chcemy przetestować poprzednią klasę [ArticlesDaoPlainJdbc] za pomocą testu JUnit. Klasa testowa JUnit posiada metodę [setUp], która jest wykonywana przed wszystkimi innymi metodami. To właśnie tam utworzymy singleton [ArticlesDaoPlainJdbc].

Jeśli zastosujemy rozwiązanie polegające na przekazywaniu informacji konfiguracyjnych za pomocą konstruktora, otrzymamy następującą klasę testową:

public class TestArticlesPlainJdbc extends TestCase {
    // testuje klasę dostępu do artykułów ArticlesDaoPlainJdbc
     // źródło danych jest zdefiniowane w sprintest

     // instancja testowanej klasy
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // pobiera instancję klasy dostępu do danych
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

Klasa wywołująca [TestArticlesPlainJdbc] musi znać cztery informacje niezbędne do zainicjowania singletonu [ArticlesDaoPlainJdbc], który ma zostać utworzony.

Jeśli zastosujemy rozwiązanie polegające na przekazywaniu informacji konfiguracyjnych za pomocą pliku konfiguracyjnego, moglibyśmy uzyskać następującą klasę testową, korzystając z opisanego powyżej pliku Spring.

public class TestSpringArticlesPlainJdbc extends TestCase {
    // testuje klasę dostępu do artykułów ArticlesDaoJdbc
     // źródło danych jest zdefiniowane w sprintest

     // instancja testowanej klasy
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // pobiera instancję klasy dostępu do danych
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

W tym przypadku klasa wywołująca [TestSpringArticlesPlainJdbc] nie musi znać informacji niezbędnych do zainicjowania tworzonego singletonu. Musi jedynie znać:

  1. [springArticlesPlainJdbc.xml]: nazwę opisanego powyżej pliku konfiguracyjnego Spring
  2. [articlesDao]: nazwę singletonu, który ma zostać utworzony

Zmiana w pliku konfiguracyjnym, poza tymi dwoma elementami, nie ma żadnego wpływu na kod Java. Ta metoda konfiguracji obiektów aplikacji jest bardzo elastyczna. Aby się skonfigurować, aplikacja musi znać tylko dwie rzeczy:

  • nazwę pliku Spring zawierającego definicje singletonów, które mają zostać utworzone
  • nazwy tych singletonów, które służą kodowi Java do uzyskania odwołania do obiektów, z którymi zostały powiązane za pomocą pliku konfiguracyjnego

2.2. Wstrzykiwanie zależności i odwrócenie kontroli

Wprowadźmy teraz pojęcie wstrzykiwania zależności (Dependency Injection) stosowanego przez Spring do konfiguracji aplikacji. Używa się również terminu odwrócenie kontroli (IoC, Inversion of Control). Rozważmy utworzenie singletonu [ArticlesManagerWithDataBase] w warstwie biznesowej naszej aplikacji:

Aby uzyskać dostęp do danych singletonu SGBD, warstwa biznesowa musi korzystać z usług obiektu implementującego interfejs [IArticlesDao], na przykład obiektu typu [ArticlesDaoPlainJdbc]. Kod klasy [ArticlesManagerWithDataBase] mógłby wyglądać następująco:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // instancja modułu dostępu do danych
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // utworzenie usługi dostępu do danych
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

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

Klasa [ArticlesDaoPlainJdbc] ma tutaj implementować interfejs [IArticlesDao]:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

Aby utworzyć singleton typu [IArticlesDao] niezbędny do działania klasy, jej konstruktor wyraźnie wykorzystuje nazwę klasy implementującej interfejs [IArticlesDao]:

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

W kodzie występuje zatem twarda zależność od nazwy klasy. Gdyby zmieniła się klasa implementująca interfejs [IArticlesDao], kod powyższego konstruktora musiałby zostać zmodyfikowany. Pomiędzy obiektami występują następujące relacje:

Klasa [ArticlesManagerWithDataBase] sama inicjuje utworzenie obiektu [ArticlesDaoPlainJdbc], którego potrzebuje. Wracając do terminu „odwrócenie kontroli”, można powiedzieć, że to ona ma „kontrolę” nad utworzeniem obiektu, którego potrzebuje.

Gdybyśmy mieli napisać klasę testową JUnit dla klasy [ArticlesManagerWithDataBase], mogłaby ona wyglądać mniej więcej tak:

public class TestArticlesManagerWithDataBase extends TestCase {
    // instancja testowanej klasy biznesowej
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // tworzy instancję testowanej klasy biznesowej
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

Klasa testowa tworzy instancję klasy biznesowej [ArticlesManagerWithDataBase], która z kolei w swoim konstruktorze tworzy instancję klasy dostępu do danych [ArticlesDaoPlainJdbc].

Rozwiązanie oparte na Springiem wyeliminuje konieczność, aby klasa biznesowa [ArticlesManagerWithDataBase] znała nazwę [ArticlesDaoPlainJdbc] klasy dostępu do danych, której potrzebuje. Umożliwi to zmianę tej nazwy bez ingerencji w kod Java klasy biznesowej. Spring umożliwi jednoczesne utworzenie dwóch singletonów: dla warstwy dostępu do danych oraz dla warstwy biznesowej. Plik konfiguracyjny Springa zdefiniuje nową fasolkę:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- klasa dostępu do danych -->
    <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>

Nowością jest bean definiujący singleton klasy biznesowej, który ma zostać utworzony:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. zdefiniowano klasę implementującą bean [articlesManager]: [ArticlesManagerWithDataBase]
  2. pole [articlesDao] fasoli otrzymuje wartość za pomocą tagu <property name="articlesDao">. Jest to pole zdefiniowane w klasie [ArticlesManagerWithDataBase]:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // interfejs dostępu do danych
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

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

Aby pole [articlesDao] mogło zostać zainicjowane przez Springa za pomocą tagu <property>, pole to musi być zgodne ze standardem JavaBean oraz musi istnieć metoda [setArticlesDao] służąca do inicjalizacji pola [articlesDao]. Należy zwrócić uwagę na nazwę metody, która jest ściśle wyprowadzona z nazwy pola. Równolegle często istnieje metoda [get...] służąca do pobrania wartości pola. W tym przypadku jest to metoda [getArticlesDao]. W tej nowej wersji klasa [ArticlesManagerWithDataBase] nie posiada już konstruktora. Nie jest on już potrzebny.

  • Wartość, która zostanie przypisana do pola [articlesDao] przez Spring, pochodzi z bean’a [articlesDao] zdefiniowanego w jego pliku konfiguracyjnym:
    <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>
  • gdy Spring utworzy singleton [ArticlesManagerWithDataBase], będzie musiał również utworzyć singleton [ArticlesDaoPlainJdbc]:
    • Spring utworzy graf zależności beanów i stwierdzi, że bean [articlesManager] zależy od beana [articlesDao]
    • utworzy bean [articlesDao], a więc obiekt typu [ArticlesDaoPlainJdbc]
    • następnie utworzy bean [articlesManager] typu [ArticlesManagerWithDataBase]

Wyobraźmy sobie teraz test JUnit dla klasy [ArticlesManagerWithDataBase]. Mógłby on wyglądać następująco:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // testuje klasę biznesową [ArticlesManagerWithDataBase]

    // instancja testowanej klasy biznesowej
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // pobiera instancję interfejsu dostępu do danych
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

Prześledźmy proces tworzenia dwóch singletonów zdefiniowanych w pliku Spring o nazwie [springArticlesManagerWithDataBase.xml].

  • Powyższa metoda [setUp] żąda odwołania do bean o nazwie [articlesManager]
  • Spring sprawdza swój plik konfiguracyjny i znajduje bean o nazwie [articlesManager]. Jeśli jest on już utworzony, zwraca po prostu odwołanie do obiektu (singletonu), w przeciwnym razie tworzy go.
  • Spring wykrywa zależność beana [articlesManager] od beana [articlesDao]. W związku z tym tworzy singleton [articlesDao] typu [ArticlesDaoPlainJdbc], jeśli ten jeszcze nie został utworzony (singleton).
  • Tworzy singleton [articlesManager] typu [ArticlesManagerWithDataBase]

Mechanizm ten można przedstawić schematycznie w następujący sposób:

Przypomnijmy szkielet klasy [ArticlesManagerWithDataBase]:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // interfejs dostępu do danych
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

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

Po zakończeniu tworzenia singletonów przez Spring otrzymujemy obiekt typu [ArticlesManagerWithDataBase], którego pole [articlesDao] zostało zainicjowane w sposób dla niego nieznany. Mówi się, że do obiektu [ArticlesManagerWithDataBase] wstrzyknięto zależność. Mówi się również, że nastąpiło odwrócenie kontroli: to już nie obiekt [ArticlesManagerWithDataBase] podejmuje inicjatywę samodzielnego tworzenia obiektu implementującego interfejs [IArticlesDao], którego potrzebuje, ale to aplikacja na najwyższym poziomie (podczas inicjalizacji) dba o utworzenie wszystkich potrzebnych jej obiektów, zarządzając wzajemnymi zależnościami między nimi.

Główną zaletą konfiguracji singletonu [ArticlesManagerWithDataBase] za pomocą pliku Spring jest to, że teraz można zmienić klasę implementującą odpowiadającą polu [articlesDao] w klasie [ArticlesManagerWithDataBase] bez konieczności modyfikowania kodu tej klasy. Wystarczy zmienić nazwę klasy w definicji beana [articlesDao] w pliku Spring:

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

stanie się na przykład:

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

Bean [ArticlesManagerWithDataBase] będzie współpracował z tą nową klasą dostępu do danych, nawet o tym nie wiedząc.

2.3. Spring IoC w praktyce

2.3.1. Przykład 1

Rozważmy następującą klasę:

package istia.st.springioc.domain;

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

   // wyświetlanie osoby
  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() + "]");
  }

   // metody pobierające i ustawiające
  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;
  }
}

Klasa ta zawiera:

  • dwa pola prywatne: „nazwa” i „wiek”
  • metody odczytu (get) i zapisu (set) tych dwóch pól
  • metodę toString służącą do pobrania wartości obiektu [Personne] w postaci ciągu znaków
  • metodę init, która zostanie wywołana przez Spring podczas tworzenia obiektu, oraz metodę close, która zostanie wywołana podczas niszczenia obiektu

Aby utworzyć obiekty typu [Personne], wykorzystamy następujący plik Spring:

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

Plik ten będzie nosił nazwę config.xml.

  • Definiuje on dwa bean'y o kluczach „osoba1” i „osoba2” typu [Personne]
  • inicjuje pola [nom, age] dla każdej osoby
  • definiuje metody, które mają zostać wywołane podczas początkowego tworzenia obiektu [init-method] oraz podczas niszczenia obiektu [destroy-method]

Do naszych testów wykorzystamy jedną klasę testową JUnit, do której będziemy sukcesywnie dodawać metody. Pierwsza wersja tej klasy będzie wyglądać następująco:

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 {

   // fabryka obiektów
  private ListableBeanFactory bf;

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

  public void test1() {
    // pobieranie beanów [Personne] z pliku Spring na podstawie ich kluczy
    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());
  }
}

Uwagi:

  • aby uzyskać komponenty zdefiniowane w pliku [config.xml], używamy obiektu typu [ListableBeanFactory]. Istnieją inne typy obiektów umożliwiające dostęp do beanów. Obiekt [ListableBeanFactory] jest pobierany w metodzie [setUp] klasy testowej i zapisywany w zmiennej prywatnej. Dzięki temu będzie on dostępny dla wszystkich metod testowych.
  • Plik [config.xml] zostanie umieszczony w katalogu [ClassPath] aplikacji, c.a.d, w jednym z katalogów przeszukiwanych przez maszynę wirtualną Java podczas wyszukiwania klasy, do której odwołuje się aplikacja. Obiekt [ClassPathResource] służy do wyszukiwania zasobu w pliku [ClassPath] aplikacji, w tym przypadku w pliku [config.xml].
  • Spring może korzystać z plików konfiguracyjnych w różnych formatach. Obiekt [XmlBeanFactory] umożliwia analizę pliku konfiguracyjnego w formacie XML.
  • Przetworzenie pliku Springa zwraca obiekt typu [ListableBeanFactory], w tym przypadku obiekt bf. Za pomocą tego obiektu można uzyskać bean oznaczony kluczem C poprzez wyrażenie bf.getBean(C).
  • Metoda [test1] pobiera i wyświetla wartości beanów o kluczach „personne1” i „personne2”.

Struktura projektu Eclipse naszej aplikacji wygląda następująco:

Image

Uwagi:

  • folder [src] zawiera kod źródłowy. Skompilowany kod trafi do folderu [bin], który nie jest tutaj przedstawiony.
  • Plik [config.xml] znajduje się w katalogu głównym folderu [src]. Podczas kompilacji projektu plik ten jest automatycznie kopiowany do folderu [bin], który stanowi część folderu aplikacji [ClassPath]. To właśnie tam jest on wyszukiwany przez obiekt [ClassPathResource].
  • Folder [lib] zawiera trzy biblioteki Java niezbędne dla aplikacji:
      • commons-logging.jar i spring-core.jar dla klas Spring
      • junit.jar dla klas JUnit
  • folder [lib] również stanowi część pliku [ClassPath] aplikacji

Wywołanie metody [test1] w teście JUnit daje następujące wyniki:

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]

Uwagi:

  • Spring rejestruje pewną liczbę zdarzeń za pomocą biblioteki [commons-logging.jar]. Te logi pozwalają nam lepiej zrozumieć działanie Springa.
  • Plik [config.xml] został załadowany, a następnie wykorzystany
  • operacja*
Personne personne1 = (Personne) bf.getBean("personne1");

spowodowała utworzenie beana [personne1]. Widoczny jest odpowiedni wpis w dzienniku Springa. Ponieważ w definicji beana [personne1] wpisano [init-method="init"], wykonano metodę [init] utworzonego obiektu [Personne]. Wyświetlany jest odpowiedni komunikat.

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

spowodowała wyświetlenie wartości utworzonego obiektu [Personne].

  • To samo zjawisko powtarza się w przypadku fasoli klucza [personne2].
  • Ostatnia operacja
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

nie spowodowała utworzenia nowego obiektu typu [Personne]. Gdyby tak było, wyświetlono by metodę [init], co jednak nie ma miejsca w tym przypadku. Na tym polega zasada singletonu. Spring domyślnie tworzy tylko jeden egzemplarz beanów z pliku konfiguracyjnego. Jest to usługa referencji obiektowych. Jeśli poprosimy go o referencję do obiektu, który nie został jeszcze utworzony, Spring tworzy go i zwraca referencję. Jeśli obiekt został już utworzony, Spring po prostu zwraca referencję do niego.

  • Można zauważyć, że nie ma żadnego śladu metody [close] obiektu [Personne], mimo że zapisaliśmy ją w definicji beanów [destroy-method=close]. Możliwe, że metoda ta jest wykonywana dopiero wtedy, gdy pamięć zajmowana przez obiekt zostanie zwolniona przez moduł czyszczący pamięć (garbage collector). W momencie, gdy to nastąpi, aplikacja jest już zakończona, a zapis na ekranie nie ma żadnego efektu. Należy to sprawdzić.

Ponieważ opanowaliśmy już podstawy konfiguracji Springa, nasze wyjaśnienia będą odtąd nieco szybsze.

2.3.2. Przykład 2

Rozważmy następującą nową klasę [Voiture]:

package istia.st.springioc.domain;

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

   // konstruktory

  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 + "]";
  }

     // metody 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() + "]");
  }

}

Klasa zawiera:

  • trzy pola prywatne: typ, marka i właściciel. Pola te można inicjować i odczytywać za pomocą publicznych metod get i set klasy. Można je również zainicjować za pomocą konstruktora Voiture(String, String, Personne). Klasa posiada również konstruktor bez argumentów, aby spełniać normę JavaBean.
  • metodę toString służącą do pobrania wartości obiektu [Voiture] w postaci ciągu znaków
  • metodę init, która zostanie wywołana przez Springa zaraz po utworzeniu obiektu, oraz metodę close, która zostanie wywołana podczas niszczenia obiektu

Aby utworzyć obiekty typu [Voiture], wykorzystamy następujący plik Spring [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>

Plik ten dodaje do poprzednich definicji bean o kluczu „voiture1” typu [Voiture]. Aby zainicjować ten bean, można by napisać:

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

Zamiast skorzystać z tej metody, którą już przedstawiliśmy, zdecydowaliśmy się w tym przypadku użyć konstruktora Voiture(String, String, Personne) tej klasy. Ponadto bean [voiture1] definiuje metodę, która ma zostać wywołana podczas początkowego tworzenia obiektu [init-method], oraz metodę, która ma zostać wywołana podczas niszczenia obiektu [destroy-method].

Do naszych testów wykorzystamy już przedstawioną klasę testową JUnit, dodając do niej następującą metodę [test2]:

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

Metoda [test2] pobiera bean [voiture1] i wyświetla go.

Struktura projektu Eclipse pozostaje taka sama jak w poprzednim teście. Wywołanie metody [test2] w teście JUnit daje następujące wyniki:

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]]

Uwagi:

  1. metoda [test2] żąda odwołania do bean’a [voiture1]
  2. wiersz 4: Spring rozpoczyna tworzenie bean’a [voiture1], ponieważ ten bean nie został jeszcze utworzony (singleton)
  3. wiersz 6: ponieważ bean [voiture1] odwołuje się do beana [personne2], ten ostatni bean jest z kolei tworzony
  4. wiersz 7: bean [personne2] został utworzony. Następnie wykonywana jest jego metoda [init].
  5. wiersz 9: Spring informuje, że użyje konstruktora do utworzenia bean’a [voiture1]
  6. wiersz 10: utworzono bean [voiture1]. Następnie wykonywana jest jego metoda [init].
  7. wiersz 11: metoda [test2] wyświetla wartość fasoli [voiture1]

2.3.3. Przykład 3

Wprowadzamy następującą nową klasę [GroupePersonnes]:

package istia.st.springioc.domain;

import java.util.Map;

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

   // metody pobierające i ustawiające
  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;
  }

   // wyświetlanie
  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();
  }

   // inicjacja-zamknięcie
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

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

Jej dwa elementy prywatne to:

elementy: tablica osób należących do grupy

groupesDeTravail: słownik przypisujący osobę do grupy roboczej

Warto tutaj zauważyć, że klasa [GroupePersonnes] nie definiuje konstruktora bez argumentów, aby zachować zgodność ze standardem JavaBean. Przypominamy, że w przypadku braku jakiegokolwiek konstruktora istnieje konstruktor „domyślny”, który jest konstruktorem bez argumentów i nie wykonuje żadnej operacji.

Celem tego przykładu jest pokazanie, w jaki sposób Spring umożliwia inicjalizację złożonych obiektów, takich jak obiekty posiadające pola typu tablica lub słownik. Do poprzedniego pliku Spring [config.xml] dodajemy nową fasolkę:

    <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. tag <list> pozwala zainicjować pole typu tablica lub implementujące interfejs List z różnymi wartościami.
  2. tag <map> pozwala zrobić to samo z polem implementującym interfejs Map

Do naszych testów wykorzystamy już przedstawioną klasę testową JUnit, dodając do niej następującą metodę [test3]:

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

Metoda [test3] pobiera bean [groupe1] i wyświetla go.

Struktura projektu Eclipse pozostaje taka sama jak w poprzednim teście. Wywołanie metody [test3] w teście JUnit daje następujące wyniki:

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}

Uwagi:

  • metoda [test3] wymaga odwołania do bean’a [groupe1]
  • wiersz 4: Spring rozpoczyna tworzenie tego beana
  • ponieważ bean [groupe1] odwołuje się do beanów [personne1] i [personne2], oba te beany są tworzone (wiersze 6 i 9), a ich metoda init jest wykonywana (wiersze 7 i 10)
  • wiersz 11: utworzono bean [groupe1]. Teraz wykonywana jest jego metoda [init].
  • wiersz 12: wywołano wyświetlanie za pomocą metody [test3].

2.4. Spring do konfiguracji trójwarstwowych aplikacji internetowych

2.4.1. Ogólna architektura aplikacji

Chcemy zbudować aplikację trójwarstwową o następującej strukturze:

  • trzy warstwy zostaną oddzielone dzięki zastosowaniu interfejsów Java
  • integracja trzech warstw zostanie zrealizowana za pomocą Springa
  • dla każdej z trzech warstw utworzymy oddzielne pakiety o nazwach Control, Domain i Dao. Dodatkowy pakiet będzie zawierał aplikacje testowe.

Struktura aplikacji w środowisku Eclipse mogłaby wyglądać następująco:

Image

2.4.2. Warstwa DAO odpowiedzialna za dostęp do danych

Warstwa DAO zaimplementuje następujący interfejs:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • Należy napisać dwie klasy Dao1Impl1 i Dao1Impl2 implementujące interfejs IDao1. Metoda Dao1Impl1. doSomethingInDaoLayer zwróci a+b, a metoda Dao1Impl2. doSomethingInDaoLayer zwróci a-b.
  • Napisz klasę testową JUnit, która przetestuje dwie poprzednie klasy

2.4.3. Warstwa biznesowa

Warstwa biznesowa będzie implementować następujący interfejs:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • Należy napisać dwie klasy Domain1Impl1 i Domain1Impl2 implementujące interfejs IDomain1. Klasy te będą miały konstruktor przyjmujący parametr typu IDao1. Metoda Domain1Impl1.doSomethingInDomainLayer zwiększy wartości a i b o jedność, a następnie przekaże te dwa parametry do metody doSomethingInDaoLayer obiektu typu IDao1, który została przekazana. Z kolei metoda Domain1Impl2.doSomethingInDomainLayer zmniejszy wartości a i b o jeden, a następnie wykona tę samą operację.
  • napisać klasę testową JUnit sprawdzającą obie poprzednie klasy

2.4.4. Warstwa interfejsu użytkownika

Warstwa interfejsu użytkownika zaimplementuje następujący interfejs:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • Należy napisać dwie klasy Control1Impl1 i Control1Impl2 implementujące interfejs IControl1. Klasy te będą miały konstruktor przyjmujący parametr typu IDomain1. Metoda Control1Impl1.doSomethingInControlLayer zwiększy wartości a i b o jedność, a następnie przekaże te dwa parametry do metody doSomethingInDomainLayer obiektu typu IDomain1, który została przekazana. Z kolei metoda Control11Impl2.doSomethingInControlLayer zmniejszy wartości a i b o jeden, a następnie wykona tę samą operację.
  • napisać klasę testową JUnit sprawdzającą obie poprzednie klasy

2.4.5. Integracja ze Springiem

  • Napisz plik konfiguracyjny Springa, który określi, z jakich klas będzie korzystać każda z trzech poprzednich warstw
  • Napisz klasę testową JUnit wykorzystującą różne konfiguracje Springa, aby podkreślić elastyczność napisanej aplikacji
  • napisz samodzielną aplikację (metoda main), przekazującą dwa parametry do interfejsu IControl1 i wyświetlającą wynik zwrócony przez interfejs.

2.4.6. Rozwiązanie

2.4.6.1. Projekt Eclipse

Image

Archiwa z folderu [lib] zostały dodane do folderu [ClassPath] w ramach projektu.

2.4.6.2. Pakiet [istia.st.demo.dao]

Interfejs:

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);
}

Pierwsza klasa implementacji:

package istia.st.demo.dao;

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

     // wykonujemy jakąś czynność w warstwie [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

Druga klasa implementacji:

package istia.st.demo.dao;

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

     // wykonuje się jakąś czynność w warstwie [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. Pakiet [istia.st.demo.domain]

Interfejs:

package istia.st.demo.domain;

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

     // wykonujemy pewne czynności w warstwie [domain]
  public int doSomethingInDomainLayer(int a, int b);
}

Pierwsza klasa implementacyjna:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

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

     // usługa dostępu do warstwy [dao]
  private IDao1 dao1;

  public Domain1Impl1() {
     // konstruktor bez argumentów
  }

     // zapisuje usługę dostępu do warstwy [dao]
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // wykonuje się jakąś czynność w warstwie [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

Druga klasa implementacji:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

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

     // usługa dostępu do warstwy [dao]
  private IDao1 dao1;

  public Domain1Impl2() {
     // konstruktor bez argumentów
  }

     // zapisuje usługę dostępu do warstwy [dao]
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // wykonuje się pewne czynności w warstwie [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. Pakiet [istia.st.demo.control]

Interfejs

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);
}

Pierwsza klasa implementacyjna:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // klasa biznesowa w warstwie [domain]
    private IDomain1 domain1;

  public Control1Impl1() {
     // konstruktor bez argumentów
  }

     // zapisanie usługi dostępu do warstwy [domain]
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // wykonujemy jakąś czynność
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

Druga klasa implementacji:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

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

     // klasa dostępu do warstwy [domain]
    private IDomain1 domain1;

  public Control1Impl2() {
     // konstruktor bez argumentów
  }

     // zapisuje klasę dostępu do warstwy [domain]
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // wykonuje się jakąś czynność
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. Pliki konfiguracyjne [Spring]

Pierwszy plik [springMainTest1.xml]:

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

Drugi plik [springMainTest2.xml]:

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

2.4.6.6. Pakiet testowy [istia.st.demo.tests]

Test typu [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) {
     // pobieramy implementację interfejsu IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // używa się klasy
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // wyświetla się wynik
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Wyniki uzyskane w konsoli Eclipse:

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

Kolejny test wykorzystujący drugi plik konfiguracyjny [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) {
     // pobieramy implementację interfejsu IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // używa się klasy
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // wyświetla się wynik
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Wyniki uzyskane w konsoli Eclipse:

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

Na koniec test Junit:

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() {
     // pobieramy implementację interfejsu IControl1
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // używa się klasy
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // pobieramy inną implementację interfejsu IControl1
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // używamy klasy
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusion

Framework Spring zapewnia prawdziwą elastyczność zarówno w zakresie architektury aplikacji, jak i ich konfiguracji. Wykorzystaliśmy koncepcję IoC, jeden z dwóch filarów Springa. Drugim filarem jest AOP (programowanie zorientowane aspektowo), którego nie omówiliśmy. Pozwala ono, poprzez konfigurację, dodawać „zachowanie” do metody klasy bez modyfikowania jej kodu. W uproszczeniu AOP umożliwia filtrowanie wywołań określonych metod:

  • Filtr może być uruchomiony przed lub po docelowej metodzie M, albo w obu przypadkach.
  • Metoda M ignoruje istnienie tych filtrów. Są one definiowane w pliku konfiguracyjnym Springa.
  • Kod metody M nie ulega zmianie. Filtry są klasami Java, które należy skonstruować. Spring udostępnia predefiniowane filtry, w szczególności do obsługi transakcji SGBD.
  • Filtry są komponentami typu bean i jako takie są zdefiniowane w pliku konfiguracyjnym Springa jako komponenty typu bean.

Typowym filtrem jest filtr transakcyjny. Weźmy metodę M z warstwy biznesowej, realizującą dwie nierozerwalnie powiązane operacje na danych (jednostka pracy). Wykorzystuje ona dwie metody M1 i M2 z warstwy DAO w celu wykonania tych dwóch operacji.

Ponieważ metoda M znajduje się w warstwie biznesowej, nie zajmuje się nośnikiem tych danych. Nie musi na przykład zakładać, że dane znajdują się w SGBD i że musi umieścić dwa wywołania metod M1 oraz M2 w ramach transakcji SGBD. To warstwa DAO powinna zająć się tymi szczegółami. Rozwiązaniem powyższego problemu jest zatem utworzenie metody w warstwie DAO, która sama wywoływałaby metody M1 i M2, które to wywołania obejmowałaby w transakcji SGBD.

Rozwiązanie oparte na filtrowaniu AOP jest bardziej elastyczne. Umożliwia ono zdefiniowanie filtra, który przed wywołaniem funkcji M rozpocznie transakcję, a po jej zakończeniu wykona commit lub rollback w zależności od sytuacji.

Takie podejście ma kilka zalet:

  • po zdefiniowaniu filtra można go zastosować do wielu metod, na przykład do wszystkich, które wymagają transakcji
  • metody objęte filtrowaniem nie wymagają przepisywania
  • filtry, które mają być używane, są definiowane w konfiguracji, więc można je zmieniać

Oprócz koncepcji IoC i AOP, Spring oferuje wiele klas wspierających aplikacje trójwarstwowe:

  • dla JDBC – SqlMap (iBatis), dla Hibernate – JDO (Java Data Object) w warstwie DAO
  • dla modelu MVC w warstwie interfejsu użytkownika

Więcej informacji: http://www.springframework.org.