Изучить архитектуру современного Spring Boot приложения и реализовать REST API для управления книгами.
Перед выполнением задания внимательно изучите предоставленный пример кода создания инвестиционного фонда. Разберите каждую аннотацию:
@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 - активирует валидацию:
Вопросы для размышления:
@Valid связана с аннотациями валидации в DTO?@Override
public ResponseEntity<FundShortInfoDtoV1> createInvestmentFund(FundCreationRequestDtoV1 creationRequestDtoV1) {
var fundCreationRequest = fundMapper.mapFundCreationRequestDtoV1(creationRequestDtoV1);
var createdFund = fundFacade.createInvestmentFund(fundCreationRequest);
return ResponseEntity.ok(fundMapper.mapFundToFundDtoV1(createdFund));
}Архитектурные слои и их назначение:
Controller → Facade → Service → Repository
Зачем такая архитектура?
Создайте REST API для управления книгами по аналогии с примером управления инвестиционными фондами.
POST /api/v1/booksid - Long (автогенерируемый)title - String (название книги, обязательное)author - String (автор, обязательное)isbn - String (уникальный, обязательное)publicationDate - LocalDate (дата публикации)price - BigDecimal (цена)description - String (описание, необязательное)createdAt - LocalDateTime (время создания)updatedAt - LocalDateTime (время обновления)Создайте проект на https://start.spring.io со следующими зависимостями:
OpenAPI/Swagger - для автоматической генерации документации API:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>Зачем? Создает веб-интерфейс для тестирования API и автоматически генерирует документацию из аннотаций.
MapStruct - для автоматической генерации маппинга между объектами:
<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>Зачем? Избавляет от написания кода преобразования вручную. Генерирует быстрый и безопасный код маппинга.
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 не будет создавать таблицыСоздайте JPA Entity Book с:
createdAt и updatedAtПодсказки:
@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();
}
}Создайте файлы:
src/main/resources/db/changelog/db.changelog-master.xmlsrc/main/resources/db/changelog/changeset/001-create-books-table.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">Создайте DTO для входящих данных с:
@SchemaПример структуры:
@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;
// Конструкторы, геттеры, сеттеры
}Создайте DTO для ответа с краткой информацией о книге.
Подсказка: Этот класс только для чтения, валидация не нужна. Добавьте @Schema для документации.
Создайте MapStruct маппер для преобразования:
BookCreationRequestDto → BookBook → BookShortInfoDtoПример структуры:
@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 автоматически генерирует реализацию во время компиляции.
Создайте репозиторий с методами:
Пример структуры:
@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);
}Создайте сервис с методом создания книги:
Пример структуры:
@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 - оптимизация для операций чтенияСоздайте фасад для оркестрации:
Создайте интерфейс контроллера с аннотациями:
Пример структуры:
@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
);
}Создайте реализацию контроллера:
Пример структуры:
@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);
}
}Почему интерфейс отделен от реализации?
Создайте глобальный обработчик ошибок для:
Пример структуры:
@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);
}
}Не забудьте создать:
public class BookAlreadyExistsException extends RuntimeException {
public BookAlreadyExistsException(String message) {
super(message);
}
}POST /api/v1/books/swagger-ui/index.html{
"title": "Война и мир",
"author": "Лев Толстой",
"isbn": "9785699123456",
"publicationDate": "2023-01-15",
"price": 599.99,
"description": "Классическое произведение русской литературы"
}Ошибка: "Cannot find symbol" при использовании mapper
Решение: Убедитесь, что добавили mapstruct-processor в dependencies
Ошибка: "Table 'books' doesn't exist" Решение:
Ошибка: Принимаются некорректные данные
Решение: Убедитесь, что добавили @Valid перед @RequestBody
Ошибка: Пустая страница на /swagger-ui/index.html
Решение: Проверьте, что добавили зависимость springdoc-openapi-starter-webmvc-ui
Ошибка: "No qualifying bean of type" Решение: Убедитесь, что классы помечены аннотациями:
@Repository для репозиториев@Service для сервисов@Component для фасадов@RestController для контроллеровПеред тестированием убедитесь, что:
Структура проекта:
Код:
Аннотации:
Тестирование:
Рекомендуется выполнять задание в следующем порядке:
com.example.bookapi.entity, com.example.bookapi.dto.requestBook, BookCreationRequestDto, BookServicecreateBook, findByIsbn, mapBookToShortInfomvn clean compilemvn spring-boot:runjdbc:h2:mem:testdb