В этой статье мы напишем маленькое приложение на Spring Boot, которое предоставляет REST-сервисы.
Архитектура приложения будет стандартна и включать несколько слоев: dao, (service отсутствует) и controller. Все шаги по построению приложения просты. Код приложения можно скачать на GitHub.
Spring Initializr
Заготовку любого проекта на Spring Boot удобно взять на https://start.spring.io/. Здесь мы придумываем имя группы и имя артифакта будущего проекта на Maven, выбираем dependency, которые нам точно понадобятся и генерируем проект. А потом импортируем его в Eclipse как Maven-проект.
Нам понадобятся зависимости 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-сервис. Как обрабатывать исключения описано в следующей части.
Есть ли статья по тестированию?
Будет в ближайшее время
Аесть статья, как кастомизировать response body таким образом, чтобы в нем можно было возвращать не все поля энтити? Например, я хочу исключить из респонза ID. Мне на ум приходит два варианта, оба убогие.
1. В теории, я могу сделать ResponseEntity и исключить из переопределения toString нужное поле, но я не хочу возвращать стринг, я хочу вернуть именно json. Есть возможность дефолтными методами это как-то сделать или необходимо только всякие манипуляции проводить с кастованием стрингов в жсон и обратно?
2. Кастомизировать Query, чтобы он возвращал только нужные поля. Но тогда возвращаемым значением будет Object, которые никак не получится кастовать к моему классу. Да и работать с таким значением невозможно.
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
Спасибо!!!
Огромная благодарность sysout!!
Класс! Спасибо! Хороший пример для пощупать REST и Postman.
В updatePerson, очевидно, не хватает
p.get().setName(person.getName());
и тогда:
return ResponseEntity.ok().body(personRepository.save(p.get()));
иначе он новую запись создаёт.