Skip to content

5. [Cours]: Wprowadzenie do frameworka Spring

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności.

Spring pojawił się w 2004 roku, początkowo jako kontener obiektów. Od tego czasu rozwinął się w wiele gałęzi: Spring MVC, Spring Data, Spring Batch, ... [http://spring.io]. W niniejszym rozdziale omówimy wyłącznie kontener obiektów. Oto kilka kluczowych informacji:

  • Aplikacja składa się z wielu klas, a niektóre z nich współdzielą obiekty, które muszą być unikalne (singletony). Spring tworzy i zarządza tymi singletonami;
  • Spring umieszcza te singletony w strukturze zwanej kontekstem;
  • klasy mają dostęp do singletonów aplikacji, zwracając się do Springa z prośbą o nie, podając ich nazwę, typ lub oba te elementy;
  • Spring tworzy singletony i zarządza ich ewentualnymi zależnościami: singleton może bowiem zawierać odwołania do jednego lub kilku innych singletonów. Kiedy Spring tworzy singleton, tworzy również jego zależności;
  • gdy uruchamia się aplikacja oparta na Spring, może ona poprosić Spring o utworzenie wszystkich singletonów aplikacji. Będą one następnie dostępne w kontekście Spring;
  • Spring ułatwia stosowanie architektur warstwowych oraz programowanie oparte na interfejsach. W prostych przypadkach każda warstwa jest realizowana przez singleton i implementuje interfejs. Jeśli aplikacja korzysta z interfejsów warstw, a nie z ich klas implementacyjnych, uzyskuje się architekturę skalowalną, która pozwala na zmianę implementacji jednej warstwy bez zmiany pozostałych dzięki dwóm następującym cechom:
    • aplikacja uzyskuje odwołanie do warstwy poprzez jej nazwę. Spring dostarcza jej odwołanie do klasy implementującej tę warstwę;
    • aplikacja wykorzystuje to odwołanie jako odwołanie do interfejsu warstwy, a nie do klasy;

Deklarację singletonów można wykonać na trzy sposoby, które można łączyć:

  • w pliku XML,
  • w specjalnej klasie konfiguracyjnej;
  • w dowolnej klasie za pomocą adnotacji;

Poniżej przedstawiamy trzy przykłady konfiguracji:

  • [exemple-01]: konfiguracja scentralizowana w jednym pliku XML;
  • [exemple-02]: konfiguracja scentralizowana w jednej klasie Java;
  • [exemple-03]: konfiguracja rozproszona na kilka klas Java;

Ostatni przykład, [exemple-04], dotyczy konfiguracji Spring w architekturze warstwowej. Jest to najważniejszy przykład. Będzie on stale wykorzystywany do konfiguracji architektur opisanych w niniejszym dokumencie.

Te cztery przykłady stanowią podstawę dla dalszych rozważań:

  • konfiguracja Springa i wstrzykiwanie zależności;
  • wykorzystanie Mavena do zarządzania zależnościami projektu;
  • wykorzystanie JUnit do testowania projektów;

5.1. Support

 

Folder [support / chap-05] zawiera projekty Eclipse z tego rozdziału.

5.2. Exemple-01

5.2.1. Projekt Eclipse

 

5.2.2. Klasa [Personne]

 

package istia.st.spring.core;

public class Personne {

    // pola
    private String nom;
    private String prenom;
    private int age;

    // konstruktory
    public Personne() {

    }

    public Personne(String nom, String prénom, int âge) {
        this.nom = nom;
        this.prenom = prénom;
        this.age = âge;
    }

    // toString
    public String toString() {
        return String.format("Personne[%s, %s,%d]", prenom, nom, age);
    }

    // metody pobierające i ustawiające

    public String getNom() {
        return nom;
    }

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

    public String getPrenom() {
        return prenom;
    }

    public void setPrenom(String prenom) {
        this.prenom = prenom;
    }

    public int getAge() {
        return age;
    }

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

}

Uwaga: metody getter i setter mogą być generowane automatycznie w następujący sposób: [1-2]:

5.2.3. Klasa [Appartement]

 

package istia.st.spring.core;

public class Appartement {

    // pola
    private Personne proprietaire;
    private int surface;

    // metody pobierające i ustawiające

    public Personne getProprietaire() {
        return proprietaire;
    }

    public void setProprietaire(Personne proprietaire) {
        this.proprietaire = proprietaire;
    }

    public int getSurface() {
        return surface;
    }

    public void setSurface(int surface) {
        this.surface = surface;
    }

    // toString
    public String toString() {
        return String.format("Appartement[%s, %s]", proprietaire, surface);
    }

}

Uwaga: ta klasa nie posiada jawnego konstruktora. W takim przypadku domyślnie zawsze istnieje konstruktor bez parametrów, który nie wykonuje żadnej operacji. Po utworzeniu konstruktorów ten domyślny konstruktor nie istnieje już w sposób dorozumiany. Należy go wówczas zdefiniować jawnie:

public Appartement(){
}

5.2.4. Plik konfiguracyjny Springa

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
    <!-- Osoba 01 -->
    <bean id="personne_01" class="istia.st.spring.core.Personne">
        <constructor-arg index="0" value="dubois" />
        <constructor-arg index="1" value="paul" />
        <constructor-arg index="2" value="34" />
    </bean>
    <!-- Osoba 02 -->
    <bean id="personne_02" class="istia.st.spring.core.Personne">
        <property name="nom" value="martin" />
        <property name="prenom" value="micheline" />
        <property name="age" value="18" />
    </bean>
    <!-- lista osób -->
    <util:list id="club">
        <ref bean="personne_01" />
        <ref bean="personne_02" />
    </util:list>
    <!-- mieszkanie -->
    <bean id="appartement" class="istia.st.spring.core.Appartement">
        <property name="surface" value="100" />
        <property name="proprietaire" ref="personne_01" />
    </bean>
</beans>
  • wiersze 2, 27: singletony są zdefiniowane w tagu <beans>;
  • wiersze 6–10: każdy singleton jest zdefiniowany za pomocą tagu <bean>;
  • wiersz 6: [id] to identyfikator singletonu. [class] to pełna nazwa klasy, która ma zostać zainicjowana;
  • wiersze 7–9: trzy wartości, które należy przekazać do konstruktora klasy [Personne];
  • wiersze 12–16: klasa [Personne] jest najpierw tworzona za pomocą jej domyślnego konstruktora [new Personne()]. Następnie dla każdego elementu [property] wykorzystywana jest metoda ustawiająca tej klasy. Na przykład w wierszu 13 zostanie wykonana metoda [setNom("martin")]. Musi więc istnieć metoda [setNom]. Należy o tym pamiętać;
  • wiersze 18–21: tag <util:list> pozwala zdefiniować singleton będący listą;
  • wiersz 19: odwołuje się do singletonu [personne_01] zdefiniowanego w wierszu 6. Mamy tu do czynienia z tzw. wstrzykiwaniem zależności. Do zainicjowania pola singletonu można użyć dwóch atrybutów:
    • [value]: w celu przypisania do pola wartości pierwotnej (ciąg znaków, liczba, data itp.),
    • [ref]: w celu przypisania do pola odwołania do obiektu Spring;

Uwaga: plik konfiguracyjny Springa można wygenerować w następujący sposób: [1-4]:

5.2.5. Klasa wykonywalna

 

package istia.st.spring.core;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Demo01 {

    @SuppressWarnings({ "unchecked", "resource" })
    public static void main(String[] args) {
        // pobieranie kontekstu Spring
        ApplicationContext ctx = new ClassPathXmlApplicationContext("config-01.xml");
        // pobieramy bean'y
        Personne p01 = ctx.getBean("personne_01", Personne.class);
        Personne p02 = ctx.getBean("personne_02", Personne.class);
        List<Personne> club = ctx.getBean("club", new ArrayList<Personne>().getClass());
        Appartement appart01 = ctx.getBean(Appartement.class);
        // wyświetlanie ich
        System.out.println("personnes--------");
        System.out.println(p01);
        System.out.println(p02);
        System.out.println("club--------");
        for (Personne p : club) {
            System.out.println(p);
        }
        System.out.println("appartement--------");
        System.out.println(appart01);
        // pobrane bean'y są singletonami
        // można je wywoływać wielokrotnie, zawsze zwracany jest ten sam bean
        Personne p01b = ctx.getBean("personne_01", Personne.class);
        System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
    }
}
  • wiersz 14: tworzy kontekst Spring. Wszystkie singletony zdefiniowane w pliku [config-01.xml] są następnie instancjonowane;
  • wiersz 16: żąda odwołania do singletonu o identyfikatorze [personne_01] typu [Personne]. Ten drugi parametr jest opcjonalny, ale w takim przypadku otrzymujemy odwołanie do typu [Object], które należy następnie przekształcić na typ [Personne];
  • wiersz 19: nie używa się nazwy bean, a jedynie jego typ, ponieważ istnieje tylko jeden singleton typu [Appartement];
  • wiersz 18: użyto zarówno identyfikatora, jak i typu pożądanego singletonu. Identyfikator jest zbędny, ponieważ istnieje tylko jeden singleton typu [new ArrayList<Personne>().getClass()];
  • wiersze 32–33: pokazują, że jeśli wielokrotnie żądamy tego samego singletonu, zawsze otrzymujemy to samo odwołanie, co potwierdza, że mamy do czynienia z singletonem. Ważne jest, aby to zrozumieć;

Uwaga: klasę wykonywalną można wygenerować w następujący sposób: [1-6]:

  • to właśnie zaznaczenie opcji [6] spowoduje, że wygenerowana klasa będzie zawierała metodę statyczną [main], która sprawi, że będzie ona wykonywalna;

5.2.6. Zależności projektu

 
  • Zależności Spring: [spring-core, spring-beans, spring-context, spring-expression, commons-logging];

Zależności dodaje się do projektu w następujący sposób:

  • w przypadku [1]: kliknij prawym przyciskiem myszy na projekcie / [Build Path] / [Configure Build Path];
  • na [2]: [Add JARs], jeśli pliki JARs, które mają zostać dodane, znajdują się w folderze projektu. W przeciwnym razie [Add External JARs];
  • na [3], wybierz pliki JARs, które mają zostać dodane do pliku ClassPath w projekcie (znajdują się one tutaj w folderze [lib] wewnątrz projektu);

Definicja: plik [ClassPath] projektu to zbiór folderów przeszukiwanych przez plik JVM (Java Virtual Machine), który uruchamia projekt w celu znalezienia klasy, do której projekt ten odwołuje się. W przypadku projektu Eclipse [ClassPath] składa się z następujących elementów:

  • katalog [bin] projektu;
  • elementy [Build Path] projektu;

Folder [bin] jest folderem utworzonym w wyniku kompilacji folderu [src]. Zatem wszystko, co znajduje się w folderze [src], automatycznie staje się częścią folderu [ClassPath] (nawet jeśli nie jest to plik .java). W związku z tym w poprzednim projekcie plik konfiguracyjny Springa o nazwie [config-01.xml], znajdujący się w folderze [src], będzie częścią folderu [Classpath] projektu w momencie uruchomienia.

5.2.7. Wyniki

févr. 21, 2014 1:16:23 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
Infos: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3ac67f69: startup date [Fri Feb 21 13:16:23 CET 2014]; root of context hierarchy
févr. 21, 2014 1:16:23 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
Infos: Loading XML bean definitions from class path resource [config-01.xml]
personnes--------
Personne[paul, dubois,34]
Personne[micheline, martin,18]
club--------
Personne[paul, dubois,34]
Personne[micheline, martin,18]
appartement--------
Appartement[Personne[paul, dubois,34], 100]
beans [p01,p01b] identiques ? true

5.3. Exemple-02

5.3.1. Projekt Eclipse

 

5.3.2. Klasa konfiguracyjna Spring


package istia.st.spring.core;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Config {

    @Bean
    public Personne personne_01() {
        return new Personne("Paul", "Dubois", 34);
    }

    @Bean
    public Personne personne_02() {
        return new Personne("Martin", "Micheline", 18);
    }

    @Bean
    public List<Personne> club(Personne personne_01, Personne personne_02) {
        List<Personne> personnes = new ArrayList<Personne>();
        personnes.add(personne_01);
        personnes.add(personne_02);
        return personnes;
    }

    @Bean
    public Appartement appartement(Personne personne_01) {
        Appartement appartement = new Appartement();
        appartement.setSurface(200);
        appartement.setPropriétaire(personne_01);
        return appartement;
    }
}
  • wiersz 9: adnotacja [@Configuration] jest adnotacją Springa. Wskazuje ona, że klasa opatrzona tą adnotacją definiuje singletony. Są one definiowane za pomocą adnotacji [@Bean]. Spring wykona wszystkie metody opatrzone adnotacją [@Bean]. Tworzą one singletony aplikacji;
  • wiersze 12–15: definiują singleton oznaczony przez [personne_01], czyli nazwę metody.
  • wiersz 23: parametry [personne_01, personne_02] noszą nazwy singletonów. Spring automatycznie zainicjuje je odwołaniami do tych singletonów. Mówimy tu o wstrzykiwaniu parametrów;

Ten sposób konfiguracji singletonów jest bardziej przejrzysty niż ten wykorzystujący plik XML. W rzeczywistości sami odtwarzamy to, co Spring robił niejawnie na podstawie pliku XML.

5.3.3. Klasa wykonalna

 

package istia.st.spring.core;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo02 {

    @SuppressWarnings({ "unchecked", "resource" })
    public static void main(String[] args) {
        // pobieranie kontekstu Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
        // pobieramy bean'y
        Personne p01 = ctx.getBean("personne_01", Personne.class);
        Personne p02 = ctx.getBean("personne_02", Personne.class);
        List<Personne> club = ctx.getBean("club", new ArrayList<Personne>().getClass());
        Appartement appart01 = ctx.getBean(Appartement.class);
        // wyświetlanie ich
        System.out.println("personnes--------");
        System.out.println(p01);
        System.out.println(p02);
        System.out.println("club--------");
        for (Personne p : club) {
            System.out.println(p);
        }
        System.out.println("appartement--------");
        System.out.println(appart01);
        // pobrane bean są singletonami
        // można je wywoływać wielokrotnie, zawsze pobierany jest ten sam bean
        Personne p01b = ctx.getBean("personne_01", Personne.class);
        System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
    }
}
  • w wierszu 13 powoduje instancjonowanie wszystkich beanów zdefiniowanych w klasie [Config];
  • pozostała część kodu pozostaje bez zmian;

5.3.4. Zależności projektu

Zależności są określone w następującym pliku [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring.core</groupId>
    <artifactId>spring-core-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-core-02</name>
    <description>Introduction à Spring</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
    </dependencies>
    <!-- wtyczki -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Ręczne zarządzanie zależnościami projektu staje się prawdziwym wyzwaniem, gdy korzystamy z bibliotek Java, których zależności nie znamy. Na przykład framework [Hibernate], który zarządza dostępem do baz danych, ma dziesiątki zależności. Projekt [Maven] rozwiązuje ten problem. Wystarczy podać nazwę potrzebnej zależności. Jest ona automatycznie wyszukiwana w repozytoriach Maven rozmieszczonych w sieci. Jeśli żądana zależność ma swoje własne zależności, są one również automatycznie pobierane. Pobrane zależności są przechowywane w lokalnym repozytorium na komputerze. Jeśli później inna aplikacja będzie potrzebowała tej samej zależności, nie zostanie ona ponownie pobrana, lecz wyszukana w lokalnym repozytorium. Zależność charakteryzują następujące elementy:

  • wiersz 17: tag <dependency>;
  • wiersz 18: atrybut [groupId], który zazwyczaj identyfikuje firmę, która utworzyła zależność;
  • wiersz 19: atrybut [artifactId], który identyfikuje zależność;
  • wiersz 20: atrybut [version], który identyfikuje pożądaną wersję;

Sam proces generowania projektu spowoduje utworzenie komponentu Maven zdefiniowanego w wierszach 4–8:

  • wiersze 4–6: atrybuty [ groupId, artifactId,version], które właśnie opisaliśmy;
  • wiersze 7–8: są to atrybuty opcjonalne;

Nieco później powrócimy do roli wierszy 24–40. Aby przekształcić zwykły projekt Eclipse w projekt Maven, należy wykonać dwie czynności:

  • utworzyć powyższy plik [pom.xml];
  • oświadczyć, że projekt jest teraz projektem Maven [1-4]:

Ikona projektu Maven ma oznaczenie M [4]. Litera S oznacza, że projekt zawiera elementy Spring. Nie zaleca się przekształcania (tak jak to właśnie zrobiliśmy) projektu Eclipse w projekt Maven, ponieważ w takim przypadku projekt nie ma struktury oczekiwanej dla projektu Maven, co może czasami prowadzić do nieoczekiwanych problemów.

5.3.5. Generowanie artefaktu Maven projektu

Artefaktem Maven projektu nazywamy element zdefiniowany w wierszach 4–6 pliku [pom.xml]:


    <groupId>istia.st.spring.core</groupId>
    <artifactId>spring-core-02</artifactId>
<version>0.0.1-SNAPSHOT</version>

Aby wygenerować ten artefakt, w pliku [pom.xml] muszą znajdować się następujące wiersze 3–7:


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
</build>

Określają one wtyczkę Maven umożliwiającą wygenerowanie artefaktu projektu. Następnie postępuje się w następujący sposób:

Wygenerowany w ten sposób artefakt trafia do lokalnego repozytorium Maven. Jego lokalizację można znaleźć w konfiguracji Eclipse:

 

Można wówczas sprawdzić, czy artefakt Maven został poprawnie zainstalowany:

 

Od tej pory inny lokalny projekt Maven będzie mógł korzystać z tego archiwum.

5.4. Exemple-03

5.4.1. Projekt Eclipse

Tym razem tworzymy projekt Maven o nazwie [1-8]:

  • w [3b]: wskaż pusty folder, w którym zostanie wygenerowany projekt;
  • w [4]: identyfikator grupy Maven, do której będzie należał projekt;
  • w [5]: nazwa generowanego artefaktu Maven;
  • w [6]: jego wersja;
  • w [7]: jego tryb pakowania (istnieją również war, ear, apk itp.);
  • w [8]: tak utworzony projekt;

Projekt Maven ma domyślnie ściśle określoną strukturę katalogów:

  • [src / main / java]: kod źródłowy projektu. Skompilowane pliki z tych źródeł trafią do folderu [target/classes] projektu;
  • [src / main / resources]: zasoby, które muszą znajdować się w ścieżce Classpath projektu, nie będąc jednak kodami źródłowymi Java. Zostaną one skopiowane w niezmienionej postaci do folderu [target/classes] projektu;
  • [src / test / java]: kody źródłowe testów projektu. Skompilowane pliki z tych kodów źródłowych trafią do folderu [target/test-classes] projektu. Elementy te nie są dołączane do archiwum Maven projektu;
  • [src / test / resources]: zasoby, które muszą znajdować się w ścieżce Classpath projektu na potrzeby testów, ale nie są kodami źródłowymi Java. Zostaną one skopiowane w niezmienionej postaci do folderu [target/test-classes] projektu;

Uzupełniamy projekt w następujący sposób:

 

5.4.2. Konfiguracja Maven

Domyślnie generowany jest plik [pom.xml]. Modyfikujemy go w następujący sposób:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring.core</groupId>
    <artifactId>spring-core-03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-core-03</name>
    <description>Introduction à Spring</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <!-- nadrzędny projekt Maven -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- kontekst Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!-- logi -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

    </dependencies>

    <!-- wtyczki -->
    <build>
        <plugins>
            <!-- do generowania archiwum projektu wraz z jego zależnościami -->
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- do instalacji artefaktu projektu w lokalnym repozytorium Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersz 11: projekt jest zakodowany jako UTF-8;
  • wiersz 12: do kompilacji projektu używamy JDK 1.8;
  • wiersze 16–20: w przypadku projektów korzystających z bibliotek Spring praktyczne jest użycie nadrzędnego projektu Maven o nazwie [spring-boot-starter-parent]. Projekt ten określa wersje różnych bibliotek Spring oraz ich zależności. Dzięki temu nie trzeba już definiować ich w sekcji zależności. W związku z tym w wierszach 24–27 nie określa się żądanej wersji [spring-context]. Będzie to wersja zdefiniowana przez projekt nadrzędny [spring-boot-starter-parent]. Technika ta pozwala uniknąć martwienia się o ewentualne niezgodności wersji między zależnościami. Te zdefiniowane przez projekt nadrzędny są ze sobą zgodne;
  • wiersze 29–32: Spring wyświetla na konsoli znaczną ilość informacji za pośrednictwem biblioteki logów. Jest ona tutaj importowana;
  • wiersze 40–47: wtyczka Maven, do której jeszcze wrócimy;
  • wiersze 50–52: wtyczka do generowania artefaktu Maven projektu;

5.4.3. Klasa konfiguracyjna Springa

  

Klasa [Config] wygląda następująco:


package istia.st.spring.core.config;

import istia.st.spring.core.entities.Personne;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan({ "spring.core.entities" })
public class Config {

    @Bean
    public Personne personne_01() {
        return new Personne("Paul", "Dubois", 34);
    }

    @Bean
    public Personne personne_02() {
        return new Personne("Martin", "Micheline", 18);
    }

    @Bean
    public List<Personne> club(Personne personne_01, Personne personne_02) {
        List<Personne> personnes = new ArrayList<Personne>();
        personnes.add(personne_01);
        personnes.add(personne_02);
        return personnes;
    }

    @Bean
    public int mySurface() {
        return 200;
    }
}
  • znajduje się tu kod, który został już skomentowany, z dwoma nowymi elementami:
    • wiersz 13: wskazuje, że w pakiecie [spring.core.entities] znajdują się inne bean, które należy zainicjować,
    • wiersze 34–37: bean [mySurface];

5.4.4. Klasa [Appartement]

 

package spring.core.entities;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class Appartement {

    // pola wstrzykiwane przez Spring
    @Autowired
    @Qualifier("personne_01")
    private Personne propriétaire;

    @Autowired
    @Qualifier("mySurface")
    private int surface;

    // metody pobierające i ustawiające
    public Personne getPropriétaire() {
        return propriétaire;
    }

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

    public int getSurface() {
        return surface;
    }

    public void setSurface(int surface) {
        this.surface = surface;
    }

    // toString
    public String toString() {
        return String.format("Appartement[%s, %s]", propriétaire, surface);
    }

}
  • wiersz 7: adnotacja [@Component] informuje Spring, że klasa jest singletonem, który framework musi zainicjować i zarządzać nim. To właśnie dlatego, że w klasie [Config] zapisaliśmy [@ComponentScan({ "istia.st.spring.core.entities" })], ten singleton zostanie znaleziony;
  • wiersz 11: prośba skierowana do Springa o wstrzyknięcie do pola odwołania do jednego z singletonów. Można je zdefiniować na dwa sposoby:
    • poprzez jego identyfikator (wiersze 12, 16),
    • poprzez jego typ, jeśli istnieje tylko jeden singleton tego typu;

5.4.5. Uruchomienie projektu

Uruchomienie projektu daje następujący wynik w konsoli:

17:32:39.797 [main] DEBUG o.s.core.env.StandardEnvironment - Adding [systemProperties] PropertySource with lowest search precedence
....
17:32:40.134 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'appartement'
personnes--------
Personne[Dubois, Paul,34]
Personne[Micheline, Martin,18]
club--------
Personne[Dubois, Paul,34]
Personne[Micheline, Martin,18]
appartement--------
Appartement[Personne[Dubois, Paul,34], 200]
17:32:40.135 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'personne_01'
beans [p01,p01b] identiques ? true
  • wiersze 1–3: Spring generuje bardzo dużą liczbę wpisów logów, sięgającą kilkudziesięciu wierszy. Wpisy te mogą być bardzo przydatne podczas debugowania projektu, który nie działa. Gdy projekt działa poprawnie, można ograniczyć liczbę wpisów logów w następujący sposób:
  

W folderze [src / main / resources] tworzymy następujący plik [logback.xml]:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- kodery mają domyślnie przypisany typ
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- kontrola poziomu logów -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • w wierszu 12 ustawia się poziom logów. [debug] to poziom bardzo szczegółowy, [info] znacznie mniej szczegółowy;

Oto wyniki uzyskane przy użyciu pliku [level=info]:

17:39:58.580 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@7cf10a6f: startup date [Tue Apr 07 17:39:58 CEST 2015]; root of context hierarchy
personnes--------
Personne[Dubois, Paul,34]
Personne[Micheline, Martin,18]
club--------
Personne[Dubois, Paul,34]
Personne[Micheline, Martin,18]
appartement--------
Appartement[Personne[Dubois, Paul,34], 200]
beans [p01,p01b] identiques ? true

Pozostał tylko jeden wiersz logów.

5.4.6. Tworzenie archiwum projektu wraz z zależnościami

Archiwum utworzone w poprzednim projekcie może być również wykorzystane przez projekt Eclipse nieoparty na Mavenie. Niektóre projekty korzystają z wielu bibliotek i trudno jest o żadnej nie zapomnieć. Właśnie w tym zakresie Maven sprawdza się znakomicie, ponieważ wystarczy podać nazwę zależności najwyższego poziomu, a pozostałe, niższych poziomów, zostaną automatycznie dodane do ścieżki Classpath projektu. Gdy projekt Eclipse nieoparty na Mavenie musi korzystać z archiwów projektu opartego na Mavenie, możliwe jest wygenerowanie artefaktu tego ostatniego wraz ze wszystkimi jego zależnościami (co nie miało miejsca w poprzednim projekcie). Aby to zrobić, w pliku [pom.xml] muszą znajdować się następujące wiersze 3–10:


    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
</build>

Określają one wtyczkę Maven umożliwiającą wygenerowanie artefaktu projektu wraz z jego zależnościami. Następnie należy postępować w następujący sposób [1-6]:

  • [4-6] reprezentują konfigurację uruchomienia Mavena;
  • w [4] należy wpisać dowolną nazwę;
  • w polu [5] należy wskazać folder projektu;
  • w polu [6] należy wpisać cele Maven (goals):
    • [clean]: folder projektu [target] zostanie usunięty;
    • [compile]: projekt zostaje skompilowany. Wyniki kompilacji są umieszczane w odnowionym folderze [target];
    • [assembly:single]: klasy projektu i jego zależności są umieszczane w jednym archiwum jar w folderze [target];

Po uruchomieniu otrzymujemy następujący wynik:

Archiwum jar to plik skompresowany w formacie ZIP, który można otworzyć za pomocą programu do rozpakowywania. Po rozpakowaniu powyższego archiwum otrzymujemy następującą strukturę katalogów:

  • w pliku [8] znajdują się klasy zależności projektu;
  • w pliku [9] znajdują się klasy samego projektu;

5.5. Exemple-04

5.5.1. Cel

Ten przykład nawiązuje do jednego z przykładów przedstawionych w dokumencie [Introduction à Spring IoC], w którym pokazano wkład Springa w konfigurację architektur wielowarstwowych. W oryginalnym dokumencie przykład ten został omówiony przy użyciu konfiguracji Springa utworzonej za pomocą pliku XML. W tym przypadku przykład ten jest realizowany przy użyciu konfiguracji opartej na klasach Java i adnotacjach.

Chcemy skonfigurować projekt Springa dla następującej architektury:

Każda warstwa posiada interfejs zaimplementowany przez dwie klasy. Chcemy pokazać, że dzięki Springowi można zmienić implementację jednej warstwy bez żadnego wpływu na kod pozostałych warstw.

5.5.2. Projekt Eclipse

5.5.2.1. Génération

Tworzymy nowy typ projektu:

  • w [4] wpisz nazwę projektu Eclipse;
  • w polu [5] wybierz projekt Maven;
  • w [6] wybierz wersję Javy >=1.7;
  • w [7] należy wybrać proponowaną wersję Spring Boot;
  • informacje w polu [8-11] dotyczą Maven;
  • w [12] można wybrać jedną lub więcej z proponowanych zależności. Spowoduje to dodanie pewnej liczby zależności do pliku Maven o nazwie [pom.xml];
  • w pliku [13] należy wskazać istniejący, pusty katalog, w którym zostanie umieszczony projekt;
  • w pliku [14] – wygenerowany projekt. Przeanalizujmy teraz poszczególne elementy;

Projekt jest projektem Maven skonfigurowanym przez następujący plik [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>istia.st.spring.core</groupId>
    <artifactId>spring-core-04</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-core-04</name>
    <description>Programmation par interfaces</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath/> <!-- wyszukiwanie elementu nadrzędnego z repozytorium -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.SpringCore04Application</start-class>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • wiersze 6–12: zawierają informacje wprowadzone w kreatorze tworzenia projektu;
  • wiersze 14–19: nadrzędny projekt Maven, który definiuje pewną liczbę bibliotek wraz z ich wersjami. Jeśli któraś z nich jest zależnością projektu, jest wymieniona w pliku [pom.xml] bez podania wersji;
  • wiersz 23: ten wiersz ma znaczenie tylko wtedy, gdy zamierzamy wygenerować archiwum wykonywalne projektu. W przeciwnym razie pozostaje on niewykorzystany;
  • wiersze 28–31: minimalne zależności projektu Spring Boot. Przypominamy, że nie zaznaczyliśmy żadnej zależności na liście wyboru;
  • wiersze 33–37: zależność niezbędna do obsługi testów jednostkowych JUnit i [http://junit.org/] zintegrowanych ze Springiem. Wiersz 36 wskazuje, że zależność ta jest potrzebna wyłącznie do testów. W związku z tym nie zostanie ona dołączona do archiwum projektu;
  • wiersze 42–45: wtyczka umożliwiająca wygenerowanie artefaktu Maven projektu;

Lista zależności zawartych w tym pliku jest następująca: [1]:

Zobaczymy, że są one wystarczające do tego, co chcemy tutaj osiągnąć.

5.5.2.2. Klasa wykonywalna

  

Klasa wykonywalna [SpringCore04Application] [[2] ma następującą postać:


package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringCore04Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringCore04Application.class, args);
    }
}
  • w wierszu 6 adnotacja [@SpringBootApplication] stanowi skrót dla trzech adnotacji [@Configuration, @EnableAutoConfiguration, @ComponentScan], co oznacza, że:
    • klasa [SpringCore04Application] jest klasą konfiguracyjną Springa;
    • że Spring Boot ma przeprowadzać konfiguracje na podstawie klas, które znajdzie w ścieżce Classpath projektu, a więc w tym przypadku w zależnościach Maven;
    • aby sprawdzić bieżący katalog (ten, w którym znajduje się klasa [SpringCore04Application]) w poszukiwaniu ewentualnych innych komponentów Spring;
  • wiersz 10: wykonywana jest metoda statyczna [SpringApplication.run]. Jej pierwszym parametrem jest klasa konfiguracyjna Springa, w tym przypadku klasa [SpringCore04Application]. Drugim parametrem jest tutaj lista argumentów przekazanych do metody [main] (wiersz 9). Zadaniem metody statycznej [SpringApplication.run] jest utworzenie kontekstu Spring, tj. utworzenie różnych beanów znalezionych albo w klasach konfiguracyjnych, albo w folderach przeszukiwanych przez adnotację [@ComponentScan]. Metoda [main] nie wykonuje tutaj żadnych innych czynności. Aby nadać jej nieco większą treść, przekształcimy ją w następujący sposób:

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class SpringCore04Application {

    public static void main(String[] args) {
        // instancjonowanie kontekstu Spring
        ConfigurableApplicationContext context = SpringApplication.run(SpringCore04Application.class, args);
        // wyświetlanie kontekstu
        System.out.println("---------------- Liste des beans Spring");
        for (String beanName : context.getBeanDefinitionNames()) {
            System.out.println(beanName);
        }
        // zamknięcie kontekstu
        context.close();
    }
}
  • wiersz 12: statyczna metoda [SpringApplication.run] zwraca kontekst Springa, który utworzyła;
  • wiersze 15–17: wyświetla się nazwy wszystkich beanów w tym kontekście;

Aplikację można uruchomić w następujący sposób: [1-3]. Standardowa metoda [Run As Java Application] jest również poprawna.

Otrzymujemy następujący wynik:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.3.RELEASE)

2015-04-08 11:18:38.254  INFO 4796 --- [           main] demo.SpringCore04Application             : Starting SpringCore04Application on Gportpers3 with PID 4796 (D:\data\istia-1415\polys\istia\dvp-spring-database\codes\original\intro-spring-core\spring-core-04\target\classes started by ST in D:\data\istia-1415\polys\istia\dvp-spring-database\codes\original\intro-spring-core\spring-core-04)
2015-04-08 11:18:38.295  INFO 4796 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@64485a47: startup date [Wed Apr 08 11:18:38 CEST 2015]; root of context hierarchy
2015-04-08 11:18:38.776  INFO 4796 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2015-04-08 11:18:38.788  INFO 4796 --- [           main] demo.SpringCore04Application             : Started SpringCore04Application in 0.773 seconds (JVM running for 1.335)
---------------- Liste des beans Spring
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
springCore04Application
org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor
org.springframework.context.annotation.ConfigurationClassPostProcessor.enhancedConfigurationProcessor
org.springframework.boot.autoconfigure.AutoConfigurationPackages
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.condition.BeanTypeRegistry
propertySourcesPlaceholderConfigurer
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
mbeanExporter
objectNamingStrategy
mbeanServer
2015-04-08 11:18:38.789  INFO 4796 --- [           main] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@64485a47: startup date [Wed Apr 08 11:18:38 CEST 2015]; root of context hierarchy
2015-04-08 11:18:38.790  INFO 4796 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
  • wiersze 14–28: bean’y kontekstu Spring. Nie znamy ich roli. W wierszu 18 znajdujemy bean [springCore04Application], który dzięki adnotacji [@SpringBootApplication] automatycznie staje się bean’em Spring;
  • pozostałe wiersze to logi Springa o poziomie [INFO]. Jak już widzieliśmy, logi te można kontrolować za pomocą pliku [logback.xml] umieszczonego w ścieżce Classpath projektu:
  

<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- kodery mają domyślnie przypisany typ
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- kontrola poziomu logów -->
  <root level="warn"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Jeśli w wierszu 12 powyżej zastąpimy poziom [info] poziomem [warn], otrzymamy następujący wynik:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.3.RELEASE)

---------------- Liste des beans Spring
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
springCore04Application
org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor
org.springframework.context.annotation.ConfigurationClassPostProcessor.enhancedConfigurationProcessor
org.springframework.boot.autoconfigure.AutoConfigurationPackages
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.condition.BeanTypeRegistry
propertySourcesPlaceholderConfigurer
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
mbeanExporter
objectNamingStrategy
mbeanServer

Logi zniknęły. Wyświetlane są tylko komunikaty o poziomie [warn], a w tym przypadku żadnego takiego nie było.

5.5.3. Wdrożenie poszczególnych warstw architektury

Teraz zaimplementujemy trzy warstwy powyższej architektury:

  

Warstwa [DAO] jest zaimplementowana przez pakiet [spring.core.dao]. Posiada ona następujący interfejs [IDao]:


package spring.core.dao;

public interface IDao {

    public int doSomethingInDaoLayer(int a, int b);
}

Interfejs ten ma dwie implementacje: [Dao1] i [Dao2]:


package spring.core.dao;

public class Dao1 implements IDao {

    public int doSomethingInDaoLayer(int a, int b) {
        return a+b;
    }

}

package spring.core.dao;

public class Dao2 implements IDao {

    public int doSomethingInDaoLayer(int a, int b) {
        return a-b;
    }

}

Warstwa [métier] jest zaimplementowana przez pakiet [spring.core.metier]. Posiada ona następujący interfejs [IMetier]:


package spring.core.metier;

public interface IMetier {

    public int doSomethingInMetierLayer(int a, int b);
}

Interfejs ten ma dwie implementacje: [Metier1] i [Metier2]:


package spring.core.metier;

import spring.core.dao.IDao;

public class Metier1 implements IMetier {

    private IDao dao;

    public int doSomethingInMetierLayer(int a, int b) {
        a++;
        b++;
        return dao.doSomethingInDaoLayer(a, b);
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }
}

package spring.core.metier;

import spring.core.dao.IDao;

public class Metier2 implements IMetier {

    private IDao dao;

    public int doSomethingInMetierLayer(int a, int b) {
        a--;
        b++;
        return dao.doSomethingInDaoLayer(a, b);
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }


}

Warstwa [UI] jest zaimplementowana przez pakiet [spring.core.ui]. Posiada ona następujący interfejs [IUi]:


package spring.core.ui;

public interface IUi {

    public int doSomethingInUiLayer(int a, int b);
}

Interfejs ten ma dwie implementacje: [Ui1] i [Ui2]:


package spring.core.ui;

import spring.core.metier.IMetier;

public class Ui1 implements IUi {

    private IMetier metier;
    
    public int doSomethingInUiLayer(int a, int b) {
        a++;
        b++;
        return metier.doSomethingInMetierLayer(a, b);
    }

    public void setMetier(IMetier metier) {
        this.metier = metier;
    }

}

package spring.core.ui;

import spring.core.metier.IMetier;

public class Ui2 implements IUi {

    private IMetier metier;

    public int doSomethingInUiLayer(int a, int b) {
        a--;
        b++;
        return metier.doSomethingInMetierLayer(a, b);
    }

    public void setMetier(IMetier metier) {
        this.metier = metier;
    }

}

5.5.4. Konfiguracja projektu Spring

  

Klasa konfiguracyjna [Config] wygląda następująco:


package spring.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import spring.core.dao.Dao1;
import spring.core.dao.Dao2;
import spring.core.dao.IDao;
import spring.core.metier.IMetier;
import spring.core.metier.Metier1;
import spring.core.metier.Metier2;
import spring.core.ui.IUi;
import spring.core.ui.Ui1;
import spring.core.ui.Ui2;

@Configuration
public class Config {

    // -------------- implementacja [Ui1, Metier1, Dao1]
    @Bean
    public IDao dao1() {
        return new Dao1();
    }

    @Bean
    public IMetier metier1(IDao dao1) {
        Metier1 metier = new Metier1();
        metier.setDao(dao1);
        return metier;
    }

    @Bean
    public IUi ui1(IMetier metier1) {
        Ui1 ui = new Ui1();
        ui.setMetier(metier1);
        return ui;
    }

    // -------------- implementacja [Ui2, Metier2, Dao2]
    @Bean
    public IDao dao2() {
        return new Dao2();
    }

    @Bean
    public IMetier metier2(IDao dao2) {
        Metier2 metier = new Metier2();
        metier.setDao(dao2);
        return metier;
    }

    @Bean
    public IUi ui2(IMetier metier2) {
        Ui2 ui = new Ui2();
        ui.setMetier(metier2);
        return ui;
    }
}
  • wiersze 20–23: bean o nazwie [dao1] (nazwa metody) jest instancją klasy [Dao1] (wiersz 22), traktowanej jako implementacja interfejsu [IDao] (wiersz 21). Bean [dao1] jest zatem traktowany jako instancja interfejsu (terminologia ta jest nieprawidłowa, ale można ją zrozumieć), a nie jako instancja klasy. Jest to ważna kwestia, którą należy zrozumieć. Wszystkie pozostałe beany będą również instancjami interfejsów;
  • wiersze 25–30: instancja interfejsu [IMetier] zaimplementowanego przez klasę [Metier1];
  • wiersze 32–37: instancja interfejsu [IUi] zaimplementowana przez klasę [Ui1];
  • wiersze 20–37: implementują warstwy [UI, Metier, DAO] za pomocą instancji [Ui1, Metier1, Dao1];
  • wiersze 40–57: implementują warstwy [UI, Metier, DAO] za pomocą instancji [Ui2, Metier2, Dao2];

5.5.5. Test jednostkowy [JUnitTest]

  

Klasa [JUnitTest] znajduje się w gałęzi [src / test / java] projektu Maven. Elementy tej gałęzi nie są umieszczane w ostatecznym archiwum projektu. Jej kod wygląda następująco:


package spring.core.tests;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.core.config.Config;
import spring.core.dao.IDao;
import spring.core.metier.IMetier;
import spring.core.ui.IUi;

@SpringApplicationConfiguration(classes = { Config.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTest {
...
}
  • wiersz 16: adnotacja [@SpringApplicationConfiguration] jest adnotacją projektu Spring Boot Test (wiersz 8). Wynika ona z następującej zależności pliku [pom.xml]:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
</dependency>

Anotacja ta ma jako parametr listę klas konfiguracyjnych, które mają zostać użyte do zbudowania kontekstu Spring niezbędnego do przeprowadzenia testu. W tym przypadku wykorzystujemy klasę konfiguracyjną [Config], którą już przedstawiono;

  • wiersz 17: adnotacja [@RunWith] jest adnotacją JUnit (wiersz 5). Jej parametrem jest klasa odpowiedzialna za przeprowadzanie testów zamiast domyślnej klasy frameworka JUnit. Klasa ta jest klasą Spring (wiersz 9). Będzie ona wykorzystywać adnotacje Spring obecne w klasie testowej;

Pełna klasa wygląda następująco


...

@SpringApplicationConfiguration(classes = { Config.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTest {

    // warstwa [UI]
    @Autowired
    @Qualifier("ui1")
    private IUi ui1;

    @Autowired
    @Qualifier("ui2")
    private IUi ui2;

    // warstwa [métier]
    @Autowired
    @Qualifier("metier1")
    private IMetier metier1;

    @Autowired
    @Qualifier("metier2")
    private IMetier metier2;

    // warstwa [dao]
    @Autowired
    @Qualifier("dao1")
    private IDao dao1;

    @Autowired
    @Qualifier("dao2")
    private IDao dao2;

    @Test
    public void testDao() {
        Assert.assertEquals(30, dao1.doSomethingInDaoLayer(10, 20));
        Assert.assertEquals(-10, dao2.doSomethingInDaoLayer(10, 20));
    }

    @Test
    public void testMetier() {
        Assert.assertEquals(32, metier1.doSomethingInMetierLayer(10, 20));
        Assert.assertEquals(-12, metier2.doSomethingInMetierLayer(10, 20));
    }

    @Test
    public void testUI() {
        Assert.assertEquals(34, ui1.doSomethingInUiLayer(10, 20));
        Assert.assertEquals(-14, ui2.doSomethingInUiLayer(10, 20));
    }

}
  • wiersze 8–10: wstrzykuje się (wiersz 8) bean o nazwie (wiersz 9) [ui1]. W wierszu 10 należy zauważyć, że wstrzykuje się instancję interfejsu, a nie instancję klasy;
  • wiersze 21–32: w ten sam sposób wstrzykuje się pozostałe bean-y zdefiniowane w klasie [Config];
  • wiersz 34: adnotacja [@Test] wskazuje metodę, która ma zostać wykonana podczas testów. Inne możliwe adnotacje to:
    • [@BeforeClass]: metoda do wykonania przed rozpoczęciem testów;
    • [@AfterClass]: metoda do wykonania po zakończeniu wszystkich testów;
    • [@Before]: metoda do wykonania przed każdym testem;
    • [@After]: metoda do wykonania po każdym teście;
  • linia 36: sprawdzamy, czy wywołanie [dao1.doSomethingInDaoLayer(10, 20)] rzeczywiście zwraca 30. Zgodnie z konwencją pierwszy parametr to wartość oczekiwana, a drugi – wartość rzeczywista;
  • wiersz 36: testuje instancję [dao1] interfejsu [IDao];
  • wiersz 37: testuje instancję [dao2] interfejsu [IDao];
  • wiersz 42: testuje instancję [metier1] interfejsu [IMetier];
  • wiersz 43: testuje instancję [metier2] interfejsu [IMetier];
  • wiersz 48: testuje instancję [ui1] interfejsu [IUi];
  • wiersz 36: testuje instancję [ui2] interfejsu [IUi];

W metodzie testowej można stosować następujące asercje:

  • assertEquals(wyrażenie1, wyrażenie2): sprawdza, czy wartości obu wyrażeń są równe. Akceptowanych jest wiele typów wyrażeń (int, String, float, double, boolean, char, short). Jeśli oba wyrażenia nie są równe, generowany jest wyjątek typu [AssertionFailedError ],
  • assertEquals(liczbowa1, liczbowa2, delta): sprawdza, czy dwie liczby rzeczywiste są równe z dokładnością do delta, c.a.d abs(liczbowa1-liczbowa2)<=delta. Można na przykład zapisać assertEquals(liczbowa1, liczbowa2, 1E-6), aby sprawdzić, czy dwie wartości są równe z dokładnością do 10⁻⁶,
  • assertEquals(komunikat, wyrażenie1, wyrażenie2) oraz assertEquals(komunikat, liczba1, liczba2, delta) to warianty pozwalające określić komunikat o błędzie, który ma być powiązany z wyjątkiem typu [AssertionFailedError] zgłaszanym w przypadku niepowodzenia metody [assertEquals],
  • assertNotNull(Object) oraz assertNotNull(message, Object): sprawdzają, czy odwołanie Object nie jest równe null,
  • assertNull(Object) oraz assertNull(message, Object): sprawdza, czy odwołanie Object jest równe null,
  • assertSame(Object1, Object2) oraz assertSame(message, Object1, Object2): sprawdza, czy odwołania Object1 i Object2 wskazują na ten sam obiekt,
  • assertNotSame(Object1, Object2) oraz assertNotSame(message, Object1, Object2): sprawdza, czy referencje Object1 i Object2 nie wskazują na ten sam obiekt;

Aby przeprowadzić test, można postępować w następujący sposób:

Otrzymujemy następujący wynik:

 

W tym przypadku wszystkie testy zakończyły się powodzeniem. Co pokazuje ten przykład? Pokazuje elastyczność, jaką zapewnia framework Spring w konfiguracji architektury warstwowej. Można zdecydować się na użycie implementacji [Ui1, Metier1, Dao1] lub [Ui2, Metier2, Dao2] po prostu poprzez konfigurację. Tak więc w poprzednim teście JUnit, jeśli zachowamy jedynie wstrzykiwanie beanów [ui1, metier1, dao1], pracujemy z pierwszą architekturą. Aby zmienić architekturę, wystarczy zmienić wstrzykiwane beany. Odbywa się to bez zmiany kodu warstw implementujących interfejsy. Ten rodzaj programowania nazywamy programowaniem opartym na interfejsach, ponieważ nie wykorzystujemy instancji klas implementujących warstwy, lecz instancje ich interfejsów.

5.6. Conclusion

  • Spring zarządza obiektami, które są singletonami (pojedynczym egzemplarzem). Spring zarządza również obiektami, które są instancjonowane za każdym razem, gdy żądamy od Springa ich instancji. Ten przypadek zostanie również przedstawiony w niniejszym dokumencie;
  • obiekty te można deklarować na różne sposoby, które można łączyć:
    • w pliku XML,
    • w klasie Java opatrzonej adnotacją [@Configuration],
    • w dowolnej klasie Java opatrzonej adnotacją [@Component, @Service, ...];
  • obiekt Springa może zostać wstrzyknięty do innego obiektu Springa za pomocą adnotacji [@Autowired]. Mówimy wtedy o wstrzykiwaniu zależności (DI: Dependency Injection);
  • Spring okazuje się bardzo przydatny do konfigurowania architektur warstwowych przy jednoczesnym wykorzystaniu paradygmatu programowania przez interfejsy;