Content is user-generated and unverified.

Домашнее задание: Создание REST API контроллера

Цель

Изучить архитектуру современного Spring Boot приложения и реализовать REST API для управления книгами.

Анализ примера кода

Перед выполнением задания внимательно изучите предоставленный пример кода создания инвестиционного фонда. Разберите каждую аннотацию:

Интерфейс контроллера (API Contract)

java
@Operation(summary = "", description = "API метод создания инвестиционного фонда", tags={ "fund-v1" })
@ApiResponses(value = { 
    @ApiResponse(responseCode = "200", description = "Успешный запрос", content = @Content(mediaType = "application/json", schema = @Schema(implementation = FundShortInfoDtoV1.class))),
    @ApiResponse(responseCode = "404", description = "Запрашиваемый ресурс не найден", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Object.class))) })
@RequestMapping(value = "/api/v1/funds", produces = { "application/json" }, consumes = { "application/json" }, method = RequestMethod.POST)
ResponseEntity<FundShortInfoDtoV1> createInvestmentFund(@Parameter(in = ParameterIn.DEFAULT, description = "", schema=@Schema()) @Valid @RequestBody FundCreationRequestDtoV1 body);

Разбор аннотаций:

@Operation - описывает операцию для Swagger UI:

  • summary - краткое описание (отображается в списке)
  • description - подробное описание
  • tags - группировка операций (для организации в UI)

@ApiResponses - описывает возможные ответы:

  • responseCode = "200" - HTTP статус код
  • description - описание когда возвращается этот код
  • content - тип содержимого и схема ответа
  • @Schema(implementation = Class.class) - указывает класс для примера

@RequestMapping - настройка HTTP эндпоинта:

  • value - URL путь
  • produces - какой тип данных возвращает
  • consumes - какой тип данных принимает
  • method - HTTP метод (POST, GET, PUT, DELETE)

@Parameter - описание параметра запроса:

  • in = ParameterIn.DEFAULT - параметр в теле запроса
  • description - описание параметра
  • schema - схема для валидации

@Valid - активирует валидацию:

  • Проверяет аннотации валидации в DTO (@NotNull, @Size, etc.)
  • Если валидация не прошла - возвращает 400 Bad Request

Вопросы для размышления:

  1. Зачем выделять интерфейс контроллера отдельно от его реализации?
  2. Почему важно документировать каждый возможный код ответа?
  3. Как @Valid связана с аннотациями валидации в DTO?

Реализация контроллера

java
@Override
public ResponseEntity<FundShortInfoDtoV1> createInvestmentFund(FundCreationRequestDtoV1 creationRequestDtoV1) {
    var fundCreationRequest = fundMapper.mapFundCreationRequestDtoV1(creationRequestDtoV1);
    var createdFund = fundFacade.createInvestmentFund(fundCreationRequest);
    return ResponseEntity.ok(fundMapper.mapFundToFundDtoV1(createdFund));
}

Архитектурные слои и их назначение:

ControllerFacadeServiceRepository

  • Controller - принимает HTTP запросы, валидирует данные, возвращает ответы
  • Facade - оркестрирует вызовы нескольких сервисов, преобразует данные
  • Service - содержит бизнес-логику, транзакции
  • Repository - работает с базой данных
  • Mapper - преобразует объекты между слоями (DTO ↔ Entity)
  • DTO - объекты для передачи данных (Data Transfer Object)

Зачем такая архитектура?

  • Разделение ответственности - каждый слой решает свою задачу
  • Тестируемость - можно тестировать каждый слой отдельно
  • Переиспользование - сервисы можно использовать в разных контроллерах
  • Гибкость - можно менять реализацию одного слоя, не затрагивая другие

Техническое задание

Что нужно реализовать

Создайте REST API для управления книгами по аналогии с примером управления инвестиционными фондами.

Требования к архитектуре

  1. Интерфейс контроллера (отдельный от реализации)
  2. Реализация контроллера
  3. Facade для оркестрации бизнес-логики
  4. Service для работы с бизнес-логикой
  5. Repository для доступа к данным
  6. Entity для представления данных в БД
  7. Mapper для преобразования объектов
  8. DTO для запросов и ответов
  9. Liquibase миграция для создания таблицы

Endpoint для реализации

POST /api/v1/books

Модель данных Book

  • id - Long (автогенерируемый)
  • title - String (название книги, обязательное)
  • author - String (автор, обязательное)
  • isbn - String (уникальный, обязательное)
  • publicationDate - LocalDate (дата публикации)
  • price - BigDecimal (цена)
  • description - String (описание, необязательное)
  • createdAt - LocalDateTime (время создания)
  • updatedAt - LocalDateTime (время обновления)

Шаг 1: Настройка проекта

Spring Initializer

Создайте проект на https://start.spring.io со следующими зависимостями:

  • Spring Web
  • Spring Data JPA
  • H2 Database
  • Validation
  • Liquibase Migration

Дополнительные зависимости

OpenAPI/Swagger - для автоматической генерации документации API:

xml
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>

Зачем? Создает веб-интерфейс для тестирования API и автоматически генерирует документацию из аннотаций.

MapStruct - для автоматической генерации маппинга между объектами:

xml
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>

Зачем? Избавляет от написания кода преобразования вручную. Генерирует быстрый и безопасный код маппинга.

Конфигурация application.yml

yaml
spring:
  datasource:
    url: jdbc:h2:mem:testdb      # База данных в памяти (данные не сохраняются)
    driver-class-name: org.h2.Driver
    username: sa
    password: password
  jpa:
    hibernate:
      ddl-auto: validate         # Проверяет соответствие Entity и БД (не создает таблицы)
    show-sql: true              # Показывает SQL запросы в логах
  h2:
    console:
      enabled: true             # Включает веб-консоль H2 для просмотра БД
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.xml  # Путь к миграциям

Важно!

  • ddl-auto: validate означает, что Hibernate не будет создавать таблицы
  • Таблицы должны быть созданы через Liquibase миграции
  • H2 консоль доступна по адресу: http://localhost:8080/h2-console

Шаг 2: Создание Entity и миграции

Задача 2.1: Book Entity

Создайте JPA Entity Book с:

  • Правильными JPA аннотациями
  • Валидацией полей
  • Автоматическим заполнением createdAt и updatedAt

Подсказки:

java
@Entity
@Table(name = "books")
public class Book {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Title is required")
    @Column(name = "title", nullable = false)
    private String title;
    
    // Для автоматического заполнения времени:
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
}

Задача 2.2: Liquibase миграция

Создайте файлы:

  1. src/main/resources/db/changelog/db.changelog-master.xml
  2. src/main/resources/db/changelog/changeset/001-create-books-table.xml

Структура мастер-файла:

xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
                   http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">

    <include file="db/changelog/changeset/001-create-books-table.xml"/>
</databaseChangeLog>

Подсказка для создания таблицы:

  • Используйте <createTable tableName="books">
  • Для автоинкремента: <column name="id" type="bigint" autoIncrement="true">
  • Для уникальных полей: <constraints unique="true">

Шаг 3: DTO классы

Задача 3.1: BookCreationRequestDto

Создайте DTO для входящих данных с:

  • Валидацией всех полей
  • Swagger аннотациями @Schema
  • Правильными форматами дат

Пример структуры:

java
@Schema(description = "Запрос на создание книги")
public class BookCreationRequestDto {
    
    @NotBlank(message = "Title is required")
    @Schema(description = "Название книги", example = "Война и мир")
    private String title;
    
    @Pattern(regexp = "^\\d{10}(\\d{3})?$", message = "ISBN должен содержать 10 или 13 цифр")
    @Schema(description = "ISBN книги", example = "9785699123456")
    private String isbn;
    
    @JsonFormat(pattern = "yyyy-MM-dd")
    @Schema(description = "Дата публикации", example = "2023-01-15")
    private LocalDate publicationDate;
    
    // Конструкторы, геттеры, сеттеры
}

Задача 3.2: BookShortInfoDto

Создайте DTO для ответа с краткой информацией о книге.

Подсказка: Этот класс только для чтения, валидация не нужна. Добавьте @Schema для документации.


Шаг 4: Mapper

Задача 4.1: BookMapper

Создайте MapStruct маппер для преобразования:

  • BookCreationRequestDtoBook
  • BookBookShortInfoDto

Пример структуры:

java
@Mapper(componentModel = "spring")
public interface BookMapper {
    
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    Book mapCreationRequestToBook(BookCreationRequestDto requestDto);
    
    BookShortInfoDto mapBookToShortInfo(Book book);
}

Важно! MapStruct автоматически генерирует реализацию во время компиляции.


Шаг 5: Repository

Задача 5.1: BookRepository

Создайте репозиторий с методами:

  • Поиск по ISBN
  • Проверка существования по ISBN

Пример структуры:

java
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    
    Optional<Book> findByIsbn(String isbn);
    
    boolean existsByIsbn(String isbn);
    
    // Альтернативный способ через @Query:
    @Query("SELECT COUNT(b) > 0 FROM Book b WHERE b.isbn = :isbn")
    boolean checkIsbnExists(@Param("isbn") String isbn);
}

Шаг 6: Service

Задача 6.1: BookService

Создайте сервис с методом создания книги:

  • Проверка на дубликат ISBN
  • Сохранение в БД
  • Обработка ошибок

Пример структуры:

java
@Service
@Transactional(readOnly = true)  // По умолчанию только чтение
public class BookService {
    
    private final BookRepository bookRepository;
    
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    @Transactional  // Переопределяем для записи
    public Book createBook(Book book) {
        if (bookRepository.existsByIsbn(book.getIsbn())) {
            throw new BookAlreadyExistsException("Book with ISBN " + book.getIsbn() + " already exists");
        }
        return bookRepository.save(book);
    }
}

Зачем @Transactional?

  • Гарантирует целостность данных
  • Если произойдет ошибка - откатывает изменения
  • readOnly = true - оптимизация для операций чтения

Шаг 7: Facade

Задача 7.1: BookFacade

Создайте фасад для оркестрации:

  • Преобразование DTO в Entity
  • Вызов сервиса
  • Возврат результата

Шаг 8: Controller

Задача 8.1: BookControllerApi (интерфейс)

Создайте интерфейс контроллера с аннотациями:

Пример структуры:

java
@Tag(name = "Books", description = "API для управления книгами")
public interface BookControllerApi {
    
    @Operation(
        summary = "Создание новой книги",
        description = "API метод для создания новой книги в системе"
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "201", 
            description = "Книга успешно создана",
            content = @Content(mediaType = "application/json", schema = @Schema(implementation = BookShortInfoDto.class))
        ),
        @ApiResponse(
            responseCode = "400", 
            description = "Некорректные данные запроса",
            content = @Content(mediaType = "application/json", schema = @Schema(implementation = Object.class))
        ),
        @ApiResponse(
            responseCode = "409", 
            description = "Книга с таким ISBN уже существует",
            content = @Content(mediaType = "application/json", schema = @Schema(implementation = Object.class))
        )
    })
    @PostMapping(value = "/api/v1/books", produces = "application/json", consumes = "application/json")
    ResponseEntity<BookShortInfoDto> createBook(
        @Parameter(description = "Данные для создания книги", required = true)
        @Valid @RequestBody BookCreationRequestDto requestDto
    );
}

Задача 8.2: BookController (реализация)

Создайте реализацию контроллера:

Пример структуры:

java
@RestController
public class BookController implements BookControllerApi {
    
    private final BookFacade bookFacade;
    private final BookMapper bookMapper;
    
    public BookController(BookFacade bookFacade, BookMapper bookMapper) {
        this.bookFacade = bookFacade;
        this.bookMapper = bookMapper;
    }
    
    @Override
    public ResponseEntity<BookShortInfoDto> createBook(BookCreationRequestDto requestDto) {
        var createdBook = bookFacade.createBook(requestDto);
        var responseDto = bookMapper.mapBookToShortInfo(createdBook);
        return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
    }
}

Почему интерфейс отделен от реализации?

  • API контракт отделен от бизнес-логики
  • Можно менять реализацию, не затрагивая контракт
  • Удобно для написания тестов
  • Swagger генерирует документацию из интерфейса

Шаг 9: Обработка ошибок

Задача 9.1: Exception Handler

Создайте глобальный обработчик ошибок для:

  • Ошибок валидации
  • Дубликатов ISBN
  • Других исключений

Пример структуры:

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BookAlreadyExistsException.class)
    public ResponseEntity<Map<String, String>> handleBookAlreadyExists(BookAlreadyExistsException ex) {
        Map<String, String> response = new HashMap<>();
        response.put("error", "BOOK_ALREADY_EXISTS");
        response.put("message", ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.CONFLICT);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

Не забудьте создать:

java
public class BookAlreadyExistsException extends RuntimeException {
    public BookAlreadyExistsException(String message) {
        super(message);
    }
}

Требования к реализации

Обязательные требования:

  1. ✅ Приложение запускается без ошибок
  2. ✅ Работает эндпоинт POST /api/v1/books
  3. ✅ Корректная валидация входных данных
  4. ✅ Обработка дубликатов ISBN (код 409)
  5. ✅ Документация API доступна по /swagger-ui/index.html
  6. ✅ Интерфейс контроллера отделен от реализации
  7. ✅ Используются все аннотации из примера

Технические требования:

  • Используйте var вместо типов (как в примере)
  • Следуйте архитектуре: Controller → Facade → Service → Repository
  • Правильно настройте MapStruct
  • Используйте @Transactional где необходимо

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

Как проверить работу:

  1. Запустите приложение
  2. Откройте Swagger UI: http://localhost:8080/swagger-ui/index.html
  3. Протестируйте эндпоинт со следующим JSON:
json
{
  "title": "Война и мир",
  "author": "Лев Толстой", 
  "isbn": "9785699123456",
  "publicationDate": "2023-01-15",
  "price": 599.99,
  "description": "Классическое произведение русской литературы"
}

Ожидаемые результаты:

  • Успешное создание: HTTP 201 с данными книги
  • Повторный запрос: HTTP 409 (дубликат ISBN)
  • Некорректные данные: HTTP 400 с описанием ошибок

Частые ошибки и их решения

1. Ошибка компиляции MapStruct

Ошибка: "Cannot find symbol" при использовании mapper Решение: Убедитесь, что добавили mapstruct-processor в dependencies

2. Liquibase не создает таблицы

Ошибка: "Table 'books' doesn't exist" Решение:

  • Проверьте путь к changelog в application.yml
  • Убедитесь, что XML файлы миграций в правильной папке
  • Посмотрите логи при запуске - там должны быть сообщения Liquibase

3. Валидация не работает

Ошибка: Принимаются некорректные данные Решение: Убедитесь, что добавили @Valid перед @RequestBody

4. Swagger UI не показывает документацию

Ошибка: Пустая страница на /swagger-ui/index.html Решение: Проверьте, что добавили зависимость springdoc-openapi-starter-webmvc-ui

6. Ошибка внедрения зависимостей

Ошибка: "No qualifying bean of type" Решение: Убедитесь, что классы помечены аннотациями:

  • @Repository для репозиториев
  • @Service для сервисов
  • @Component для фасадов
  • @RestController для контроллеров

Чек-лист для самопроверки

Перед тестированием убедитесь, что:

Структура проекта:

  • Созданы все необходимые пакеты (entity, dto, service, repository, controller, facade, mapper)
  • Файлы миграций в правильных папках
  • application.yml настроен корректно

Код:

  • Entity содержит все нужные поля с правильными аннотациями
  • DTO классы содержат валидацию и Swagger аннотации
  • Mapper интерфейс правильно настроен
  • Repository наследует JpaRepository
  • Service содержит @Transactional аннотации
  • Контроллер разделен на интерфейс и реализацию
  • Exception Handler обрабатывает ошибки

Аннотации:

  • @Entity на классе Book
  • @Service на сервисе
  • @Repository на репозитории
  • @RestController на контроллере
  • @Component на фасаде
  • @RestControllerAdvice на обработчике ошибок
  • @Valid перед @RequestBody в контроллере

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

  • Приложение запускается без ошибок
  • Swagger UI доступен по адресу /swagger-ui/index.html
  • H2 Console доступен по адресу /h2-console
  • Создание книги работает (возвращает 201)
  • Повторное создание с тем же ISBN возвращает 409
  • Некорректные данные возвращают 400

Дополнительные задачи (для продвинутых)

  1. Добавьте эндпоинт для получения книги по ID
  2. Реализуйте поиск книг по автору
  3. Добавьте пагинацию для списка книг
  4. Напишите unit-тесты для сервиса
  5. Добавьте кеширование

Советы для успешного выполнения

1. Порядок выполнения

Рекомендуется выполнять задание в следующем порядке:

  1. Настройка проекта и зависимостей
  2. Entity и миграции Liquibase
  3. DTO классы
  4. Repository
  5. Service
  6. Mapper (MapStruct)
  7. Facade
  8. Controller (интерфейс + реализация)
  9. Exception Handler
  10. Тестирование

2. Отладка

  • Всегда смотрите логи при запуске приложения
  • Проверяйте, что Liquibase успешно применил миграции
  • Используйте H2 Console для проверки созданных таблиц
  • Тестируйте каждый компонент отдельно

3. Соглашения именования

  • Пакеты: com.example.bookapi.entity, com.example.bookapi.dto.request
  • Классы: Book, BookCreationRequestDto, BookService
  • Методы: createBook, findByIsbn, mapBookToShortInfo

4. Полезные команды

  • Сборка проекта: mvn clean compile
  • Запуск приложения: mvn spring-boot:run
  • Проверка таблиц в H2: подключитесь к jdbc:h2:mem:testdb

5. Что изучить дополнительно

  • Паттерн Repository и его преимущества
  • Принципы SOLID и как они применяются в Spring
  • Жизненный цикл Spring Bean
  • Транзакции в Spring (@Transactional)
  • REST API best practices
Content is user-generated and unverified.
    Домашнее задание: Создание REST API контроллера | Claude