Skip to content
Vladimir edited this page Feb 9, 2019 · 1 revision

Онлайн проект Topjava

Материалы занятия (скачать все патчи можно через Download папки patch)

hw Разбор домашнего задания HW1:

  • Перед сборкой проекта (или запуском Tomcat) откройте вкладку Maven Projects и сделайте clean
  • Если страничка в браузере работает неверно, очистите кэш (Ctrl+F5 в хроме)

Apply 2_1_HW1.patch

  • Изменения в MealsUtil:
    • Сделал константу List<Meal> MEALS
    • Сделал вспомогательный метод getWithExceeded. Для фильтрации передаю реализацию Predicate (см. паттерн Стратегия)
  • Форматирование даты сделал на основе JSTL LocalDateTime format
  • Переименовал TimeUtil в DateTimeUtil
  • Переименовал mealList.jsp в meals.jsp
  • Добавил еще один способ вывести dateTime через стандартную JSTL функцию replace (префикс fn в шапке также надо поменять)

Apply 2_2_HW1_optional.patch

Про использование паттерна 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").

Дополнительно:

question Вопросы по HW1

Зачем в 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).

Занятие 2:

Apply 2_3_app_layers.patch

  • Переименовал 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")

Слои приложения

question Ваши вопросы

Какова цель деления приложения на слои?

Управляемость проекта (особенно большого) повышается на порядок:

  • Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают.
  • Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно)

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 будет адекватнее.

Apply 2_4_add_spring_context.patch

Apply 2_5_add_dependency_injection.patch

Apply 2_6_add_annotation_processing.patch

  • Закомментировал ненужный context:annotation-config: сканирование аннотаций подключаются при context:component-scan

Apply 2_7_constructor_injection.patch

В контроллерах Constructor Injection делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет.

Дополнительно видео по Spring

<context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте постпроцессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.

question Ваши вопросы

Что такое схема в 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, сессия и куки)
  • Перед началом выполнения ДЗ (ели есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ.

hw Домашнее задание HW02

  • 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: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать)
  • 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 все корректно заинжектил)

Optional

  • 6: в MealServlet сделать инициализацию Spring, достать MealRestController из контекста и работать с едой через него (как в SpringMain). pom.xml НЕ менять, работаем со spring-context. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий.
    • 6.1: учесть, что когда будем работать через Spring MVC, MealServlet удалим, те вся логика должна быть в контроллере
  • 7: добавить в meals.jsp и MealServlet две отдельные фильтрации еды: по дате и по времени (см. демо)
  • 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в index.html и SecurityUtil.setAuthUserId(userId) в UserServlet). Настоящая атворизация будет через Spring Security позже.

error Подсказки по HW02 (для проверки, сначала сделайте самостоятельно!)

  • 1: В реализации InMemoryUserRepositoryImpl
    • 1.1: getByEmail попробуйте сделать через stream
    • 1.2: delete попробуйте сделать за одно обращение к map (без containsKey)
    • 1.3: предусмотрите случай одинаковых User.name (порядок должен быть зафиксированным).
  • 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: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных
  • 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 без применения фильтра
  • 6: Проверьте корректную обработку пустых значений date и time в контроллере
  • 7: id авторизированного пользователя получаем так: SecurityUtil.authUserId(), cм. ProfileRestController
  • 8: В MealServlet
    • 8.1: Закрывать springContext в сервлете грамотнее всего в HttpServlet.destroy(): если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно.
    • 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров.

Если с ДЗ большие сложности, можно получить итоговые Meal интерфейсы для сверки в личке (у меня, Татьяны, Катерины).

Clone this wiki locally