- Создавать и запускать проект Spring Boot
- Объяснять слои
@RestController,@Service,@Repositoryи внедрение зависимостей - Писать CRUD API с правильными HTTP-статусами и проверять его через curl
Мобильное приложение — например, электронный школьный дневник — получает оценки с сервера: приложение отправляет HTTP-запрос (GET /api/students), а сервер возвращает JSON. Такой сервер называют REST API. В мире Java самый популярный способ его написать — Spring Boot: несколько аннотаций превращают обычные классы в веб-сервер, а JSON, базы данных и настройки подключаются автоматически.
Создание проекта
- 1Открой start.spring.io
Это официальный генератор проектов (в IntelliJ IDEA Ultimate есть такой же мастер). Выбери Project — Maven или Gradle, Language — Java, Java — 21.
- 2Заполни метаданные
Group —
com.educora, Artifact —school, Name —SchoolApi. Пакет будетcom.educora.school, а главный класс —SchoolApiApplication. - 3Добавь зависимости
Нажми ADD DEPENDENCIES и выбери Spring Web. В настоящих проектах обычно добавляют ещё Spring Data JPA, драйвер базы и Validation.
- 4Сгенерируй и открой
GENERATE скачивает ZIP-архив. Распакуй его, открой папку в IDE и запусти главный класс.
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.
./mvnw spring-boot:run... Tomcat started on port 8080 (http) with context path '/' ... Started SchoolApiApplication in 1.9 seconds (process running for 2.2)
./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);
}@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: запросы приходят в параллельных потоках, поэтому выбрана потокобезопасная коллекция.
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;
}
}@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@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); }
}| Запрос | Что делает | Успешный ответ |
|---|---|---|
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 после двоеточия — значение по умолчанию.
spring.application.name=school-api
server.port=8080
school.max-score=100src/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 — тело запроса. Сначала создадим двух учеников и прочитаем список:
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}id назначает серверТеперь изменение, удаление и ошибочные случаи. -w '%{http_code}' печатает только код статуса, а -s -o /dev/null скрывает тело ответа и служебный вывод:
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@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.