Создание RESTful сервиса цитат с Spring

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

Выбор инструментов

Чтобы избавиться от лишнего, я написал список того, какие по моему мнению будут инструменты правильным выбором для создания RESTful web сервиса.

  • Spring Data JPA — быстрая предзагрузка, выборка и обновление контента
  • Spring MVC — надежная поддержка REST, начиная с Spring 3
  • Spring Boot — создание всех необходимых компонентов практически без усилий
  • start.spring.io — сборка проекта простым выбором необходимых компонентов

Я быстро ушел от решения добавления, удаления, управления или просмотра данных через web-страницу. Вместо этого, я обратил внимание на подаче фиксированного количества данных с точно такой же структурой, которая необходима учебному материалу.

Выбор контента

Оригинальный контент для учебного материала был из серии шуток «Chunk Norris». Я люблю хорошо посмеяться. Но когда я пересмотрел публичное API, я увидел, что некоторые из шуток были немного прогорклыми. После небольшого обсуждения с коллегами, пришла идея приводить цитаты из истории. Я взял эту идею и изменил её немного. Для моей предстоящей книги, Learning Spring Boot, я собрал цитаты от различных разработчиков Spring Boot. Поэтому я решил использовать её в качестве источника.

Время для кода!

Чтобы избавиться от лишнего, я посетил start.spring.io. Это Spring Boot приложение позволяет уточнить детали вашего нового проекта, выбрать версию Java и компоненты запуска Spring Boot при необходимости. Я использовал свой список(см. выше) и создал новый gradle-проект.

Определение домена

Первое, что я сделал после распаковки и импорта в IDE, скопировал объекты домена как показано в учебном материале. Таким образом, я могу убедиться, что данные, отправленные моим REST сервисом были корректны. Поскольку POJO-классы в моем приложении идентично, я не буду их приводить здесь.

Затем я создал Spring Data репозиторий.

public interface QuoteRepository extends CrudRepository {}

Это пустой интерфейс, определяющий обработчики Quote объектов с внутренним первичным ключем типа Long. Расширяясь от Spring Data Commons CrudRepository, он наследует методы для операций с БД.

Следующий шаг? Инициализируем некоторые данные. Я создал DatabaseLoader:

@Service
public class DatabaseLoader {

    private final QuoteRepository repository;

    @Autowired
    public DatabaseLoader(QuoteRepository repository) {
        this.repository = repository;
    }

    @PostConstruct
    void init() {
        repository.save(new Quote("Working with Spring Boot is like pair-programming with the Spring developers."));
        // more quotes...
    }
}
  • Помеченный как @Service, он автоматически будет подхвачен @ComponentScan при запуске приложения
  • Используется конструктор с автоподключением копии QuoteRepository для большей доступности
  • @PostConstruct говорит Spring MVC запускать метод загрузки данных после того, как все бины будут созданы
  • В итоге, метод init() используется Spring Data JPA для создания целой кучи цитат

Т.к. я выбрал Н2 в качестве моей БД(com.h2database:h2) в build.gradle, здесь нет всего того, что требуется для её установки(спасибо Spring Boot).

Создание контроллера

После того, как я создал этот слой БД, я захотел создать API. C Spring MVC это было не трудно.

@RestController
public class QuoteController {

    private final QuoteRepository repository;

    private final static Quote NONE = new Quote("None");

    private final static Random RANDOMIZER = new Random();

    @Autowired
    public QuoteController(QuoteRepository repository) {
        this.repository = repository;
    }

    @RequestMapping(value = "/api", method = RequestMethod.GET)
    public List getAll() {
        return StreamSupport.stream(repository.findAll().spliterator(), false)
            .map(q -> new QuoteResource(q, "success"))
            .collect(Collectors.toList());
    }

    @RequestMapping(value = "/api/{id}", method = RequestMethod.GET)
    public QuoteResource getOne(@PathVariable Long id) {
        if (repository.exists(id)) {
            return new QuoteResource(repository.findOne(id), "success");
        } else {
            return new QuoteResource(NONE, "Quote " + id + " does not exist");
        }
    }

    @RequestMapping(value = "/api/random", method = RequestMethod.GET)
    public QuoteResource getRandomOne() {
        return getOne(nextLong(1, repository.count() + 1));
    }

    private long nextLong(long lowerRange, long upperRange) {
        return (long)(RANDOMIZER.nextDouble() * (upperRange - lowerRange)) + lowerRange;
    }
}

Давайте разберем его:

  • Весь класс помечен как @RestController. Это означает, что все адреса возвращают объекты не для просмотра
  • У меня есть несколько статических объектов, в частности NONE цитата и Java 8 Random для произвольного выбора цитат
  • Используется конструктор с автоподключением QuoteRepository

/api — выборка всех цитат
/api/{id} — выбока цитаты по id
/api/random — выборка произвольной цитаты

Для выборки ВСЕХ цитат я использовал Java 8 stream, чтобы обернуть метод findAll(), чтобы в свою очередь обернуть каждое значение в QuoteResource. Результат будет получен типа List.

Для выборки одной цитаты, проверяется на существование по переданному id. Если не существует, то возвращается NONE. Иначе — обернутая цитата.

И наконец, для выборки произвольной цитаты я использовал метод утилитарного класса Random из Java 8 nextLong() для выбора Long в диапазоне между lowerRange и upperRange включительно.

Почему я использую QuoteResource? Quote — центральный объект домена, возвращаемый QuoteRepository. В соответствии с предыдущим публичным API, я обернул каждый экземпляр в QuoteResource, который включает в себя код состояния.

Тестирование результатов

В данном случае, по умолчанию Application класс был создан в start.spring.io и готов к запуску.

$ curl localhost:8080/api/random { type: "success", value: { id: 1, quote: "Working with Spring Boot is like pair-programming with the Spring developers." } }

Та дам!

В завершение, я собрал JAR-файл и отправил в Pivotal Web Services. Вы можете сами увидеть результат на сайте http://gturnquist-quoters.cfapps.io/api/random.

Осталось сказать, что я изменил для учебного материала ТОЛЬКО ОДНУ СТРОЧКУ КОДА.

Чтобы посмотреть весь код, посетите github.com/gregturn/quoters.

Нерешенные вопросы

  • Этот RESTful сервис соответствует Level 2 — HTTP Verbs Richardson Maturity Model. В то время как лучше поддерживать Level 3 — Hypermedia. С Spring HATEOAS это легче, чем добавление каждой hypermedia ссылки. Оставайтесь с нами
  • Здесь нет привычной web-страницы. Это было бы замечательно, это не требуется
  • Контент фиксирован и определяется внутри приложения. Для создания динамического контента, нам необходимо открыть двери для POST и PUT. Это предполагает желание введения обеспечения безопасности должным образом

Это некоторые нерешенные вопросы, которые не входят в бюджет по времени и не требуются для решения исходной проблемы. Но они являются хорошими упражнениями, которые вы можете изучить! Вы можете клонировать проект в GitHub и стрельнуть самим себе!

comments powered by Disqus