1. Роль «карты» локального API
Теперь у проекта есть не только CRUD, но и валидационная граница, единый ErrorResponse, 405/Allow, Postman collection и README. В таком состоянии особенно полезно оглянуться на весь путь запроса целиком. Не ради recap-а, а чтобы увидеть: сколько шагов мы тащили руками и почему после такого опыта Spring уже выглядит не магией, а очень понятной экономией рутины. Чем мы сейчас и займемся.
Когда проект становится чуть больше одного endpoint-а, мозг начинает мстить. Сначала он забывает, где именно вы возвращаете 400, потом путает 404 и 405, а потом вы случайно читаете body у GET — и удивляетесь, почему оно «иногда пустое». «Карта» — это не диаграмма ради диаграммы, а способ держать в голове один и тот же предсказуемый маршрут: от входящего HTTP-запроса до JSON-ответа и логов.
Сейчас эта карта нужна не затем, чтобы ещё раз пересказать HttpServer. Она нужна, чтобы связать ручной pipeline со следующими abstractions. Как на кухне: если вы уже один раз сами чистили, резали, варили и мыли посуду, вам намного проще понять ценность техники, которая снимает именно эту рутину, а не обещает «волшебно приготовить всё сама». Иначе получается тот самый борщ на клавиатуре.
У backend-кода есть неприятное свойство: он будет меняться, и чаще всего — в тот день, когда вы не выспались. В такой день «карта» спасает. Ниже соберём полный путь запроса и посмотрим, какие части в нём потом автоматизируют разные слои Spring-стека.
2. Путь запроса: от HttpExchange к ответу
Представьте, что к нам прилетел запрос POST /api/v1/reading-list. У HttpServer нет магии: он не знает про DTO, не знает про ваш ReadingListService, и точно не собирается угадывать, где у вас Location header. Он просто отдаёт вам HttpExchange, а дальше — «держись, Java-разработчик, ты всё делал ради этого момента».
Чтобы видеть весь маршрут целиком, удобно держать в голове вот такую схему:
flowchart TD
A[HttpServer принял запрос] --> B[HttpExchange попал в handler]
B --> C["Routing: method + path"]
C --> D{Путь найден?}
D -- нет --> D404["404 + ErrorResponse"]
D -- да --> E{Метод разрешён?}
E -- нет --> E405["405 + Allow + ErrorResponse"]
E -- да --> F{Нужен body?}
F -- нет --> H[Service call]
F -- да --> G["Read body + JSON parsing"]
G --> G1{JSON распарсился?}
G1 -- нет --> G400[400 MALFORMED_JSON]
G1 -- да --> V[Validation]
V --> V1{Ошибки валидации?}
V1 -- да --> V400[400 VALIDATION_ERROR]
V1 -- нет --> H[Service call]
H --> R[Repository call]
R --> S[Response DTO mapping]
S --> W["Write JSON + status + headers"]
W --> L[Логи: что произошло]
На схеме видно главное: у нас нет одной волшебной операции «обработать запрос». Есть routing, body parsing, validation, service, repository, response writing и логи. Пока вы делаете всё руками, эти шаги очень легко перепутать местами.
И вот это уже не про HttpServer как технологию. Это про реальный web-pipeline, из которого потом вырастают mapping-аннотации, @RequestBody, exception handling, DI-контейнер и bootstrapping приложения. Spring не отменяет эту схему; он просто перестаёт заставлять вас собирать её голыми руками в каждом проекте.
3. Где ручной web-layer начинает просить Spring MVC
Сильнее всего ручная работа видна на HTTP-границе. Вы сами различали 404 и 405, сами собирали Allow, сами доставали path/query, сами читали body, вызывали Jackson, валидировали DTO и сериализовали ErrorResponse обратно в JSON.
Это не значит, что всё это было “зря”. Наоборот: теперь хорошо видно, что именно делает нормальный web framework.
| Ручной шаг | Что вы делали сами | Что потом даёт Spring web-layer |
|---|---|---|
| Routing по method + path | if/switch, allowedMethods, 404/405, Allow | handler mapping и аннотации вроде @GetMapping, @PostMapping, @PatchMapping |
| Чтение входа | HttpExchange, readAllBytes(...), разбор path/query | @RequestBody, @PathVariable, @RequestParam |
| JSON binding | ручной ObjectMapper.readValue(...) и сериализация ответа | встроенные message converters и Jackson-integration |
| Validation и ошибки | свой validator, свои exception types, ErrorMapper | Bean Validation и exception handling механика web-layer |
| Ответ клиенту | руками ставили status, headers, Location, Content-Type, писали JSON | возврат объекта/ResponseEntity и автоматическая запись response |
Полезно заметить одну вещь: Spring MVC не отменяет уже увиденные различения. Сломанный JSON по-прежнему не то же самое, что пустой title. Путь, который существует, но не поддерживает метод, по-прежнему означает 405, а не «ну какой-нибудь 404». Framework не меняет смысл; он делает его короче в коде.
И ещё важнее: Spring MVC не принимает продуктовые решения за вас. Он не решает, когда нужен 409, каким будет errorCode, нужна ли вам строгая семантика PATCH, и что считать конфликтом. Он убирает glue-код, а HTTP-смысл остаётся вашей ответственностью.
4. Почему из explicit wiring вырастает Spring Core
Есть ещё одна боль, уже не transport-уровня. Как только в приложении появились validator, service, repository, jsonWriter, errorMapper, handler и конфиг, кто-то должен собрать их в рабочий object graph.
В plain Java это выглядело примерно так:
ReadingListRepository repository = new InMemoryReadingListRepository();
ReadingListService service = new ReadingListService(repository);
ReadingListValidator validator = new ReadingListValidator();
JsonHttpWriter jsonWriter = new JsonHttpWriter(objectMapper);
ErrorMapper errorMapper = new ErrorMapper();
ErrorWriter errorWriter = new ErrorWriter(errorMapper, jsonWriter, logger);
ReadingListHttpHandler handler = new ReadingListHttpHandler(
objectMapper,
validator,
service,
jsonWriter,
errorWriter,
logger
);
В маленьком проекте такой composition root даже полезен: видно все зависимости, ничего не спрятано, легко понять, кто с кем сотрудничает.
Но как только граф растёт, ручной wiring начинает разрастаться быстрее, чем прикладная логика. Именно здесь и появляется Spring Core. Контейнер берёт на себя создание beans, constructor-based wiring и управление object graph-ом. После plain Java опыта это уже выглядит очень приземлённо: вы руками сделали ту же работу, только без контейнера.
И отсюда же лучше видно границу хорошего DI: бизнес-класс получает зависимости снаружи, а не бегает сам за new и не ищет их по всему приложению. Контейнер полезен не потому, что «где-то там всё само», а потому что он делает уже знакомый вам graph управляемым и прозрачным.
5. Почему из startup/config/logging вырастает Spring Boot
Web-layer и DI — не вся картина. Даже в этом маленьком API вы уже руками держали application.properties, режимы запуска, порт сервера, логирование, README, Postman collection и optional dist-архив. То есть приложение нужно не только написать, но ещё поднять, сконфигурировать, проверить и упаковать.
Эта боль разбросана по нескольким местам сразу:
- старт server-mode и выбор порта;
- чтение properties/env/args;
- wiring Jackson, logging и HTTP-сервера;
- сборка jar и optional ZIP;
- понятный reproducible run story для другого человека.
Именно тут начинает помогать Spring Boot. Не тем, что “сам пишет backend”, а тем, что даёт согласованный baseline: запуск приложения, externalized configuration, встроенный web stack, логирование и разумный operational старт без горы glue-кода вокруг.
После такого baseline вы тратите силы не на то, как в сотый раз склеить запуск, конфиг и инфраструктуру, а на сам контракт и доменную логику. При этом README, Postman collection и smoke-сценарии никуда не исчезают. Boot сокращает рутину старта, но не отменяет дисциплину вокруг сервиса.
6. Что Spring не делает вместо вас
После такого списка легко впасть в другую крайность: будто Spring всё решит сам. Не решит.
- Он не выберет за вас 200 vs 201 vs 204.
- Он не придумает нормальный ErrorResponse за плохо спроектированный API.
- Он не решит, считать ли дубликат externalId конфликтом.
- Он не сделает controller тонким, если вы сами затолкаете туда весь service/repository зоопарк.
- Он не заменит понимание того, почему malformed JSON и пустой title — разные ситуации.
Framework забирает boilerplate. Инженерные решения, границы ответственности и HTTP-semantic thinking остаются вашими. Если вы не понимаете, почему 404 отличается от 405, никакая аннотация не телепортирует это знание в голову.
7. Почему следующий шаг теперь выглядит естественно
После такого plain Java опыта следующий шаг уже не выглядит прыжком в аннотации.
Сначала нужен Spring Core: он закрывает боль ручного object graph и делает wiring приложения нормальной инфраструктурой.
Потом нужен Spring Boot: он собирает запуск, конфигурацию, logging и web-baseline вокруг контейнера в одну удобную платформу.
А на самой HTTP-границе внутри этой платформы работает Spring MVC: mapping, binding, validation и exception handling перестают быть пачкой самодельных helper-ов. @RequestBody уже читается не как магия, а как нормальный ответ на ваш ручной readBody + objectMapper.readValue(...). Точно так же @GetMapping выглядит не как заклинание, а как аккуратная оболочка над тем самым method + path, который вы разбирали руками.
И уже на таком baseline имеет смысл глубже обсуждать web/API design, а не тратить весь фокус на то, как руками читать body, собирать Allow и прокидывать зависимости. Вот поэтому после plain Java опыта естественно идти сначала в Spring Core, а затем в Spring Boot: вы идёте не за магией, а за сокращением очень конкретного списка ручных шагов.
8. Типичные ошибки при переходе к Spring
Ошибка №1: думать, что Spring сам выберет за вас правильную HTTP-семантику.
Если вы сами не различаете 400, 404, 405 и 409, аннотации не спасут. Framework умеет помочь с маршрутизацией и binding-ом, но продуктовый смысл ответа остаётся вашей ответственностью.
Ошибка №2: просто переименовать handle в controller и оставить тот же комбайн внутри.
Если controller и читает запрос, и валидирует всё подряд, и лазает в repository, и сам собирает error bodies, это всё тот же хаос, только под аннотациями. Название класса архитектуру не лечит.
Ошибка №3: воспринимать DI как магию, а не как управление object graph-ом.
Тогда зависимости начинают прятать, создавать на лету или тянуть их из контейнера вручную, и прозрачность снова исчезает. Контейнер полезен ровно потому, что делает уже знакомый graph управляемым.
Ошибка №4: ждать, что Spring Boot отменит README, Postman и smoke-проверки.
Boot сильно упрощает запуск и конфигурацию, но не превращает непроверенный API в хороший API. Контракт всё равно нужно фиксировать снаружи.
Ошибка №5: думать, что framework исправит плохо продуманную границу API.
Если у вас не разведены malformed JSON, validation, not found и conflict, framework просто быстрее вернёт плохо продуманные ответы. Сначала инженерная мысль, потом удобные abstractions.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ