Статьи

Начало работы с Dropwizard — операции CRUD

Dropwizard — это Java-фреймворк для создания веб-сервисов RESTful. По сути, это связующий фреймворк, который объединяет популярные и проверенные на практике библиотеки Java и фреймворки, чтобы упростить начало создания новых веб-сервисов RESTful. В этом посте рассматривается, как реализовать операции создания, чтения, обновления, удаления (CRUD) на ресурсе.

В прошлый раз мы отправились в путешествие по созданию простого веб-сервиса RESTful с Dropwizard. Цель состояла в том, чтобы создать бэкэнд для приложения гипотетических событий, которое позволяло бы вам искать события на основе ваших критериев поиска. Он должен иметь возможность предоставлять список событий, добавлять новые события и изменять существующие. Обязательно прочитайте предыдущий пост, потому что мы продолжим, если мы остановились. Если вы хотите следовать, клонируйте репозиторий GitHub и проверьте тег register-resource .

Быстрый обзор

В предыдущей статье мы создали новый проект Dropwizard, добавили пользовательскую конфигурацию, создали представление и очень простой ресурс. Поскольку у нас не было хранилища данных, клиенты могли только делать GET-запрос к /events и получать жестко закодированные данные. В этом последующем уроке мы собираемся расширить набор функций приложения для удовлетворения требований, которые мы изначально установили.

Представляем хранилище данных

В этом руководстве будет рассказано, как реализовать функциональность CRUD для нашего приложения. Чтобы упростить ситуацию и не утомлять вас спецификой базы данных, я не собираюсь ее настраивать. Вместо этого мы собираемся реализовать очень простую базу данных в памяти, поддерживаемую java.util.List .

Чтобы добиться лучшего повторного использования кода, лучше полагаться на абстракции, что в терминах Java означает отсутствие зависимости от конкретных типов. Поэтому мы объявим открытый интерфейс нашего хранилища данных в памяти в интерфейсе.

Создайте новый интерфейс в пакете com.mycompany.core именем EventRepository .

 public interface EventRepository { } 

Мы будем расширять этот интерфейс до конца статьи, добавляя методы по мере необходимости.

Реализация EventRepository

Мы создали интерфейс, который определяет публичный API EventRepository . Теперь нам нужно придумать реализацию.

Обратите внимание, что следующий класс ни в коем случае не является решением, которое вы развернете в производство. Для начала, поскольку мы собираемся хранить события в списке, мы столкнемся с проблемами параллелизма. Список является общим ресурсом, что означает, что он должен быть потокобезопасным . Поскольку параллелизм не является темой этой статьи, представьте, что вы единственный пользователь. Идея состоит не в том, чтобы беспокоиться о хранении данных, а о том, как создавать методы в EventResource чтобы мы могли соответствовать требованиям, которые мы изначально установили.

Создайте новый класс с именем DummyEventRepository в пакете com.mycompany.core и EventRepository его реализовать интерфейс EventRepository .

 public class DummyEventRepository implements EventRepository { private static final String DATA_SOURCE = "dummy_data.json"; private List<Event> events; public DummyEventRepository() { try { initData(); } catch (IOException e) { throw new RuntimeException( DATA_SOURCE + " missing or is unreadable", e); } } private void initData() throws IOException { URL url = Resources.getResource(DATA_SOURCE); String json = Resources.toString(url, Charsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); CollectionType type = mapper .getTypeFactory() .constructCollectionType(List.class, Event.class); events = mapper.readValue(json, type); } } 

Когда DummyEventRepository экземпляр initData , initData . Этот метод читает файл JSON, содержащий фиктивные данные, которые будут использоваться для заполнения нашего хранилища данных в памяти. Файл JSON читается классом Resource Guava и анализируется классом Джексона ObjectMapper . Создайте новый документ JSON с именем dummy_data.json и поместите его в src/main/resources . Ниже приведены данные, которые я буду использовать.

 [ { "id": 1, "name": "Czech National Symphony Orchestra - I. Concert", "description": "PERFORMERS: Libor Pešek - conductor, Natalie Clein - violoncello", "location": "náměstí Republiky 5, Praha 1 - Staré Město, 110 00", "date": "2016-10-25T19:30+0200" }, { "id": 2, "name": "Salsa Festival", "description": "World class shows & performances by some of the best artists in the world.", "location": "Copenhagen", "date": "2017-05-05T17:00+0200" }, { "id": 3, "name": "National Restaurant Day", "description": "As autumn arrives the National Restaurant Day kicks off, awaiting the lovers of gastronomy for the 11th time.", "location": "Budapest, Hungary", "date": "2016-10-16T12:00+0200" }, { "id": 4, "name": "UEFA Europa League: Austria Vienna vs. Roma", "description": "Don't miss this spectacular game and get tickets to see Austria Vienna v Roma now, before they run out.", "location": "Ernst-Happel Stadion, Vienna, Austria", "date": "2016-11-03T19:00+0200" } ] 

EventResource, EventRepository и DummyEventRepository

На следующей диаграмме вы можете увидеть три типа в действии. EventResource отвечает за обработку HTTP-запросов. Когда ему нужно получить доступ к данным или изменить их, он делегирует эту проблему экземпляру EventRepository . DummyEventRepository является реализацией EventRepository и будет действовать как хранилище данных в памяти.

class_diagram

EventResource нужна ссылка на реализацию EventRepository чтобы выполнить свою работу. Я не хочу создавать экземпляр DummyEventRepository в классе EventResource напрямую. Это было бы нарушением принципа инверсии зависимостей . Чем меньше он знает о конкретных реализациях других классов, тем лучше. Поэтому я добавил параметр конструктора в класс EventResource который можно использовать для внедрения конкретной реализации EventRepository .

 private EventRepository repository; public EventResource(EventRepository repository) { this.repository = repository; } 

Наконец, нам нужно связать вместе наш граф объектов. В методе run нашего приложения получите ссылку на экземпляр DummyEventRepository и передайте его в EventResource .

 @Override public void run(EventsConfiguration configuration, Environment environment) { //irrelevant code not shown EventRepository repository = new DummyEventRepository(); EventResource eventResource = new EventResource(repository); environment.jersey().register(eventResource); } 

Получение всех событий

Мы можем начать использовать наш недавно созданный EventRepository путем рефакторинга метода EventResource классе EventResource . На данный момент он возвращает список событий, которые создаются на лету. Но было бы более разумно, если бы он возвращал все события из хранилища данных в памяти. Итак, давайте внесем необходимые изменения, чтобы это произошло.

Измените интерфейс EventRepository и объявите метод для поиска всех событий.

 List<Event> findAll(); 

Создайте реализацию метода, объявленного в интерфейсе в классе DummyEventRepository . Возвращение списка событий — это все, что вам нужно сделать на данный момент.

 @Override public List<Event> findAll() { return events; } 

Теперь измените EventResource чтобы делегировать поиск всех событий реализации EventRepository .

 @GET public List<Event> allEvents() { return repository.findAll(); } 

При запуске приложения вместо получения жестко закодированных данных, как мы делали в предыдущем посте, вы должны увидеть список событий, которые извлекаются из EventRepository при посещении http://localhost:8080/api/events .

В действительности вы не хотите получать все данные одновременно. Может быть тысячи записей, и получение всех из них приведет к ненужным накладным расходам. Во фронтэнде целесообразно показывать только небольшой выбор событий. Например, когда вы выполняете поиск в Google, вы получите результаты, которые распределены по нескольким страницам. Этот акт называется пагинацией. Хотя это важная функция, мы рассмотрим ее в следующей статье.

Получение одного события

В RESTful API извлечение отдельного ресурса обычно осуществляется по его идентификатору. Например, если мы заинтересованы в чтении состояния события с идентификатором 123, мы должны сделать GET-запрос к /events/123 . Но как мы можем легко получить доступ к идентификатору, не выполняя сложных операций со строками? Решением является аннотация @PathParam .

Давайте посмотрим, как использовать его в коде. Добавьте следующий метод в EventResource .

 @GET @Path("{id}") public Event event(@PathParam("id") LongParam id) { //method body shown later } 

Сначала мы объявляем, что этот метод должен отвечать на запросы GET, содержащие идентификатор в пути HTTP. Помните, что в начале класса EventResource мы использовали @Path("events") чтобы указать, что путь к ресурсу начинается с /events . @Path метода с @Path аннотации @Path добавляет еще один сегмент к пути ресурса. Фигурные скобки вокруг id указывают, что это следует рассматривать как параметр пути. Использование типа LongParam гарантирует, что значение id считанного из пути, должно иметь тип Long . В противном случае Dropwizard вернет ответ HTTP 400.

Найти событие по идентификатору относительно легко. Добавьте еще один метод в интерфейс EventRepository .

 Optional<Event> findById(Long id); 

Метод возвращает Optional тип Event . Я использую Optionals для обработки возможного случая, когда событие с данным ID может не существовать. Ниже приведена реализация этого метода в DummyEventRepository .

 @Override public Optional<Event> findById(Long id) { return events.stream().filter(e -> e.getId() == id).findFirst(); } 

Теперь вернемся к завершению метода event в классе EventResource . Поскольку существует вероятность того, что запрошенное событие не существует, мы должны быть готовы вернуть соответствующий результат клиенту.

 @GET @Path("{id}") public Event event(@PathParam("id") LongParam id) { return repository.findById(id.get()) .orElseThrow(() -> new WebApplicationException("Event not found", 404)); } 

Использование Optionals позволяет действительно легко и лаконично обрабатывать случай, когда найденный объект может быть null . В этом примере, если событие существует, оно возвращается клиенту. В противном случае WebApplicationException новое исключение WebApplicationException , исключение времени выполнения из JavaEE . Dropwizard заботится о обработке исключения и возвращает клиенту сообщение об ошибке.

Создание нового события

Создание нового события выполняется с помощью глагола HTTP POST. Для ускорения давайте сначала напишем необходимый код, чтобы добавить новое событие в наше хранилище данных. Поскольку в настоящее время мы храним записи в списке, добавление нового события является простым.

Добавьте новый метод save в интерфейс EventRepository . Хранилище назначит идентификатор сохраняемому событию, и мы хотим сообщить его клиенту. Поэтому тип возвращаемого значения должен быть установлен на Event .

 Event save(Event event); 

Ниже приведена реализация метода save, который вы должны поместить в класс DummyEventRepository .

 @Override public Event save(Event event) { Optional<Long> maxId = events.stream() .map(Event::getId) .max(Long::compare); long nextId = maxId.map(x -> x + 1).orElse(1L); event.setId(nextId); events.add(event); return event; } 

Поскольку мы пытаемся избежать использования реальной базы данных в демонстрационных целях, мы должны выполнить часть работы сами, которую база данных могла бы сделать для нас. Например, при создании нового события мы заранее не знаем его идентификатор. Поэтому я использую Streams API, чтобы найти текущее максимальное значение идентификатора, увеличить его на единицу, чтобы получить следующий идентификатор и назначить его новому объекту события. Когда нет сохраненных событий, новое событие получит идентификатор 1. Конечно, базы данных гораздо лучше справляются с генерацией следующего значения для идентификатора, но, поскольку это небольшое демонстрационное приложение, вероятно, никогда не увидит большие объемы данных, это должно быть хорошо.

Как только мы закончим с сохранением нового события, давайте EventRepository необходимые изменения в класс EventRepository . Добавьте следующий метод.

 @POST public Event create(Event event) { return repository.save(event); } 

Чтобы Джерси мог получать запрос HTTP POST, все, что нам нужно сделать, — это аннотировать метод с помощью @POST в нашем классе ресурсов. Чтение сущности запроса сделано и для вас. Добавление аргумента типа Event в объявление метода информирует Jersey о необходимости проанализировать тело запроса и сопоставить его с экземпляром Event . Имейте в виду, что проверка не выполняется. Если ни одно из полей в теле запроса не совпадает с полями в классе Event , новый экземпляр Event все равно будет создан, но все его поля экземпляра будут null (за исключением id который равен 0L, поскольку это тип примитива ).

Для тестирования я собираюсь использовать утилиту командной строки curl.

 curl -X POST http://localhost:8080/api/events \ -d '{"name": "My Birthday", "description": "Time to celebrate!", "location": "My place", "date": "2016-10-27T19:00+0200"}' \ -H "Content-Type: application/json" 

Ответ на эту команду должен быть следующим.

 { "id":5, "name":"My Birthday", "description":"Time to celebrate!", "location":"My place", "date":"2016-10-27T20:00+0300" } 

Метод HTTP POST не идемпотентен. Это означает, что если вы выполняете один и тот же запрос несколько раз, вы будете создавать новое состояние на сервере с каждым запросом.

Изменение существующего события

Чтобы обновить существующее событие, мы будем использовать метод HTTP PUT. PUT — это идемпотентный метод, который означает, что выполнение одного и того же запроса несколько раз не создает дополнительного состояния на сервере. Как и при извлечении одного события, нам нужно будет прочитать идентификатор события из пути, используя аннотацию @PathParam . Прежде чем мы сделаем необходимые изменения в нашем классе ресурсов, давайте реализуем логику обновления событий.

EventRepository метод update в интерфейсе EventRepository .

 Optional<Event> update(Long id, Event event); 

Мы также должны сообщить обновленную сущность обратно клиенту. Существует вероятность того, что событие с данным идентификатором не существует. Поэтому я использую Optional типа Event в качестве типа возвращаемого значения. Чтобы упростить обновление события, я создал новый метод updateExceptId который обновляет имя, описание, местоположение и дату события, к которому оно вызывается, по сравнению с тем, которое было присвоено методу.

Ниже приводится итоговая реализация метода update который вы должны поместить в DummyEventRepository .

 @Override public Optional<Event> update(Long id, Event event) { Optional<Event> existingEvent = findById(id); existingEvent.ifPresent(e -> e.updateExceptId(event)); return existingEvent; } 

Далее вы увидите, как реализовать метод, который должен получать запрос PUT в классе EventResource .

 @PUT @Path("{id}") public Event update(@PathParam("id") LongParam id, Event event) { return repository.update(id.get(), event) .orElseThrow(() -> new WebApplicationException("Event not found", 404)); } 

Технически, для вас уже не должно быть ничего нового. Когда мы смотрели, как извлечь отдельное событие, мы смотрели, как прочитать идентификатор из пути, используя аннотацию @PathParam . Позже, когда мы реализовали добавление нового события, вы увидели, как Джерси может отобразить тело JSON в сущность Event.

Удаление события

Хотя мы не ставили его в качестве цели в начальных требованиях, давайте рассмотрим, как удалить событие. Спецификация HTTP определяет метод DELETE, который используется для удаления ресурса. Это идемпотентная операция.

Прежде всего, я собираюсь реализовать логику удаления. Наш интерфейс хранилища будет нуждаться в дополнительном методе.

 void delete(Long id); 

Далее нам нужно реализовать это в DummyEventRepository .

 @Override public void delete(Long id) { events.removeIf(e -> e.getId() == id); } 

Теперь давайте перейдем к реализации конечной точки REST для удаления события. Добавьте следующий метод в класс EventResource .

 @DELETE @Path("{id}") public Response delete(@PathParam("id") LongParam id) { repository.delete(id.get()); return Response.ok().build(); } 

Как и при чтении одного события, мы используем аннотацию @PathParam для получения идентификатора события из HTTP-пути. Но что сейчас отличается, так это тип возвращаемого значения. Поскольку нет возвращаемого события, метод имеет возвращаемый тип Response . Таким образом, мы можем отправить ответ HTTP 200 OK без содержимого.

Резюме

Поздравляем! Теперь вы должны хорошо понимать, что такое Dropwizard. В предыдущей статье мы рассмотрели, как создать новое приложение Dropwizard и настроить его. Также был дан краткий обзор структуры приложения. В этой статье вы узнали, как реализовать простой RESTful API, который позволяет создавать, читать, обновлять и удалять события. Мы почти достигли нашей цели. За исключением поиска, мы реализовали все функции, которые изначально намеревались выполнить. Я рекомендую вам поиграть с этим приложением и реализовать функцию, которая, по вашему мнению, отсутствует.

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