-
Notifications
You must be signed in to change notification settings - Fork 35
Lesson 2
Онлайн проект Topjava
Материалы занятия (скачать все патчи можно через Download папки patch)
- Перед сборкой проекта (или запуском Tomcat) откройте вкладку Maven Projects и сделайте
clean - Если страничка в браузере работает неверно, очистите кэш (
Ctrl+F5в хроме)
- Изменения в
MealsUtil:
- Сделал константу
List<Meal> MEALS- Сделал вспомогательный метод
getWithExceeded. Для фильтрации передаю реализациюPredicate(см. паттерн Стратегия)- Форматирование даты сделал на основе JSTL LocalDateTime format
- Переименовал
TimeUtilвDateTimeUtil- Переименовал
mealList.jspвmeals.jsp- Добавил еще один способ вывести
dateTimeчерез стандартную JSTL функциюreplace(префиксfnв шапке также надо поменять)
Про использование паттерна Repository будет подробно рассказано в видео "Слои приложения"
- Поправил
InMemoryMealRepositoryImpl.save(). Если обновляется еда, которой нет в хранилище (c несуществующим id), вставка не происходит.- В
MealServlet.doGet()сделал выбор черезswitch- В местах, где требуется
int, заменилInteger.valueOf()наInteger.parseInt()- В
meal.jspиспользуется параметр запросаparam.action, который не кладется в атрибуты.- Переименовал
mealEdit.jspвmealForm.jsp. Поля ввода формы добавилrequired- Пофиксил багу c
history.back()вmealForm.jspдля FireFox (коммит формы при Cancel, сделалtype="button").
Дополнительно:
Зачем в
InMemoryMealRepositoryImplнаполнять map с помощью нестатического блока инициализации, а не в конструкторе?
Разницы нет. Сделал чтобы напомнить вам про эту конструкцию. Малоизвестные особенности Java
Почему
InMemoryMealRepositoryImplне singleton?
Начиная с Servlet API 2.3 пул сервлетов не создается, создается только один инстанс сервлетов. Те. InMemoryMealRepositoryImpl в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте).
Objects.requireNonNullвMealServlet.getId(request)если у нас нетidв запросе бросает NPE (NullPointerException). Но оно вылетит и без этого метода. Зачем он нужен и почему мы его не обрабатываем?
Objects.requireNonNull - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. What is the purpose of Objects#requireNonNull. Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling).
-
Apache Commons, Guava
- Guava используется на проекте Многомодульный maven. Многопоточность. XML (JAXB/StAX). Веб сервисы (JAX-RS/SOAP). Удаленное взаимодействие (JMS/AKKA)
- Переименовал
ExceptionUtilвValidationUtil- Поменял
LoggedUserнаSecurityUtil. Это класс, из которого приложение будет получать данные авторизированного пользователя (пока авторизации нет, он реализован как заглушка). Находится в пакетеweb, т.к. авторизация происходит на слое контроллеров и остальные слои приложения про нее знать не должны.- Добавил проверку id пользователя, пришедшего в контроллер (treat IDs in REST body, "If it is a public API you should be conservative when you reply, but accept liberally")
- Паттерн "Слои приложения"
- Data Access Object
- Паттерн DTO
- Value Object и Data Transfer Object
- Should services always return DTOs, or can they also return domain models?
- Дополнительно:
Какова цель деления приложения на слои?
Управляемость проекта (особенно большого) повышается на порядок:
- Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают.
- Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно)
DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать?
(D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities.
В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например MealsWithExceeded мы отдаем наружу и он является Transfer Object, его надо перенести в пакет to.
На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля.
Хороший ответ когда TO обязательны есть на stackoverflow: When to Use.
Почему контроллеры положили в папку web, а не в conrollers?
То же самое что domain/model - просто разные названия.
Зачем мы наследуем
NotFoundExceptionотRuntimeException?
Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions
Зачем в API пишем
NotFoundException, если ониRuntimeException?
Обычно не пишут. Я написал для информации разработчикам - здесь делаем проверку и может быть брошено.
Зачем в
AdminRestControllerпереопределяются методы родителя с вызовом тех же родительских?
Сделано на будущее, мы будем работать с AdminRestController.
И что такое
ProfileRestController?
Контроллер, где авторизованный пользователь будет работать со своими данными
Что лучше возвращать из API:
CollectionилиList
Вообще, как правило, возвращают List, если не просится по коду более общий случай (например возможный Set или Collection, возвращаемый Map.values()). Если возвращается отсортированный список, то List будет адекватнее.
- Закомментировал ненужный
context:annotation-config: сканирование аннотаций подключаются приcontext:component-scan
- Difference between @Component, @Repository & @Service annotations in Spring
- Spring Auto Scanning Components
- Использование аннотации @Autowired
- Дополнительное:
- Inject 2 beans of same type
- Перевод "Field Dependency Injection Considered Harmful"
- Tutorial: testing with AssertJ
- Field vs Constructor vs Setter DI
- Implicit constructor injection for single-constructor scenarios
В контроллерах Constructor Injection делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет.
<context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте постпроцессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.
Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна
XML схема нужна для валидации xml, IDEA делает по ней автозаполнение.
Что означает для Spring
<bean class="ru.javawebinar.topjava.service.UserServiceImpl">
<property name="repository" ref="mockUserRepository"/>
</bean> ?
Можно сказать так: создай и занеси в свой контекст экземпляр класса (бин) UserServiceImpl и заинжекть в его проперти из своего контекста бин mockUserRepository.
Как биндинг происходит для
@Autowired? Как поступать, если у нас больше одной реализацииUserRepository?
@Autowired инжектит по типу (т.е. ижектит класс который реализует UserRepository). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - No unique bean.
В этом случае можно уточнить имя бина через @Qualifier. @Qualifier обычно добавляют только в случае нескольких реализаций.
Почему нельзя сервлет помещать в Spring контекст?
Сервлеты- это исключительно классы servlet-api (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без init/destroy), но идеологически - неверно. Также НЕ надо работать с cервлетом из SpringMain.
- Еще раз смотрим на демо приложение и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий "чудесным образом" попадают в
SecurityUtil.authUserId()/authUserCaloriesPerDay()и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки) - Перед началом выполнения ДЗ (ели есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ.
- 1: переименовать
MockUserRepositoryImplвInMemoryUserRepositoryImplи имплементировать по аналогии сInMemoryMealRepositoryImpl(список пользователей возвращать отсортированным по имени) - 2: сделать
Meal extends AbstractBaseEntity,MealWithExceedперенести в пакетru.javawebinar.topjava.to(transfer objects) - 3: Изменить
MealRepositoryиInMemoryMealRepositoryImplтаким образом, чтобы вся еда всех пользователей находилась в одном общем хранилище, но при этом каждый конкретный авторизованный пользователь мог видеть и редактировать только свою еду.- 3.1: реализовать хранение еды для каждого пользователя можно с добавлением поля
userIdвMealИЛИ без него (как нравится). Напомню, что репозиторий один и приложение может работать одновременно с многими пользователями. - 3.2: если по запрошенному id еда отсутствует или чужая, возвращать
null/false(см. комментарии вUserRepository) - 3.3: список еды возвращать отсортированный в обратном порядке по датам
- 3.4: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать)
- 3.1: реализовать хранение еды для каждого пользователя можно с добавлением поля
- 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. демо).
- Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка- я сделаю и напишу всем.
- 4.1: после авторизации (сделаем позднее), id авторизованного юзера можно получить из
SecurityUtil.authUserId(). Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды (не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду. - 4.2:
SecurityUtilможет использоваться только на слое web (см. реализациюProfileRestController).MealServiceможно тестировать без подмены логики авторизации, принимаем в методах сервиса и репозитория параметрuserId: id владельца еды. - 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в
MealServiceImplбросатьNotFoundException. - 4.4: конвертацию в
MealWithExceededможно делать как в слое web, так и в service (Mapping Entity->DTO: Controller or Service?) - 4.5: в
MealServiceImplпостараться сделать в каждом методе только одни запрос кMealRepository - 4.6 еще раз: не надо в названиях методов повторять названия класса (
Meal).
- 5: включить классы еды в контекст Spring (добавить аннотации) и вызвать из
SpringMainлюбой методMealRestController(проверить что Spring все корректно заинжектил)
- 6: в
MealServletсделать инициализацию Spring, достатьMealRestControllerиз контекста и работать с едой через него (как вSpringMain).pom.xmlНЕ менять, работаем соspring-context. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий.- 6.1: учесть, что когда будем работать через Spring MVC,
MealServletудалим, те вся логика должна быть в контроллере
- 6.1: учесть, что когда будем работать через Spring MVC,
- 7: добавить в
meals.jspиMealServletдве отдельные фильтрации еды: по дате и по времени (см. демо) - 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в
index.htmlиSecurityUtil.setAuthUserId(userId)вUserServlet). Настоящая атворизация будет через Spring Security позже.
- 1: В реализации
InMemoryUserRepositoryImpl- 1.1:
getByEmailпопробуйте сделать черезstream - 1.2:
deleteпопробуйте сделать за одно обращение к map (безcontainsKey) - 1.3: предусмотрите случай одинаковых
User.name(порядок должен быть зафиксированным).
- 1.1:
- 2: В реализации
InMemoryMealRepositoryImpl- 2.1: В
Meal, которая приходит в контроллер нет никакой информации о пользователе (еда приходит в контроллер БЕЗuser/userId). Она может быть только авторизованного пользователя, поэтому что-то сравнивать с ним никакого смысла нет. Поidеды и авторизованному пользователю нужно проверить ее принадлежность. - 2.2:
get\update\delete- следите, чтобы не было NPE (NullPointExceptionможет быть, если в хранилище отсутствует юзер или еда). - 2.3: Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит).
- 2.4: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы первый и последний день не были обрезаны, иначе сумма калорий будет неверная.
- 2.5: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска
NullPoinException. - 2.6: Не дублируйте код в
getAllи метод с фильтрацией - 2.7: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных
- 2.1: В
- 3: Проверьте, что удалили
Meal.idи связанные с ним методы (он уже есть в базовомBaseEntity) - 4: Проверку
isBetweenDateсделать вDateTimeUtil. Попробуйте использовать дженерики и объединить ее сisBetweenTime(см. Generics Tutorials) - 5: Реализация 'MealRestController' должен уметь обрабатывать запросы:
- 5.1: Отдать свою еду (для отображения в таблице, формат
List<MealWithExceed>), запрос БЕЗ параметров - 5.2: Отдать свою еду, отфильтрованную по startDate, startTime, endDate, endTime
- 5.3: Отдать/удалить свою еду по id, параметр запроса - id еды. Если еда с этим id чужая или отсутствует -
NotFoundException - 5.4: Сохранить/обновить еду, параметр запроса - Meal. Если обновляемая еда с этим id чужая или отсутствует -
NotFoundException - 5.5: Сервлет мы удалим, а контроллер останется, поэтому возвращать
List<MealWithExceed>надо из контроллера. И userId принимать в контроллере НЕЛЬЗЯ (иначе - для чего аторизация?). ПодменуMIX/MAXдляDate/Timeтакже сделайте здесь. - 5.6: В REST при update принято передавать id (см.
AdminRestController.update) - 5.7: Сделайте отдельный
getAllбез применения фильтра
- 5.1: Отдать свою еду (для отображения в таблице, формат
- 6: Проверьте корректную обработку пустых значений date и time в контроллере
- 7:
idавторизированного пользователя получаем так:SecurityUtil.authUserId(), cм.ProfileRestController - 8: В
MealServlet- 8.1: Закрывать springContext в сервлете грамотнее всего в
HttpServlet.destroy(): если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно. - 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров.
- 8.1: Закрывать springContext в сервлете грамотнее всего в
Разбор домашнего задания HW1:
1.
Вопросы по HW1
Подсказки по HW02 (для проверки, сначала сделайте самостоятельно!)