Серия подготовки: к обзору тем Java-собеседования.

Предположим, вы уже писали небольшие программы на Java, но ещё не создавали сервер. Серверное приложение ждёт запросов от браузера или другой программы: например, «забронируй место». Оно проверяет данные, обращается к хранилищу и возвращает ответ. Одновременно таких запросов может быть много. Spring помогает собрать части приложения и организовать их работу.

Ниже — 18 вопросов о Spring и Spring Boot для собеседования. Разберём, кто создаёт объекты, как запрос доходит до Java-метода и почему изменения в базе иногда не сохраняются. Основа примеров — Java 17/21, Spring Framework 6.2 и Spring Boot 3.5: в других версиях отдельные настройки отличаются. Все ситуации учебные; фрагменты показывают часть приложения, а не готовый сервер.

Контейнер и зависимости: как появляются рабочие объекты

1. Что такое IoC, DI и bean?

Класс описывает поля и методы; объект — созданный по этому описанию экземпляр, например new BookingService(...). Сервис бронирования выполняет операцию, а объект хранилища читает и записывает бронирования. Сервису нужна ссылка на хранилище, иначе он не сможет проверить свободное место. Такой нужный другому объект называется зависимостью.

Можно создать хранилище внутри сервиса. Другой путь — сначала создать хранилище, затем передать его конструктору сервиса. Второй способ называется DI, внедрением зависимостей. Он работает и без Spring: в проверке можно передать хранилище с заранее подготовленными данными.

Spring берёт эту сборку на себя. Его контейнер, обычно ApplicationContext, хранит правила создания объектов, создаёт их и соединяет. Управляемый им объект называется bean. Передача управления созданием контейнеру — пример IoC, инверсии управления. Вместо ручной сборки в каждом месте программа сообщает, какие части нужны.

Подвох: пометка класса аннотацией не делает каждый его экземпляр управляемым. Вручную созданный через new объект сам по себе не получает обработку Spring. Интерфейс для DI тоже не обязателен: он полезен, когда нужно описать общие операции нескольких реализаций.

2. Почему зависимости часто передают через конструктор и что происходит при цикле?

Конструктор сразу показывает, без чего объект не может работать. В примере ниже BookingRepository — хранилище, а Clock — источник текущего времени. Spring должен получить оба объекта, передать их конструктору и только после этого отдать готовый сервис другим частям приложения.

Вход метода canBook — номер места slotId. Метод получает время через переданные часы и спрашивает хранилище, доступно ли место в этот момент. Результат — true или false. Реализация хранилища в этом фрагменте не показана: она регистрируется отдельно.

@Service
class BookingService {
    private final BookingRepository repository;
    private final Clock clock;

    BookingService(BookingRepository repository, Clock clock) {
        this.repository = repository;
        this.clock = clock;
    }

    boolean canBook(long slotId) {
        return repository.isAvailable(slotId, Instant.now(clock));
    }
}

@Service помогает Spring найти класс. При единственном конструкторе @Autowired обычно не нужен. Поля final не позволяют заменить полученные ссылки после создания; они не запрещают менять содержимое самих объектов.

Подвох: если для создания A уже нужен B, а для создания B уже нужен A, сборка заходит в круг. Два обязательных конструктора так не завершить. Часто стоит вынести совместную операцию в третий сервис, который вызывает A и B. Простая смена аннотации не объясняет, зачем этим двум частям взаимно управлять друг другом.

3. Чем отличаются @Component, @Service и @Bean? Как выбрать одну из реализаций?

Компонентом называют отдельную часть приложения с определённой работой. @Component помечает класс, объект которого Spring может создать при поиске компонентов. @Service — более конкретная такая пометка: класс выполняет прикладную операцию, например создаёт бронирование. Для работы с базой может понадобиться транзакция — общая операция из нескольких изменений, которые нужно окончательно принять или отменить вместе. Пометка @Service сама её не включает.

@Bean ставят на метод в классе настроек: метод создаёт объект и возвращает его контейнеру. Так удобно подключить библиотечный Clock, в исходники которого вы не добавляете свои аннотации. У контейнера появляется правило создания часов, затем он может передать их сервису.

Допустим, есть два объекта отправки уведомлений: по почте и по SMS. Одного типа параметра уже мало для выбора. @Qualifier уточняет нужный вариант, а @Primary назначает предпочтительный объект для одиночной зависимости. Запросить список всех отправителей — отдельная задача. Имя параметра тоже может участвовать в выборе, поэтому случайное переименование способно изменить поведение.

Подвох: обычный метод, создающий объект, ещё не является правилом @Bean. И успешная регистрация не делает совместное использование объекта несколькими запросами безопасным.

4. Когда можно пользоваться зависимостями и что делает @PostConstruct?

Объект проходит несколько шагов: Spring вызывает конструктор, передаёт предусмотренные зависимости, выполняет подготовку и затем предоставляет его остальному приложению. При внедрении через конструктор его аргументы уже доступны внутри конструктора. Для подготовки после всех предусмотренных внедрений есть @PostConstruct: Spring вызывает помеченный метод, например чтобы проверить настройки или подготовить внутреннюю таблицу значений.

В Spring 6 аннотация находится в jakarta.annotation. При закрытии контейнера можно выполнить и обратное действие — освободить ресурс через предусмотренный метод завершения. Это часть жизненного цикла: от создания до окончания использования объекта.

Подвох: готовность одного объекта не означает готовность всего приложения. Другие компоненты ещё могут создаваться. Кроме того, в этот момент нельзя рассчитывать на вызов своего транзакционного метода через внешнюю обёртку Spring; её устройство разберём ниже. Для объектов prototype, которые контейнер создаёт по запросу, полный цикл освобождения ресурсов автоматически не сопровождается так же, как у общих singleton-объектов.

5. Означает ли singleton один объект на JVM и безопасен ли он для потоков?

Singleton — настройка, при которой контейнер повторно выдаёт один и тот же объект для одного правила создания bean. Это не обещание единственного экземпляра класса во всей Java-программе: другой контейнер или другое правило создания могут дать другой экземпляр.

Представьте два одновременных запроса на бронирование. Их могут обрабатывать разные потоки — независимые последовательности выполняемых команд. Оба обращаются к одному singleton-сервису. Если первый запишет номер пользователя в поле currentUserId, второй может заменить его до окончания первой операции. Тогда действия перемешаются.

Данные отдельного запроса обычно передают аргументами и держат в локальных переменных метода. Общее изменяемое состояние требует отдельного решения: например, согласованного доступа к нему. Подвох: final Map запрещает подменить ссылку, но разрешает менять содержимое карты. Spring не делает каждую операцию неделимой и не устраняет такие гонки. Подробности есть в статье о многопоточности.

6. Даёт ли prototype новую зависимость при каждом вызове метода singleton?

Нет. Scope задаёт, когда контейнер создаёт объект и кому возвращает тот же экземпляр. При prototype новый объект появляется при новом запросе к контейнеру. Но если Spring один раз передал его конструктору singleton, тот сохранил ссылку. Следующий вызов метода сервиса использует эту же ссылку; повторной сборки не происходит.

Если каждой операции нужен собственный объект, можно передать ObjectProvider<T> — объект для явного получения зависимости у контейнера. Тогда сервис запрашивает новый экземпляр именно в нужном месте. Для данных одного HTTP-запроса есть request scope: HTTP — протокол, по которому клиент отправляет серверу запрос и получает ответ.

Подвох: долгоживущий сервис должен получать объект текущего запроса через подходящий механизм, например scope proxy — посредника, выбирающего экземпляр для текущего запроса. Просто сохранить один короткоживущий объект навсегда неправильно. И prototype не спасает, если созданный экземпляр затем вручную разделили между потоками или данные всё равно лежат в общем статическом поле.

Spring Boot и HTTP: что находится за готовыми настройками

7. Чем Spring Boot отличается от Spring Framework?

Spring Framework содержит сами механизмы: контейнер объектов, обработку веб-запросов, управление работой с базой. Spring Boot помогает собрать из них приложение с меньшим количеством ручных настроек. Он предлагает согласованные версии библиотек, наборы зависимостей starters и готовые правила настройки.

Эти правила называют автоконфигурацией. Boot проверяет, какие классы доступны программе, какие параметры заданы и какие объекты уже зарегистрированы. Если условия подходят, он создаёт нужные компоненты. Например, своё определение подключения к базе может заменить соответствующую стандартную настройку.

Подвох: одна новая библиотека не гарантирует нужного объекта: другое условие могло не выполниться. И собственный bean не отключает весь Boot. Проверяют конкретное правило в отчёте условий и смотрят, какие объекты действительно созданы. Правила Boot 3.5 нельзя автоматически приписывать всем крупным версиям.

8. Что включает @SpringBootApplication и почему компонент не найден?

Эта аннотация на основном классе объединяет три задачи: объявить настройки приложения, включить автоконфигурацию и искать ваши компоненты. По умолчанию поиск идёт в пакете основного класса и его подпакетах. Пакет — группа Java-классов с общим началом имени, например example.booking.

Если основной класс находится в example.booking, а сервис — в example.mail, сервис не попадает в эту область поиска. Поэтому сначала проверяют расположение класса. Затем — условия его регистрации: например, профиль, то есть выбранный набор настроек для конкретного запуска. Ещё важно, какой контейнер создаётся: тест отдельной части приложения может намеренно не загружать все сервисы.

Подвох: расширить поиск до всех доступных пакетов — значит рискнуть подключить лишние настройки. Укажите, какие части нужны приложению, и исправьте границу поиска. Поиск ваших классов и подключение готовых правил библиотек — разные действия.

9. Что происходит с HTTP-запросом в Spring MVC и что меняет @RestController?

У HTTP-запроса есть адрес, метод действия, заголовки и иногда тело с данными. Например, POST /bookings с именем гостя просит создать бронирование. Ответ содержит числовой статус, заголовки и тело: статус 201 означает создание, 404 — отсутствие запрошенного ресурса.

В Spring MVC запрос проходит предварительную обработку, в том числе фильтры. Затем DispatcherServlet, общий вход для MVC, находит Java-метод по адресу и виду действия. Spring читает параметры, преобразует тело в Java-объект и вызывает контроллер — класс, принимающий веб-запросы. Контроллер вызывает сервис; возвращённый результат преобразуется обратно в тело HTTP-ответа.

@RestController сообщает, что результаты методов контроллера нужно отдавать как данные ответа: в нём объединены @Controller и @ResponseBody. Часто это JSON — текстовый формат с именами полей и значениями. Но возможны и другие форматы: всё зависит от доступных преобразователей и согласования типа содержимого.

Подвох: текст «not found» со статусом 200 сообщает клиенту успех. Значение имеют и статус, и тело. Сервису лучше поручить само бронирование, оставив контроллеру чтение запроса и оформление ответа.

10. Делает ли @Valid входные данные правильными и где обрабатывать ошибки?

Сначала нужно описать, что считать допустимым вводом. DTO — объект для передачи данных, здесь номер места и имя гостя. Аннотации @Positive и @NotBlank требуют положительного номера и непустого имени. @Valid просит выполнить заданные проверки; без ограничений ему нечего проверять.

Ниже в запрос приходит тело с этими полями. @RequestBody указывает прочитать его в CreateBooking. Если проверки прошли, контроллер передаёт объект сервису; тот создаёт бронирование. Контроллер возвращает полученный BookingView в ответе со статусом 201.

record CreateBooking(
    @Positive long slotId,
    @NotBlank String guestName
) {}

@PostMapping("/bookings")
ResponseEntity<BookingView> create(
        @Valid @RequestBody CreateBooking request) {
    BookingView result = bookingService.create(request);
    return ResponseEntity.status(201).body(result);
}

Фрагмент требует импортов Jakarta Validation и Spring MVC, библиотеки, выполняющей проверки, и заранее переданного bookingService. Класс record здесь компактно описывает неизменяемый набор полей запроса. Проверка положительного номера ещё не доказывает, что место существует и свободно: это выясняется в хранилище с учётом одновременных бронирований.

Подвох: в MVC 6.2 ошибка проверки одного DTO и ошибка ограничений на параметрах самого метода могут иметь разные типы исключений. @RestControllerAdvice обозначает общий обработчик ошибок для контроллеров; он должен учитывать оба используемых случая. ProblemDetail задаёт согласованный вид сообщения, но подходящий статус и смысл ошибки выбирает приложение.

Proxy и транзакции: где проходит граница вызова

Для оставшихся вопросов понадобятся два понятия. Транзакция объединяет изменения в базе в одну операцию: например, занять место и создать бронирование. При commit база окончательно принимает изменения; при rollback отменяет изменения этой транзакции. Выполнение одной команды внутри ещё не означает, что всё уже принято.

Proxy, или прокси, — объект-посредник перед вашим сервисом. Вместо прямой ссылки на сервис вызывающая часть получает ссылку на посредника. Тот может начать транзакцию, вызвать метод сервиса и после его возврата решить вопрос завершения. Обработку до и после метода выполняет транзакционный перехватчик.

Как проследить транзакционный вызов
  1. Вызов

    Другой объект обращается к ссылке, полученной от Spring.

  2. Посредник

    Прокси начинает транзакцию или присоединяется к существующей.

  3. Сервис

    Метод выполняет действия с данными и возвращается либо выбрасывает ошибку.

  4. Завершение

    Владелец транзакции фиксирует общие изменения или откатывает их.

11. Как устроен Spring AOP и что нельзя перехватить?

AOP — способ добавить общую обработку к вызовам методов, не повторяя её в каждом методе. Например, управление транзакцией можно вынести в описанного выше посредника. Метод сервиса занимается бронированием, а посредник — началом и завершением работы с базой.

Spring умеет создавать посредника через интерфейс, используя JDK dynamic proxy, либо через наследника вашего класса. Во втором случае используется CGLIB: созданный подкласс переопределяет доступные методы, чтобы добавить обработку. Отсюда ограничения: нельзя унаследовать final-класс, переопределить final-метод или таким образом перехватить private-метод. У JDK proxy доступны операции его интерфейсов.

Подвох: Spring не всегда использует один вид прокси. Выбор зависит от настройки; Boot и чистый Framework могут иметь разные значения по умолчанию. Есть и AspectJ weaving — добавление обработки в байткод классов. Его нельзя смешивать с моделью внешнего посредника: пути вызова у них различаются.

12. Почему @Transactional не срабатывает при вызове своего метода?

@Transactional сообщает Spring, какие правила применить к транзакции. В обычном режиме с прокси они применяются при входе через посредника. Если метод объекта вызывает другой метод этого же объекта, новый заход через прокси не происходит. Поэтому собственная аннотация внутреннего метода не создаёт дополнительную транзакционную границу.

На вход importAll ниже приходит список запросов бронирования. Цикл передаёт каждый элемент saveOne, тот вызывает вставку в хранилище. Автор рассчитывает на отдельную транзакцию для каждого элемента из-за REQUIRES_NEW. Но оба метода принадлежат одному объекту, поэтому такого результата аннотация здесь не обеспечивает.

public void importAll(List<CreateBooking> requests) {
    for (CreateBooking request : requests) {
        saveOne(request); // вызов того же объекта
    }
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveOne(CreateBooking request) {
    repository.insert(request);
}

Если внешняя транзакция уже была, вставки могут работать в ней. Если её не было, этот внутренний вызов сам по себе её не создаёт. Когда отдельные транзакции действительно нужны, операцию можно поместить в другой сервис и обращаться к нему через ссылку от Spring.

Подвох: теперь успешные элементы могут сохраниться, даже если следующий элемент не обработан. Прежде чем исправлять путь вызова, нужно решить, разрешён ли такой частичный импорт.

13. При каком исключении транзакция откатывается?

Допустим, сервис занял место, но затем не смог создать бронирование. Чтобы оба изменения отменились, ошибка должна привести к откату общей транзакции. При стандартных правилах Spring вышедшие из метода через перехватчик RuntimeException и Error запускают откат.

Checked exception — проверяемое исключение, которое Java требует обработать или указать в throws. Его выход по умолчанию не требует отката. Правило можно изменить для метода через rollbackFor = BookingRejectedException.class. В Spring 6.2 можно изменить и общую политику на ALL_EXCEPTIONS.

Подвох: наличие throw где-то в коде не определяет результат. Нужно узнать, какое исключение увидел перехватчик, какие правила настроены и какой менеджер транзакций управляет ресурсом. Менеджер — компонент, который выполняет начало и завершение транзакции. Тип checked сам по себе не означает, что по смыслу ошибки изменения надо сохранить.

14. Если исключение поймали, транзакция точно сохранится?

Нет. Если метод сам поймал ошибку и нормально вернулся, внешняя обёртка может не узнать об исключении. Но база или уже отработавший внутренний перехватчик могли поставить отметку rollback-only: эту транзакцию теперь разрешено только откатить.

Разберём последовательность. Первый сервис начал транзакцию и вызвал второй через его прокси. Второй присоединился к той же транзакции и выбросил RuntimeException. Его перехватчик поставил отметку отката. Первый сервис поймал исключение и продолжил работу. Когда он закончит и попытается зафиксировать изменения, Spring может сообщить UnexpectedRollbackException: вместо ожидаемого сохранения произошёл откат.

Подвох: catch и запись ошибки в журнал не убирают эту отметку. Если журнал должен сохраниться даже после неудачного бронирования, его независимую транзакцию проектируют отдельно. Тогда запись об ошибке может остаться, хотя основная операция целиком отменена.

15. Чем REQUIRED, REQUIRES_NEW и NESTED отличаются на практике?

Это настройки propagation: что делать с транзакцией при вызове очередного метода через прокси. REQUIRED использует уже начатую транзакцию либо создаёт новую, если её нет. REQUIRES_NEW начинает независимую, временно приостанавливая участие внешней. NESTED при поддержке ресурса и менеджера создаёт точку сохранения внутри общей транзакции.

Физическая транзакция — реальная операция базы между её началом и завершением. Аннотации на нескольких методах не обязательно означают несколько таких операций. Для внешнего сервиса бронирования и вызываемого им второго сервиса варианты выглядят так:

Границы транзакций при внешнем вызове через proxy
РежимЧто делает базаЧто будет при ошибке
REQUIREDОба метода обычно работают в одной транзакции.Отметка отката внутреннего метода влияет на всю операцию.
REQUIRES_NEWВнутренний метод получает отдельную транзакцию.Его успешные изменения могут остаться после отката внешнего метода.
NESTEDВ общей транзакции создаётся точка сохранения, если это поддержано.Можно откатить часть работы к этой точке; отдельного окончательного сохранения нет.

Подвох: для независимой транзакции часто нужно второе соединение с базой, пока первое удерживается внешней. Соединения выдаёт ограниченный пул; при нехватке вызовы начинают ждать. NESTED тоже не гарантирован для любого JPA-менеджера. Выбор зависит от нужного результата и возможностей подключения.

16. Что гарантируют readOnly и isolation? Откатится ли внешний HTTP-вызов?

readOnly сообщает, что операция предназначена для чтения. Менеджер и драйвер базы могут использовать эту настройку для оптимизации или ограничений; она не заменяет права доступа и не означает одинаковый запрет записи во всех базах.

isolation, уровень изоляции, задаёт правила того, какие изменения параллельных транзакций видит операция. Он имеет значение при создании физической транзакции. Если внутренний метод лишь присоединился к существующей, его аннотация не создаёт новый снимок данных. Точные гарантии зависят от базы; они разобраны в статье о JDBC, JPA и транзакциях.

Подвох: транзакция вашей базы не управляет чужим почтовым или платёжным сервисом. Если письмо уже отправлено по HTTP, откат бронирования не вернёт его назад. Один из подходов — сохранить в той же базе намерение отправить письмо, а после commit выполнить отправку с защитой от повторной обработки. Сначала перечисляют все участвующие системы, затем выбирают способ согласования их действий.

Конфигурация и фоновые операции

17. Когда удобнее @ConfigurationProperties, чем несколько @Value?

Приложению нужны настройки: например, сколько минут держать место за пользователем. @Value удобно получать отдельное значение. Когда связанных параметров несколько, @ConfigurationProperties собирает их в объект с понятными именами и типами. Его нужно зарегистрировать, а проверку допустимых значений подключить отдельно.

Первый фрагмент описывает BookingProperties с целочисленным holdMinutes. Префикс booking связывает объект с группой настроек; @EnableConfigurationProperties регистрирует его. @Validated вместе с @Min(1) требует значение не меньше единицы.

@Validated
@ConfigurationProperties(prefix = "booking")
public record BookingProperties(@Min(1) int holdMinutes) {}

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(BookingProperties.class)
class BookingConfiguration {}

Вход для такой сборки — следующая строка локального файла application.properties. Boot читает число 10, преобразует его в int и передаёт объекту настроек. Полученный результат — holdMinutes, равное 10; сервис может использовать его без разбора строк.

booking.hold-minutes=10

Подвох: для выполнения ограничений нужен подключённый validation provider — библиотека проверки. При нуле ожидается ошибка подготовки настроек вместо запуска с бессмысленным сроком. Ещё проверьте, какой источник параметров имеет приоритет: наличие типизированного объекта не гарантирует, что использована именно увиденная вами строка файла.

18. Переносится ли @Transactional в @Async и кто обрабатывает ошибку фонового метода?

@Async позволяет передать метод на выполнение в другой поток, чтобы вызывающий код мог продолжить работу. Задачи принимает executor: компонент с потоками и очередью. Нужно настроить вместимость очереди, завершение задач и поведение при нехватке ресурсов.

Обычная транзакция Spring привязана к выполняющему её потоку. Если сервис передал отправку отчёта в фон, транзакция сервиса автоматически туда не переносится. Фоновый метод при правильном вызове через прокси может начать свою транзакцию. Её завершение — отдельное событие: отправивший задачу метод мог закончиться раньше.

Если возвращается CompletableFuture, это объект будущего результата; он может завершиться ошибкой, которую нужно получить и обработать. Для void-метода предусмотрен отдельный обработчик непойманных ошибок. Обычный try/catch вокруг отправки задачи не ловит все ошибки её будущего выполнения.

Подвох: вызов своего @Async-метода тоже может обойти прокси. И объект, загруженный ORM — библиотекой сопоставления Java-объектов со строками базы, — не получает действующее управление в новом потоке автоматически: попытка дочитать ещё не загруженные данные может не сработать. Безопаснее передать необходимые данные и определить самостоятельную фоновую операцию.

Как проверить понимание перед собеседованием

Нарисуйте обработку бронирования: пришёл HTTP-запрос, контроллер прочитал данные, сервис занял место и создал запись, база приняла изменения, клиент получил ответ. Отметьте, какие объекты создал Spring, где стоят посредники и кто завершает транзакцию.

Теперь изменяйте по одному условию: метод вызвал сам себя; возникло checked exception; ошибка поймана снаружи; внутренняя операция сохранилась отдельно; действие передано в фон. Каждый раз объясняйте, какие изменения останутся и когда об ошибке узнает вызывающая часть. Такая последовательность помогает отвечать по смыслу, даже если вопрос сформулирован непривычно.