Перейти к содержанию
Educora
Продвинутый25 мин17 / 18

REST API на Spring Boot

Создай проект Spring Boot, свяжи слои контроллера, сервиса и репозитория через внедрение зависимостей, построй CRUD API и проверь его с помощью curl.

Проверь себя
В этом уроке ты узнаешь
  • Создавать и запускать проект Spring Boot
  • Объяснять слои @RestController, @Service, @Repository и внедрение зависимостей
  • Писать CRUD API с правильными HTTP-статусами и проверять его через curl

Мобильное приложение — например, электронный школьный дневник — получает оценки с сервера: приложение отправляет HTTP-запрос (GET /api/students), а сервер возвращает JSON. Такой сервер называют REST API. В мире Java самый популярный способ его написать — Spring Boot: несколько аннотаций превращают обычные классы в веб-сервер, а JSON, базы данных и настройки подключаются автоматически.

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

  1. 1
    Открой start.spring.io

    Это официальный генератор проектов (в IntelliJ IDEA Ultimate есть такой же мастер). Выбери Project — Maven или Gradle, Language — Java, Java — 21.

  2. 2
    Заполни метаданные

    Group — com.educora, Artifact — school, Name — SchoolApi. Пакет будет com.educora.school, а главный класс — SchoolApiApplication.

  3. 3
    Добавь зависимости

    Нажми ADD DEPENDENCIES и выбери Spring Web. В настоящих проектах обычно добавляют ещё Spring Data JPA, драйвер базы и Validation.

  4. 4
    Сгенерируй и открой

    GENERATE скачивает ZIP-архив. Распакуй его, открой папку в IDE и запусти главный класс.

Java
package com.educora.school;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SchoolApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(SchoolApiApplication.class, args);
    }
}
Точка входа: @SpringBootApplication и однострочный main

@SpringBootApplication объединяет три вещи: делает класс источником конфигурации и включает поиск компонентов в пакете (component scan) и автоконфигурацию. Последняя смотрит на classpath: если там есть Spring Web, на порту 8080 запускается встроенный сервер Tomcat, а для JSON подключается Jackson. Отдельный сервер устанавливать не нужно — приложение является JAR-файлом с обычным методом main.

Terminal
./mvnw spring-boot:run
Ожидаемый результат
... Tomcat started on port 8080 (http) with context path '/'
... Started SchoolApiApplication in 1.9 seconds (process running for 2.2)
Пример вывода (сокращён); в Gradle — ./gradlew bootRun

Слои и внедрение зависимостей

СлойАннотацияЗадача
Контроллер@RestControllerпринимает HTTP-запросы и возвращает JSON
Сервис@Serviceбизнес-правила: проверки, вычисления
Репозиторий@Repositoryхранит и читает данные

Классы, помеченные этими аннотациями, Spring создаёт сам — их называют бинами (beans) — и хранит в IoC-контейнере (обычно по одному экземпляру каждого класса). Если конструктору класса нужен другой бин, Spring передаёт его автоматически: это называется внедрением зависимостей (dependency injection). В итоге классы не создают друг друга через new, а в тесте зависимость легко заменить моком — как в уроке о JUnit.

Класс сам создаёт свои зависимости
@RestController
public class StudentController {
    // the controller builds its own dependencies:
    // hard to test, and every class gets its own copy
    private final StudentService service =
            new StudentService(new StudentRepository(), 100);
}
Spring передаёт зависимость в конструктор
@RestController
public class StudentController {
    private final StudentService service;

    // Spring creates StudentService once and passes it in
    public StudentController(StudentService service) {
        this.service = service;
    }
}

CRUD API для учеников

Теперь соберём полноценный CRUD API для учеников: создание (Create), чтение (Read), изменение (Update), удаление (Delete). Для простоты репозиторий хранит данные в памяти, в ConcurrentHashMap: запросы приходят в параллельных потоках, поэтому выбрана потокобезопасная коллекция.

Java
public record Student(Long id, String name, int score) {}

@Repository
public class StudentRepository {
    private final Map<Long, Student> students = new ConcurrentHashMap<>();
    private final AtomicLong nextId = new AtomicLong(1);

    public List<Student> findAll() {
        return students.values().stream().sorted(Comparator.comparing(Student::id)).toList();
    }

    public Optional<Student> findById(long id) {
        return Optional.ofNullable(students.get(id));
    }

    public Student save(Student s) {
        long id = s.id() != null ? s.id() : nextId.getAndIncrement();
        Student saved = new Student(id, s.name(), s.score());
        students.put(id, saved);
        return saved;
    }

    public boolean deleteById(long id) {
        return students.remove(id) != null;
    }
}
Модель и репозиторий (импорты опущены для краткости)
Java
@Service
public class StudentService {
    private final StudentRepository repository;
    private final int maxScore;

    public StudentService(StudentRepository repository,
                          @Value("${school.max-score:100}") int maxScore) {
        this.repository = repository;
        this.maxScore = maxScore;
    }

    public List<Student> all() { return repository.findAll(); }

    public Student get(long id) {
        return repository.findById(id).orElseThrow(() ->
                new ResponseStatusException(HttpStatus.NOT_FOUND, "Student " + id + " not found"));
    }

    public Student save(Long id, Student s) {
        if (s.score() < 0 || s.score() > maxScore)
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Score must be 0-" + maxScore);
        if (id != null) get(id);                  // 404 if we update a missing student
        return repository.save(new Student(id, s.name(), s.score()));
    }

    public void delete(long id) {
        if (!repository.deleteById(id)) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
    }
}
Сервис: правила проверки и school.max-score из application.properties
Java
@RestController
@RequestMapping("/api/students")
public class StudentController {
    private final StudentService service;

    public StudentController(StudentService service) {
        this.service = service;
    }

    @GetMapping
    public List<Student> all() { return service.all(); }

    @GetMapping("/{id}")
    public Student one(@PathVariable long id) { return service.get(id); }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Student create(@RequestBody Student student) { return service.save(null, student); }

    @PutMapping("/{id}")
    public Student update(@PathVariable long id, @RequestBody Student student) {
        return service.save(id, student);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable long id) { service.delete(id); }
}
Контроллер: каждый метод привязан к одной HTTP-операции
ЗапросЧто делаетУспешный ответ
GET /api/studentsвсе ученики200 OK
GET /api/students/{id}один ученик; 404, если его нет200 OK
POST /api/studentsсоздаёт ученика201 Created
PUT /api/students/{id}обновляет ученика200 OK
DELETE /api/students/{id}удаляет ученика204 No Content

В таблице видны главные соглашения REST: URL называет ресурс существительным во множественном числе (/api/students), а что сделать, говорит HTTP-метод. Поэтому пути вроде /api/getStudents или /api/deleteStudent?id=2 считаются плохим стилем. GET никогда не должен менять данные, PUT заменяет ресурс целиком, а о результате сообщает код статуса — клиент понимает, что произошло, не читая текст ответа.

@RequestMapping задаёт общий путь для всего контроллера. @PathVariable превращает {id} из URL в параметр, а @RequestBody — JSON-тело запроса в объект Student; объект ответа обратно в JSON превращает библиотека Jackson. ResponseStatusException позволяет из любого места вернуть правильный HTTP-статус — например, 404 или 400. @Value("${school.max-score:100}") читает параметр из конфигурации; 100 после двоеточия — значение по умолчанию.

Text
spring.application.name=school-api
server.port=8080
school.max-score=100
src/main/resources/application.properties — настройки приложения

Настройки можно менять, не трогая код: java -jar school.jar --server.port=9090 или через переменную окружения SERVER_PORT. Для разных сред есть профили: например, файл application-dev.properties включается параметром --spring.profiles.active=dev.

Проверка через curl

Пока сервер работает, открой второй терминал. curl — утилита командной строки для отправки HTTP-запросов: -X задаёт метод, -H — заголовок, -d — тело запроса. Сначала создадим двух учеников и прочитаем список:

Terminal
curl -X POST localhost:8080/api/students -H 'Content-Type: application/json' -d '{"name":"Aysel","score":95}'
curl -X POST localhost:8080/api/students -H 'Content-Type: application/json' -d '{"name":"Murad","score":78}'
curl localhost:8080/api/students
curl localhost:8080/api/students/1
Ожидаемый результат
{"id":1,"name":"Aysel","score":95}
{"id":2,"name":"Murad","score":78}
[{"id":1,"name":"Aysel","score":95},{"id":2,"name":"Murad","score":78}]
{"id":1,"name":"Aysel","score":95}
Пример вывода: поля JSON идут в порядке компонентов записи, а id назначает сервер

Теперь изменение, удаление и ошибочные случаи. -w '%{http_code}' печатает только код статуса, а -s -o /dev/null скрывает тело ответа и служебный вывод:

Terminal
curl -X PUT localhost:8080/api/students/2 -H 'Content-Type: application/json' -d '{"name":"Murad","score":84}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:8080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/api/students \
     -H 'Content-Type: application/json' -d '{"name":"Elvin","score":120}'
Ожидаемый результат
{"id":2,"name":"Murad","score":84}
204
404
400
Пример вывода: 204 — удалено, 404 — больше нет, 400 — 120 баллов не принимается
Java
@SpringBootTest
@AutoConfigureMockMvc
class StudentControllerTest {

    @Autowired
    MockMvc mvc;

    @Test
    void createsStudent() throws Exception {
        mvc.perform(post("/api/students")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"name\":\"Aysel\",\"score\":95}"))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.name").value("Aysel"));
    }
}
Автоматический тест: MockMvc имитирует HTTP-запрос без запуска настоящего сервера (импорты опущены)

Главное

  • Spring Boot: @SpringBootApplication + автоконфигурация + встроенный Tomcat; запуск — ./mvnw spring-boot:run.
  • Слои: @RestController → @Service → @Repository; Spring создаёт бины и передаёт их в конструкторы.
  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PathVariable, @RequestBody; преобразование в JSON и обратно делает Jackson.
  • Правильные статусы: 200, 201, 204, 400, 404 — через @ResponseStatus и ResponseStatusException.
  • Настройки хранятся в application.properties и читаются через @Value; API проверяют через curl или MockMvc.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какой аннотацией помечают класс, который принимает HTTP-запросы и возвращает JSON?