Skip to content

2. Вступ до фреймворку Spring

Spring з’явився у 2004 році спочатку як контейнер об’єктів. З того часу він розвинувся у численні напрямки: Spring MVC, Spring Data, Spring Batch, ... [http://spring.io]. У цьому розділі ми розглянемо лише контейнер об’єктів. Ось кілька основних моментів:

  • додаток має багато класів, і деякі з них спільно використовують об’єкти, які мають бути унікальними (синглтони). Spring створює та керує цими синглтонами;
  • Spring розміщує ці синглтони в структурі, яка називається контекстом;
  • класи отримують доступ до синглетонів додатка, запитуючи їх у Spring за іменем, типом або за обома параметрами;
  • Spring створює синглтони та керує їхніми можливими залежностями: синглтон може мати посилання на один або кілька інших синглтонів. Коли Spring створює синглтон, він також створює їхні залежності;
  • коли запускається додаток, що базується на Spring, він може попросити Spring створити всі синглтони додатка. Після цього вони стануть доступними в контексті Spring;
  • Spring полегшує використання багаторівневих архітектур та програмування за допомогою інтерфейсів. У простих випадках кожен рівень реалізується синглтоном і реалізує інтерфейс. Якщо додаток працює з інтерфейсами рівнів, а не з їхніми класами реалізації, то отримуємо еволюційну архітектуру, яка дозволяє змінювати реалізацію одного рівня, не змінюючи інших, завдяки двом таким характеристикам:
    • додаток отримує посилання на рівень за його назвою. Spring надає йому посилання на клас, що реалізує цей рівень;
    • додаток використовує це посилання як посилання на інтерфейс шару, а не як посилання на клас;

Оголошення синглетонів можна здійснювати трьома способами, які можна комбінувати:

  • у файлі XML,
  • у спеціальному класі конфігурації;
  • у будь-якому класі за допомогою анотацій;

Далі наведено три приклади конфігурації:

  • [exemple-01]: централізована конфігурація в одному файлі XML;
  • [exemple-02]: централізована конфігурація в одному класі Java;
  • [exemple-03]: конфігурація, розподілена між кількома класами Java;

Останній приклад [exemple-04] присвячений конфігурації Spring для багатошарової архітектури. Це найважливіший приклад. Саме він постійно використовуватиметься для конфігурації архітектур, описаних у цьому документі.

Ці чотири приклади закладають основи для подальшого викладу:

  • конфігурація Spring та ін’єкція залежностей;
  • використання Maven для управління залежностями проєкту;
  • використання JUnit для тестування проектів;

2.1. Налаштування робочого середовища

Вам необхідно мати:

  • встановити JDK (Java Development Kit) (розділ 23.1);
  • встановлений менеджер залежностей Maven (параграф 23.2);
  • встановити IDE Spring Tool Suite (STS) (параграф 23.3);
  • завантажив коди з документа [http://tahe.developpez.com/java/spring-database];

Імпортуйте в STS конфігурації виконання з папки [eclipse config], що містить приклади. Ці конфігурації є особливо важливими. Деякі проекти для виконання вимагають передачі аргументів до JVM, і налаштування такого типу зазвичай викликають складнощі. Крім того, у цьому документі використовуються проекти Maven. Якщо ви побачите таке попередження:

Примітка: запустіть [Alt-F5], щоб перегенерувати всі проекти Maven.

Наполегливо рекомендується дотримуватися цієї поради. Без цього запобіжного заходу у проектах можуть виникати незрозумілі помилки лише через те, що залежності Maven між проектами є неправильними.

  • у [1], клацніть правою кнопкою миші на [Package Explorer];
  • у [4a-4b-4c] вкажіть папку [eclipse config / launch configurations] [4b] із прикладами;
  • у [5] — доступні конфігурації. Виберіть їх усі;
  • у [6] завершіть роботу майстра;
  • у [7-8] перегляньте імпортовані конфігурації виконання;
  • у [8-9] зніміть галочку з [9], щоб відобразити конфігурації проектів, які не завантажені в STS. Це саме той випадок;
  • у [10] — Java-додатки, а в [11] — конфігурації виконання перших трьох прикладів, які ми будемо вивчати;
  • у [12] — тести JUnit, а в [13] — конфігурація виконання четвертого прикладу цього параграфа;

Тепер створіть у Eclipse змінну з назвою [M2_REPO], яка вказуватиме на папку локального репозиторію Maven (див. розділ 23.2). Ця змінна використовується в декількох конфігураціях виконання:

Змінній [M2_REPO] було присвоєно значення, яке можна побачити в [6] нижче:

Тепер імпортуйте чотири приклади з папки [spring-core]:

  • у файл [1], клацніть правою кнопкою миші на [Package Explorer];
  • у [4a-4b], виберіть папку [spring-core] із прикладами;
  • у [5], вибрати всі проекти з папки та створити [Finish];
  • у [6] — чотири проекти з [Package Explorer];

2.2. Exemple-01

2.2.1. Проект Eclipse

 

2.2.2. Клас [Personne]

 

package istia.st.spring.core;

public class Personne {

    // поля
    private String nom;
    private String prenom;
    private int age;

    // конструктори
    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);
    }

    // гетери та сеттери

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

}

Примітка: методи getter та setter можна автоматично згенерувати таким чином: [1-2]:

2.2.3. Клас [Appartement]

 

package istia.st.spring.core;

public class Appartement {

    // поля
    private Personne proprietaire;
    private int surface;

    // геттери та сеттери

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

}

Примітка: цей клас не має явного конструктора. У цьому випадку за замовчуванням завжди існує конструктор без параметрів, який нічого не робить. Коли створюються конструктори, цей конструктор за замовчуванням більше не існує неявно. Тоді його потрібно явно визначити:

public Appartement(){
}

2.2.4. Файл конфігурації Spring

 

<?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">
    <!-- Особа 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>
    <!-- Особа 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>
    <!-- список осіб -->
    <util:list id="club">
        <ref bean="personne_01" />
        <ref bean="personne_02" />
    </util:list>
    <!-- квартира -->
    <bean id="appartement" class="istia.st.spring.core.Appartement">
        <property name="surface" value="100" />
        <property name="proprietaire" ref="personne_01" />
    </bean>
</beans>
  • рядки 2, 27: синглтони визначаються в тезі <beans>;
  • рядки 6–10: кожен синглтон визначається тегом <bean>;
  • рядок 6: [id] — ідентифікатор синглтона. [class] — повна назва класу, який потрібно інстанціювати;
  • рядки 7–9: три значення, які потрібно передати конструктору класу [Personne];
  • рядки 12–16: спочатку створюється клас [Personne] за допомогою його конструктора за замовчуванням [new Personne()]. Потім для кожного тегу [property] використовується метод-сеттер класу. Наприклад, для рядка 13 буде виконано метод [setNom("martin")]. Отже, метод [setNom] має існувати. Це важливо пам’ятати;
  • рядки 18–21: тег <util:list> дозволяє визначити синглтон, який є списком;
  • рядок 19: вказує на синглтон [personne_01], визначений у рядку 6. Тут маємо так зване введення залежностей. Для ініціалізації поля синглтона можна використовувати два атрибути:
    • [value]: для присвоєння полю примітивного значення (рядок, число, дата тощо),
    • [ref]: для присвоєння полю посилання на об’єкт Spring;

Примітка: файл конфігурації Spring можна згенерувати таким чином: [1-4]:

2.2.5. Виконуваний клас

 

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) {
        // отримання контексту Spring
        ApplicationContext ctx = new ClassPathXmlApplicationContext("config-01.xml");
        // отримано біни
        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);
        // їх відображення
        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);
        // отримані біни є синглтонами
        // їх можна запитувати кілька разів, завжди отримується той самий бін
        Personne p01b = ctx.getBean("personne_01", Personne.class);
        System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
    }
}
  • рядок 14: створює контекст Spring. Після цього створюються екземпляри всіх синглетонів, визначених у файлі [config-01.xml];
  • рядок 16: запитує посилання на синглтон, ідентифікований за кодом [personne_01], типу [Personne]. Цей другий параметр є необов’язковим, але в такому разі отримується посилання на тип [Object], яке потім потрібно перетворити на тип [Personne];
  • рядок 19: не використовується ім’я біна, а лише його тип, оскільки існує лише один синглтон типу [Appartement];
  • рядок 18: використано як ідентифікатор, так і тип потрібного синглтона. Ідентифікатор є зайвим, оскільки існує лише один синглтон типу [new ArrayList<Personne>().getClass()];
  • рядки 32–33: показують, що якщо кілька разів запитувати один і той самий синглтон, то завжди отримуємо одне й те саме посилання, що свідчить про те, що ми дійсно маємо справу з синглтоном. Цей момент важливо зрозуміти;

Примітка: виконуваний клас можна згенерувати таким чином: [1-6]:

  • саме встановлення прапорця біля [6] призведе до того, що згенерований клас міститиме статичний метод [main], який зробить його виконуваним;

2.2.6. Залежності проєкту

 
  • залежності Spring: [spring-core, spring-beans, spring-context, spring-expression, commons-logging];

Залежності додаються до проекту наступним чином:

  • у [1]: клацнути правою кнопкою миші на проекті / [Build Path] / [Configure Build Path];
  • на [2]: [Add JARs], якщо JARs, які потрібно додати, знаходяться в папці проекту. В іншому випадку — [Add External JARs];
  • в [3] виберіть JARs, які потрібно додати до ClassPath проекту (вони знаходяться тут у папці [lib] всередині проекту);

Визначення: [ClassPath] проекту — це сукупність папок, які обстежає JVM (Java Virtual Machine), що виконує проект, з метою пошуку класу, на який він посилається. Для проекту Eclipse [ClassPath] складається з таких елементів:

  • папка [bin] проекту;
  • елементи [Build Path] проекту;

Папка [bin] — це папка, створена в результаті компіляції папки [src]. Отже, все, що розміщено в папці [src], автоматично стає частиною [ClassPath] (навіть якщо це не файл .java). Отже, у попередньому проєкті файл конфігурації Spring [config-01.xml], що знаходиться у папці [src], під час виконання стане частиною папки [Classpath] цього проєкту.

2.2.7. Результати

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

2.3. Exemple-02

2.3.1. Проєкт Eclipse

 

2.3.2. Клас конфігурації 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;
    }
}
  • рядок 9: анотація [@Configuration] є анотацією Spring. Вона вказує, що анотований клас визначає синглтони. Вони визначаються за допомогою анотації [@Bean]. Spring виконає всі методи, анотовані [@Bean]. Вони створюють синглтони додатка;
  • рядки 12–15: визначають синглтон, ідентифікований за допомогою [personne_01], тобто за назвою методу.
  • рядок 23: параметри [personne_01, personne_02] мають імена синглетонів. Spring автоматично ініціалізує їх за допомогою посилань на ці синглетони. Це називається ін’єкцією параметрів;

Цей спосіб налаштування синглетонів є більш зрозумілим, ніж той, що використовує файл XML. Фактично, ми самостійно відтворюємо те, що Spring робив неявно на основі файлу XML.

2.3.3. Виконуваний клас

 

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) {
        // отримання контексту Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
        // отримуємо біни
        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);
        // вони відображаються
        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);
        // отримані біни є синглтонами
        // їх можна запитувати кілька разів, завжди отримується той самий бін
        Personne p01b = ctx.getBean("personne_01", Personne.class);
        System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
    }
}
  • у рядку 13 відбувається інстанціювання всіх бінів, визначених у класі [Config];
  • решта коду залишається без змін;

2.3.4. Залежності проекту

Залежності визначаються наступним файлом [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>
    <!-- плагіни -->
    <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>

Ручне управління залежностями проекту стає справжньою головоломкою, коли використовуються бібліотеки Java, залежності яких невідомі. Наприклад, фреймворк [Hibernate], що керує доступом до баз даних, має десятки залежностей. Проект [Maven] вирішує цю проблему. Ви вказуєте назву необхідної залежності. Вона автоматично шукається в репозиторіях Maven, розміщених у мережі. Якщо запитувана залежність сама має залежності, то вони також автоматично завантажуються. Ці завантажені залежності зберігаються в локальному репозиторії на комп’ютері. Якщо пізніше інша програма потребуватиме тієї самої залежності, її не завантажуватимуть, а шукатимуть у локальному репозиторії. Залежність характеризується такими елементами:

  • рядок 17: тег <dependency>;
  • рядок 18: атрибут [groupId], який зазвичай ідентифікує компанію, що створила залежність;
  • рядок 19: атрибут [artifactId], який ідентифікує залежність;
  • рядок 20: атрибут [version], який ідентифікує бажану версію;

Генерація проєкту сама по собі створить компонент Maven, визначений рядками 4–8:

  • рядки 4–6: атрибути [ groupId, artifactId,version], які ми щойно описали;
  • рядки 7–8: це необов’язкові атрибути;

Трохи пізніше ми повернемося до ролі рядків 24–40. Щоб перетворити звичайний проект Eclipse на проект Maven, потрібно зробити дві речі:

  • створити вищезазначений файл [pom.xml];
  • заявити, що проект відтепер є проектом Maven [1-4]:

Піктограма проекту Maven має вигляд M [4]. Літера S вказує на те, що проект містить елементи Spring. Не рекомендується перетворювати (як ми щойно зробили) проект Eclipse на проект Maven, оскільки в такому разі проект не матиме структури, очікуваної для проекту Maven, що іноді може призвести до несподіваних проблем.

2.3.5. Створення артефакту Maven для проекту

Артефактом Maven-проекту ми називаємо елемент, визначений у рядках 4–6 файлу [pom.xml]:


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

Щоб створити цей артефакт, у файлі [pom.xml] мають бути присутні такі рядки 3–7:


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

Вони визначають плагін Maven, здатний згенерувати артефакт проєкту. Далі слід виконати такі дії:

Створений таким чином артефакт потрапляє до локального репозиторію Maven. Його розташування можна знайти в налаштуваннях Eclipse:

 

Після цього можна перевірити правильність встановлення артефакту Maven:

 

Відтепер інший локальний проект Maven зможе використовувати цей архів.

2.4. Exemple-03

2.4.1. Проєкт Eclipse

Цього разу ми створюємо проект Maven [1-8]:

  • у [3b]: вкажіть порожню папку, в якій буде створено проєкт;
  • в [4]: ідентифікатор групи Maven, до якої належатиме проект;
  • в [5]: ім’я створеного артефакту Maven:
  • в [6]: його версія;
  • у [7]: його режим упаковки (існують також war, ear, apk тощо);
  • в [8]: так створений проект;

Проект Maven за замовчуванням має чітку структуру:

  • [src / main / java]: вихідний код проекту. Скомпільовані з цього коду файли будуть розміщені в папці [target/classes] проекту;
  • [src / main / resources]: ресурси, які мають бути у Classpath проекту, не будучи при цьому Java-кодом. Вони будуть скопійовані без змін у папку [target/classes] проекту;
  • [src / test / java]: вихідні коди тестів проекту. Скомпільовані з цих вихідних кодів файли будуть розміщені в папці [target/test-classes] проекту. Ці елементи не включаються до архіву Maven проекту;
  • [src / test / resources]: ресурси, які повинні бути в Classpath проекту для тестів, але не є Java-кодом. Вони будуть скопійовані без змін у папку [target/test-classes] проекту;

Ми доповнюємо проект наступним чином:

 

2.4.2. Конфігурація Maven

За замовчуванням генерується файл [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-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>

    <!-- батьківський проект Maven -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <!-- контекст Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <!-- журнали -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

    </dependencies>

    <!-- плагіни -->
    <build>
        <plugins>
            <!-- для створення архіву проєкту разом із його залежностями -->
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <!-- для встановлення артефакту проєкту в локальне сховище Maven -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • рядок 11: проект кодується як UTF-8;
  • рядок 12: для компіляції проєкту використовується JDK 1.8;
  • рядки 16–20: для проектів, що використовують бібліотеки Spring, зручно використовувати батьківський проект Maven під назвою [spring-boot-starter-parent]. Він визначає версії різних бібліотек Spring, а також версії їхніх залежностей. Це дозволяє більше не вказувати їх у визначенні залежностей. Отже, у рядках 24–27 не вказується бажана версія [spring-context]. Це буде та версія, яка визначена батьківським проєктом [spring-boot-starter-parent]. Ця техніка дозволяє не турбуватися про можливі несумісності версій між залежностями. Ті, що визначені батьківським проєктом, є сумісними між собою;
  • рядки 29–32: Spring виводить значний обсяг інформації на консоль за допомогою бібліотеки логів. Ця бібліотека імпортується тут;
  • рядки 40–47: плагін Maven, до якого ми ще повернемося;
  • рядки 50–52: плагін для генерації артефакту Maven проекту;

2.4.3. Клас конфігурації Spring

  

Клас [Config] має такий вигляд:


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;
    }
}
  • тут міститься вже прокоментований код із двома нововведеннями:
    • рядок 13: вказує, що в пакеті [spring.core.entities] є інші біни, які потрібно інстанціювати,
    • рядки 34–37: bean [mySurface];

2.4.4. Клас [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 {

    // поля, вставлені Spring
    @Autowired
    @Qualifier("personne_01")
    private Personne propriétaire;

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

    // гетери та сеттери
    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);
    }

}
  • рядок 7: анотація [@Component] вказує Spring, що клас є синглтоном, який фреймворк повинен інстанціювати та керувати ним. Саме тому, що в класі [Config] ми написали [@ComponentScan({ "istia.st.spring.core.entities" })], цей синглтон буде знайдено;
  • рядок 11: запит до Spring про введення в поле посилання на один із синглетонів. Його можна визначити двома способами:
    • за його ідентифікатором (рядки 12, 16),
    • за типом, якщо існує лише один синглтон цього типу;

2.4.5. Виконання проєкту

Виконання проекту дає такий результат у консолі:

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
  • рядки 1–3: Spring генерує дуже велику кількість логів — кілька десятків рядків. Ці логи можуть бути дуже корисними для налагодження проекту, який не працює. Коли проект працює, логи можна скоротити таким чином:
  

У папці [src / main / resources] створюємо такий файл [logback.xml]:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- кодерам за замовчуванням присвоюється тип
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- контроль рівня журналів -->
  <root level="info"> <!-- інформація, налагодження, попередження -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • у рядку 12 встановлюється рівень журналів. [debug] — це дуже детальний рівень, [info] — набагато менш детальний;

Ось результати з [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

Залишився лише один рядок журналу.

2.4.6. Створення архіву проєкту разом із його залежностями

Архів, створений у попередньому проєкті, також може використовуватися проєктом Eclipse, що не використовує Maven. Деякі проєкти використовують багато бібліотек, і може бути складно не пропустити жодної з них. Саме тут Maven показує свою перевагу, адже достатньо вказати залежність найвищого рівня, щоб інші, нижчого рівня, автоматично додавалися до Classpath проєкту. Коли проект Eclipse, що не використовує Maven, має використовувати архіви проекту Maven, можна згенерувати артефакт останнього разом з усіма його залежностями (чого не було в попередньому проекті). Для цього у файлі [pom.xml] мають бути присутні такі рядки 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>

Вони визначають плагін Maven, здатний згенерувати артефакт проєкту разом із його залежностями. Далі слід виконати такі дії: [1-6]:

  • [4-6] представляють конфігурацію виконання Maven;
  • у [4] вкажіть будь-яку назву;
  • у полі [5] вкажіть папку проекту;
  • у полі [6] вкажіть цілі Maven (goals):
    • [clean]: папка [target] проекту видаляється;
    • [compile]: проект компілюється. Результати компіляції розміщуються у відновленій папці [target];
    • [assembly:single]: класи проекту та його залежностей розміщуються в єдиному архіві jar у папці [target];

Після виконання отримуємо такий результат:

Архів jar — це заархівований файл, який можна розпакувати за допомогою програми для розпакування. Після розпакування попереднього архіву отримуємо таку структуру:

  • у [8] — класи залежностей проєкту;
  • у [9] — класи самого проєкту;

2.5. Exemple-04

2.5.1. Мета

Цей приклад повторює один із тих, що наведено в документі [Introduction à Spring IoC], у якому показано внесок Spring у конфігурацію багаторівневих архітектур. У оригінальному документі приклад розглядається з конфігурацією Spring, створеною за допомогою файлу XML. Тут ми розглядаємо приклад із конфігурацією за допомогою класів Java та анотацій.

Ми хочемо налаштувати проект Spring для такої архітектури:

Кожен рівень має інтерфейс, реалізований двома класами. Ми хочемо показати, що завдяки Spring можна змінити реалізацію одного рівня, не впливаючи на код інших рівнів.

2.5.2. Проєкт Eclipse

2.5.2.1. Génération

Ми створюємо новий тип проєкту:

  • у [4] вкажіть назву проекту Eclipse;
  • у полі [5] виберіть проект Maven;
  • у [6] виберіть версію Java >=1.7;
  • у полі [7] виберіть запропоновану версію Spring Boot;
  • інформація в полі [8-11] стосується Maven;
  • у [12] можна вибрати одну або кілька із запропонованих залежностей. Це призведе до включення певної кількості залежностей у файл Maven [pom.xml];
  • у файлі [13] слід вказати існуючу порожню папку для розміщення проєкту;
  • у [14] — згенерований проєкт. Розглянемо його елементи детальніше;

Проект — це проект Maven, налаштований за допомогою такого файлу [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/> <!-- пошук батьківського об’єкта в репозиторії -->
    </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>
  • рядки 6–12: містять інформацію, введену в майстрі створення проєкту;
  • рядки 14–19: батьківський проект Maven, який визначає певну кількість бібліотек із зазначенням їхніх версій. Якщо одна з них є залежністю проекту, вона згадується у файлі [pom.xml] без зазначення її версії;
  • рядок 23: цей рядок використовується лише в тому випадку, якщо планується створити виконуваний архів проекту. В іншому випадку він не використовується;
  • рядки 28–31: мінімальні залежності для проекту Spring Boot. Нагадаємо, що ми не вибрали жодної залежності у списку;
  • рядки 33–37: залежність, необхідна для управління модульними тестами JUnit та [http://junit.org/], інтегрованими зі Spring. Рядок 36 вказує, що ця залежність потрібна лише для тестів. Відповідно, вона не буде включена до архіву проєкту;
  • рядки 42–45: плагін, що дозволяє згенерувати артефакт Maven для проєкту;

Список залежностей, що містяться в цьому файлі, є таким: [1]:

Ми побачимо, що їх цілком достатньо для того, що ми хочемо тут зробити.

2.5.2.2. Виконуваний клас

  

Виконуваний клас [SpringCore04Application] [[2] має такий вигляд:


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);
    }
}
  • у рядку 6 анотація [@SpringBootApplication] є скороченням для трьох анотацій [@Configuration, @EnableAutoConfiguration, @ComponentScan], що означає:
    • що клас [SpringCore04Application] є класом конфігурації Spring;
    • що Spring Boot має виконувати конфігурації на основі класів, які він знайде у Classpath проєкту, тобто в даному випадку у залежностях Maven;
    • перевірити поточний каталог (каталог класу [SpringCore04Application]) на наявність інших можливих компонентів Spring;
  • рядок 10: виконується статичний метод [SpringApplication.run]. Його першим параметром є клас конфігурації Spring, у даному випадку клас [SpringCore04Application]. Другим параметром тут є список аргументів, переданих методу [main] (рядок 9). Статичний метод [SpringApplication.run] призначений для створення контексту Spring, тобто для створення різних бінів, знайдених або в класах конфігурації, або в папках, просканованих за допомогою анотації [@ComponentScan]. Метод [main] тут нічого іншого не робить. Щоб надати йому трохи більше змісту, ми перетворимо його наступним чином:

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) {
        // інстанціювання контексту Spring
        ConfigurableApplicationContext context = SpringApplication.run(SpringCore04Application.class, args);
        // виведення контексту
        System.out.println("---------------- Liste des beans Spring");
        for (String beanName : context.getBeanDefinitionNames()) {
            System.out.println(beanName);
        }
        // закриття контексту
        context.close();
    }
}
  • рядок 12: статичний метод [SpringApplication.run] повертає контекст Spring, який він створив;
  • рядки 15–17: виводиться назва всіх бінів цього контексту;

Запустити додаток можна таким чином: [1-3]. Звичайний метод [Run As Java Application] також є правильним.

Отримуємо такий результат:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • рядки 14–28: біни контексту Spring. Ми не знаємо їхньої ролі. У рядку 18 ми бачимо бін [springCore04Application], який завдяки своїй анотації [@SpringBootApplication] автоматично стає біном Spring;
  • інші рядки — це журнали Spring рівня [INFO]. Як ми вже бачили, ці журнали можна контролювати за допомогою файлу [logback.xml], розміщеного в Classpath проекту:
  

<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- кодерам за замовчуванням присвоюється тип
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- контроль рівня журналів -->
  <root level="warn"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Якщо у рядку 12 вище замінити рівень [info] на [warn], отримаємо такий результат:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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

Журнали зникли. З’являються лише повідомлення рівня [warn], а тут їх не було.

2.5.3. Реалізація різних рівнів архітектури

Тепер ми реалізуємо три рівні архітектури, наведені вище:

  

Рівень [DAO] реалізовано пакетом [spring.core.dao]. Він має такий інтерфейс [IDao]:


package spring.core.dao;

public interface IDao {

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

Цей інтерфейс має дві реалізації: [Dao1] та [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;
    }

}

Рівень [métier] реалізовано пакетом [spring.core.metier]. Він має такий інтерфейс [IMetier]:


package spring.core.metier;

public interface IMetier {

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

Цей інтерфейс має дві реалізації: [Metier1] та [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;
    }


}

Рівень [UI] реалізовано пакетом [spring.core.ui]. Він має такий інтерфейс [IUi]:


package spring.core.ui;

public interface IUi {

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

Цей інтерфейс має дві реалізації: [Ui1] та [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;
    }

}

2.5.4. Налаштування проекту Spring

  

Клас конфігурації [Config] має такий вигляд:


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 {

    // -------------- реалізація [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;
    }

    // -------------- реалізація [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;
    }
}
  • рядки 20–23: бін із назвою [dao1] (назва методу) є екземпляром класу [Dao1] (рядок 22), який розглядається як реалізація інтерфейсу [IDao] (рядок 21). Отже, bean [dao1] розглядається як екземпляр інтерфейсу (термінологія є неточною, але її можна зрозуміти), а не як екземпляр класу. Це важливий момент, який слід усвідомити. Усі інші bean також будуть екземплярами інтерфейсів;
  • рядки 25–30: екземпляр інтерфейсу [IMetier], реалізованого класом [Metier1];
  • рядки 32–37: екземпляр інтерфейсу [IUi], реалізований класом [Ui1];
  • рядки 20–37: реалізують шари [UI, Metier, DAO] за допомогою екземплярів [Ui1, Metier1, Dao1];
  • рядки 40–57: реалізують шари [UI, Metier, DAO] за допомогою екземплярів [Ui2, Metier2, Dao2];

2.5.5. Одиничний тест [JUnitTest]

  

Клас [JUnitTest] розміщено у гілці [src / test / java] проєкту Maven. Елементи цієї гілки не включаються до кінцевого архіву проєкту. Його код такий:


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 {
...
}
  • рядок 16: анотація [@SpringApplicationConfiguration] є анотацією проєкту Spring Boot Test (рядок 8). Вона вноситься через таку залежність файлу [pom.xml]:

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

Ця анотація має як параметр список класів конфігурації, які слід використовувати для побудови контексту Spring, необхідного для тестування. Тут ми використовуємо клас конфігурації [Config], який вже було представлено;

  • рядок 17: анотація [@RunWith] є анотацією JUnit (рядок 5). Її параметром є клас, відповідальний за виконання тестів замість класу за замовчуванням фреймворку JUnit. Цей клас є класом Spring (рядок 9). Він використовуватиме анотації Spring, присутні в тестовому класі;

Повний клас виглядає так


...

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

    // шар [UI]
    @Autowired
    @Qualifier("ui1")
    private IUi ui1;

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

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

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

    // шар [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));
    }

}
  • рядки 8–10: ін’єктується (рядок 8) бін із назвою (рядок 9) [ui1]. Зверніть увагу, що в рядку 10 ін’єктується екземпляр інтерфейсу, а не екземпляр класу;
  • рядки 21–32: аналогічним чином вводимо інші біни, визначені в класі [Config];
  • рядок 34: анотація [@Test] позначає метод, який слід виконати під час тестування. Інші можливі анотації такі:
    • [@BeforeClass]: метод, який слід виконати перед початком тестів;
    • [@AfterClass]: метод, який слід виконати після завершення всіх тестів;
    • [@Before] — метод, який слід виконати перед кожним тестом;
    • [@After]: метод, який слід виконати після кожного тесту;
  • рядок 36: перевіряється, чи виклик [dao1.doSomethingInDaoLayer(10, 20)] дійсно повертає 30. За угодою перший параметр — це очікуване значення, а другий — фактичне;
  • рядок 36: перевіряє екземпляр [dao1] інтерфейсу [IDao];
  • рядок 37: перевіряє екземпляр [dao2] інтерфейсу [IDao];
  • рядок 42: перевіряє екземпляр [metier1] інтерфейсу [IMetier];
  • рядок 43: перевіряє екземпляр [metier2] інтерфейсу [IMetier];
  • рядок 48: перевіряє екземпляр [ui1] інтерфейсу [IUi];
  • рядок 36: тестує екземпляр [ui2] інтерфейсу [IUi];

У тестовому методі можна використовувати такі твердження:

  • assertEquals(вираз1, вираз2): перевіряє, чи значення обох виразів рівні. Допускаються різні типи виразів (int, String, float, double, boolean, char, short). Якщо обидва вирази не рівні, то генерується виняток типу [AssertionFailedError ],
  • assertEquals(число1, число2, дельта): перевіряє, чи два числа з дійсної області збігаються з похибкою дельта, c.a.d abs(число1-число2)<=дельта. Наприклад, можна написати assertEquals(число1, число2, 1E-6), щоб перевірити, чи два значення рівні з похибкою 10⁻⁶,
  • assertEquals(message, expression1, expression2) та assertEquals(message, réel1, réel2, delta) — це варіанти, що дозволяють вказати повідомлення про помилку, яке буде пов’язане з винятком типу [AssertionFailedError], що генерується у разі невдачі методу [assertEquals],
  • assertNotNull(Object) та assertNotNull(message, Object): перевіряють, чи посилання Object не дорівнює null,
  • assertNull(Object) та assertNull(message, Object): перевіряє, чи посилання Object дорівнює null,
  • assertSame(Object1, Object2) та assertSame(message, Object1, Object2): перевіряє, чи посилання Object1 та Object2 вказують на один і той самий об’єкт,
  • assertNotSame(Object1, Object2) та assertNotSame(message, Object1, Object2): перевіряє, чи посилання Object1 та Object2 не вказують на один і той самий об’єкт;

Щоб виконати тест, можна вчинити наступним чином:

Отримуємо такий результат:

 

У цьому випадку всі тести пройшли успішно. Що ж показує цей приклад? Він демонструє гнучкість, яку надає фреймворк Spring у налаштуванні багатошарової архітектури. Можна вибрати використання реалізації [Ui1, Metier1, Dao1] або [Ui2, Metier2, Dao2] просто шляхом налаштування. Так, у попередньому тесті JUnit, якщо залишити лише ін’єкцію бінів [ui1, metier1, dao1], ми працюємо з першою архітектурою. Щоб змінити архітектуру, достатньо змінити ін’єктовані біни. Це робиться без зміни коду шарів, що реалізують інтерфейси. Такий тип програмування називають програмуванням через інтерфейси, оскільки використовуються не екземпляри класів, що реалізують шари, а екземпляри їхніх інтерфейсів.

2.6. Conclusion

  • Spring керує об’єктами, які є синглтонами (єдиним екземпляром). Spring також керує об’єктами, екземпляри яких створюються щоразу, коли до Spring надходить запит на їх створення. Цей випадок також буде розглянуто в цьому документі;
  • ці об’єкти можна оголошувати різними способами, які можна поєднувати:
    • у файлі XML,
    • у класі Java з анотацією [@Configuration],
    • за допомогою будь-якого класу Java, анотованого [@Component, @Service, ...];
  • об’єкт Spring можна ін’єктувати в інший об’єкт Spring за допомогою анотації [@Autowired]. У цьому випадку йдеться про ін’єкцію залежностей (DI: Dependency Injection);
  • Spring виявляється дуже зручним для налаштування багаторівневих архітектур із супутнім використанням парадигми програмування через інтерфейси;