Первое, что мы должны сделать, это сделать веб-приложение из того, что мы имеем до сих пор. Мы добавим папку web / WEB-INF в корневой каталог нашего проекта. Внутри WEB-INF создайте папку jsp . Мы разместим наши JSP в этом месте. Внутри этой папки мы поместим файл описания дескриптора web.xml со следующим содержимым:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<? xml version = '1.0' encoding = 'UTF-8' ?> xsi:schemaLocation='http://java.sun.com/xml/ns/javaee version = '3.0' > < display-name >timesheet-app</ display-name > < context-param > < param-name >contextConfigLocation</ param-name > < param-value > classpath:persistence-beans.xml </ param-value > </ context-param > < listener > < listener-class >org.springframework.web.context.ContextLoaderListener</ listener-class > </ listener > < servlet > < servlet-name >timesheet</ servlet-name > < servlet-class >org.springframework.web.servlet.DispatcherServlet</ servlet-class > < load-on-startup >1</ load-on-startup > </ servlet > < servlet-mapping > < servlet-name >timesheet</ servlet-name > < url-pattern >/</ url-pattern > </ servlet-mapping > </ web-app > |
Обратите внимание, что мы используем сервлет, называемый расписанием. Это диспетчерский сервлет. На следующем рисунке показано, как работает сервлет-диспетчер Spring (на рисунке ниже он называется Front controller):
- Запрос обрабатывается диспетчерским сервлетом
- Сервлет-диспетчер решает, какому контроллеру он должен доставить запрос (путем сопоставления запросов мы увидим это позже), а затем делегирует запрос
- Контроллер создает модель и доставляет ее обратно сервлету диспетчера.
- Сервлет-диспетчер разрешает логическое имя представления, связывает модель и отображает представление
Последний шаг довольно загадочный. Как сервлет-диспетчер разрешает логическое имя представления? Он использует что-то под названием ViewResolver. Но мы не собираемся создавать свои собственные вручную, вместо этого мы просто создадим другой файл конфигурации, определим bean-компонент с ViewResolver и добавим его Spring. В WEB-INF создайте еще один конфигурационный файл Spring. По соглашению он должен называться timesheet-servlet.xml , потому что мы назвали наш DispatcherServlet «timesheet», и это имя файла, где Spring будет искать конфигурацию по умолчанию. Также создайте пакет org.timesheet.web . Это где мы будем помещать наши контроллеры (которые опять только аннотированные POJO).
Вот расписание-сервлет.xml
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
<? xml version = '1.0' encoding = 'UTF-8' ?> xsi:schemaLocation = 'http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd' > < context:component-scan base-package = 'org.timesheet.web' /> < bean class = 'org.springframework.web.servlet.view.InternalResourceViewResolver' > < property name = 'prefix' value = '/WEB-INF/jsp/' /> < property name = 'suffix' value = '.jsp' /> </ bean > </ beans > |
Мы определили префикс и суффикс для разрешения логических имен. Это действительно просто. Мы рассчитываем имя так:
полное имя = префикс + логическое имя + суффикс
Поэтому с нашим InternalResourceViewResolver логическое имя «index» будет преобразовано в «/WEB-INF/jsp/index.jsp».
Для представлений мы будем использовать технологию JSP с JSTL (библиотеку тегов), поэтому нам нужно добавить другие зависимости в наш файл pom.xml :
01
02
03
04
05
06
07
08
09
10
|
< dependency > < groupId >jstl</ groupId > < artifactId >jstl</ artifactId > < version >1.2</ version > </ dependency > < dependency > < groupId >javax.servlet</ groupId > < artifactId >javax.servlet-api</ artifactId > < version >3.0.1</ version > </ dependency > |
Теперь мы бы хотели обработать GET в / timesheet-app / welcome. Поэтому нам нужно написать view и controller (для Model мы будем использовать один из объектов Spring). Начнем с контроллера:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
package org.timesheet.web; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.Date; @Controller @RequestMapping ( '/welcome' ) public class WelcomeController { @RequestMapping (method = RequestMethod.GET) public String showMenu(Model model) { model.addAttribute( 'today' , new Date()); return 'index' ; } } |
Поэтому, когда кто-то получает доступ к приветствию URL (в нашем случае http: // localhost: 8080 / timesheet-app / welcome ), этот контроллер будет обрабатывать запрос. Мы также используем Model и связываем там значение с именем «today». Вот как мы получаем значение для просмотра страницы.
Обратите внимание, что корнем моего приложения является / timesheet-app. Это называется контекстом приложения . Конечно, вы можете изменить это, но все остальные коды предполагают, что ваш прикладной контекст настроен так. Если вы развертываете WAR, он основан на имени WAR.
Из метода showMenu мы возвращаем «index» — который будет преобразован в WEB-INF / jsp / index.jsp, поэтому давайте создадим такую страницу и поместим туда некоторый основной контент:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<%@ page contentType='text/html;charset=UTF-8' language='java' %> <%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %> <%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %> < html > < head > < title >Welcome to Timesheet app!</ title > </ head > < body > < h1 >Welcome to the Timesheet App!</ h1 > < ul > < li >< a href = 'managers' >List managers</ a ></ li > < li >< a href = 'employees' >List employees</ a ></ li > < li >< a href = 'tasks' >List tasks</ a ></ li > < li >< a href = 'timesheets' >List timesheets</ a ></ li > </ ul > < h2 >Also check out < a href = 'timesheet-service' >extra services!</ a ></ h2 > Today is: < fmt:formatDate value = '${today}' pattern = 'dd-MM-yyyy' /> </ body > </ html > |
Напомним, что мы добавили файл web.xml, файл конфигурации timesheet-servlet.xml Spring, класс контроллера и страницу jsp. Давайте попробуем запустить это на каком-то веб-контейнере. Я буду использовать Tomcat7, но если вам удобнее использовать другой веб-контейнер или даже сервер приложений — смело переключайтесь. Сейчас есть много способов, как запустить Tomcat и как развернуть приложение. Ты сможешь:
- Используйте встроенный плагин Tomcat с Maven
- Запустите Tomcat напрямую из IntelliJ
- Запустите Tomcat напрямую из Eclipse / STS
Что бы вы ни выбрали, убедитесь, что вы можете получить доступ к URL, упомянутому выше, прежде чем продолжить. Честно говоря, развертывание в Java для меня наименее увлекательно, поэтому не сдавайтесь, если вы расстроены, это может не сработать в первый раз. Но с уроками выше, у вас, вероятно, не будет никаких проблем. Также не забудьте установить правильный контекст приложения.
Прежде чем мы напишем больше контроллеров, давайте подготовим некоторые данные. Когда Spring создает bean-компонент welcomeController, мы хотели бы получить некоторые данные. Итак, пока давайте просто напишем фиктивный генератор, который просто создаст некоторые объекты. Позже в уроке мы увидим более реалистичное решение.
Поместите пакет помощников в веб-пакет, где находятся контроллеры класса EntityGenerator :
01
02
03
04
05
06
07
08
09
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
|
package org.timesheet.web.helpers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.timesheet.domain.Employee; import org.timesheet.domain.Manager; import org.timesheet.domain.Task; import org.timesheet.domain.Timesheet; import org.timesheet.service.GenericDao; import org.timesheet.service.dao.EmployeeDao; import org.timesheet.service.dao.ManagerDao; import org.timesheet.service.dao.TaskDao; import org.timesheet.service.dao.TimesheetDao; import java.util.List; /** * Small util helper for generating entities to simulate real system. */ @Service public final class EntityGenerator { @Autowired private EmployeeDao employeeDao; @Autowired private ManagerDao managerDao; @Autowired private TaskDao taskDao; @Autowired private TimesheetDao timesheetDao; public void generateDomain() { Employee steve = new Employee( 'Steve' , 'Design' ); Employee bill = new Employee( 'Bill' , 'Marketing' ); Employee linus = new Employee( 'Linus' , 'Programming' ); // free employees (no tasks/timesheets) Employee john = new Employee( 'John' , 'Beatles' ); Employee george = new Employee( 'George' , 'Beatles' ); Employee ringo = new Employee( 'Ringo' , 'Beatles' ); Employee paul = new Employee( 'Paul' , 'Beatles' ); Manager eric = new Manager( 'Eric' ); Manager larry = new Manager( 'Larry' ); // free managers Manager simon = new Manager( 'Simon' ); Manager garfunkel = new Manager( 'Garfunkel' ); addAll(employeeDao, steve, bill, linus, john, george, ringo, paul); addAll(managerDao, eric, larry, simon, garfunkel); Task springTask = new Task( 'Migration to Spring 3.1' , eric, steve, linus); Task tomcatTask = new Task( 'Optimizing Tomcat' , eric, bill); Task centosTask = new Task( 'Deploying to CentOS' , larry, linus); addAll(taskDao, springTask, tomcatTask, centosTask); Timesheet linusOnSpring = new Timesheet(linus, springTask, 42 ); Timesheet billOnTomcat = new Timesheet(bill, tomcatTask, 30 ); addAll(timesheetDao, linusOnSpring, billOnTomcat); } public void deleteDomain() { List<Timesheet> timesheets = timesheetDao.list(); for (Timesheet timesheet : timesheets) { timesheetDao.remove(timesheet); } List<Task> tasks = taskDao.list(); for (Task task : tasks) { taskDao.remove(task); } List<Manager> managers = managerDao.list(); for (Manager manager : managers) { managerDao.remove(manager); } List<Employee> employees = employeeDao.list(); for (Employee employee : employees) { employeeDao.remove(employee); } } private <T> void addAll(GenericDao<T, Long> dao, T... entites) { for (T o : entites) { dao.add(o); } } } |
Теперь давайте использовать код для WelcomeController. Мы добавим туда генератор и разместим специальный метод с аннотацией @PostConstruct . Это аннотация JSR-250 для жизненного цикла бина, и Spring поддерживает его. Это означает, что этот метод будет вызван сразу после того, как контейнер Spring IoC создаст экземпляр объекта welcomeController.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package org.timesheet.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.timesheet.web.helpers.EntityGenerator; import javax.annotation.PostConstruct; import java.util.Date; @Controller @RequestMapping ( '/welcome' ) public class WelcomeController { @Autowired private EntityGenerator entityGenerator; @RequestMapping (method = RequestMethod.GET) public String showMenu(Model model) { model.addAttribute( 'today' , new Date()); return 'index' ; } @PostConstruct public void prepareFakeDomain() { entityGenerator.deleteDomain(); entityGenerator.generateDomain(); } } |
Хорошо, давайте сейчас напишем несколько контроллеров для логики домена!
Мы начнем с написания контроллера сотрудника. Сначала создайте класс EmployeeController в пакете org.timesheet.web. Отметить класс как веб-контроллер и обрабатывать запросы «/ employee»:
1
2
3
|
@Controller @RequestMapping ( '/employees' ) public class EmployeeController { ... |
Для обработки постоянных данных (в данном случае сотрудников) нам нужен DAO, и он автоматически подключается к контейнеру Spring IoC, поэтому давайте сделаем именно это:
1
2
3
4
5
6
|
private EmployeeDao employeeDao; @Autowired public void setEmployeeDao(EmployeeDao employeeDao) { this .employeeDao = employeeDao; } |
Теперь мы хотим обработать метод HTTP GET. Когда пользователь обращается к http: // localhost: 8080 / timesheet-app / employee через веб-браузер, контроллер должен обработать запрос GET. Это только связывается с DAO, собирает всех сотрудников и помещает их в модель.
1
2
3
4
5
6
7
|
@RequestMapping (method = RequestMethod.GET) public String showEmployees(Model model) { List<Employee> employees = employeeDao.list(); model.addAttribute( 'employees' , employees); return 'employees/list' ; } |
В папке jsp создайте папку сотрудников, в которую мы поместим все соответствующие JSP сотрудников. Возможно, вы уже заметили, что страница со списком сотрудников будет преобразована в /WEB-INF/jsp/employees/list.jsp. Так что создайте такую страницу. Мы увидим содержимое позже, если вы хотите, вы можете поместить туда случайный текст, чтобы увидеть, работает ли он.
На странице JSP мы покажем ссылку рядом с сотрудником для его личной страницы, которая будет выглядеть следующим образом : http: // localhost: 8080 / timesheet-app / employee / {id}, где ID — это идентификатор сотрудника. Это RESTful URL, потому что он ориентирован на ресурсы, и мы напрямую идентифицируем ресурс. URL без REST будет выглядеть примерно так: http: // localhost: 8080 / timesheet-app / employee.html? Id = 123. Это ориентировано на действие и не определяет ресурс.
Давайте добавим еще один метод в контроллер, который обрабатывает этот URL:
1
2
3
4
5
6
7
|
@RequestMapping (value = '/{id}' , method = RequestMethod.GET) public String getEmployee( @PathVariable ( 'id' ) long id, Model model) { Employee employee = employeeDao.find(id); model.addAttribute( 'employee' , employee); return 'employees/view' ; } |
Опять же, создайте страницу view.jsp в папке / jsp / employee. На этой странице мы также хотели бы изменить сотрудника. Мы просто обращаемся к одному и тому же URL, но другим веб-методом — POST. Это означает, что мы даем данные из ограниченной модели для обновления.
Этот метод обрабатывает обновления сотрудника:
1
2
3
4
5
6
7
|
@RequestMapping (value = '/{id}' , method = RequestMethod.POST) public String updateEmployee( @PathVariable ( 'id' ) long id, Employee employee) { employee.setId(id); employeeDao.update(employee); return 'redirect:/employees' ; } |
В этом случае мы обращались к employee / {id} методом GET или POST. Но что, если мы хотим удалить сотрудника? Мы получим доступ к одному и тому же URL, но другим способом — УДАЛИТЬ . Мы будем использовать дополнительную бизнес-логику в EmployeeDao. Если что-то пойдет не так, мы сгенерируем исключение, содержащее сотрудника, которое нельзя удалить. Поэтому давайте добавим метод контроллера для этого случая:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
/** * Deletes employee with specified ID * @param id Employee's ID * @return redirects to employees if everything was ok * @throws EmployeeDeleteException When employee cannot be deleted */ @RequestMapping (value = '/{id}' , method = RequestMethod.DELETE) public String deleteEmployee( @PathVariable ( 'id' ) long id) throws EmployeeDeleteException { Employee toDelete = employeeDao.find(id); boolean wasDeleted = employeeDao.removeEmployee(toDelete); if (!wasDeleted) { throw new EmployeeDeleteException(toDelete); } // everything OK, see remaining employees return 'redirect:/employees' ; } |
Обратите внимание, что мы возвращаем перенаправление из этого метода. Префикс redirect: указывает, что запрос должен быть перенаправлен на путь, которому он предшествует.
Создайте пакет org.timesheet.web.exceptions и поместите туда следующее EmployeeDeleteException :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
package org.timesheet.web.exceptions; import org.timesheet.domain.Employee; /** * When employee cannot be deleted. */ public class EmployeeDeleteException extends Exception { private Employee employee; public EmployeeDeleteException(Employee employee) { this .employee = employee; } public Employee getEmployee() { return employee; } } |
Возможно, это исключение можно было бы напрямую выбросить из DAO. Теперь, как мы справимся с этим? Spring имеет специальную аннотацию под названием @ExceptionHandler . Мы разместим его в нашем контроллере, и когда будет сгенерировано указанное исключение, метод, аннотированный ExceptionHandler, обработает его и разрешит правильное представление:
01
02
03
04
05
06
07
08
09
10
11
|
/** * Handles EmployeeDeleteException * @param e Thrown exception with employee that couldn't be deleted * @return binds employee to model and returns employees/delete-error */ @ExceptionHandler (EmployeeDeleteException. class ) public ModelAndView handleDeleteException(EmployeeDeleteException e) { ModelMap model = new ModelMap(); model.put( 'employee' , e.getEmployee()); return new ModelAndView( 'employees/delete-error' , model); } |
Хорошо, время для JSP. Мы будем использовать некоторые ресурсы (например, * .css или * .js), поэтому в корневой папке вашего веб-приложения создайте папку ресурсов . WEB-INF — это не root, это папка выше. Таким образом, ресурсы и WEB-INF теперь должны находиться на одном уровне в дереве каталогов. Мы настроили наш сервлет-диспетчер для обработки каждого запроса (с шаблоном / url), но мы не хотим, чтобы он обрабатывал статические ресурсы. Мы решим это, просто поместив отображение для сервлета по умолчанию в наш файл web.xml :
1
2
3
4
|
<servlet-mapping> <servlet-name> default </servlet-name> <url-pattern>/resources/*</url-pattern> </servlet-mapping> |
Под этим ресурсом создайте файл styles.css . Мы поместим CSS для всего приложения прямо сейчас, хотя мы будем использовать этот материал позже.
01
02
03
04
05
06
07
08
09
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
|
table, th { margin: 10px; padding: 5px; width: 300px; } .main-table { border: 2px solid green; border-collapse: collapse; } .wide { width: 600px; } .main-table th { background-color: green; color: white; } .main-table td { border: 1px solid green; } th { text-align: left; } h1 { margin: 10px; } a { margin: 10px; } label { display: block; text-align: left; } #list { padding-left: 10px; position: relative; } #list ul { padding: 0 ; } #list li { list-style: none; margin-bottom: 1em; } .hidden { display: none; } .delete { margin: 0 ; text-align: center; } .delete-button { border: none; background: url( '/timesheet-app/resources/delete.png' ) no-repeat top left; color: transparent; cursor: pointer; padding: 2px 8px; } .task-table { width: 150px; border: 1px solid #dcdcdc; } .errors { color: # 000 ; background-color: #ffEEEE; border: 3px solid #ff0000; padding: 8px; margin: 16px; } |
Давайте теперь создадим страницу Employess / list.jsp :
01
02
03
04
05
06
07
08
09
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
|
<%@ page contentType='text/html;charset=UTF-8' language='java' %> <%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %> <%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %> <%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%> <%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%> < html > < head > < title >Employees</ title > < link rel = 'stylesheet' href = '/timesheet-app/resources/style.css' type = 'text/css' > </ head > < body > < h1 >List of employees</ h1 > < a href = 'employees?new' >Add new employee</ a > < table cellspacing = '5' class = 'main-table' > < tr > < th >Name</ th > < th >Department</ th > < th >Details</ th > < th >Delete</ th > </ tr > < c:forEach items = '#{employees}' var = 'emp' > < tr > < td >${emp.name}</ td > < td >${emp.department}</ td > < td > < a href = 'employees/${emp.id}' >Go to page</ a > </ td > < td > < sf:form action = 'employees/${emp.id}' method = 'delete' cssClass = 'delete' > < input type = 'submit' class = 'delete-button' value = '' /> </ sf:form > </ td > </ tr > </ c:forEach > </ table > < br /> < a href = 'welcome' >Go back</ a > </ body > </ html > |
На этой странице мы связываем наш CSS с ресурсами (с полным именем, включая контекст приложения). Также есть ссылка на страницу сведений о сотруднике (view.jsp), которая разрешается из идентификатора сотрудника.
Самая интересная часть — это использование sf taglib. Чтобы оставаться дружественным к Web 1.0, мы, к сожалению, не можем напрямую использовать DELETE. До HTML4 и XHTML1 HTML-формы могут использовать только GET и POST. Обходной путь должен использовать скрытое поле, которое отмечает, если POST фактически должен использоваться как УДАЛИТЬ. Это именно то, что Spring делает для нас бесплатно — просто используя префикс sf: form . Таким образом, мы туннелируем DELETE через HTTP POST, но он будет отправлен правильно.
Чтобы это работало, мы должны добавить в наш web.xml специальный фильтр Spring для этого:
01
02
03
04
05
06
07
08
09
10
11
|
< filter > < filter-name >httpMethodFilter</ filter-name > < filter-class > org.springframework.web.filter.HiddenHttpMethodFilter </ filter-class > </ filter > < filter-mapping > < filter-name >httpMethodFilter</ filter-name > < url-pattern >/*</ url-pattern > </ filter-mapping > |
Несмотря на то, что JSP — это технология, специфичная для Java, которая на самом деле компилируется в сервлет, мы можем использовать ее почти так же, как любую HTML-страницу. Мы добавили немного CSS и теперь добавляем самую популярную библиотеку javascript — jQuery. Перейдите на https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.js и загрузите файл jquery.js и поместите его в папку ресурсов. Мы позволим пользователям обновлять ресурс с помощью POST, поэтому мы будем использовать jQuery для некоторых манипуляций с DOM — просто ради фантазии. Вы можете использовать практически все, что можете на обычных HTML-страницах.
Давайте теперь создадим /employees/view.jsp — это своего рода страница подробностей для сотрудника.
01
02
03
04
05
06
07
08
09
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
|
<%@ page contentType='text/html;charset=UTF-8' language='java' %> <%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%> < html > < head > < title >Employee page</ title > < link rel = 'stylesheet' href = '/timesheet-app/resources/style.css' type = 'text/css' > </ head > < body > < h2 >Employee info</ h2 > < div id = 'list' > < sf:form method = 'post' > < ul > < li > < label for = 'name' >Name:</ label > < input name = 'name' id = 'name' value = '${employee.name}' disabled = 'true' /> </ li > < li > < label for = 'department' >Department:</ label > < input name = 'department' id = 'department' value = '${employee.department}' disabled = 'true' /> </ li > < li > < input type = 'button' value = 'Unlock' id = 'unlock' /> < input type = 'submit' value = 'Save' id = 'save' class = 'hidden' /> </ li > </ ul > </ sf:form > </ div > < br />< br /> < a href = '../employees' >Go Back</ a > < script src = '/timesheet-app/resources/jquery-1.7.1.js' ></ script > < script > (function() { $('#unlock').on('click', function() { $('#unlock').addClass('hidden'); // enable stuff $('#name').removeAttr('disabled'); $('#department').removeAttr('disabled'); $('#save').removeClass('hidden'); }); })(); </ script > </ body > </ html > |
Внутри страницы мы обращаемся к файлу jQuery, и у нас есть самовывозная анонимная функция — после нажатия кнопки с идентификатором «unlock» мы скрываем ее, выдвигаем кнопку отправки и разблокируем поля, чтобы сотрудник мог обновляться. После нажатия кнопки Сохранить мы перенаправлены обратно в список сотрудников, и этот список был обновлен.
Последняя функциональность, которую мы собираемся выполнить для CRUD в Employee, — это добавление. Мы справимся с этим, получив доступ к сотрудникам с GET и дополнительным параметром, который мы назовем новым . Поэтому URL для добавления сотрудника будет: http: // localhost: 8080 / timesheet-app / employee? New
Давайте изменим наш контроллер для этого:
1
2
3
4
5
|
@RequestMapping (params = 'new' , method = RequestMethod.GET) public String createEmployeeForm(Model model) { model.addAttribute( 'employee' , new Employee()); return 'employees/new' ; } |
Это будет служить новой странице JSP — / WEB-INF / jsp / employee / new.jsp :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %> <%@ page contentType='text/html;charset=UTF-8' language='java' %> < html > < head > < title >Add new employee</ title > < link rel = 'stylesheet' href = '/timesheet-app/resources/style.css' type = 'text/css' > </ head > < body > < h2 >Add new Employee</ h2 > < div id = 'list' > < sf:form method = 'post' action = 'employees' > < ul > < li > < label for = 'name' >Name:</ label > < input name = 'name' id = 'name' value = '${employee.name}' /> </ li > < li > < label for = 'department' >Department:</ label > < input name = 'department' id = 'department' value = '${employee.department}' /> </ li > < li > < input type = 'submit' value = 'Save' id = 'save' /> </ li > </ ul > </ sf:form > </ div > < br />< br /> < a href = 'employees' >Go Back</ a > </ body > </ html > |
Страница очень похожа на view.jsp. В реальных приложениях мы использовали бы что-то вроде Apache Tiles для сокращения избыточного кода, но теперь давайте не будем об этом беспокоиться.
Обратите внимание, что мы отправляем форму с действием «сотрудники». Возвращаясь к нашему контроллеру, давайте обработаем сотрудников методом POST http:
1
2
3
4
5
6
|
@RequestMapping (method = RequestMethod.POST) public String addEmployee(Employee employee) { employeeDao.add(employee); return 'redirect:/employees' ; } |
И давайте не будем забывать об ошибке JSP-страницы, когда мы не можем удалить сотрудника, jsp / employee / delete-error.jsp :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
< html > < head > < title >Cannot delete employee</ title > </ head > < body > Oops! Resource < a href = '${employee.id}' >${employee.name}</ a > can not be deleted. < p > Make sure employee doesn't have assigned any task or active timesheet. </ p > < br />< br />< br /> < a href = '../welcome' >Back to main page.</ a > </ body > </ html > |
Вот и все, у нас есть целая функциональность CRUD для наших сотрудников. Давайте вспомним основные шаги, которые мы только что сделали:
- Добавлен класс EmployeeController
- Создать папку ресурсов в корне сети для статического содержимого
- Добавлено отображение для сервлета по умолчанию в web.xml
- Добавлен styles.css в папку ресурсов
- Настроил POST-DELETE туннелирование с фильтром в web.xml
- Скачал jQuery.js и добавил в нашу папку ресурсов
- Добавлена страница employeeess / list.jsp
- Добавлена страница empess / view.jsp
- Добавлена страница employeeess / new.jsp
- Добавлена страница сотрудников / delete-error.jsp
Теперь вот полный код для контроллера:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
|
package org.timesheet.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import org.timesheet.domain.Employee; import org.timesheet.service.dao.EmployeeDao; import org.timesheet.web.exceptions.EmployeeDeleteException; import java.util.List; /** * Controller for handling Employees. */ @Controller @RequestMapping ( '/employees' ) public class EmployeeController { private EmployeeDao employeeDao; @Autowired public void setEmployeeDao(EmployeeDao employeeDao) { this .employeeDao = employeeDao; } public EmployeeDao getEmployeeDao() { return employeeDao; } /** * Retrieves employees, puts them in the model and returns corresponding view * @param model Model to put employees to * @return employees/list */ @RequestMapping (method = RequestMethod.GET) public String showEmployees(Model model) { List<Employee> employees = employeeDao.list(); model.addAttribute( 'employees' , employees); return 'employees/list' ; } /** * Deletes employee with specified ID * @param id Employee's ID * @return redirects to employees if everything was ok * @throws EmployeeDeleteException When employee cannot be deleted */ @RequestMapping (value = '/{id}' , method = RequestMethod.DELETE) public String deleteEmployee( @PathVariable ( 'id' ) long id) throws EmployeeDeleteException { Employee toDelete = employeeDao.find(id); boolean wasDeleted = employeeDao.removeEmployee(toDelete); if (!wasDeleted) { throw new EmployeeDeleteException(toDelete); } // everything OK, see remaining employees return 'redirect:/employees' ; } /** * Handles EmployeeDeleteException * @param e Thrown exception with employee that couldn't be deleted * @return binds employee to model and returns employees/delete-error */ @ExceptionHandler (EmployeeDeleteException. class ) public ModelAndView handleDeleteException(EmployeeDeleteException e) { ModelMap model = new ModelMap(); model.put( 'employee' , e.getEmployee()); return new ModelAndView( 'employees/delete-error' , model); } /** * Returns employee with specified ID * @param id Employee's ID * @param model Model to put employee to * @return employees/view */ @RequestMapping (value = '/{id}' , method = RequestMethod.GET) public String getEmployee( @PathVariable ( 'id' ) long id, Model model) { Employee employee = employeeDao.find(id); model.addAttribute( 'employee' , employee); return 'employees/view' ; } /** * Updates employee with specified ID * @param id Employee's ID * @param employee Employee to update (bounded from HTML form) * @return redirects to employees */ @RequestMapping (value = '/{id}' , method = RequestMethod.POST) public String updateEmployee( @PathVariable ( 'id' ) long id, Employee employee) { employee.setId(id); employeeDao.update(employee); return 'redirect:/employees' ; } /** * Creates form for new employee * @param model Model to bind to HTML form * @return employees/new */ @RequestMapping (params = 'new' , method = RequestMethod.GET) public String createEmployeeForm(Model model) { model.addAttribute( 'employee' , new Employee()); return 'employees/new' ; } /** * Saves new employee to the database * @param employee Employee to save * @return redirects to employees */ @RequestMapping (method = RequestMethod.POST) public String addEmployee(Employee employee) { employeeDao.add(employee); return 'redirect:/employees' ; } } |
Если вы используете SpringSource Tool Suite, вы можете проверить сопоставления непосредственно в IDE. Добавьте в свой проект «Spring Project Nature», в Свойствах-> Spring-> Bean Support настройте конфигурационный файл Spring. Затем щелкните правой кнопкой мыши проект и нажмите Spring Tools-> Show Request Mappings, и вы должны увидеть что-то вроде этого:
Последнее, что осталось от сотрудников, — это написать тест JUnit. Поскольку у нас есть наш файл timesheet-servlet.xml в WEB-INF, мы не можем получить доступ к его bean-компонентам в тесте JUnit. Что мы будем делать, это удалить следующую строку из timesheet-servlet.xml :
1
|
< context:component-scan base-package = 'org.timesheet.web' /> |
Теперь мы создадим новую конфигурацию bean-компонента Spring в src / main / resources и назовем его controllers.xml . Единственное, о чем мы заботимся, — это поставить здесь автосканирование для контроллеров, поэтому контент довольно прост:
1
2
3
4
5
6
7
8
9
|
<? xml version = '1.0' encoding = 'UTF-8' ?> xsi:schemaLocation = 'http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd' > < context:component-scan base-package = 'org.timesheet.web' /> </ beans > |
Чтобы сделать контекст осведомленным об этих пружинных компонентах, измените context-param в web.xml следующим образом:
1
2
3
4
5
6
7
|
< context-param > < param-name >contextConfigLocation</ param-name > < param-value > classpath:persistence-beans.xml classpath:controllers.xml </ param-value > </ context-param > |
Кроме того, теперь мы должны импортировать bean-компоненты из controllers.xml в timesheet-servlet.xml, поэтому вместо удаленной строки <context: component-scan… из timesheet-servlet.xml мы добавим следующее:
1
|
< import resource = 'classpath:controllers.xml' /> |
Это позволит нам автоматически подключать контроллеры к тестам. Итак, в папке с исходным кодом теста создайте пакет org.timesheet.web и поместите там EmployeeControllerTest . Это довольно просто, и мы тестируем только контроллер как POJO и как это влияет на уровень персистентности (проверка через DAO). Однако мы сделали одно исключение. В методе testDeleteEmployeeThrowsException мы явно сообщим DAO возвращать false при попытке удалить сотрудника. Это избавит нас от сложного создания объектов и внедрения дополнительных DAO. Для этого мы будем использовать популярный фреймворк Mockito .
Добавьте зависимость к вашему pom.xml :
1
2
3
4
5
|
< dependency > < groupId >org.mockito</ groupId > < artifactId >mockito-all</ artifactId > < version >1.9.0</ version > </ dependency > |
Тест для EmployeeController:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
|
package org.timesheet.web; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.web.servlet.ModelAndView; import org.timesheet.DomainAwareBase; import org.timesheet.domain.Employee; import org.timesheet.service.dao.EmployeeDao; import org.timesheet.web.exceptions.EmployeeDeleteException; import java.util.Collection; import java.util.List; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ContextConfiguration (locations = { '/persistence-beans.xml' , '/controllers.xml' }) public class EmployeeControllerTest extends DomainAwareBase { @Autowired private EmployeeDao employeeDao; @Autowired private EmployeeController controller; private Model model; // used for controller @Before public void setUp() { model = new ExtendedModelMap(); } @After public void cleanUp() { List<Employee> employees = employeeDao.list(); for (Employee employee : employees) { employeeDao.remove(employee); } } @Test public void testShowEmployees() { // prepare some data Employee employee = new Employee( 'Lucky' , 'Strike' ); employeeDao.add(employee); // use controller String view = controller.showEmployees(model); assertEquals( 'employees/list' , view); List<Employee> listFromDao = employeeDao.list(); Collection<?> listFromModel = (Collection<?>) model.asMap().get( 'employees' ); assertTrue(listFromModel.contains(employee)); assertTrue(listFromDao.containsAll(listFromModel)); } @Test public void testDeleteEmployeeOk() throws EmployeeDeleteException { // prepare ID to delete Employee john = new Employee( 'John Lennon' , 'Singing' ); employeeDao.add(john); long id = john.getId(); // delete & assert String view = controller.deleteEmployee(id); assertEquals( 'redirect:/employees' , view); assertNull(employeeDao.find(id)); } @Test (expected = EmployeeDeleteException. class ) public void testDeleteEmployeeThrowsException() throws EmployeeDeleteException { // prepare ID to delete Employee john = new Employee( 'John Lennon' , 'Singing' ); employeeDao.add(john); long id = john.getId(); // mock DAO for this call EmployeeDao mockedDao = mock(EmployeeDao. class ); when(mockedDao.removeEmployee(john)).thenReturn( false ); EmployeeDao originalDao = controller.getEmployeeDao(); try { // delete & expect exception controller.setEmployeeDao(mockedDao); controller.deleteEmployee(id); } finally { controller.setEmployeeDao(originalDao); } } @Test public void testHandleDeleteException() { Employee john = new Employee( 'John Lennon' , 'Singing' ); EmployeeDeleteException e = new EmployeeDeleteException(john); ModelAndView modelAndView = controller.handleDeleteException(e); assertEquals( 'employees/delete-error' , modelAndView.getViewName()); assertTrue(modelAndView.getModelMap().containsValue(john)); } @Test public void testGetEmployee() { // prepare employee Employee george = new Employee( 'George Harrison' , 'Singing' ); employeeDao.add(george); long id = george.getId(); // get & assert String view = controller.getEmployee(id, model); assertEquals( 'employees/view' , view); assertEquals(george, model.asMap().get( 'employee' )); } @Test public void testUpdateEmployee() { // prepare employee Employee ringo = new Employee( 'Ringo Starr' , 'Singing' ); employeeDao.add(ringo); long id = ringo.getId(); // user alters Employee in HTML form ringo.setDepartment( 'Drums' ); // update & assert String view = controller.updateEmployee(id, ringo); assertEquals( 'redirect:/employees' , view); assertEquals( 'Drums' , employeeDao.find(id).getDepartment()); } @Test public void testAddEmployee() { // prepare employee Employee paul = new Employee( 'Paul McCartney' , 'Singing' ); // save but via controller String view = controller.addEmployee(paul); assertEquals( 'redirect:/employees' , view); // employee is stored in DB assertEquals(paul, employeeDao.find(paul.getId())); } } |
Обратите внимание, как мы используем mocked dao для установки его в блок try / finally. Это только для одного вызова, чтобы убедиться, что выдается правильное исключение. Если вы никогда не видели насмешек, я определенно советую больше узнать об этой технике. Есть много насмешливых рамок. Тот, который мы выбрали, — Mockito — имеет действительно аккуратный синтаксис, который активно использует статический импорт Java.
Теперь менеджеры очень похожи на сотрудников, поэтому без особых проблем добавим довольно похожие вещи для менеджеров:
Сначала создайте папку manager в WEB-INF / jsp.
Теперь давайте напишем контроллер и введем соответствующий DAO:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@Controller @RequestMapping ( '/managers' ) public class ManagerController { private ManagerDao managerDao; @Autowired public void setManagerDao(ManagerDao managerDao) { this .managerDao = managerDao; } public ManagerDao getManagerDao() { return managerDao; } } |
Добавьте метод для менеджеров списков:
01
02
03
04
05
06
07
08
09
10
11
12
|
/** * Retrieves managers, puts them in the model and returns corresponding view * @param model Model to put employees to * @return managers/list */ @RequestMapping (method = RequestMethod.GET) public String showManagers(Model model) { List<Manager> employees = managerDao.list(); model.addAttribute( 'managers' , employees); return 'managers/list' ; } |
Добавьте list.jsp в jsp / manager :
01
02
03
04
05
06
07
08
09
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
|
<%@ page contentType='text/html;charset=UTF-8' language='java' %> <%@ taglib prefix='fmt' uri='http://java.sun.com/jsp/jstl/fmt' %> <%@ taglib prefix='spring' uri='http://www.springframework.org/tags' %> <%@ taglib prefix='c' uri='http://java.sun.com/jsp/jstl/core'%> <%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%> < html > < head > < title >Managers</ title > < link rel = 'stylesheet' href = '/timesheet-app/resources/style.css' type = 'text/css' > </ head > < body > < h1 >List of managers</ h1 > < a href = 'managers?new' >Add new manager</ a > < table cellspacing = '5' class = 'main-table' > < tr > < th >Name</ th > < th >Details</ th > < th >Delete</ th > </ tr > < c:forEach items = '#{managers}' var = 'man' > < tr > < td >${man.name}</ td > < td > < a href = 'managers/${man.id}' >Go to page</ a > </ td > < td > < sf:form action = 'managers/${man.id}' method = 'delete' cssClass = 'delete' > < input type = 'submit' value = '' class = 'delete-button' /> </ sf:form > </ td > </ tr > </ c:forEach > </ table > < br /> < a href = 'welcome' >Go back</ a > </ body > </ html > |
Добавить метод для удаления менеджеров:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
/** * Deletes manager with specified ID * @param id Manager's ID * @return redirects to managers if everything was OK * @throws ManagerDeleteException When manager cannot be deleted */ @RequestMapping (value = '/{id}' , method = RequestMethod.DELETE) public String deleteManager( @PathVariable ( 'id' ) long id) throws ManagerDeleteException { Manager toDelete = managerDao.find(id); boolean wasDeleted = managerDao.removeManager(toDelete); if (!wasDeleted) { throw new ManagerDeleteException(toDelete); } // everything OK, see remaining managers return 'redirect:/managers' ; } |
Исключение при удалении не удается:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
package org.timesheet.web.exceptions; import org.timesheet.domain.Manager; /** * When manager cannot be deleted */ public class ManagerDeleteException extends Exception { private Manager manager; public ManagerDeleteException(Manager manager) { this .manager = manager; } public Manager getManager() { return manager; } } |
Метод для обработки этого исключения:
01
02
03
04
05
06
07
08
09
10
11
|
/** * Handles ManagerDeleteException * @param e Thrown exception with manager that couldn't be deleted * @return binds manager to model and returns managers/delete-error */ @ExceptionHandler (ManagerDeleteException. class ) public ModelAndView handleDeleteException(ManagerDeleteException e) { ModelMap model = new ModelMap(); model.put( 'manager' , e.getManager()); return new ModelAndView( 'managers/delete-error' , model); } |
Добавьте метод для получения страницы менеджера:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
/** * Returns manager with specified ID * @param id Managers's ID * @param model Model to put manager to * @return managers/view */ @RequestMapping (value = '/{id}' , method = RequestMethod.GET) public String getManager( @PathVariable ( 'id' ) long id, Model model) { Manager manager = managerDao.find(id); model.addAttribute( 'manager' , manager); return 'managers/view' ; } |
Добавьте страницу менеджера view.jsp в jsp / Manager :
01
02
03
04
05
06
07
08
09
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
|
<%@ page contentType='text/html;charset=UTF-8' language='java' %> <%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form'%> < html > < head > < title >Manager page</ title > < link rel = 'stylesheet' href = '/timesheet-app/resources/style.css' type = 'text/css' > </ head > < body > < h2 >Manager info</ h2 > < div id = 'list' > < sf:form method = 'post' > < ul > < li > < label for = 'name' >Name:</ label > < input name = 'name' id = 'name' value = '${manager.name}' disabled = 'true' /> </ li > < li > < input type = 'button' value = 'Unlock' id = 'unlock' /> < input type = 'submit' value = 'Save' id = 'save' class = 'hidden' /> </ li > </ ul > </ sf:form > </ div > < br />< br /> < a href = '../managers' >Go Back</ a > < script src = '/timesheet-app/resources/jquery-1.7.1.js' ></ script > < script > (function() { $('#unlock').on('click', function() { $('#unlock').addClass('hidden'); // enable stuff $('#name').removeAttr('disabled'); $('#save').removeClass('hidden'); }); })(); </ script > </ body > </ html > |
Страница JSP для обработки ошибки при удалении:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
< html > < head > < title >Cannot delete manager</ title > </ head > < body > Oops! Resource < a href = '${manager.id}' >${manager.name}</ a > can not be deleted. < p > Make sure manager doesn't have assigned any task or active timesheet. </ p > < br />< br />< br /> < a href = '../welcome' >Back to main page.</ a > </ body > </ html > |
Добавить метод для обновления менеджера:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
/** * Updates manager with specified ID * @param id Manager's ID * @param manager Manager to update (bounded from HTML form) * @return redirects to managers */ @RequestMapping (value = '/{id}' , method = RequestMethod.POST) public String updateManager( @PathVariable ( 'id' ) long id, Manager manager) { manager.setId(id); managerDao.update(manager); return 'redirect:/managers' ; } |
Добавьте метод для возврата формы нового менеджера:
01
02
03
04
05
06
07
08
09
10
|
/** * Creates form for new manager * @param model Model to bind to HTML form * @return manager/new */ @RequestMapping (params = 'new' , method = RequestMethod.GET) public String createManagerForm(Model model) { model.addAttribute( 'manager' , new Manager()); return 'managers/new' ; } |
Добавьте страницу для нового менеджера new.jsp в jsp / Manager :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<%@ taglib prefix='sf' uri='http://www.springframework.org/tags/form' %> <%@ page contentType='text/html;charset=UTF-8' language='java' %> < html > < head > < title >Add new manager</ title > < link rel = 'stylesheet' href = '/timesheet-app/resources/style.css' type = 'text/css' > </ head > < body > < h2 >Add new Manager</ h2 > < div id = 'list' > < sf:form method = 'post' action = 'managers' > < ul > < li > < label for = 'name' >Name:</ label > < input name = 'name' id = 'name' value = '${manager.name}' /> </ li > < li > < input type = 'submit' value = 'Save' id = 'save' /> </ li > </ ul > </ sf:form > </ div > < br />< br /> < a href = 'managers' >Go Back</ a > </ body > </ html > |
И, наконец, добавьте метод добавления менеджера:
01
02
03
04
05
06
07
08
09
10
11
|
/** * Saves new manager to the database * @param manager Manager to save * @return redirects to managers */ @RequestMapping (method = RequestMethod.POST) public String addManager(Manager manager) { managerDao.add(manager); return 'redirect:/managers' ; } |
Хорошо, последний кусок кода для этой части является тестовым примером для ManagerController:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
|
package org.timesheet.web; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.web.servlet.ModelAndView; import org.timesheet.DomainAwareBase; import org.timesheet.domain.Manager; import org.timesheet.service.dao.ManagerDao; import org.timesheet.web.exceptions.ManagerDeleteException; import java.util.Collection; import java.util.List; import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ContextConfiguration (locations = { '/persistence-beans.xml' , '/controllers.xml' }) public class ManagerControllerTest extends DomainAwareBase { @Autowired private ManagerDao managerDao; @Autowired private ManagerController controller; private Model model; // used for controller @Before public void setUp() { model = new ExtendedModelMap(); } @After public void cleanUp() { List<Manager> managers = managerDao.list(); for (Manager manager : managers) { managerDao.remove(manager); } } @Test public void testShowManagers() { // prepare some data Manager manager = new Manager( 'Bob Dylan' ); managerDao.add(manager); // use controller String view = controller.showManagers(model); assertEquals( 'managers/list' , view); List<Manager> listFromDao = managerDao.list(); Collection<?> listFromModel = (Collection<?>) model.asMap().get( 'managers' ); assertTrue(listFromModel.contains(manager)); assertTrue(listFromDao.containsAll(listFromModel)); } @Test public void testDeleteManagerOk() throws ManagerDeleteException { // prepare ID to delete Manager john = new Manager( 'John Lennon' ); managerDao.add(john); long id = john.getId(); // delete & assert String view = controller.deleteManager(id); assertEquals( 'redirect:/managers' , view); assertNull(managerDao.find(id)); } @Test (expected = ManagerDeleteException. class ) public void testDeleteManagerThrowsException() throws ManagerDeleteException { // prepare ID to delete Manager john = new Manager( 'John Lennon' ); managerDao.add(john); long id = john.getId(); // mock DAO for this call ManagerDao mockedDao = mock(ManagerDao. class ); when(mockedDao.removeManager(john)).thenReturn( false ); ManagerDao originalDao = controller.getManagerDao(); try { // delete & expect exception controller.setManagerDao(mockedDao); controller.deleteManager(id); } finally { controller.setManagerDao(originalDao); } } @Test public void testHandleDeleteException() { Manager john = new Manager( 'John Lennon' ); ManagerDeleteException e = new ManagerDeleteException(john); ModelAndView modelAndView = controller.handleDeleteException(e); assertEquals( 'managers/delete-error' , modelAndView.getViewName()); assertTrue(modelAndView.getModelMap().containsValue(john)); } @Test public void testGetManager() { // prepare manager Manager george = new Manager( 'George Harrison' ); managerDao.add(george); long id = george.getId(); // get & assert String view = controller.getManager(id, model); assertEquals( 'managers/view' , view); assertEquals(george, model.asMap().get( 'manager' )); } @Test public void testUpdateManager() { // prepare manager Manager ringo = new Manager( 'Ringo Starr' ); managerDao.add(ringo); long id = ringo.getId(); // user alters manager in HTML form ringo.setName( 'Rango Starr' ); // update & assert String view = controller.updateManager(id, ringo); assertEquals( 'redirect:/managers' , view); assertEquals( 'Rango Starr' , managerDao.find(id).getName()); } @Test public void testAddManager() { // prepare manager Manager paul = new Manager( 'Paul McCartney' ); // save but via controller String view = controller.addManager(paul); assertEquals( 'redirect:/managers' , view); // manager is stored in DB assertEquals(paul, managerDao.find(paul.getId())); } } |
Запрос отображения теперь выглядит так:
Итак, в этой части мы узнали, что такое Spring MVC, как использовать наши сущности в качестве моделей, как писать контроллеры в стиле POJO, как выглядит дизайн RESTful, как создавать представления с JSP и как настроить приложение для использования CSS и JavaScript. ,
Мы написали контроллеры для сотрудников и менеджеров. В следующей части мы продолжим писать контроллеры для задач и расписаний. Перед тем, как перейти к следующей части, убедитесь, что все работает хорошо.
Вот папка src (раскрывается только новый материал. Не беспокойтесь о файлах .iml, они для IntelliJ):
Вот веб-папка:
Ссылка: Часть 4 — Добавление Spring MVC — часть 1 от нашего партнера JCG Михала Вртиака в блоге vrtoonjava .