О сайте Начало Java Студенческий отдел кадров Статьи Курсы по Java Вопросы/Ответы

Полный пример бизнес-уровня на Spring

Итак, мы увидели возможности Spring на примере одной функциональности - Profession. Теперь мы сделаем следующее: для каждой из наших таблиц создадим подобную функциональность, которая будет включать в себя полный набор классов и интерфейсов:

  • Entity
  • DAO интерфейс
  • DAO реализация
  • Facade
  • View для UI
Это сделает наш код чуть-чуть сложнее и запутаннее. Но именно, что "чуть-чуть". В принципе можно было бы обойтись одним DAO, одним фасадом и даже убрать DAO-интерфейс. Вы можете реализовать такой упрощенный вариант сами. Исходный код для всех классов вы можете найти в проекте Spring_04. Здесь мы опишем только функциональность каждого фасада, чтобы вы имели представление кто что делает. А дальше по коду вы сможете разобраться сами. Я очень хочу, чтобы вы читали код самостоятельно - умение быстро читать код вом пригодится.
При чтении кода я вам рекомендую обратить внимание на то, что класса Main, который мы использовали для "тестирования" работоспособности нашего приложения, уже нет. Вместо него я написал специальный класс для тестирования SpringStudentFacadeTest. Этот класс использует еще одну функциональность Spring - тестирование. Пакет предоставляет несколько очень удобных инструментов, которые мы еще рассмотрим. А пока давайте посмотрим на код этого класса.

package students.test;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import junit.framework.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.transaction.TransactionConfiguration;
import students.facade.ApplicantFacade;
import students.facade.ProfessionFacade;
import students.facade.SubjectFacade;
import students.view.ApplicantResultView;
import students.view.ApplicantView;
import students.view.ProfessionView;
import students.view.SubjectView;

@ContextConfiguration(locations = {"/StudentExample.xml", "/StudentDatabase.xml"})
@TransactionConfiguration(transactionManager = "txManager")
public class StudentFacadeTest extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    private SubjectFacade subjectFacade;
    @Autowired
    private ProfessionFacade professionFacade;
    @Autowired
    private ApplicantFacade applicantFacade;

    @Test
    @Rollback(false)
    public void subjectTest() {
        SubjectView sv = new SubjectView();

        // Установим данные для предмета
        sv.setSubjectName("Mathematic");
        // Добавим
        Long idSubj = subjectFacade.addSubject(sv);
        // Перечитаем
        sv = subjectFacade.getSubject(idSubj);
        // Убедимся, что считывание совпадает с тем, что записывали
        Assert.assertTrue(sv.getSubjectName().equals("Mathematic"));
        // Изменим название предмета, запишем и снова убедимся, что все в порядке
        sv.setSubjectName("Mathematics");
        subjectFacade.updateSubject(sv);
        sv = subjectFacade.getSubject(idSubj);
        Assert.assertTrue(sv.getSubjectName().equals("Mathematics"));
        // Убедимся, что всего предметов пока один
        Assert.assertTrue(subjectFacade.findSubject().size() == 1);

        sv.setSubjectName("Physics");
        idSubj = subjectFacade.addSubject(sv);
        sv = subjectFacade.getSubject(idSubj);
        Assert.assertTrue(sv.getSubjectName().equals("Physics"));
        Assert.assertTrue(subjectFacade.findSubject().size() == 2);

        sv.setSubjectName("Chemist");
        idSubj = subjectFacade.addSubject(sv);
        sv = subjectFacade.getSubject(idSubj);
        Assert.assertTrue(sv.getSubjectName().equals("Chemist"));
        Assert.assertTrue(subjectFacade.findSubject().size() == 3);

        sv.setSubjectName("Chemist2");
        idSubj = subjectFacade.addSubject(sv);
        sv = subjectFacade.getSubject(idSubj);
        Assert.assertTrue(sv.getSubjectName().equals("Chemist2"));
        Assert.assertTrue(subjectFacade.findSubject().size() == 4);
        // Удалим предмет и убедимся, что общее количество уменьшилось
        subjectFacade.deleteSubject(sv);
        Assert.assertTrue(subjectFacade.findSubject().size() == 3);

        sv.setSubjectName("Literature");
        idSubj = subjectFacade.addSubject(sv);
        sv = subjectFacade.getSubject(idSubj);
        Assert.assertTrue(sv.getSubjectName().equals("Literature"));
        Assert.assertTrue(subjectFacade.findSubject().size() == 4);

        // Проверим, что работает поиск по списку ID
        List<SubjectView> list = subjectFacade.findSubject();
        List<Long> check = new LinkedList<Long>();
        for (SubjectView s : list) {
            check.add(s.getSubjectId());
        }
        Assert.assertTrue(subjectFacade.findSubjectById(check).size() == 4);
    }

    @Test
    @Rollback(false)
    public void professionTest() {
        ProfessionView pv = new ProfessionView();

        // Добавим новую специальность
        pv.setProfessionName("Chemists");
        Long idProf = professionFacade.addProfession(pv);
        pv = professionFacade.getProfession(idProf);
        Assert.assertTrue(pv.getProfessionName().equals("Chemists"));
        // Исправим значение и убедимся. что так и сделано
        pv.setProfessionName("Chemist");
        professionFacade.updateProfession(pv);
        pv = professionFacade.getProfession(idProf);
        Assert.assertTrue(pv.getProfessionName().equals("Chemist"));
        // Всего специальностей одна штука
        Assert.assertTrue(professionFacade.findProfession().size() == 1);
        // Создадим список предметов для специальности
        List<SubjectView> svList = subjectFacade.findSubject();
        List<Long> check = new LinkedList<Long>();
        for (SubjectView sv : svList) {
            if (sv.getSubjectName().equals("Chemist") || sv.getSubjectName().equals("Physics")) {
                check.add(sv.getSubjectId());
            }
        }
        professionFacade.updateSubjectList(idProf, check);
        Assert.assertTrue(subjectFacade.findSubjectByProfession(idProf).size() == 2);

        pv.setProfessionName("Mathematician");
        idProf = professionFacade.addProfession(pv);
        pv = professionFacade.getProfession(idProf);
        Assert.assertTrue(pv.getProfessionName().equals("Mathematician"));
        Assert.assertTrue(professionFacade.findProfession().size() == 2);
        svList = subjectFacade.findSubject();
        check = new LinkedList<Long>();
        for (SubjectView sv : svList) {
            if (sv.getSubjectName().equals("Mathematics") 
              || sv.getSubjectName().equals("Physics") || sv.getSubjectName().equals("Literature")) {
                check.add(sv.getSubjectId());
            }
        }
        professionFacade.updateSubjectList(idProf, check);
        Assert.assertTrue(subjectFacade.findSubjectByProfession(idProf).size() == 3);

        pv.setProfessionName("Removed");
        pv.setSubjectList(new HashSet(svList));
        Long idProf2 = professionFacade.addProfession(pv);
        pv = professionFacade.getProfession(idProf2);
        Assert.assertTrue(pv.getProfessionName().equals("Removed"));
        Assert.assertTrue(professionFacade.findProfession().size() == 3);
        professionFacade.deleteProfession(pv);
        Assert.assertTrue(professionFacade.findProfession().size() == 2);
    }

    @Test
    @Rollback(false)
    public void applicantTest() {
        // Получаем список специальностей
        List<ProfessionView> pList = professionFacade.findProfession();
        Assert.assertTrue(professionFacade.findProfession().size() == 2);
        ProfessionView pr1 = professionFacade.getProfession(pList.get(0).getProfessionId());
        ProfessionView pr2 = professionFacade.getProfession(pList.get(1).getProfessionId());
        Long applicantId = 0L;

        // Заполняем данные для абитуриента
        ApplicantView av = new ApplicantView();
        av.setLastName("Стрельцов1");
        av.setFirstName("Павел");
        av.setMiddleName("Сергеевич");
        av.setEntranceYear(2009);
        av.setProfessionId(pr1.getProfessionId());
        // Записываем
        applicantId = applicantFacade.addApplicant(av);
        // Считываем
        av = applicantFacade.getApplicant(applicantId);
        // Проверяем, что оценок у только что введенного абитуриента нет
        Assert.assertTrue(av.getApplicantResultList().size() == 0);
        // Добавляем оценки абитуриенту
        av.setApplicantResultList(createMark(pr1, applicantId, 1));
        applicantFacade.updateApplicantResult(av);
        // Перечитываем и убеждаемся, что оценки теперь есть
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == pr1.getSubjectList().size());
        // Поробуем поменять фамилию у абитуриента
        av.setLastName("Стрельцов");
        applicantFacade.updateApplicant(av);
        av = applicantFacade.getApplicant(applicantId);
        // Убеждаемся, что изменения произошли
        Assert.assertTrue(av.getLastName().equals("Стрельцов"));
        // Перечитываем и убеждаемся, что оценки остались
        Assert.assertTrue(av.getApplicantResultList().size() == pr1.getSubjectList().size());


        av.setLastName("Иванов");
        av.setFirstName("Андрей");
        av.setMiddleName("Васильевич");
        av.setEntranceYear(2009);
        av.setProfessionId(pr1.getProfessionId());
        applicantId = applicantFacade.addApplicant(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == 0);
        av.setApplicantResultList(createMark(pr1, applicantId, 2));
        applicantFacade.updateApplicantResult(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == pr1.getSubjectList().size());

        av.setLastName("Смирнов");
        av.setFirstName("Сергей");
        av.setMiddleName("Петрович");
        av.setEntranceYear(2009);
        av.setProfessionId(pr2.getProfessionId());
        applicantId = applicantFacade.addApplicant(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == 0);
        av.setApplicantResultList(createMark(pr2, applicantId, 3));
        applicantFacade.updateApplicantResult(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == pr2.getSubjectList().size());

        av.setLastName("Затейников");
        av.setFirstName("Виктор");
        av.setMiddleName("Капитонович");
        av.setEntranceYear(2009);
        av.setProfessionId(pr2.getProfessionId());
        applicantId = applicantFacade.addApplicant(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == 0);
        av.setApplicantResultList(createMark(pr2, applicantId, 4));
        applicantFacade.updateApplicantResult(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == pr2.getSubjectList().size());

        av.setLastName("Федоров");
        av.setFirstName("Алексей");
        av.setMiddleName("Дмитриевич");
        av.setEntranceYear(2009);
        av.setProfessionId(pr2.getProfessionId());
        applicantId = applicantFacade.addApplicant(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == 0);
        av.setApplicantResultList(createMark(pr2, applicantId, 5));
        applicantFacade.updateApplicantResult(av);
        av = applicantFacade.getApplicant(applicantId);
        Assert.assertTrue(av.getApplicantResultList().size() == pr2.getSubjectList().size());

        Assert.assertTrue(applicantFacade.findApplicant().size() == 5);
    }

    @Test
    @Rollback(false)
    public void applicantDeleteTest() {
        List<ApplicantView> avList = applicantFacade.findApplicant();
        Assert.assertTrue(avList.size() == 5);
        applicantFacade.deleteApplicant(avList.get(0));
        Assert.assertTrue(applicantFacade.findApplicant().size() == 4);
    }

    // Вспомогательная процедура для установки оценок
    private List<ApplicantResultView> createMark(ProfessionView pv, Long applicantId, Integer mark) {
        List<ApplicantResultView> arvList = new LinkedList<ApplicantResultView>();
        for (SubjectView sv : pv.getSubjectList()) {
            ApplicantResultView ar = new ApplicantResultView();
            ar.setApplicantId(applicantId);
            ar.setSubjectId(sv.getSubjectId());
            ar.setMark(mark);
            arvList.add(ar);
        }

        return arvList;
    }
}
        

Как видите мы не делаем что-то особенное - просто пытаемся вызывать методы, которые мы написали и убеждаемся в том, что они работают. Думаю. что профессионалы могут упрекнуть меня в таком не очень разумном варианте кодирования - создание данных можно было бы сделать и покомпактнее, да и делать проверки на количество записей в базе данных тоже не самое лучшее решение. Мне просто хотелось показать, что можно проверять и как. Надеюсь на вашу сниходительность.
Автоматическое тестирование - очень мощный инструмент при создании и поддержке больших проектов. Если ваш код покрыт тестами, то ошибочные изменения сразу проявятся если тесты написаны хорошо. И скорость проверки повышается гораздо больше, чем кажущаяся экономия времени при отказе от написания тестов. После того, как система станет очень большой, то любое изменение будет очень тяжелым делом - вам же придется проверять море функциональности. Вручную. Но сейчас нам надо обратить внимание на самое начало файла. Рассмотрим несколько моментов:

  1. Наш класс StudentFacadeTest, который занимается тестированием, наследуется от класса AbstractTransactionalJUnit4SpringContextTests. Это позволяет нам использовать транзакции и даже отменять сделанные изменения. Что достаточно удобно.
  2. Мы используем аннотацию @ContextConfiguration для указания, какие файлы используются для конфигурации Spring
  3. Аннотация @TransactionConfiguration позволяет нам выбрать тот менеджер транзакций, который нами будет использоваться в случае тестирования. В нашем случае он у нас один, но вполне может быть ситуация, что ваше приложение в реальности будет работать под управлением Application Server J2EE. Значит и менеджер транзакций будет использоваться от самого сервера. Когда же мы тестируем нашу программу, то вполне вероятно, что использование J2EE сервера не очень удачная мысль и тогда нам потребуется иной менеджер транзакций. Вот для этого и нужна данная аннотация.
  4. Еще одна аннотация - @Autowired. Она позволяет автоматически установить значение поля, что конечно же удобно.
  5. Остальные две аннотации: @Test служит для обозначения методов, которые надо вызывать в процессе тестирования. И @Rollback - значение false говорит, что изменения в базе данных, которые сделаны в этом методе должны быть оставлены. Я это сделал намеренно.
Для тестирования проще всего просто пересоздать базу данных и запустить тест - он специально написан в расчете на пустую базу данных. Зато сразу получим какие-нибудь тестовые данные.

А теперь коротко рассмотрим функциональность фасадов. Только предварительно я хочу сделать небольшое отступление. При написании приложения возникает некоторая сложность при работе со списком и единичным экземпляром. Суть ее в том, что информация, которая требуется для списка, может быть более экономичной, чем для одного экземпляра. Когда же мы смотрим например одну специальность, то хорошо сразу иметь и список предметов для этой специальности. Для списка специальностей эта информация будет скорее всего излишней. Можно сделать отдельный класс View для списка и для одного экземпляра. Здесь я предлагаю иной вариант - класс один, но его заполнение может проиходить двумя способами - полное и частичное. Дополнительный логический аргумент в конструкторе View позволяет выбрать режим заполнения.

ProfessionFacade.java

  • addProfession - добавить новую специальность
  • updateProfession - изменить существующую специальность
  • updateSubjectList - изменить список прдеметов, которые соответствуют данной специальности.
  • updateSubjectList - иной вариант изменить список предметов
  • deleteProfession - удалить специальность
  • getProfession - получить одну специальность
  • findProfession - получить полный список специальностей

SubjectFacade.java

  • addSubject - добавить новый предмет
  • updateSubject - изменить существующий предмет
  • deleteSubject - удалить предмет
  • getSubject - получить один предмет
  • findSubject - получить полный список предметов
  • findSubjectById - получить список предметов по набору их ID
  • findSubjectByProfession - получить список предметов для выбранной специальности

ApplicantFacade.java

  • addApplicant - добавить абитуриента
  • updateApplicant - изменить данные абитуриента
  • updateApplicantResult - записать результаты абитуриента
  • deleteApplicant - удалить абитуриента
  • getApplicant - получить данные для одного абитуриента
  • findApplicant - получить список абитуриентов

ApplicantResultFacade.java - разберите его сами. Названия достаточно очевидны. Я сознательно не занимался проверкой этого класса в нашем тесте - попробуйте придумать что-то сами. Кроме этого можно придумать еще несколько функций для анализа данных. Например:

  • Список оценок по предмету в каком-то году
  • Список профессий по предмету
  • Средняя оценка по предмету среди абитуриентов
  • и многое другое
Как говорил мой хороший знакомый: "Есть два способа научиться программированию. Первый - пытаться создавать что-то самому. Второй - пойти на курсы, прочитать рекомендуемую литературу и ... пытаться создавать что-то самому."

Остальной код вы можете рассмотреть самостоятельно. Для запуска теста откройте его в редакторе и нажмите Shift+F6 или выберите пункт меню Run->Run File.

Библиотеки

Для текущего проекта нам понадобятся следующие библиотеки:
antlr-2.7.6.jar
asm.jar
cglib-2.1.jar
commons-collections-3.1.jar
commons-logging.jar
dom4j-1.6.1.jar
ejb3-persistence.jar
hibernate-annotations.jar
hibernate-commons-annotations.jar
hibernate3.jar
javaee.jar
javassist-3.4.GA.jar
jta-1.1.jar
log4j-1.2.15.jar
mysqlJDBC-3.1.13.jar
slf4j-api-1.5.3.jar
slf4j-log4j12-1.5.3.jar
spring.jar
spring-test.jar
spring-webmvc.jar
junit-4.4.jar
jstl.jar
standard.jar

Переходим на уровень Web

Как только мы начинаем заниматься Web-программированием, у нас возникает потребность удобно делать несколько вещей, главными из которых на мой взгляд являются:

  1. User Interface
  2. Вызов обработчика запроса от клиента на сервере
  3. Передача данных с клиента серверу и обратно
О первом пункте мы поговорим в другой раз. А вот пункты 2 и 3 мы рассмотрим, т.к. Spring включает в себя инструменты для них.

Для решения данной задачи был разработан шаблон MVC - Model-View-Controller (Модель, Представление, Контроллер). Их функции можно описать так:
Controller - это компонент, задача которого каким-либо образом решить, что конкретно надо делать. Можно сформулировать иначе и более конкретно: какой метод какого класса должен обработать данный запрос.
Model - компонент, который является хранителем данных, которые будут отображаться. Важно отметить, что модели совершенно неважно как данные будут отображаться на экране. Это очень важный момент, который позволяет иметь несколько вариантов отображения - для броузера, для телефона или даже для печатной формы.
View - компонент, который умеет отображать данные модели. Для отображения одной модели может быть использовано несколько View.

Таким образом, схема работы этих трех компонентов может быть описана следующим образом: Controller получает запрос и по определенным настройкам (правилам) с учетом полученных параметров определяет, что именно надо делать для получения данных (Model). После работы с данными Model может быть передана View и с помощью этого View данные будут отображаться на экране.

Очень часто контроллером может быть какой-то сервлет, который по определенной конфигурации делает что-либо. Model - здесь не могу однозначно что-то сказать. Это может быть самый обычный класс с нужными полями. А в качестве View выступает чаще всего JSP-страница.
Систем, реализующих данный шаблон, достаточно много. В данной части мы рассмотрми реализацию MVC на Spring.

Spring MVC

Как я уже упоминал, в качестве контроллера часто выступает сервлет. Spring следует этому правилу и для начала мы рассмотрим файл web.xml - вместилище сервлетов. Кстати сам web.xml можно также рассматривать в качестве несложного контроллера. Он ведь занимается вызовами разных сервлетов по определенным маскам URL. Итак, вот наш web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/classes/StudentDatabase.xml,
            /WEB-INF/classes/StudentExample.xml,
            /WEB-INF/classes/StudentController.xml
        </param-value>
    </context-param>

    <servlet>
        <servlet-name>context</servlet-name>
        <servlet-class>
            org.springframework.web.context.ContextLoaderServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet>
        <servlet-name>applicantServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value/>
        </init-param>
        <load-on-startup>2</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>applicantServlet</servlet-name>
        <url-pattern>*.std</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <resource-ref>
        <description>DB Connection</description>
        <res-ref-name>studentDS</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>

</web-app>
        

Давайте внимательно и подробно рассмотрим все детали - здесь вважно будет практически все.

  • ContextLoaderServlet - это сервлет из пакета Spring, который берет на себя обязанности по загрузке контента Spring. если web-сервер поддерживает спецификацию Servlet 2.4 и выше, то документация советует использовать ContextLoaderListener. Но мы оставим наш пример в таком виде. Как видите мы его загружаем сразу и первым (см. load-on-startup)
  • DispatcherServlet - в обязанности этого класса входит обработать все запросы. Это по сути и есть контроллер. Я бы назвал его предварительным. Как мы увидим чуть позже, он не единственный. Его мы тоже загружаем сразу. Отметьте, что он будет обрабатывать все запросы, которые оканчиваются на .std
  • contextConfigLocation - этот параметр содержит список всех файлов для конфигурации Spring
Также обратите внимание на определение ресурса класса DataSource (он в самом низу файла - ресурс studentDS). Мы уже пользовались таким определением в Часть 9 - Простое Web-приложение.
ВАЖНО: Не забудьте скопировать файл mysql-connector-java-3.1.13-bin.jar в каталог /lib корневого каталога Tomcat (я говорю о Tomcat 6). Для Tomcat 5 каталог /common/lib Если в двух словах: Tomcat предоставляет возможность воспользоваться реализацией интерфейса javax.sql.DataSource, которая является пулом коннектов к базе данных. параметры для коннекта находятся в файле META_INF/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<Context path="/Spring_05">
   <Resource
      name="studentDS"
      type="javax.sql.DataSource"
      username="root"
      password="root"
      driverClassName="com.mysql.jdbc.Driver"
      maxIdle="2"
      maxWait="5000"
      validationQuery="SELECT 1"
      url="jdbc:mysql://127.0.0.1:3306/db_applicant?characterEncoding=UTF-8"
      maxActive="4"/>
</Context>
        

Надеюсь, что больших вопросов содержание данного файла у вас не вызовет. Если что - читайте 9-ю часть. Там все описано более подробно.
Гораздо более интересным будет файл StudentDatabase.xml, где мы увидим как использовать заново определенный DataSource

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

    <bean name="studentDS" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="java:comp/env/studentDS"/>
        <property name="resourceRef" value="true"/>
    </bean>

    <bean name="sessionFactory"
        class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="studentDS" />
        <property name="annotatedClasses">
            <list>
                <value>students.entity.Profession</value>
                <value>students.entity.Subject</value>
                <value>students.entity.Applicant</value>
                <value>students.entity.ApplicantResult</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.MySQLInnoDBDialect
                hibernate.show_sql=true
            </value>
        </property>
    </bean>

    <bean name="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

    <bean name="abstractTransactionProxy"
            class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
            abstract="true">
        <property name="transactionManager" ref="txManager" />
        <property name="transactionAttributes">
            <props>
                <prop key="find*">PROPAGATION_REQUIRED, readOnly
                </prop>
                <prop key="get*">PROPAGATION_REQUIRED, readOnly
                </prop>
                <prop key="add*">PROPAGATION_REQUIRED,-Exception
                </prop>
                <prop key="update*">PROPAGATION_REQUIRED,-Exception
                </prop>
                <prop key="delete*">PROPAGATION_REQUIRED,-Exception
                </prop>
            </props>
        </property>
    </bean>

    <bean name="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

</beans>
        

Как видите, теперь мы обращаемся к ресурсу по имени и этот ресурс предоставляет нам Web-контейнер Tomcat. Т.е. теперь параметры коннекта зарегистрированы у Tomcat и каждый, кто захочет, может им пользоваться. Опять разделение труда и облегчение работы. Что приятно.

Разобравшись с DataSource и вопросом загрузки контента Spring давайте рассмотрим что и как делает Spring после этого для реализации шаблона MVC. Но прежде чем рассматривать xml-файлы и конкретные классы, давайте более подробно остановимся на принципах организации всго механищма MVC в Spring. Я воспользовался картинкой из документации Spring.

Как видим, все начинается с прихода запроса в DispatcherServlet (Front Controller). В нем определяется какой именно класс (который реализует интерфейс org.springframework.web.servlet.mvc.Controller) будет использоваться для обработки конкретного запроса. Именно в этом классе мы будем организовывать логику получения данных для отображения. Если быть более точным, то DispatcherServlet использует объект/класс, который реализует интерфейс HandlerMapping. Вобщем-то никто не мешает использовать уже готовые классы от Spring - SimpleUrlHandlerMapping или BeanNameUrlHandlerMapping. После того, как сделаны нужные изменения и данные готовы, контроллер решает, какое именно представление (View) будет использовано для отображения. Имя этого View передается так называемому ViewResolver'у (точный перевод сделать сложно, но наверно лучшим будет что-то вроде "определитель/выбиратель" View). Если быть более точным - ViewResolver'ов может быть несколько. Они организуются в последовательность (причем порядком вы можете управлять) и каждый пытается определить, какой View скрывается под указанным именем. Когда View определен (в большинстве случаев это какая-то JSP-страница) ему передаются данные и уже сформированная HTML-страница (а может WML) отправляется в броузер.
Еще раз кратко опишем всю цепочку: HTTP-запрос получает DispatcherServlet, который передает управление нужному контроллеру (в соответствии со своей конфигурацией). Контроллер получает данные и передает их набору ViewResolver'ов, которые по очереди пытаются найти нужный View. После того, как View найден, он получает данные, подготавливает HTML-страницу и отправляет ее в броузер.

А теперь давайте рассмотрим готовый пример, в котором мы сделаем три несложные страницы для показа списка предметов, списка специальностей и списка абитуриентов. Сначала посмотрим на файл конйигурации StudentController.xml

<?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:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
            http://www.springframework.org/schema/aop 
            http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">


    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.JstlView</value>
        </property>
        <property name="order" value="2"/>
        <property name="prefix">
            <value>/</value>
        </property>
        <property name="suffix">
            <value>.jsp</value>
        </property>
    </bean>

    <bean id="professionController" class="students.web.controller.ProfessionController">
        <property name="professionFacade" ref="professionFacade" />
    </bean>

    <bean id="subjectController" class="students.web.controller.SubjectController">
        <property name="subjectFacade" ref="subjectFacade" />
    </bean>

    <bean id="applicantController" class="students.web.controller.ApplicantController">
        <property name="applicantFacade" ref="applicantFacade" />
    </bean>

    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/profession.std">professionController</prop>
                <prop key="/subject.std">subjectController</prop>
                <prop key="/applicant.std">applicantController</prop>
            </props>
        </property>
    </bean>
</beans>
        

Начнем рассматривать этот файл снизу вверх. Как видите, для определения имени контроллера, который будет обрабатывать апросы, мы выбрали SimpleUrlHandlerMapping. Думаю, что вы уже догадались, как происходит выбор контроллера - это обычное совпадение по маске. В маске можно использовать * для обобщения нескольких страниц.

Далее идут три наших контроллера - для показа наших трех тсраниц. И самое интересное - это "выбиратель" страниц - в данном случае мы воспользовались классом InternalResourceViewResolver. Принцип его работы следующий: Сначала подставляется часть из prefix, потом к ней подставляется имя View, которое нам передаст контроллер (мы чуть ниже это увидим) и в конце подставляется часть из suffix. Т.е. если в качестве имени контроллер передаст строку subject/subject, то итогом будет страница JSP /subject/subject.jsp, которой и будет передано управление.

А теперь самое время посмотреть на код одного из контроллеров - они у нас достаточно похожи и поэтому мы рассмотрим только один - ProfessionController.

package students.web.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import students.facade.ProfessionFacade;
import students.view.ProfessionView;

public class ProfessionController extends AbstractController {

    private ProfessionFacade professionFacade;

    public void setProfessionFacade(ProfessionFacade professionFacade) {
        this.professionFacade = professionFacade;
    }

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest arg0, HttpServletResponse arg1) 
        throws Exception {
        List<ProfessionView> l = professionFacade.findProfession();
        Map<String,List<ProfessionView>>  data = new HashMap<String,List<ProfessionView>>();
        data.put("professionList", l);
        return new ModelAndView("students/profession", data);
    }

}
        

Как видите, в нем нет ничего сложного. Мы унаследовали наш контроллер от класса AbstractController и переопределили метод handleRequestInternal, который в качестве параметров имеет то же, что и обычный сервлет. Обратите внимание на два момента:

  1. Мы создали объект Map, который содержит имя объекта с данными и сам объект (именно по этому имени мы будем обращаться к данным из страницы JSP)
  2. Нащ метод возвращает объект класса ModelAndView, в конструкторе которого мы указали имя View (которое позволит нам сконструировать имя для JSP) и объект с данными.
Теперь нам осталось собрать проект, положить готовый файл Spring_05.war в директорию <TOMCAT_HOME>\webapps и запустить Tomcat.

Еще один момент - это страница JSP которая будет отображать данные.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib uri="/WEB-INF/tld/c.tld" prefix="c" %>

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Profession List</title>
    </head>
    <body>
        <table border="1">
            <c:forEach var="profession" items="${professionList}">
                <tr>
                    <td>${profession.professionId}</td>
                    <td>${profession.professionName}</td>
                </tr>
            </c:forEach>
        </table>
    </body>
</html>
        

Обратите внимание на часть

            <c:forEach var="profession" items="${professionList}">
        

Если вы посмотрите снова на код нашего контроллера, то увидите, что наши данные мы поместили под именем professionList. И именно по этому имени обращаемся к данным.

Теперь вы можете проверить наше приложение подставляя разные URL:
http://localhost:8080/Spring_05/profession.std
http://localhost:8080/Spring_05/subject.std
http://localhost:8080/Spring_05/applicant.std

Исходный код для всех классов вы можете найти в проекте Spring_05.

Тестирование без Tomcat

В конце мне бы хотелось обратить ваше внимание на еще два класса, которые у нас появились в разделе Test Packages - а именно StudentControllerTest и StudentSuit

package students.test;

import javax.naming.NamingException;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;

/**
 *
 * @author ASaburov
 */
@RunWith(Suite.class)
@Suite.SuiteClasses({
    students.test.StudentFacadeTest.class,
    students.test.StudentControllerTest.class
})
public class StudentSuit {

    @BeforeClass
    public static void setUpClass() throws Exception {
        try {
            SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
            DriverManagerDataSource ds = 
                new DriverManagerDataSource("jdbc:mysql://localhost:3306/db_applicant", "root", "root");
            ds.setDriverClassName("com.mysql.jdbc.Driver");
            builder.bind("java:comp/env/studentDS", ds);
        } catch (IllegalStateException ex) {
            ex.printStackTrace();
            Assert.fail();
        } catch (NamingException ex) {
            ex.printStackTrace();
            Assert.fail();
        }
    }
}
        

Spring предоставляет немало интересных вохможностей по тестированию. Одна из них - возможность создания объектов, к котороым можно обратиться через JNDI - мы ведь используем данный способ. Чтобы не переделывать конфигурацию можно использовать нужные классы. Мы создаем эмулятор JNDI и помещаем туда DriverManagerDataSource, который связан с тем же именем, что и при использовании Tomcat. Также следует обратить внимание, каким образом создается целый набор классов, котоый мы запускаем для тестирование - я имею в виду аннотацию @Suite.SuiteClasses.

И давайте посмотрим на класс StudentControllerTest. В нем самое главное - это использование так называемых mock-объектов (я бы перевел это как подставных/тренировочных). Как вы уже видели в метод контроллера мы должны передать объекты, которые реализуют интерфейс HttpServletRequest и HttpServletResponse. Но это интерфейсы, а нам нухны реальные объекты. И Spring предоставляет нам такой набор - их возможности достаточно большие - я настоятельно советую вам посмотреть документацию на них.

ВНИМАНИЕ !!! Перед запуском тестов база данных должна быть пустой. Вы можете это сделать запустить скрипт создания базы - Часть 15 - Новая структура данных. Такой вариант не является удачным, но в данном случае мне хотелось сразу наполнить базу данными. Ну и заодно увидеть, что не так именно в таком тесте. Вы можете сделать тесты более удобными и правильными.

Архив с исходными кодами: Исходный код