5. [Cours]: Вступ до фреймворку Spring
Ключові слова: багаторівнева архітектура, 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 для тестування проектів;
5.1. Support
![]() | ![]() |
Папка [support / chap-05] містить проекти Eclipse з цього розділу.
5.2. Exemple-01
5.2.1. Проєкт Eclipse
![]() |
5.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);
}
// методи getter та setter
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]:
![]() |
5.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);
}
}
Примітка: цей клас не має явного конструктора. У цьому випадку за замовчуванням завжди існує конструктор без параметрів, який нічого не робить. Коли створюються конструктори, цей конструктор за замовчуванням більше не існує неявно. Тоді його потрібно явно визначити:
5.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]:
![]() |
5.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], який зробить його виконуваним;
5.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] цього проєкту.
5.2.7. Результати
5.3. Exemple-02
5.3.1. Проєкт Eclipse
![]() |
5.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.
5.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);
// отримані біни є синглтонами
// їх можна запитувати кілька разів, завжди отримується той самий bean
Personne p01b = ctx.getBean("personne_01", Personne.class);
System.out.println(String.format("beans [p01,p01b] identiques ? %s", p01b == p01));
}
}
- у рядку 13 відбувається інстанціювання всіх бінів, визначених у класі [Config];
- решта коду залишається без змін;
5.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, що іноді може призвести до несподіваних проблем.
5.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 зможе використовувати цей архів.
5.4. Exemple-03
5.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] проекту;
Ми доповнюємо проект наступним чином:
![]() |
5.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 проекту;
5.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];
5.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),
- за типом, якщо існує лише один синглтон цього типу;
5.4.5. Виконання проєкту
Виконання проекту дає такий результат у консолі:
- рядки 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"> <!-- info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- у рядку 12 встановлюємо рівень журналів. [debug] — це дуже детальний рівень, [info] — набагато менш детальний;
Ось результати з [level=info]:
Залишився лише один рядок журналу.
5.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] — класи самого проєкту;
5.5. Exemple-04
5.5.1. Мета
Цей приклад повторює один із тих, що наведено в документі [Introduction à Spring IoC], у якому показано внесок Spring у конфігурацію багаторівневих архітектур. У оригінальному документі приклад розглядається з конфігурацією Spring, створеною за допомогою файлу XML. Тут ми розглядаємо приклад із конфігурацією за допомогою класів Java та анотацій.
Ми хочемо налаштувати проект Spring для такої архітектури:
![]() |
Кожен рівень має інтерфейс, реалізований двома класами. Ми хочемо показати, що завдяки Spring можна змінити реалізацію одного рівня, не впливаючи на код інших рівнів.
5.5.2. Проєкт Eclipse
5.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]:
![]() |
Ми побачимо, що їх цілком достатньо для того, що ми хочемо тут зробити.
5.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] також є правильним.
![]() |
Отримуємо такий результат:
- рядки 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], отримаємо такий результат:
Журнали зникли. З’являються лише повідомлення рівня [warn], а тут їх не було.
5.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;
}
}
5.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];
5.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], ми працюємо з першою архітектурою. Щоб змінити архітектуру, достатньо змінити ін’єктовані біни. Це робиться без зміни коду шарів, що реалізують інтерфейси. Такий тип програмування називають програмуванням через інтерфейси, оскільки використовуються не екземпляри класів, що реалізують шари, а екземпляри їхніх інтерфейсів.
5.6. Conclusion
- Spring керує об’єктами, які є синглтонами (єдиним екземпляром). Spring також керує об’єктами, екземпляри яких створюються щоразу, коли до Spring надходить запит на їх створення. Цей випадок також буде розглянуто в цьому документі;
- ці об’єкти можна оголошувати різними способами, які можна поєднувати:
- у файлі XML,
- у класі Java з анотацією [@Configuration],
- за допомогою будь-якого класу Java, анотованого [@Component, @Service, ...];
- об’єкт Spring можна ввести в інший об’єкт Spring за допомогою анотації [@Autowired]. У цьому випадку йдеться про введення залежностей (DI: Dependency Injection);
- Spring виявляється дуже зручним для налаштування багаторівневих архітектур із супутнім використанням парадигми програмування через інтерфейси;














































