Spring Boot REST API

В этой статье мы напишем маленькое приложение на Spring Boot, которое предоставляет REST-сервисы.
Архитектура приложения будет стандартна и включать несколько слоев: dao, (service отсутствует) и controller. Все шаги по построению приложения просты. Код приложения можно скачать на GitHub.

Spring Initializr

Заготовку любого проекта на Spring Boot удобно взять на https://start.spring.io/. Здесь мы придумываем имя группы и имя артифакта будущего проекта на Maven, выбираем dependency, которые нам точно понадобятся и генерируем проект. А потом импортируем его в Eclipse как Maven-проект.

Инициализация Spring Boot проекта
Инициализация Spring Boot проекта

Нам понадобятся зависимости WEB, JPA и H2.

Встроенную базу данных H2 прикрепляем потому, что ее проще использовать для демонстрационных целей: не придется устанавливать настоящую базу вроде MySQL, а также прописывать ее настройки.

Maven-зависимости

В результате получаем сгенерированный POM с такими зависимостями:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>

Хоть файл и сгенерирован, это не мешает нам добавлять в него новые зависимости при необходимости.

Слои (multi-layer architecture)

Импортированный проект выглядит так, плюс мы создали пакеты для моделей, dao и контроллеров (выделены красным):

Service-layer отсутствует потому, что приложение слишком простое, бизнес-логики тут нет.

Модель

Модель будет состоять из одного класса Person:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)

    private Long id;
    @NotNull
    private String name;

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }
    // ...getters and setters
}

Person аннотирован как JPA-сущность, то есть при запуске приложения в базе данных  будет создана таблица с таким именем и полями.

DAO

DAO-layer предназначен для работы с данными. У нас он состоит из одного бина PersonRepository.

Благодаря аннотации @Repository и интерфейсу JpaRepository DAO-layer предельно прост:

@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {

}

Мы создаем бин PersonRepository, аннотируя его с помощью @Repository. Полученный бин реализует все методы интерфейса, можно ничего не писать самостоятельно, если не нужны какие-то особые запросы к базе. А стандартные операции поиска, добавления и удаления тут все реализованы.

Service-layer опускаем, поскольку приложение простое. В контроллере будем использовать бин PersonRepository.

Контроллер

Здесь реализованы запросы поиска, добавления, редактирования и удаления Person.

  • Класс аннотирован @RestController и указан основной путь к запросам  этого контроллера- «/persons».
  • С помощью аннотации @Autowired бин personRepository инжектирован в поле контроллера — теперь его можно использовать.
@RestController
@RequestMapping("/persons")
public class PersonController {

    @Autowired
    private PersonRepository personRepository;

    @GetMapping
    public ResponseEntity<List<Person>> listAllPersons() {
        List<Person> persons = personRepository.findAll();
        return ResponseEntity.ok().body(persons);

    }

    @GetMapping(value = "/{personId}")
    public ResponseEntity<Person> getPerson(@PathVariable("personId") Long personId)
      throws EntityNotFoundException {

        Optional<Person> person = personRepository.findById(personId);
        if (!person.isPresent())
            throw new EntityNotFoundException("id-" + personId);
        return ResponseEntity.ok().body(person.get());

    }

    @PostMapping
    public ResponseEntity<Person> createPerson(@RequestBody @Valid Person person) {
        Person p = personRepository.save(person);
        return ResponseEntity.status(201).body(p);
    }

    @PutMapping(value = "/{personId}")
    public ResponseEntity<Person> updatePerson(@RequestBody @Valid Person person,
            @PathVariable("personId") Long personId) throws EntityNotFoundException {
        Optional<Person> p = personRepository.findById(personId);
        if (!p.isPresent())
            throw new EntityNotFoundException("id-" + personId);
        return ResponseEntity.ok().body(personRepository.save(person));
    }

    @DeleteMapping(value = "/{personId}")
    public ResponseEntity<Person> deletePerson(@PathVariable("personId") Long personId) 
        throws EntityNotFoundException {
        Optional<Person> p = personRepository.findById(personId);
        if (!p.isPresent())
            throw new EntityNotFoundException("id-" + personId);

        personRepository.deleteById(personId);
        return ResponseEntity.ok().body(p.get());
    }
}

Тут два метода для получения данных (аннотации @GetMapping) и три — для редактирования. Все методы аннотированны:

  • @GetMapping — для GET-запросов, получения Person
  • @PostMapping — для POST-запросов, т.е. добавления Person
  • @PutMapping — для PUT-запросов, редактирования Person
  • @DeleteMapping — для DELETE-запросов, удаления Person

Возвращаем обычно ResponseEntity<Person>, это более гибкий вариант, чем вернуть просто Person, поскольку для ResponseEntity можно установить Http-статус ответа — ResponseEntity.ok() — это 200 или ResponseEntity.status(201).

В методе body() передается возвращаемая сущность — в вышеприведенных методах это Person (либо список Person). Под капотом она конвертируется в JSON благодаря тому, что у нас стоит аннотация @RestController. Для конвертации под капотом Spring Boot использует библиотеку Jackson — она включена благодаря Maven-зависимости spring-boot-starter-web.

Если надо возвратить JSON с описанием ошибки, выбрасываем исключение. Например, если запрос на редактирование содержит id несуществующего Person, то выбрасываем EntityNotFoundException. Как обрабатывать исключения и кастомизировать JSON с ошибкой, описано в следующей статье.

Запуск

Для запуска Spring Boot приложения запускаем main() этого класса:

@SpringBootApplication
public class SpringBootRestApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootRestApplication.class, args);
    }
}

При этом будет запущен веб-сервер, отдельно его устанавливать и запускать не надо — это одно из преимуществ Spring Boot приложения.

Также не надо задавать пути для поиска бинов, они найдутся автоматически. Единственное, класс SpringBootRestApplication не надо перекладывать в подпакет, он должен быть на верхнем уровне иерархии, иначе с поиском бинов возникнут проблемы. Когда мы сгенерировали заготовку приложения, этот файл уже был именно там, где надо — перекладывать его не следует.

Тестирование

Осталось проверить, что методы контроллера работают. Составлять запросы будем с помощью бесплатного графического приложения Postman. Как писать тесты, рассмотрим в другой статье.

Добавление Person
POST http://localhost:8080/persons/
{
  "name": "Joe"
}

Ответ, возвращается вновь добавленный Person с id=1:

{
    "id": 1,
    "name": "Joe"
}
Редактирование Person
PUT http://localhost:8080/persons/1
{
  "name": "Jane"
}

Ответ, возвращается отредактированный Person с id=1 и name=’Jane’:

{
    "id": 1,
    "name": "Jane"
}
Получение Person
GET http://localhost:8080/persons/1

Ответ, возвращается Person:

{
    "id": 1,
    "name": "Jane"
}
Получение списка Person
GET http://localhost:8080/persons/

Ответ, возвращается список, состоящий из одного элемента Person:

[
    {
        "id": 1,
        "name": "Jane"
    }
]
Удаление Person
DELETE http://localhost:8080/persons/1

Ответ, возвращается удаленный Person с id=1 и name=’Jane’:

{
    "id": 1,
    "name": "Jane"
}

Заключение

Мы написали маленькое приложение, предоставляющее REST-сервис. Как обрабатывать исключения описано в следующей части.

Spring Boot REST API: 8 комментариев

  1. Аесть статья, как кастомизировать response body таким образом, чтобы в нем можно было возвращать не все поля энтити? Например, я хочу исключить из респонза ID. Мне на ум приходит два варианта, оба убогие.
    1. В теории, я могу сделать ResponseEntity и исключить из переопределения toString нужное поле, но я не хочу возвращать стринг, я хочу вернуть именно json. Есть возможность дефолтными методами это как-то сделать или необходимо только всякие манипуляции проводить с кастованием стрингов в жсон и обратно?
    2. Кастомизировать Query, чтобы он возвращал только нужные поля. Но тогда возвращаемым значением будет Object, которые никак не получится кастовать к моему классу. Да и работать с таким значением невозможно.

    1. 1. Заботиться о преобразовании в объекта в JSON не надо, это на совести контроллера. Главное, чтобы у нас был объект с нужными полями.
      2. Чтобы он был, надо использовать паттерн DTO — Data Transfer Object; то есть помимо объекта Person, создаете объект PersonDTO специально для передачи — он содержит ровно те поля, которые нужно отправить/принять на фронтенд.
      В статье этого нет, т.к. случай рассмотрен примитивный. Но если бы было, возвращаемым значением метода getPerson()был бы:

      public ResponseEntity <PersonDTO>

      3. Где и как реализовать логику преобразования entity в dto — дело вкуса.
      Есть простой пример ручного преобразования прямо в классе entity https://stackoverflow.com/questions/28703401/conversion-of-dto-to-entity-and-vice-versa (в 1-ом ответе)
      А тут преобразование выполняется с помощью библиотеки modelmapper https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application

  2. В updatePerson, очевидно, не хватает
    p.get().setName(person.getName());
    и тогда:
    return ResponseEntity.ok().body(personRepository.save(p.get()));
    иначе он новую запись создаёт.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *