Полный пример бизнес-уровня на Spring
Итак, мы увидели возможности Spring на примере одной функциональности — Profession. Теперь мы сделаем следующее: для каждой из наших таблиц создадим подобную функциональность, которая будет включать в себя полный набор классов и интерфейсов:
- Entity
- DAO интерфейс
- DAO реализация
- Facade
- View для UI
Это сделает наш код чуть-чуть сложнее и запутаннее. Но именно, что «чуть-чуть». В принципе можно было бы обойтись одним DAO, одним фасадом и даже убрать DAO-интерфейс. Вы можете реализовать такой упрощенный вариант сами. Исходный код для всех классов вы можете найти в проекте Spring_04. Здесь мы опишем только функциональность каждого фасада, чтобы вы имели представление кто что делает. А дальше по коду вы сможете разобраться сами. Я очень хочу, чтобы вы читали код самостоятельно — умение быстро читать код вом пригодится.
При чтении кода я вам рекомендую обратить внимание на то, что класса Main, который мы использовали для «тестирования» работоспособности нашего приложения, уже нет. Вместо него я написал специальный класс для тестирования SpringStudentFacadeTest. Этот класс использует еще одну функциональность Spring — тестирование. Пакет предоставляет несколько очень удобных инструментов, которые мы еще рассмотрим. А пока давайте посмотрим на код этого класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
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; } } |
Как видите мы не делаем что-то особенное — просто пытаемся вызывать методы, которые мы написали и убеждаемся в том, что они работают. Думаю. что профессионалы могут упрекнуть меня в таком не очень разумном варианте кодирования — создание данных можно было бы сделать и покомпактнее, да и делать проверки на количество записей в базе данных тоже не самое лучшее решение. Мне просто хотелось показать, что можно проверять и как. Надеюсь на вашу сниходительность.
Автоматическое тестирование — очень мощный инструмент при создании и поддержке больших проектов. Если ваш код покрыт тестами, то ошибочные изменения сразу проявятся если тесты написаны хорошо. И скорость проверки повышается гораздо больше, чем кажущаяся экономия времени при отказе от написания тестов. После того, как система станет очень большой, то любое изменение будет очень тяжелым делом — вам же придется проверять море функциональности. Вручную. Но сейчас нам надо обратить внимание на самое начало файла. Рассмотрим несколько моментов:
- Наш класс StudentFacadeTest, который занимается тестированием, наследуется от классаAbstractTransactionalJUnit4SpringContextTests. Это позволяет нам использовать транзакции и даже отменять сделанные изменения. Что достаточно удобно.
- Мы используем аннотацию @ContextConfiguration для указания, какие файлы используются для конфигурации Spring
- Аннотация @TransactionConfiguration позволяет нам выбрать тот менеджер транзакций, который нами будет использоваться в случае тестирования. В нашем случае он у нас один, но вполне может быть ситуация, что ваше приложение в реальности будет работать под управлением Application Server J2EE. Значит и менеджер транзакций будет использоваться от самого сервера. Когда же мы тестируем нашу программу, то вполне вероятно, что использование J2EE сервера не очень удачная мысль и тогда нам потребуется иной менеджер транзакций. Вот для этого и нужна данная аннотация.
- Еще одна аннотация — @Autowired. Она позволяет автоматически установить значение поля, что конечно же удобно.
- Остальные две аннотации: @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-программированием, у нас возникает потребность удобно делать несколько вещей, главными из которых на мой взгляд являются:
- User Interface
- Вызов обработчика запроса от клиента на сервере
- Передача данных с клиента серверу и обратно
О первом пункте мы поговорим в другой раз. А вот пункты 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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, который в качестве параметров имеет то же, что и обычный сервлет. Обратите внимание на два момента:
- Мы создали объект Map, который содержит имя объекта с данными и сам объект (именно по этому имени мы будем обращаться к данным из страницы JSP)
- Нащ метод возвращает объект класса ModelAndView, в конструкторе которого мы указали имя View (которое позволит нам сконструировать имя для JSP) и объект с данными.
Теперь нам осталось собрать проект, положить готовый файл Spring_05.war в директорию <TOMCAT_HOME>\webapps и запустить Tomcat.
Еще один момент — это страница JSP которая будет отображать данные.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<%@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> |
Обратите внимание на часть
1 |
<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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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 — Новая структура данных. Такой вариант не является удачным, но в данном случае мне хотелось сразу наполнить базу данными. Ну и заодно увидеть, что не так именно в таком тесте. Вы можете сделать тесты более удобными и правильными.
Архив с исходными кодами: Исходный код