Skip to content

2. Konfiguracja aplikacji przy użyciu Spring

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

Załóżmy, że dostęp do warstwy DAO jest kontrolowany przez interfejs [IArticlesDao]:

....
Namespace istia.st.articles.dao

    Public Interface IArticlesDao
         ' lista wszystkich artykułów
        Function getAllArticles() As IList
         ' dodaje artykuł
        Function ajouteArticle(ByVal unArticle As Article) As Integer
         ' usuwa artykuł
        Function supprimeArticle(ByVal idArticle As Integer) As Integer
         ' modyfikuje pozycję
        Function modifieArticle(ByVal unArticle As Article) As Integer
         ' wyszukuje artykuł
        Function getArticleById(ByVal idArticle As Integer) As Article
         ' usuwa wszystkie artykuły
        Sub clearAllArticles()
         ' dodaje artykuły do transakcji
        Sub doInsertionsInTransaction(ByVal articles As Article())
         ' zmienia stan magazynowy artykułu
        Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
    End Interface
End Namespace

W warstwie dostępu do danych, czyli warstwie DAO (Data Access Object), często pracuje się z obiektem SGBD. Rozważmy przypadek, w którym dostęp do niego uzyskuje się za pośrednictwem sterownika ODBC. Szkielet klasy zapewniającej dostęp do tego źródła ODBC mógłby wyglądać następująco:

NameSpace istia.st.articles.dao

Imports System.Data.Odbc
...
    Public Class ArticlesDaoPlainODBC
        Implements istia.st.articles.dao.IArticlesDao

         ' pola prywatne
        Private connexion As OdbcConnection = Nothing
        Private DSN As String

        Public Sub New(ByVal DSN As String, ByVal user As String, ByVal passwd As String)
             'pobiera nazwę źródła ODBC
            Me.DSN = DSN
             ' tworzy się ciąg połączenia
            Dim connectString As String = String.Format("DSN={0};UID={1};PWD={2}", DSN, user, passwd)
             '– instancjonowanie połączenia
            connexion = New OdbcConnection(connectString)
        End Sub

....
    End Class
End NameSpace

Aby wykonać operację na źródle ODBC, każda metoda wymaga obiektu [OdbcConnection], który reprezentuje połączenie z bazą danych, przez które będą przebiegać wymiany danych między bazą a aplikacją. Aby utworzyć ten obiekt, potrzebne są trzy informacje:

DSN As String
nazwa źródła ODBC
user As String
identyfikator, pod którym tworzone jest połączenie
passwd As String
hasło powiązane z tym identyfikatorem

Nasza klasa [ArticlesDaoPlainODBC] uzyskuje te informacje za pośrednictwem agenta zewnętrznego, który instancjonuje element tej klasy. Można zadać sobie pytanie, w jaki sposób agent ten uzyskuje trzy informacje niezbędne do instancjonowania klasy [ArticlesDaoPlainODBC]. Weźmy przykład. Załóżmy, że chcemy napisać klasę testową warstwy [Dao]. Mielibyśmy następującą architekturę:

Szkielet klasy testowej Nunit [http://www.nunit.org/] mógłby wyglądać następująco:

Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports ArticlesDaoSqlmap = istia.st.articles.dao.ArticlesDaoSqlMap
Imports Article = istia.st.articles.domain.Article
Imports System.Threading

    <TestFixture()> _
    Public Class NunitTestArticlesDaoPlainOdbc

         ' obiekt do testowania
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
             ' tworzymy instancję obiektu testowego
            articlesDao = New ArticlesDaoPlainODBC("odbc-firebird-articles", "SYSDBA", "masterkey")
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
             ' kontrola wizualna
            listArticles()
        End Sub

         ' zrzut ekranu
        Private Sub listArticles()
            Dim articles As IList = articlesDao.getAllArticles
            For i As Integer = 0 To articles.Count - 1
                Console.WriteLine(CType(articles(i), Article).ToString)
            Next
        End Sub


    End Class

Środowisko testowe Nunit stanowi przeniesienie środowiska JUnit, istniejącego dla platformy Java, na platformę .NET. W powyższej klasie metoda posiadająca atrybut <SetUp()> jest wykonywana przed każdą metodą testową. Natomiast metoda posiadająca atrybut <TearDown()> jest wykonywana po każdym teście. W powyższym przykładzie nie ma takiej metody. Widać tutaj, że metoda [init], posiadająca atrybut <SetUp()>, tworzy instancję obiektu [ArticlesDaoPlainODBC], przekazując mu „na stałe” trzy informacje potrzebne konstruktorowi tego obiektu.

Nasza klasa testowa jest narażona na skutki zmiany którejkolwiek z tych „sztywno” zakodowanych informacji. Lepiej byłoby, gdyby informacje te były zapisane w pliku konfiguracyjnym, aby uniknąć niepotrzebnych rekompilacji w przypadku ich zmiany. Typowym rozwiązaniem przy konfigurowaniu aplikacji jest użycie pliku, w którym znajdują się wszystkie informacje, które mogą ulec zmianie w przyszłości. Istnieje wiele różnych rodzajów plików konfiguracyjnych. Obecnie dominuje tendencja do stosowania plików XML. Takie rozwiązanie wybrał Spring. Plik konfigurujący obiekt [ArticlesDaoPlainODBC] mógłby wyglądać następująco:

<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">

<objects>
     <!-- klasa implementująca interfejs IArticlesDao -->
    <description>Gestion d'une table d'articles</description>
    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, articlesdao">
      <constructor-arg index="0">
            <value>odbc-firebird-articles</value>
        </constructor-arg>
         <constructor-arg index="1">
            <value>SYSDBA</value>
        </constructor-arg>
         <constructor-arg index="2">
            <value>masterkey</value>
        </constructor-arg>
    </object>
</objects>

Plik konfiguracyjny Springa opisuje obiekty, które mają zostać zainicjowane. Z reguły nie określa on, w którym momencie zostaną one zainicjowane. O momencie ich zainicjowania decyduje zatem kod, który korzysta z tego pliku. Obiekty opisane w pliku konfiguracyjnym Springa mogą być zainicjowane i zainicjowane na dwa różne sposoby:

  • poprzez wskazanie, jak powyżej, parametrów, które mają zostać przekazane do konstruktora obiektu
  • poprzez nadanie wartości właściwościom obiektu (Property). W tym przypadku obiekt musi posiadać konstruktor domyślny, którego Spring użyje do instancjonowania.

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 przedstawionym na początku tego dokumentu dostęp do bazy artykułów będzie zapewniony przez jeden egzemplarz klasy [ArticlesDaoPlainODBC]. 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 [ArticlesDaoPlainODBC] w pakiecie o nazwie [istia.st.articles.dao]. Trzy informacje niezbędne dla konstruktora tego obiektu są zdefiniowane wewnątrz tagu <object>...</object>. Liczba takich tagów <object> będzie równa liczbie singletonów do utworzenia.

Przyjrzyjmy się szczegółowo tej konfiguracji:

<objects>
...
</objects>

<objects> jest tagiem nadrzędnym pliku konfiguracyjnego Spring. Oznacza on opis obiektów typu singleton, które mają zostać zainicjowane.

    <description>Gestion d'une table d'articles</description>

Tag <description> jest opcjonalny. Można go wykorzystać na przykład do opisania roli pliku konfiguracyjnego.

<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, articlesdao">
...    
</object>

Tag <object> służy do opisania obiektu, który ma zostać zainicjowany. Posiada on tutaj dwa atrybuty:

  • name: identyfikator obiektu. To właśnie za pomocą tej nazwy kod zewnętrzny będzie odwoływał się do obiektu.
  • class: w postaci „nazwa klasy, nazwa zestawu”. Pierwsza informacja to pełna nazwa klasy, która ma zostać zainicjowana. Druga to nazwa pliku DLL, który zawiera tę klasę. W naszym przykładzie klasa znajduje się w pliku o nazwie [articlesdao.dll]

Zawartość tagu <object> służy do opisania sposobu instancjonowania obiektu:

    <object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoPlainODBC, articlesdao">
      <constructor-arg index="0">
            <value>odbc-firebird-articles</value>
        </constructor-arg>
         <constructor-arg index="1">
            <value>SYSDBA</value>
        </constructor-arg>
         <constructor-arg index="2">
            <value>masterkey</value>
        </constructor-arg>
    </object>

Przypomnijmy sygnaturę producenta klasy [ArticlesDaoPlainODBC]:

        Public Sub New(ByVal DSN As String, ByVal user As String, ByVal passwd As String)

Obiekt Spring [articlesdao] zostanie zainicjowany przez powyższy konstruktor przy użyciu trzech informacji zawartych w pliku konfiguracyjnym: [odbc-firebird-articles, SYSDBA, masterkey].

W którym momencie nastąpi tworzenie obiektów zdefiniowanych w pliku Spring? W każdej aplikacji znajduje się metoda, która z pewnością zostanie wykonana jako pierwsza. Zazwyczaj to właśnie w niej wywoływane jest tworzenie singletonów. Inicjalizacja aplikacji może zostać powierzona metodzie main tej samej aplikacji, o ile taka istnieje. W przypadku aplikacji ASP.NET może to być metoda [Application_Start] z pliku [global.asax]. W przypadku naszej klasy testowej [Nunit] inicjalizacja aplikacji odbywa się w metodzie powiązanej z atrybutem <Setup()>.

Jak wykorzystać powyższy plik konfiguracyjny w naszej klasie [Nunit]? Oto przykład:

Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports ArticlesDaoSqlmap = istia.st.articles.dao.ArticlesDaoSqlMap
Imports Article = istia.st.articles.domain.Article
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO

    <TestFixture()> _
    Public Class NunitSpringTestArticlesDaoPlainOdbc

         ' obiekt testowy
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
       ' pobieramy instancję generatora obiektów Spring
            Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config-plainodbc.xml", FileMode.Open))
       ' żądamy instancjonowania obiektu DAO „articles”
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

        <Test()> _
        Public Sub testGetAllArticles()
             ' weryfikacja wizualna
            listArticles()
        End Sub

         ' zrzut ekranu
        Private Sub listArticles()
            Dim articles As IList = articlesDao.getAllArticles
            For i As Integer = 0 To articles.Count - 1
                Console.WriteLine(CType(articles(i), Article).ToString)
            Next
        End Sub
    End Class

Uwagi:

  • aby utworzyć instancje obiektów z pliku konfiguracyjnego Springa, wykorzystuje się obiekt typu [XmlObjectFactory]. Jest to obiekt typu „Factory”, c.a.d – obiekt służący do tworzenia innych obiektów (Factory = fabryka). Spring dysponuje kilkoma typami „Factory” w zależności od używanego pliku konfiguracyjnego. W tym przypadku jest to plik XML, dlatego stosuje się typ [XmlObjectFactory].
  • Zgodnie z logiką obiekt typu [XmlObjectFactory] wymaga nazwy pliku konfiguracyjnego XML, w tym przypadku [spring-config-plainodbc.xml]. Dokładniej rzecz biorąc, typ [XmlObjectFactory] jest instancjonowany z wykorzystaniem strumienia odczytu utworzonego na podstawie pliku XML, którego nazwę podajemy.
  • Po utworzeniu obiektu typu [XmlObjectFactory] obiekt z pliku konfiguracyjnego jest pobierany przez [XmlObjectFactory].getObject("identyfikator"), gdzie „identyfikator” jest atrybutem [id] jednego z obiektów z pliku konfiguracyjnego.
  • Jeśli żądany obiekt nie został jeszcze zainicjowany, Spring inicjuje go na podstawie informacji zawartych w pliku konfiguracyjnym i zwraca odwołanie do niego programowi wywołującemu. Jeśli obiekt został już zainicjowany, Spring po prostu zwraca odwołanie do już istniejącego obiektu. Jest to zasada działania singletonu.
  • Warto zauważyć, że klasa testowa [Nunit] nie zna nazwy klasy dostępu do danych. Nazwa ta znajduje się w pliku konfiguracyjnym. Klasa testowa ogranicza się do żądania obiektu implementującego interfejs [IArticlesDao]:
         ' obiekt do testowania
        Private articlesDao As IArticlesDao

        <SetUp()> _
        Public Sub init()
...
            articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
        End Sub

Na tym właśnie polega zaleta Springa. Jeśli zmienimy klasę implementacyjną, nie będzie trzeba modyfikować naszej klasy testowej. Wystarczy po prostu zmienić plik konfiguracyjny Springa. Klasa testowa z kolei po prostu współpracuje z interfejsem, a nie z klasą.

Zakończmy tę prezentację kilkoma praktycznymi wskazówkami.

Kiedy piszemy „Spring utworzy instancję...”, co dokładnie mamy na myśli? W przypadku platformy .NET Spring znajduje się w trzech plikach:

Image

W przypadku projektu .NET utworzonego w programie Visual Studio, który ma korzystać ze Springa, należy postępować w następujący sposób:

  • trzy powyższe pliki należy umieścić w folderze [bin] projektu
  • Plik [Spring.Core.dll] musi znaleźć się wśród odwołań projektu:

Image

  • klasy korzystające z biblioteki Spring będą musiały importować pewne przestrzenie nazw, w tym często następującą:
Imports Spring.Objects.Factory.Xml

Kolejna kwestia praktyczna: gdzie umieścić plik konfiguracyjny Springa? Istnieje kilka możliwych lokalizacji. Jedną z nich jest folder projektu [bin]. To właśnie tam umieszczono plik [spring-config-plainodbc.xml] z omawianego przykładu.