JavaRush /Курсы /Java Server /Что мы делали руками в plain Java — и что потом возьмёт н...

Что мы делали руками в plain Java — и что потом возьмёт на себя Spring

Java Server
26 уровень , 4 лекция
Открыта

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.

1
Задача
Java Server, 26 уровень, 4 лекция
Недоступна
Сделать цепочку `route -> parse -> validate -> service -> response` явной
Сделать цепочку `route -> parse -> validate -> service -> response` явной
1
Задача
Java Server, 26 уровень, 4 лекция
Недоступна
Явный composition root для server-mode
Явный composition root для server-mode
1
Опрос
Проверка API, 26 уровень, 4 лекция
Недоступен
Проверка API
Валидация входных запросов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ