Skip to content

2. Стаття 1 — Spring IoC

Мета документа:

  • ознайомитися з можливостями налаштування та інтеграції фреймворку Spring (http://www.springframework.org)
  • визначити та використовувати поняття IoC (Inversion of Control), яке також називають ін'єкцією залежностей (Dependency Injection)

2.1. Налаштування трирівневого додатка за допомогою Spring

Розглянемо класичний трирівневий додаток:

Припустимо, що доступ до бізнес-шару та DAO контролюється за допомогою Java-інтерфейсів:

  1. інтерфейс [IArticlesDao] для рівня доступу до даних
  1. інтерфейс [IArticlesManager] для бізнес-шару

У шарі доступу до даних або шарі DAO (Data Access Object) часто доводиться працювати з SGBD, а отже, і з драйвером JDBC. Розглянемо скелет класу, що здійснює доступ до таблиці товарів у SGBD:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // підключення до джерела даних
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // запит списку товарів
        try {
             // завантаження драйвера JDBC
            Class.forName(driverClassName);
            // створено з'єднання з BD
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

Щоб виконати операцію над SGBD, будь-який метод потребує об’єкта [Connection], який представляє з’єднання з базою даних, через яке відбуватиметься обмін даними між нею та кодом Java. Для створення цього об’єкта потрібні чотири параметри:

String driverClassName
ім’я класу драйвера JDBC для SGBD
String url
URL-адреса JDBC бази даних, яку потрібно використовувати
String user
ідентифікатор, під яким створюється з'єднання
String pwd
пароль для цього облікового запису

Як наш попередній клас [ArticlesDaoPlainJdbc] може отримати цю інформацію? Існують різні можливості:

варіант 1 — інформація жорстко закодована в класі:

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

     // підключення до джерела даних
    private final String driverClassName = "org.firebirdsql.jdbc.FBDriver";
    private String url = "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb";
    private String user = "someone";
    private String pwd = "somepassword";
 ....

Недоліком цього рішення є те, що при будь-якій зміні цих даних, наприклад, при зміні пароля, доводиться вносити зміни в код Java.

Варіант 2 — дані передаються об’єкту під час його створення:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // підключення до джерела даних
    private final String driverClassName;
    private String url;
    private String user;
    private String pwd;
 ....
    public ArticlesDaoPlainJdbc(String driverClassName,String url,String user,String pwd) {
      this.driverClassName=driverClassName;
    this.url=url;
    this.user=user;
    this.pwd=pwd;
    ...
    }

У цьому випадку об’єкт під час створення отримує інформацію, необхідну для роботи. Проблема переноситься на код, який передав йому ці чотири параметри. Як він їх отримав? Наступний клас [ArticlesManagerWithDataBase] з бізнес-шару міг би створити об’єкт [ArticlesDaoPlainJdbc] з шару доступу до даних:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // екземпляр доступу до даних
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // створення служби доступу до даних
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

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

Бачимо, що й тут інформація, необхідна для створення об’єкта [ArticlesDaoPlainJdbc], надається конструктору об’єкта [ArticlesManagerWithDataBase]. Можна уявити, що її передає вищий рівень, наприклад, рівень інтерфейсу з користувачем. Таким чином, крок за кроком ми доходимо до найвищого рівня додатка. З огляду на своє розташування, цей рівень не викликається жодним іншим рівнем, який міг би передати йому необхідну інформацію про конфігурацію. Тому потрібно знайти інше рішення, ніж конфігурація через конструктор. Звичайним рішенням для конфігурації додатка на рівні його найвищого шару є використання файлу, у якому міститься вся інформація, що може змінюватися з часом. Таких файлів може бути кілька. Під час запуску додатка шар ініціалізації створює всі або частину об’єктів, необхідних для різних шарів додатка.

Існує велика різноманітність конфігураційних файлів. Сучасна тенденція полягає у використанні файлів XML. Саме цей варіант обрано в Spring. Файл, що конфігурує об’єкт [ArticlesDaoPlainJdbc], може виглядати так:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
</beans>

Додаток — це сукупність об’єктів, які Spring називає «бінами» (beans), оскільки вони дотримуються стандарту JavaBean щодо іменування методів доступу та ініціалізації (getters/setters) приватних полів об’єкта. Об’єкти, які в додатку виконують роль надання послуги, часто створюються в єдиному екземплярі. Їх називають «синглтонами». Так, у нашому прикладі багаторівневого додатка, який ми тут розглядаємо, доступ до бази даних статей забезпечуватиметься єдиним екземпляром класу [ArticlesDaoPlainJdbc]. У веб-додатку ці сервісні об’єкти обслуговують одразу декількох клієнтів. Не створюється окремий сервісний об’єкт для кожного клієнта.

Наведений вище файл конфігурації Spring дозволяє створити єдиний об’єкт-сервіс типу [ArticlesDaoPlainJdbc] у пакеті з назвою [istia.st.articles.dao]. Чотири параметри, необхідні для конструктора цього об’єкта, визначені всередині тегу <bean>...</bean>. Кількість таких тегів <bean> дорівнюватиме кількості синглетонів, які потрібно створити.

Коли відбуватиметься створення об’єктів, визначених у файлі Spring? Ініціалізація додатка може бути доручена методу main цього самого додатка, якщо він такий має. Для веб-додатка це може бути метод [init] головного сервлету. У будь-якому додатку є метод, який гарантовано виконується першим. Зазвичай саме в ньому відбувається створення синглетонів.

Розглянемо приклад. Припустимо, що ми хочемо протестувати попередній клас [ArticlesDaoPlainJdbc] за допомогою тесту JUnit. Тестовий клас JUnit має метод [setUp], який виконується раніше за всі інші методи. Саме там буде створено синглтон [ArticlesDaoPlainJdbc].

Якщо дотримуватися підходу, що передбачає передачу конфігураційних даних через конструктор, отримаємо такий тестовий клас:

public class TestArticlesPlainJdbc extends TestCase {
    // тестує клас доступу до статей ArticlesDaoPlainJdbc
     // джерело даних визначено в sprintest

     // екземпляр класу, що тестується
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // отримує екземпляр доступу до даних
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

Клас-викликач [TestArticlesPlainJdbc] повинен знати чотири параметри, необхідні для ініціалізації синглтона [ArticlesDaoPlainJdbc], який потрібно створити.

Якщо дотримуватися підходу до передачі конфігураційної інформації за допомогою конфігураційного файлу, то, використовуючи описаний вище файл Spring, можна отримати такий тестовий клас.

public class TestSpringArticlesPlainJdbc extends TestCase {
    // тестує клас доступу до статей ArticlesDaoJdbc
     // джерело даних визначено в sprintest

     // екземпляр класу, що тестується
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // отримує екземпляр класу доступу до даних
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

У цьому випадку класу-виклику [TestSpringArticlesPlainJdbc] не потрібно знати інформацію, необхідну для ініціалізації синглтона, що створюється. Йому достатньо знати:

  1. [springArticlesPlainJdbc.xml] — ім’я файлу конфігурації Spring, описаного вище
  2. [articlesDao]: ім’я синглтона, який потрібно створити

Зміни у файлі конфігурації, що не стосуються цих двох елементів, жодним чином не впливають на код Java. Цей метод конфігурації об’єктів додатка є дуже гнучким. Для налаштування додатку потрібно знати лише дві речі:

  • ім’я файлу Spring, що містить визначення синглетонів, які потрібно створити
  • імена цих синглетонів, які використовуються кодом Java для отримання посилання на об’єкти, з якими вони були пов’язані завдяки файлу конфігурації

2.2. Введення залежностей та інверсія контролю

Тепер ознайомимося з поняттям ін’єкції залежностей (Dependency Injection), яке використовує Spring для налаштування додатків. Також використовується термін «інверсія контролю» (IoC, Inversion of Control). Розглянемо створення синглтона [ArticlesManagerWithDataBase] у бізнес-шарі нашого додатка:

Щоб отримати доступ до даних SGBD, бізнес-шар повинен використовувати сервіси об’єкта, що реалізує інтерфейс [IArticlesDao], наприклад, об’єкта типу [ArticlesDaoPlainJdbc]. Код класу [ArticlesManagerWithDataBase] може виглядати наступним чином:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // екземпляр модуля доступу до даних
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // створення сервісу доступу до даних
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

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

Клас [ArticlesDaoPlainJdbc] повинен реалізовувати інтерфейс [IArticlesDao]:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

Щоб створити синглтон типу [IArticlesDao], необхідний для роботи класу, його конструктор явно використовує ім’я класу, що реалізує інтерфейс [IArticlesDao]:

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

Отже, у коді є жорстка залежність від імені класу. Якщо клас реалізації інтерфейсу [IArticlesDao] зміниться, код попереднього конструктора доведеться змінити. Між об’єктами існують такі взаємозв’язки:

Клас [ArticlesManagerWithDataBase] самостійно ініціює створення об’єкта [ArticlesDaoPlainJdbc], який йому потрібен. Повертаючись до терміна «інверсія контролю», можна сказати, що саме цей клас має «контроль» над створенням об’єкта, який йому потрібен.

Якби нам довелося написати тестовий клас JUnit для класу [ArticlesManagerWithDataBase], це могло б виглядати приблизно так:

public class TestArticlesManagerWithDataBase extends TestCase {
    // екземпляр тестованого бізнес-класу
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // створює екземпляр тестованого бізнес-класу
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

Тестовий клас створює екземпляр бізнес-класу [ArticlesManagerWithDataBase], який, у свою чергу, у своєму конструкторі створює екземпляр класу доступу до даних [ArticlesDaoPlainJdbc].

Рішення на базі Spring усуне необхідність для бізнес-класу [ArticlesManagerWithDataBase] знати ім’я [ArticlesDaoPlainJdbc] класу доступу до даних, який йому потрібен. Це дозволить змінювати його, не втручаючись у Java-код бізнес-класу. Spring дозволить одночасно створити два синглтони: для рівня доступу до даних та для бізнес-рівня. У конфігураційному файлі Spring буде визначено новий bean:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас доступу до даних -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
</beans>

Нововведення полягає в біні, що визначає синглтон бізнес-класу, який потрібно створити:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. визначено клас, що реалізує бін [articlesManager]: [ArticlesManagerWithDataBase]
  2. поле [articlesDao] біна отримує значення за допомогою тегу <property name="articlesDao">. Це поле, визначене в класі [ArticlesManagerWithDataBase]:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // інтерфейс доступу до даних
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

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

Щоб поле [articlesDao] могло бути ініціалізоване Spring та його тегом <property>, необхідно, щоб поле відповідало стандарту JavaBean і щоб існував метод [setArticlesDao] для ініціалізації поля [articlesDao]. Зверніть увагу на назву методу, яка чітко походить від назви поля. Паралельно з цим часто існує метод [get...] для отримання значення поля. У даному випадку це метод [getArticlesDao]. У цій новій версії клас [ArticlesManagerWithDataBase] більше не має конструктора. Він йому більше не потрібен.

  • Значення, яке Spring присвоїть полю [articlesDao], — це значення біна [articlesDao], визначеного у його файлі конфігурації:
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
    .............
    </bean>
  • коли Spring створюватиме синглтон [ArticlesManagerWithDataBase], він також створить синглтон [ArticlesDaoPlainJdbc]:
    • Spring побудує граф залежностей бінів і побачить, що бін [articlesManager] залежить від біна [articlesDao]
    • він створить бін [articlesDao], тобто об’єкт типу [ArticlesDaoPlainJdbc]
    • потім він створить бін [articlesManager] типу [ArticlesManagerWithDataBase]

Тепер уявімо тест JUnit для класу [ArticlesManagerWithDataBase]. Він може виглядати так:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // тестує бізнес-клас [ArticlesManagerWithDataBase]

    // екземпляр тестованого бізнес-класу
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // отримує екземпляр доступу до даних
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

Простежимо процес створення двох синглетонів, визначених у файлі Spring під назвою [springArticlesManagerWithDataBase.xml].

  • Вищезазначений метод [setUp] запитує посилання на бін із назвою [articlesManager]
  • Spring звертається до свого конфігураційного файлу, знаходить бін [articlesManager]. Якщо він уже створений, Spring просто повертає посилання на об’єкт (синглтон), інакше — створює його.
  • Spring бачить залежність біна [articlesManager] від біна [articlesDao]. Тому він створює синглтон [articlesDao] типу [ArticlesDaoPlainJdbc], якщо той ще не створений (синглтон).
  • Він створює синглтон [articlesManager] типу [ArticlesManagerWithDataBase]

Цей механізм можна схематично зобразити так:

Нагадаємо структуру класу [ArticlesManagerWithDataBase]:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // інтерфейс доступу до даних
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

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

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

Головна перевага налаштування синглтона [ArticlesManagerWithDataBase] за допомогою файлу Spring полягає в тому, що тепер можна змінити клас реалізації, що відповідає полю [articlesDao] класу [ArticlesManagerWithDataBase], без зміни коду самого класу. Достатньо змінити ім’я класу у визначенні біна [articlesDao] у файлі Spring:

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

наприклад, стане:

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

Bean [ArticlesManagerWithDataBase] працюватиме з цим новим класом доступу до даних, навіть не підозрюючи про це.

2.3. Spring IoC на практиці

2.3.1. Приклад 1

Розглянемо такий клас:

package istia.st.springioc.domain;

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

   // відображення «Особа»
  public String toString() {
    return "nom=[" + this.nom + "], age=[" + this.age + "]";
  }

   // init-close
  public void init() {
    System.out.println("init personne [" + this.toString() + "]");
  }

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

   // геттери-сеттери
  public int getAge() {
    return age;
  }

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

  public String getNom() {
    return nom;
  }

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

Клас містить:

  • два приватні поля «ім'я» та «вік»
  • методи читання (get) та запису (set) цих двох полів
  • метод toString для отримання значення об’єкта [Personne] у вигляді символьного рядка
  • метод init, який буде викликаний Spring під час створення об’єкта, метод close, який буде викликаний під час знищення об’єкта

Для створення об’єктів типу [Personne] ми використаємо такий файл Spring:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

Цей файл матиме назву config.xml.

  • У ньому визначено два біни з відповідними ключами «personne1» та «personne2» типу [Personne]
  • він ініціалізує поля [nom, age] для кожної особи
  • він визначає методи, які слід викликати під час початкового створення об’єкта [init-method] та під час знищення об’єкта [destroy-method]

Для наших тестів ми будемо використовувати єдиний тестовий клас JUnit, до якого послідовно додаватимемо методи. Перша версія цього класу буде такою:

package istia.st.springioc.tests;

import istia.st.springioc.domain.Personne;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

public class Tests extends TestCase {

   // фабрика бінів
  private ListableBeanFactory bf;

   // початкові тести
  public void setUp() {
    bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
  }

  public void test1() {
    // отримання бінів за їхнім ключем [Personne] із файлу Spring
    Personne personne1 = (Personne) bf.getBean("personne1");
    System.out.println("personne1=" + personne1.toString());
    Personne personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
  }
}

Коментарі:

  • щоб отримати біни, визначені у файлі [config.xml], ми використовуємо об’єкт типу [ListableBeanFactory]. Існують й інші типи об’єктів, що дають змогу отримати доступ до бінів. Об’єкт [ListableBeanFactory] отримується в методі [setUp] тестового класу та зберігається у приватній змінній. Таким чином, він буде доступний для всіх тестових методів.
  • Файл [config.xml] буде розміщений у [ClassPath] додатка, c.a.d, в одному з каталогів, які переглядає віртуальна машина Java під час пошуку класу, на який посилається додаток. Об’єкт [ClassPathResource] слугує для пошуку ресурсу в [ClassPath] додатка, в даному випадку — файлу [config.xml].
  • Spring може використовувати конфігураційні файли різних форматів. Об’єкт [XmlBeanFactory] дозволяє аналізувати конфігураційний файл у форматі XML.
  • Обробка файлу Spring дає об’єкт типу [ListableBeanFactory], у даному випадку об’єкт bf. За допомогою цього об’єкта можна отримати бін, ідентифікований ключем C, за допомогою bf.getBean(C).
  • Метод [test1] запитує та відображає значення бінів із ключами «personne1» та «personne2».

Структура проекту Eclipse нашого додатка така:

Image

Коментарі:

  • папка [src] містить вихідний код. Скомпільований код буде розміщено в папці [bin], яка тут не показана.
  • Файл [config.xml] знаходиться в кореневій папці [src]. Під час побудови проєкту він автоматично копіюється до папки [bin], яка є частиною папки [ClassPath] додатка. Саме там його шукає об’єкт [ClassPathResource].
  • Папка [lib] містить три бібліотеки Java, необхідні для роботи додатка:
      • commons-logging.jar та spring-core.jar для класів Spring
      • junit.jar — для класів JUnit
  • папка [lib] також входить до складу [ClassPath] додатка

Виконання методу [test1] тесту JUnit дає такі результати:

18 sept. 2004 11:28:53 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
personne1=nom=[Simon], age=[40]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
personne2=nom=[Brigitte], age=[20]
personne2=nom=[Brigitte], age=[20]

Коментарі:

  • Spring реєструє певну кількість подій за допомогою бібліотеки [commons-logging.jar]. Ці журнали дозволяють нам краще зрозуміти роботу Spring.
  • Файл [config.xml] було завантажено, а потім використано
  • операція*
Personne personne1 = (Personne) bf.getBean("personne1");

призвела до створення біна [personne1]. Ми бачимо відповідний запис у журналі Spring. Оскільки у визначенні біна [personne1] було вказано [init-method="init"], було виконано метод [init] створеного об’єкта [Personne]. Відображається відповідне повідомлення.

  • Операція
System.out.println("personne1=" + personne1.toString());

призвела до відображення значення створеного об’єкта [Personne].

  • Те саме повторюється для ключового біна [personne2].
  • Остання операція
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

не призвела до створення нового об'єкта типу [Personne]. Якби це сталося, ми б побачили виведення методу [init], чого тут не відбулося. Це і є принцип синглтона. Spring за замовчуванням створює лише один екземпляр бінів зі свого файлу конфігурації. Це служба об’єктних посилань. Якщо до неї звернутися з запитом на посилання на об’єкт, який ще не створено, вона створює його та повертає посилання. Якщо об’єкт уже створено, Spring просто надає посилання на нього.

  • Можна помітити, що немає жодних слідів методу [close] об’єкта [Personne], хоча ми вказали його у визначенні бінів [destroy-method=close]. Можливо, цей метод виконується лише тоді, коли пам’ять, зайнята об’єктом, звільняється збирачем сміття (garbage collector). На момент, коли це відбувається, додаток уже завершено, і виведення на екран не має жодного ефекту. Потрібно перевірити.

Оскільки ми вже опанували основи конфігурації Spring, відтепер наші пояснення будуть дещо швидшими.

2.3.2. Приклад 2

Розглянемо новий клас [Voiture]:

package istia.st.springioc.domain;

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

   // конструктори

  public Voiture() {
  }

  public Voiture(String marque, String type, Personne propriétaire) {
    this.marque = marque;
    this.type = type;
    this.propriétaire = propriétaire;
  }

   // toString
  public String toString() {
    return "Voiture : marque=[" + this.marque + "] type=[" + this.type
        + "] propriétaire=[" + this.propriétaire + "]";
  }

     // геттери-сеттери
  public String getMarque() {
    return marque;
  }

  public void setMarque(String marque) {
    this.marque = marque;
  }

  public Personne getPropriétaire() {
    return propriétaire;
  }

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

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

   // ініціалізація-закриття
  public void init() {
    System.out.println("init voiture [" + this.toString() + "]");
  }

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

}

Клас містить:

  • три приватні поля: type, marque та propriétaire. Ці поля можна ініціалізувати та зчитувати за допомогою публічних методів get та set бінів. Їх також можна ініціалізувати за допомогою конструктора Voiture(String, String, Personne). Клас також має конструктор без аргументів, щоб відповідати стандарту JavaBean.
  • метод toString для отримання значення об’єкта [Voiture] у вигляді символьного рядка
  • метод init, який буде викликаний Spring одразу після створення об’єкта, метод close, який буде викликаний під час знищення об’єкта

Для створення об’єктів типу [Voiture] ми використаємо такий файл Spring [config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <constructor-arg index="0">
            <value>Peugeot</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>307</value>
        </constructor-arg>
        <constructor-arg index="2">
            <ref bean="personne2"></ref>
        </constructor-arg>
    </bean>
</beans>

Цей файл додає до попередніх визначень бін із ключем «voiture1» типу [Voiture]. Щоб ініціалізувати цей бін, можна було б написати:

    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <property name="marque">
            <value>Peugeot</value>
        </property>
        <property name="type">
            <value>307</value>
        </property>
        <property name="propriétaire">
            <ref bean="personne2"/>
        </property>
    </bean>

Замість цього вже описаного методу ми вирішили тут використати конструктор Voiture(String, String, Personne) класу. Крім того, бін [voiture1] визначає метод, який слід викликати під час початкового створення об’єкта [init-method], та метод, який слід викликати під час знищення об’єкта [destroy-method].

Для наших тестів ми використаємо вже представлений тестовий клас JUnit, додавши до нього наступний метод [test2]:

1
2
3
4
5
  public void test2() {
     // отримання біна [voiture1]
    Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
    System.out.println("Voiture1=" + Voiture1.toString());
  }

Метод [test2] отримує бін [voiture1] і виводить його на екран.

Структура проекту Eclipse залишається такою ж, як і в попередньому тесті. Виконання методу [test2] з тесту JUnit дає такі результати:

18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'voiture1'
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture(java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]

Коментарі:

  1. метод [test2] вимагає посилання на бін [voiture1]
  2. рядок 4: Spring починає створення біна [voiture1], оскільки цей бін ще не створено (синглтон)
  3. рядок 6: оскільки бін [voiture1] посилається на бін [personne2], останній, у свою чергу, створюється
  4. рядок 7: бін [personne2] створено. Після цього виконується його метод [init].
  5. рядок 9: Spring повідомляє, що для створення біна [voiture1] буде використано конструктор
  6. рядок 10: створено бін [voiture1]. Після цього виконується його метод [init].
  7. рядок 11: метод [test2] виводить значення біна [voiture1]

2.3.3. Приклад 3

Вводимо новий клас [GroupePersonnes] наступного вигляду:

package istia.st.springioc.domain;

import java.util.Map;

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

   // геттери — сеттери
  public Personne[] getMembres() {
    return membres;
  }

  public void setMembres(Personne[] membres) {
    this.membres = membres;
  }

  public Map getGroupesDeTravail() {
    return groupesDeTravail;
  }

  public void setGroupesDeTravail(Map groupesDeTravail) {
    this.groupesDeTravail = groupesDeTravail;
  }

   // відображення
  public String toString() {
    String liste = "membres : ";
    for (int i = 0; i < this.membres.length; i++) {
      liste += "[" + this.membres[i].toString() + "]";
    }
    return liste + ", groupes de travail = " + this.groupesDeTravail.toString();
  }

   // ініціалізація-закриття
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

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

Його два приватні члени:

члени: масив осіб, що входять до групи

groupesDeTravail: словник, що прив’язує особу до робочої групи

Слід зауважити, що клас [GroupePersonnes] не визначає конструктора без аргументів, щоб відповідати стандарту JavaBean. Нагадаємо, що за відсутності будь-якого конструктора існує конструктор «за замовчуванням», який є конструктором без аргументів і нічого не робить.

Тут ми прагнемо показати, як Spring дозволяє ініціалізувати складні об’єкти, такі як об’єкти, що містять поля типу масиву або словника. Додамо новий bean до попереднього файлу Spring [config.xml]:

    <bean id="groupe1" class="istia.st.springioc.domain.GroupePersonnes" 
        init-method="init" destroy-method="close">
        <property name="membres">
            <list>
                <ref bean="personne1"/>
                <ref bean="personne2"/>
            </list>
        </property>
        <property name="groupesDeTravail">
            <map>
                <entry key="Brigitte">
                    <value>Marketing</value>
                </entry>
                <entry key="Simon">
                    <value>Ressources humaines</value>
                </entry>
            </map>
        </property>
    </bean>
  1. тег <list> дозволяє ініціалізувати поле типу масив або таке, що реалізує інтерфейс List, з різними значеннями.
  2. тег <map> дозволяє зробити те саме з полем, що реалізує інтерфейс Map

Для наших тестів ми використаємо вже представлений тестовий клас JUnit, додавши до нього наступний метод [test3]:

1
2
3
4
5
  public void test3() {
    // отримання біна [groupe1]
    GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
    System.out.println("groupe1=" + groupe1.toString());
  }

Метод [test3] отримує бін [groupe1] і відображає його.

Структура проекту Eclipse залишається такою ж, як і в попередньому тесті. Виконання методу [test3] у тесті JUnit дає такі результати:

18 sept. 2004 15:51:45 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'groupe1'
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
init GroupePersonnes [membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}]
groupe1=membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}

Коментарі:

  • метод [test3] вимагає посилання на бін [groupe1]
  • рядок 4: Spring починає створення цього біна
  • оскільки бін [groupe1] посилається на біни [personne1] та [personne2], ці два біни створюються (рядки 6 та 9) і їхні методи init виконуються (рядки 7 та 10)
  • рядок 11: бін [groupe1] створено. Тепер виконується його метод [init].
  • рядок 12: запит на відображення, зроблений методом [test3].

2.4. Spring для налаштування трирівневих веб-додатків

2.4.1. Загальна архітектура додатка

Ми хочемо створити трирівневий додаток із такою структурою:

  • три рівні будуть незалежними завдяки використанню інтерфейсів Java
  • інтеграція трьох рівнів буде здійснена за допомогою Spring
  • для кожного з трьох рівнів будуть створені окремі пакети, які отримають назви Control, Domain та Dao. Додатковий пакет міститиме тестові програми.

Структура додатка в Eclipse може виглядати так:

Image

2.4.2. Рівень DAO доступу до даних

Рівень DAO реалізує наступний інтерфейс:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • Написати два класи Dao1Impl1 та Dao1Impl2, що реалізують інтерфейс IDao1. Метод Dao1Impl1.doSomethingInDaoLayer поверне a+b, а метод Dao1Impl2.doSomethingInDaoLayer поверне a-b.
  • написати тестовий клас JUnit для перевірки двох попередніх класів

2.4.3. Бізнес-шар

Бізнес-шар реалізує наступний інтерфейс:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • Написати два класи Domain1Impl1 та Domain1Impl2, що реалізують інтерфейс IDomain1. Ці класи матимуть конструктор, який приймає параметр типу IDao1. Метод Domain1Impl1.doSomethingInDomainLayer збільшить значення a та b на одиницю, а потім передасть ці два параметри методу doSomethingInDaoLayer об’єкта типу IDao1, що було отримано. Метод Domain1Impl2.doSomethingInDomainLayer, у свою чергу, зменшить значення a та b на одиницю, перш ніж виконати те саме.
  • Написати тестовий клас JUnit для тестування двох попередніх класів

2.4.4. Рівень користувацького інтерфейсу

Рівень користувацького інтерфейсу реалізує наступний інтерфейс:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • Написати два класи Control1Impl1 та Control1Impl2, що реалізують інтерфейс IControl1. Ці класи матимуть конструктор, що приймає параметр типу IDomain1. Метод Control1Impl1.doSomethingInControlLayer збільшить значення a та b на одиницю, а потім передасть ці два параметри методу doSomethingInDomainLayer об’єкта типу IDomain1, що було отримано. Метод Control11Impl2.doSomethingInControlLayer, у свою чергу, зменшить значення a та b на одиницю, перш ніж виконати те саме.
  • Написати тестовий клас JUnit для тестування двох попередніх класів

2.4.5. Інтеграція зі Spring

  • Написати файл конфігурації Spring, який визначатиме, які класи повинен використовувати кожен із трьох попередніх рівнів
  • Напишіть тестовий клас JUnit, що використовує різні конфігурації Spring, щоб продемонструвати гнучкість написаного додатка
  • Написати автономний додаток (метод main), який передає два параметри інтерфейсу IControl1 і відображає результат, згенерований інтерфейсом.

2.4.6. Рішення

2.4.6.1. Проєкт Eclipse

Image

Архіви з папки [lib] було додано до [ClassPath] у рамках проекту.

2.4.6.2. Пакет [istia.st.demo.dao]

Інтерфейс:

1
2
3
4
5
6
7
8
9
package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}

Перший клас реалізації:

package istia.st.demo.dao;

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

     // виконуємо якусь дію в шарі [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

Другий клас реалізації:

package istia.st.demo.dao;

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

     // виконується якась операція в шарі [dao]
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. Пакет [istia.st.demo.domain]

Інтерфейс:

package istia.st.demo.domain;

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

     // щось робиться в шарі [domain]
  public int doSomethingInDomainLayer(int a, int b);
}

Перший клас реалізації:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

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

     // служба доступу до шару [dao]
  private IDao1 dao1;

  public Domain1Impl1() {
     // конструктор без аргументів
  }

     // зберігає службу доступу до шару [dao]
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // виконується якась операція у шарі [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

Другий клас реалізації:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

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

     // служба доступу до шару [dao]
  private IDao1 dao1;

  public Domain1Impl2() {
     // конструктор без аргументів
  }

     // зберігає службу доступу до шару [dao]
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // виконується якась операція у шарі [domain]
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. Пакет [istia.st.demo.control]

Інтерфейс

1
2
3
4
5
6
7
8
9
package istia.st.demo.control;

/**
 * @author ST-ISTIA
 *  
 */
public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}

Перший клас реалізації:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // бізнес-клас у шарі [domain]
    private IDomain1 domain1;

  public Control1Impl1() {
     // конструктор без аргументів
  }

     // збереження сервісу доступу до шару [domain]
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // виконується якась дія
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

Другий клас реалізації:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

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

     // клас доступу до шару [domain]
    private IDomain1 domain1;

  public Control1Impl2() {
     // конструктор без аргументів
  }

     // зберігає клас доступу до шару [domain]
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // виконується якась операція
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. Конфігураційні файли [Spring]

Перший файл [springMainTest1.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас DAO -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
    </bean>
     <!-- бізнес-клас -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- клас контролю -->
    <bean id="control" class="istia.st.demo.control.Control1Impl1">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

Другий файл [springMainTest2.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- клас DAO -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
    </bean>
     <!-- бізнес-клас -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- клас контролю -->
    <bean id="control" class="istia.st.demo.control.Control1Impl2">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

2.4.6.6. Пакет тестів [istia.st.demo.tests]

Тест типу [main]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest1 {
  public static void main(String[] arguments) {
     // отримуємо реалізацію інтерфейсу IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // використовуємо клас
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // виводимо результат
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Результати, отримані в консолі Eclipse:

11 mars 2005 11:25:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest1.xml]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl1(istia.st.demo.dao.IDao1)]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl1(istia.st.demo.domain.IDomain1)]
control(10,20)=34

Ще один тест із використанням другого файлу конфігурації [Spring]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest2 {
  public static void main(String[] arguments) {
     // отримуємо реалізацію інтерфейсу IControl1
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // використовуємо клас
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // виводимо результат
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Результати, отримані в консолі Eclipse:

11 mars 2005 11:28:52 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest2.xml]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl2(istia.st.demo.dao.IDao1)]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl2(istia.st.demo.domain.IDomain1)]
control(10,20)=-10

І нарешті, тест Junit:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

/**
 * @author ST-ISTIA
 *  
 */
public class JunitTest2Control1 extends TestCase {
  public void testControl1() {
     // отримуємо реалізацію інтерфейсу IControl1
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // використовуємо клас
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // отримуємо іншу реалізацію інтерфейсу IControl1
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // використовуємо клас
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusion

Фреймворк Spring забезпечує справжню гнучкість як в архітектурі додатків, так і в їхній конфігурації. Ми використовували концепцію IoC — один із двох основних компонентів Spring. Іншим стовпом є AOP (аспектно-орієнтоване програмування), яке ми не розглядали. Воно дозволяє шляхом конфігурації додавати «поведінку» до методу класу, не змінюючи його код. Схематично AOP дозволяє фільтрувати виклики певних методів:

  • фільтр може виконуватися до або після цільового методу M, або в обох випадках.
  • Метод M ігнорує існування цих фільтрів. Вони визначаються у файлі конфігурації Spring.
  • Код методу M не змінюється. Фільтри — це Java-класи, які потрібно створити. Spring надає попередньо визначені фільтри, зокрема для управління транзакціями SGBD.
  • Фільтри є бінами і, як такі, визначаються у файлі конфігурації Spring як біни.

Поширеним фільтром є транзакційний фільтр. Розглянемо метод M бізнес-шару, який виконує дві нерозривно пов’язані операції над даними (одиниця роботи). Він викликає два методи M1 та M2 з шару DAO для виконання цих двох операцій.

Оскільки метод M знаходиться в бізнес-шарі, він абстрагується від носія цих даних. Наприклад, йому не потрібно припускати, що дані знаходяться в SGBD і що їй потрібно помістити обидва виклики методів M1 та M2 у транзакцію SGBD. Саме рівень DAO повинен займатися цими деталями. Тож рішенням попередньої проблеми є створення методу в шарі DAO, який сам викликатиме методи M1 та M2, які він об’єднає в транзакцію SGBD.

Рішення щодо фільтрації AOP є більш гнучким. Воно дозволить визначити фільтр, який перед викликом M розпочне транзакцію, а після виклику виконає фіксацію (commit) або відкат (rollback) залежно від ситуації.

Цей підхід має кілька переваг:

  • після визначення фільтра його можна застосувати до кількох методів, наприклад, до всіх тих, що потребують транзакції
  • методи, відфільтровані таким чином, не потрібно переписувати
  • оскільки фільтри, які слід використовувати, визначаються в конфігурації, їх можна змінювати

Окрім концепцій IoC та AOP, Spring пропонує численні класи підтримки для тришарових додатків:

  • для JDBC — SqlMap (iBatis), для Hibernate — JDO (Java Data Object) у шарі DAO
  • для моделі MVC у шарі «Інтерфейс користувача»

Додаткова інформація: http://www.springframework.org.