Собеседование Backend-Java-разработчика: вопросы и где искать ответы. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Когда-то я проходил серию собеседований на Backend-Java-разработчика и записывал вопросы себе на будущее, чтобы потом можно было пробежаться и освежить память. Подумалось, что, вероятно, данный сборник будет полезен не только мне, поэтому сдул с него пыль, набросал ответов и делюсь с сообществом. На оригинальность и исключительность не претендую: подобные статьи уже были и на Хабре, и много где ещё — в конце (во второй части) приведу список ссылок, чтобы шпаргалка была максимально полной.


Точно установить сложность всех вопросов не берусь — на разном уровне их потребуется раскрыть с различной степенью подробности. Я написал ответы где-то на плюс-минус middle, щедро приправив ссылками для дальнейших изысканий. На самые популярные вопросы сразу перенаправляю в источники с готовыми ответами. Заодно посмотрим по ссылкам в статье, насколько Хабр может помочь в подготовке к собесам.


Текста получилось много, поэтому пришлось разбить на две части. В первой поговорим про Java и Spring, а обо всём остальном — во второй. Вторая часть тут


TL;DR

GitHub-репозиторий с полной шпаргалкой тут, а Хабр всё ещё торт.


Вопросы


Java


1. Вопросы про Equals, hashcode и их связь с HashMap.

Опишите Контракт. Далее разговор переходит к устройству HashMap. Как устроена внутри? А происходит в случае возникновения коллизии? Назовите алгоритмические сложности поиска, чтения, удаления из элемента мапы. А что если ключ — это массив байтов? А может быть так, что мы положим элемент в мапу, а потом не найдем? Обсасывают бедную мапу со всех сторон. Самая популярная тема для обсуждения. Спрашивают все. Абсолютно все.


Контракт equals и hashcode:


  1. Для одного и того же объекта хэшкоды одинаковые.
  2. Если объекты равны по equals, то и хэшкоды одинаковые.
  3. Если же хэшкоды равны, то объекты могут быть не равны по equals (коллизия).
  4. Если хэшкоды разные, то и объекты разные.

В статье на Хабре это подробно разобрано, если кому-то покажется мало.


Про HashMap и вопросы по ним есть несколько отличных статей на Хабре (в картинках, с дополнениями из Java 8, а тут вопросы-ответы про коллекциям). Кроме того, можно посмотреть исходный код в вашей любимой IDE. Можете сделать себе конспект и повесить на стену :)


2. Вопросы про списки: какие есть, алгоритмическая сложность, какой брать для вставки в середину, в конец, в огурец.

По сути это вопрос про ArrayList vs LinkedList. Опять же, заезженная пластинка, разобранная на Хабре — вопросы-ответы про коллекциям, ArrayList в картинках, LinkedList в картинках, Что «под капотом» у LinkedList. Посмотреть исходники тоже полезно. Например, можно понтануться тем, что вставка в середину в ArrayList выполняется с помощью нативно реализованной функции System.arraycopy, поэтому не всё так плохо, как могло бы быть в этом случае.


3. Перечислите методы класса `Object`.

Этот вопрос далее перетекает либо в обсуждение HashMap, либо в основы многопоточного программирования на Java.


Чтобы вы вдруг внезапно не забыли каких-то методов (как это сделал я :D), привожу вам список и ссылку на JavaDoc:


  • clone
  • equals
  • finalize (Deprecated)
  • getClass
  • hashCode
  • toString
  • notify
  • notifyAll
  • wait

Также можно почитать, что там вообще есть в исходниках Object в статье на Хабре.


4. Расскажите про методы `wait`, `notify`, `notifyAll` и ключевое слово `synchronized`.

В принципе, статьи на Baeldung должно хватить. Лучше, конечно, пописать код с использованием wait, notify, notifyAll и synchronized руками. Также можно почитать официальный туториал от Oracle по Concurrency в Java.
Но если хотите пойти глубже, то хаброписатели опять спешат на помощь — тут. А также Java Language Specification, раздел 17.1 и 17.2.


5. JMM. Зачем нужно volatile. Популярный вопрос.

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


Кроме того, абсолютно не будет лишним посмотреть доклад Романа Елизарова по теоретическому минимуму JMM.


Если совсем нет времени, то можно пробежаться по небольшой статейке по JMM. Если есть время и интерес, тогда углубляемся в тему через статью на Хабре. А ещё на Хабре есть неплохой перевод статьи "Многопоточность. Java-модель памяти": часть 1 и часть 2.


Несомненным источником истины является Java Language Specification, раздел 17.4.


Также ответ на этот вопрос можно прочитать на itsobes.ru.
Не лишним будет ознакомиться с вопросом на JVM-уровне в статье How ‘volatile’ works on JVM level? на Medium.


6. Сборка мусора. Как работает? Какие сборщики знаете? Какие есть области памяти в JVM? Что будет с двумя или более объектами, которые ссылаются только друг на друга, но больше не на кого и никому не нужны - как с ними поступит сборщик и как именно это будет делать?

Память в Java делится на Stack и Heap.


Stack — это область памяти, доступ к которой организован в порядке LIFO. Сюда помещается frame — локальные переменные и параметры вызываемого метода. Здесь можно сразу уточнить, что примитивы хранятся на стеке, а вот у объектов тут хранится только ссылка, а сами объекты в Heap. НО, благодаря Escape Analysis и скаляризации из Java 6, объекты, которые являются исключительно локальными и не возвращаются за пределы выполняемого метода, также сохраняются в стеке. Про Escape Analysis и скаляризацию есть доклад (видео или текст) Руслана Черемина, или ещё тут.


Frame создаётся и кладётся на Stack при вызове метода. Frame уничтожается, когда завершается его вызов метода, как в случае нормального завершения, так и в результате выброса неперехваченного исключения. У каждого потока есть свой Stack и он имеет ограниченный размер. Подробности можно посмотреть в JVM Specification.


Теперь про Heap и сборку мусора. Тут большинство просто хочет услышать то, что написано в одном из сообщений telegram-канала Senior's Blog. Процитирую основную часть здесь:


Heap делится на два поколения:

  1. Young Generation
    1. Eden
    2. Survivor 0 и Survivor 1

  2. Old Generation
    1. Tenured



Young разделен на три части: Eden, Survivor 0 и Survivor 1. В Eden создаются все новые объекты. Один из Survivor регионов всегда пустой. При полном заполнении региона Eden запускается малая сборка мусора, и все живые объекты из Eden и Survivor перемещаются в пустой Survivor, а Eden и использующийся Survivor полностью очищается. Это делается для уменьшения фрагментации памяти. Объекты, которые несколько раз перемещаются между Survivor, затем помещаются в Tenured.

В случае, когда места для новых объектов не хватает уже в Tenured, в дело вступает полная сборка мусора, работающая с объектами из обоих поколений. При этом старшее поколение не делится на подрегионы по аналогии с младшим, а представляет собой один большой кусок памяти. Поэтому после удаления мертвых объектов из Tenured производится не перенос данных (переносить уже некуда), а их уплотнение, то есть размещение последовательно, без фрагментации. Такой механизм очистки называется Mark-Sweep-Compact по названию его шагов (пометить выжившие объекты, очистить память от мертвых объектов, уплотнить выжившие объекты).

Бывают еще объекты-акселераты, размер которых настолько велик, что создавать их в Eden, а потом таскать за собой по Survivor’ам слишком накладно. В этом случае они размещаются сразу в Tenured.

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

Существуют следующие реализации GC:


  • Serial Garbage Collector
  • Parallel Garbage Collector. По умолчанию в Java 8.
  • Concurrent Mark Sweep (CMS). Deprecated с Java 9.
  • Garbage-First (G1). По умолчанию с Java 9. Есть видео от Владимира Иванова. Ещё можно почитать о G1 в туториале по настройке от Oracle.
  • Z Garbage Collector (ZGC)
  • Shenandoah Garbage Collector. Есть в наличии с Java 12. Тут, конечно же, нужно смотреть доклады Алексея Шипилёва — раз, два

Если совсем кратко, то можно ознакомиться тут и вот тут.
Почитать на Хабре подробнее про сборку мусора в Java можно в серии статей "Дюк, вынеси мусор!" от alygin — раз, два, три.
Послушать про работу с памятью и сборщиках мусора можно в выпуске 74 подкаста Podlodka с Алексеем Шипилёвом в гостях. Обязательно загляните в полезные ссылки к выпуску.


Ещё можно вспомнить про:


  • Method Area — область памяти с информацией о классах, включая статические поля. Одна на всю JVM.
  • Program Counter (PC) Register — отдельный на каждый поток регистр для хранения адреса текущей выполняемой инструкции.
  • Run-time Constant Pool — выделяется из Method Area для каждого класса или интерфейса. Грубо говоря, хранит литералы. Подробнее.
  • Native Method Stack — собственно Stack для работы нативных методов.

Дополнительно про gc и саму JVM (ох, бохатая и животрепещущая тема):


  • На богомерзком medium в картинках
  • Перевод статьи Алексея Шипилёва на Хабре — Самодельный сборщик мусора для OpenJDK
  • Отрывок из Java Garbage Collection Handbook про reachability algorithm
  • Статейка на Википедии про Tracing garbage collection
  • Доклад Simone Bordet про ZGC и Shenandoah
  • JVM Anatomy Quarks — серия постов от Алексея Шипилёва про устройство JVM. Это просто клад, за который будут воевать пришельцы на постапокалиптическую Землю, чтобы разгадать, как работает эта чёртва шайтан-виртуал-машина и промышленный код почивших человеков.
  • Understanding JVM Internals — вот прям неплохо и с картинками.

7. Что такое Executor и ExecutorService, Thread pool и зачем нужны?

Создавать и убивать потоки — дорого. Давайте создадим N потоков (Thread pool) и будем их переиспользовать. А давайте. Вот тут описано развёрнуто.


Executor (void execute​(Runnable command) — вот и весь интерфейс) и ExecutorService (уже покруче, может запускать Callable и не только) — грубо говоря, интерфейсы выполняторов параллельных задач. А реализуют их различные выполняторы на пулах потоков. Экземпляры готовых конкретных выполняторов можно получить с помощью класса Executors. Если смелый-умелый и зачем-то надо, то можно и самому реализовать, конечно.


Также подробнее можно почитать:


  • Cтатью на Хабре
  • На Baeldung: раз и два
  • Официальный туториал от Oracle

8. Могут ли быть в Java утечки памяти и когда? Как обнаружить причину? Как снять heap-dump?

Могут. Профилировать. Снимать heap-dump, например с помощью jmap, загружать в memory profiler (например в VisualVM)


Подробнее:


  • Доступно изложено на Baeldung или то же самое тут, но на языке родных осин.
  • Ещё тут
  • здесь
  • Старенькая статья на Хабре про типичные случаи утечки памяти в Java
  • Диагностика утечек памяти в Java на Хабре
  • Ищем утечки памяти с помощью Eclipse MAT на Хабре
  • Устранение утечек памяти посредством слабых ссылок
  • Устранение утечек памяти посредством гибких ссылок
  • Бывают ли в Java утечки памяти?
  • Диагностика OutOfMemoryError подручными средствами
  • Java VisualVM — Browsing a Heap Dump
  • VisualVM: мониторинг, профилировка и диагностика Java-приложений
  • Доклад Андрея Паньгина Всё, что вы хотели знать о стек-трейсах и хип-дампах
  • Different Ways to Capture Java Heap Dumps
  • Analyze memory snapshots с помощью IntelliJ IDEA
  • Analyze objects in the JVM heap с помощью IntelliJ IDEA

9. Что внутри параллельных стримов? На каком пуле работают параллельные стримы и в чем его особенность?

По умолчанию parallel stream использует ForkJoinPool.commonPool размером Runtime.getRuntime().availableProcessors() — 1. Common pool создаётся статически при первом обращении к ForkJoinPool и живёт до System::exit (игнорирует shutdown() или shutdownNow()). Когда некий поток отправляет задачу в common pool, то pool может использовать его же в качестве воркера. Common pool один на всё приложение. Можно запустить stream на отдельном ForkJoinPool — завернуть параллельный stream в Callable и передать на вход методу submit созданного ForkJoinPool. Этот трюк работает благодаря методу fork() из ForkJoinPool (тут подробности).


Сам по себе ForkJoinPool представляет реализацию ExecutorService, выполняющую ForkJoinTask (RecursiveAction и RecursiveTask). Данный pool создан для упрощения распараллеливания рекурсивных задач и утилизации породивших подзадачу потоков. ForkJoinPool использует подход work stealing — у каждого потока есть его локальная очередь задач, из хвоста которой другие потоки могут тырить себе задачи, если у них закончились свои. Украденная задача делится и заполняет очередь задач потока.


Подробнее:


  • В статьях Stream API & ForkJoinPool и Fork/Join Framework в Java 7 на Хабре
  • Посмотреть доклад Алексея Шипилёва ForkJoinPool в Java 8
  • В статьях Guide to the Fork/Join Framework in Java и Guide to Work Stealing in Java на Baeldung
  • JavaDoc к ForkJoinPool
  • В статье Think Twice Before Using Java 8 Parallel Streams на DZone
  • В статье Java Parallel Streams Are Bad for Your Health! в блоге JRebel
  • С примерами и картинками — Java Parallel Stream
  • С графиками в How does the Fork/Join framework act under different configurations?
  • Как работают параллельные стримы?

10. Какие бывают операции в стримах? Напишите стрим?

Есть 2 вида операций в Java Stream:


  • Промежуточные (Intermediate) — filter, map, sorted, peek и т.д. Возвращают Stream.
  • Терминальные (Terminal) — collect, forEach, count, reduce, findFirst, anyMatch и т.д. Возвращают результат стрима и запускают его выполнение.

Кроме того, будет полезно ознакомиться с содержимым пакета java.util.stream и доступными коллекторами из Collectors.


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


  • Java8 Code Kata
  • Experience-Java-8
  • Может быть даже курс — Java. Functional programming

Почитать подробнее про стримы лучше в Java Doc, но можно и в статьях:


  • Java 8 Stream API
  • The Java 8 Stream API Tutorial
  • Полное руководство по Java 8 Stream API в картинках и примерах. Тут не просто в картинках, а в анимациях!
  • Шпаргалка Java программиста 4. Java Stream API
  • Java Stream API: что делает хорошо, а что не очень
  • Пишем свой Spliterator

Посмотреть:


  • На letsCode — Java Stream API: функционально, модно, молодёжно!
  • Лекция в CSCenter от Тагира Валеева — Лекция 8. Stream API
  • Доклад Тагира Валеева на Joker 2016 — Причуды Stream API

11. Что можно положить и достать из List<? extends Number>, а что с List<? super Number>? Что такое ковариантность, контрвариантность, инвариантность?

Тут речь пойдёт про PECS — Producer extends, Consumer super (Joshua Bloch, Effective Java). А также вариантность — перенос наследования исходных типов на производные от них типы (контейнеры, делегаты, обобщения).


Ковариантность (covariance) — перенос наследования исходных типов на производные от них типы в прямом порядке.
Переменной типа List<? extends T> разрешено присвоить экземпляр списка, параметризованного T или его подклассом, но не родительским классом. В список типа List<? extends T> нельзя добавить никакой объект (можно только null) — нельзя гарантировать какого именно типа экземпляр списка будет присвоен переменной, поэтому нельзя гарантировать, что добавляемый объект разрешён в таком списке. Однако, из списка можно прочитать объект и он будет типа T и экземпляром либо T, либо одного из подклассов T.
Соответственно, List<? extends Number> можно присвоить ArrayList<Number> или ArrayList<Integer>, но не ArrayList<Object>. Метод get возвращает Number, за которым может скрываться экземпляр Integer или другого наследника Number.
Массивы также ковариантны.
Переопределение методов, начиная с Java 5, ковариантно относительно типа результата и исключений.


List<?> аналогичен List<? extends Object> со всеми вытекающими.


Контрвариантность (contravariance) — перенос наследования исходных типов на производные от них типы в обратном порядке.
Переменной типа List<? super T> разрешено присвоить экземпляр списка, параметризованного T или его родительским классом, но не его подклассом. В список типа List<? super T> можно добавить экземпляр T или его подкласса, но нельзя добавить экземпляр родительских для T классов. Из такого списка с гарантией можно прочитать только Object, за которым может скрываться неизвестно какой его подкласс.
Соответственно, List<? super Number> можно присвоить либо ArrayList<Number>, либо ArrayList<Object>, но не список наследников Number(т.е. никаких ArrayList<Integer>). Можно добавить экземпляр Integer или Double (можно было бы Number, но он абстрактный), но нельзя — Object. Метод get возвращает Object — точнее сказать нельзя.


Инвариантность — наследование исходных типов не переносится на производные.
Переменной типа List<T> разрешено присвоить экземпляр списка, параметризованного только T. В список можно добавить экземпляр T или его подкласса. Список возвращает T, за которым может скрываться экземпляр его подкласса.
Соответственно, List<Number> можно присвоить ArrayList<Number>, но не ArrayList<Integer> или ArrayList<Object>. Можно добавить экземпляр Integer или Double (можно было бы Number, но он абстрактный), но нельзя — Object. Метод get возвращает Number, за которым может скрываться экземпляр Integer или другого наследника Number.


Подробнее:


  • На Хабре: Погружаемся в Generics, Используем в API, изучаем вариантность в программировании
  • Посмотреть доклад Александра Маторина Неочевидные Дженерики
  • В одном из ответов на вопрос Generics FAQ
  • Как ограничивается тип generic параметра?
  • Что такое ковариантность и контравариантность?
  • В одном из объяснений на StackOverflow: раз, два, три
  • Ковариантность и контравариантность с точки зрения математики, теории категорий и программирования
  • Ковариантность и контравариантность в Wikipedia
  • Wildcards в официальном туториале Oracle

12. Как работает ConcurrentHashMap?

ConcurrentHashMap — это потокобезопасная мапа (карта, словарь, ассоциативный массив, но тут и далее просто "мапа"), у которой отсутствуют блокировки на всю мапу целиком.
Особенности реализации:


  • Поля элемента мапы (Node<K,V>) val (значение) и next(следующее значение по данному ключу в цепочке или дереве), а также таблица бакетов (Node<K,V>[] table) объявлены как volatile
  • Для операций вставки первого элемента в бакет используется CAS — алгоритм, а для других операций обновления в этой корзине (insert, delete, replace) блокировки
  • Каждый бакет может блокироваться независимо путём блокировки первого элемента в корзине
  • Таблице бакетов требуется volatile/atomic чтения, запись и CAS, поэтому используются intrinsics-операции (jdk.internal.misc.Unsafe)
  • Concurrent resizing таблицы бакетов
  • Ленивая инициализация таблицы бакетов
  • При подсчёте количества элементов используется специальная реализация LongAdder

В результате имеем:


  • Извлечение значения возвращает последний результат завершенного обновления мапы на момент начала извлечения. Или перефразируя, любой non-null результат, возвращаемый get(key) связан отношением happens-before со вставкой или обновлением по этому ключу
  • Итераторы по ConcurrentHashMap возвращают элементы отображающие состояние мапы на определённый момент времени — они не бросают ConcurrentModificationException, но предназначены для использования одним потоком одновременно
  • Нельзя полагаться на точность агрегирующих методов (size, isEmpty, containsValue), если мапа подвергается изменениям в разных потоках
  • Не позволяет использовать null, который однозначно воспринимается как отсутствие значения
  • Поддерживает потокобезопасные, затрагивающие все (или многие) элементы мапы, операции — forEach, search, reduce (bulk operations). Данные операции принимают на вход функции, которые не должны полагаться на какой-либо порядок элементов в мапе и в идеале должны быть чистыми (за исключением forEach). На вход данные операции также принимают parallelismThreshold — операции будут выполняться последовательно, если текущий размер мапы меньше parallelismThreshold. Значение Long.MAX_VALUE сделает операцию точно последовательной. Значение 1 максимизирует параллелизм и утилизацию ForkJoinPool.commonPool(), который будет использоваться для параллельных вычислений

На Хабре есть несколько устаревшая статья — будьте внимательны и осторожны с java 8 произошли изменения. Класс Segment<K,V> максимально урезан и сохранён только для обратной совместимости при сериализации, где и используется. concurrencyLevel также оставлен лишь для обратной совместимости и теперь служит в конструкторе только для увеличения initialCapacity до количества предполагаемых потоков-потребителей мапы:


if (initialCapacity < concurrencyLevel)   // Use at least as many bins
    initialCapacity = concurrencyLevel;   // as estimated threads

Есть более современная статья с примером реализации ConcurrentMap. Также можно почитать гайд по ConcurrentMap на Baeldung.


13. Что такое Xmx и Xms, Xss?

JVM стартует с Xms количеством выделенной под heap памяти и максимально может увеличить её до значения Xmx.
Xss флаг определяет размер выделенной под стек памяти.
Общий вид:


java -Xmx<количество><единица измерения>

Можно использовать различные единицы измерения, например килобайты (k), мегабайты (m) или гигабайты (g).
Пример:


java -jar my.jar -Xms256m -Xmx2048m

Подробнее:


  • В статье на Mkyong
  • В статье Tuning JVM от Orcale
  • В статье Factors Affecting Garbage Collection Performance от Oracle
  • Список всех X опций CLI

14. Как работают Атомики?

Атомарная операция — это операция, которая выполняется полностью или не выполняется совсем, частичное выполнение невозможно.
Атомики — это классы, которые выполняют операции изменения своего значения атомарно, т.о. они поддерживают lock-free thread-safe использование переменных. Достигается это с помощью алгоритма compare-and-swap (CAS) и работает быстрее, чем аналогичные реализации с блокировками. На уровне инструкций большинства процессоров имеется поддержка CAS.


В общем случае работу Атомиков можно описать следующим образом. Атомик хранит некоторое volatile значение value, для изменения которого используется метод compareAndSet(current, new), поэтому предварительно читается текущее значение — current. Данный метод с помощью CAS изменяет значение value только в том случае, если оно равно ожидаемому значению (т.е. current), прочитанному перед запуском compareAndSet(current, new). Если значение value было изменено в другом потоке, то оно не будет равно ожидаемому. Следовательно, метод compareAndSet вернет значение false. Поэтому следует повторять попытки чтения текущего значения и запуска с ним метода compareAndSet(current, new) пока current не будет равен value.


Условно можно разделить методы Атомиков на:


  • compare-and-set — принимают current на вход и делают одну попытку записи через CAS
  • set-and-get — самостоятельно читают current и пытаются изменить значение с помощью CAS в цикле, как описано выше

Непосредственно изменение значения value делегируется либо VarHandle, либо Unsafe, которые в свою очередь выполняют его на нативном уровне. VarHandle — это динамически сильно типизированная ссылка на переменную или на параметрически определяемое семейство переменных, включающее статические поля, нестатические поля, элементы массива или компоненты структуры данных нестандартного типа. Доступ к таким переменным поддерживается в различных режимах, включая простой доступ на чтение/запись, volotile доступ на чтение/запись и доступ на compare-and-swap.


В java.util.concurrent.atomic имеется следующий набор атомиков:


  • AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray — представляют атомарные целочисленные, булевы примитивные типы, а также два массива атомарных целых чисел.
  • AtomicReference — класс для атомарных операций со ссылкой на объект.
  • AtomicMarkableReference — класс для атомарных операций над парой [reference, boolean].
  • AtomicStampedReference — класс для атомарных операций над парой [reference, int].
  • AtomicReferenceArray — массив атомарных ссылок
  • AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater — классы для атомарного обновления полей по их именам через reflection.
  • DoubleAccumulator, LongAccumulator — классы, представляющие атомарные аккумуляторы, которые принимают на вход чистую функцию-аккумулятор (BinaryOperator) и начальное значение. Сохраняет весь набор операндов, а когда необходимо получить значение, то аккумулирует их с помощью функции-аккумулятора. Порядок операндов и применения функции-аккумулятора не гарантируется. Используется, когда записей намного больше, чем чтения.
  • DoubleAdder, LongAdder — классы, представляющие атомарные счётчики. Являются частным случаем атомарных аккумуляторов, у которых функция-аккумулятор выполняет простое суммирование, а начальным значением является 0.

С помощью атомиков можно реализовать блокировку, например так:


public class NonReentrantSpinLock {

    private AtomicReference<Thread> owner = new AtomicReference<>();

    public void lock() {
      Thread currentThread = Thread.currentThread();

      while (!owner.compareAndSet(null, currentThread)) {}
    }

    public void unlock() {
      Thread currentThread = Thread.currentThread();
      owner.compareAndSet(currentThread, null);
    }
}

Подробнее:


  • Как устроены атомики?
  • Compare and Swap
  • Обзор java.util.concurrent.* на Хабре
  • Разбор основных концепций параллелизма на Хабре
  • Книга "Java Concurrency на практике" — её отрывок на Хабре
  • JDK concurrent package на Хабре
  • Atomic operations на Хабре
  • Concurrency: 6 способов жить с shared state на Хабре
  • The Art of Multiprocessor Programming
  • The JSR-133 Cookbook for Compiler Writers
  • AtomicReference: A (Sometimes Easier) Alternative to Synchronized Blocks
  • An Introduction to Atomic Variables in Java на Bealdung
  • Переход к атомарности
  • Use AtomicReference to implement Reentrant Lock
  • A comprehensive understanding of Java atomic variable classes
  • Faster Atomic*FieldUpdaters for Everyone
  • Алексей Шипилёв — Если не Unsafe, то кто: восход VarHandles
  • Introduction to nonblocking algorithms

15. Что внутри и как работают TreeSet/TreeMap? В чем идея Красно-черного дерева?

TreeMap — реализация NavigableMap, основанная на красно-чёрном дереве. Элементы отсортированы по ключам в натуральном порядке или с помощью Comparator, указанного при создании мапы, в зависимости от использовавшегося конструктора. Гарантирует логарифмическое время выполнения методов containsKey, get, put и remove.
TreeSet — реализация NavigableSet, основанная на TreeMap. Элементы отсортированы в натуральном порядке или с помощью Comparator, указанного при создании множества, в зависимости от использовавшегося конструктора. Гарантирует логарифмическое время выполнения методов add, contains и remove.


Обе коллекции НЕ synchronized и итератор по ним может выбросить ConcurrentModificationException.


Если в эти коллекции при использовании натурального порядка сортировки в качестве ключа попытаться положить null, то получим NullPointerException. В случае с компаратором поведение с null будет зависеть от реализации компаратора. До 7-й Java с добавлением null в TreeMap и TreeSet был баг.


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


  • Вершина может быть либо красной, либо чёрной и имеет двух потомков
  • Красная вершина не может быть дочерней для красной вершины
  • Количество чёрных вершин от корня до листа включительно одинаково для любого листа
  • Корень дерева является чёрным
  • Все листья — чёрные и не содержат данных

Подробнее:


  • Статья про сбалансированные бинарные деревья на Хабре
  • Java собеседование. Коллекции на Хабре
  • Java TreeMap vs HashMap
  • 10 TreeMap Java Interview Questions и TreeSet Interview Questions
  • Internal Working of TreeMap in Java
  • A Guide to TreeMap in Java и A Guide to TreeSet in Java на Bealdung
  • Красно-черные деревья: коротко и ясно на Хабре
  • Балансировка красно-чёрных деревьев — Три случая на Хабре
  • Красно-чёрное дерево
  • Визуализация красно-чёрного дерева. И ещё. А вот исходники

16. Что поменялось с Java 8 по Java <CURRENT_VERSION>?

Java имеет богатую историю. На данный момент проекты чаще всего разделяются на:


  • legacy-проекты с версией Java меньше 8
  • проекты на Java 8, самая распрастранённая и популярная
  • проекты на Java 9+ (точнее либо 11 LTS, либо последние полугодовые релизы)

Между 8 и 9 версиями случился небольшой разлом с частичной потерей обратной совместимости, а потом приколы лицензирования подъехали, поэтому миграция и в без того консервативном мире Java-приложений идёт медленно. Однако идёт, и если вы собеседуетесь в компанию, где этот переход уже осуществили, то, вероятно, у вас поинтересуются, что же там с Java 8 поменялось, чем живёт и дышит современная Java.


На момент выхода статьи, имеем:


  • 9: Project Jigsaw aka Модули, HTTP/2 Client (Incubator), jshell, G1 GC по умолчанию, Compact Strings и другие.
  • 10: Local-Variable Type Inference (var), Parallel Full GC для G1, Graal можно использовать как основной JIT-компилятор и другие.
  • 11 LTS: var в лямбдах, компиляция и запуск single-file программ через java, новые методы для String, Epsilon GC (Experimental), ZGC (Experimental) и другие.
  • 12: Switch Expressions (Preview), Shenandoah (Experimental), улучшения в G1, JMH и другие
  • 13: Text Blocks (Preview) и другое
  • 14: Pattern Matching для instanceof (Preview), Packaging Tool (Incubator), улучшили сообщение для NullPointerExceptions, Records (Preview) и другие.
  • 15: Sealed Classes (Preview), Hidden Classes, удаление Nashorn JavaScript Engine из JDK и другие.

Найти ссылки на документацию к API, языку и виртуальной машине, release notes и сравнить API между версиями можно в Java-альманахе.


Кроме всего прочего, есть ряд проектов, в рамках которых развиваются большие и ожидаемые сообществом изменения Java:


  • Amber — проект по реализации маленьких, но продуктивных улучшений языка Java. В рамках данного проекта постепенно реализуется и независимо выходит целый набор JEP: var (JDK 10), Switch Expressions, Sealed Types, Records, Text Blocks, Pattern Matching для instanceof и другие.
  • Panama — проект по улучшению взаимодействия между JVM и нативным кодом. На Хабре есть статья с разъяснениями и интервью с Владимиром Ивановым на эту тему.
  • Loom — проект по внедрению в Java легковесных потоков. На Хабре есть две прекрасные статьи с разъяснениями: раз и два.
  • Valhalla — это проект по созданию нескольких больших и сложных улучшений языка и VM. В него входят: Inline types, Generics over Primitive Types, Enhanced volatiles и другие возможные или необходимые в рамках проекта улучшения.
  • Lanai — проект по улучшению рендеринга настольных Java-приложений на MacOS путём использования Metal Apple platform API. C 14 мая 2020 появились Early-Access сборки.
  • и другие

Отдельно нужно упомянуть GraalVM — это JDK и виртуальная машина Java, которая создана, чтобы объединить необъединяемое:


  • быстрое выполнение Java
  • уменьшение времени старта и потребления памяти для Java
  • комбинирование и исполнение программ, написанных на различных ЯП, в том числе на платформо-зависимых
  • общие инструменты для всех ЯП
  • поддержка JIT и AOT-компиляции
  • и т.п.

Послушать на тему:


  • Два выпуска подкаста Javaswag: раз и два
  • Выпуск 172 Java подкаста Подлодка, в гости к которому пришёл Тагир Валеев

Почитать на Хабре:


  • Руководство по возможностям Java версий 8-14
  • API, ради которых наконец-то стоит обновиться с Java 8. Часть 1
  • JAVA 9. Что нового?
  • Обзор Java 9
  • Модульность в Java 9
  • Компактные строки в Java 9
  • Java 10 General Availability
  • Изменения в стандартной библиотеке Java 10
  • Записки о миграции на Java 10
  • Как Java 10 изменяет способ использования анонимных внутренних классов
  • "Жизнь после Java 10": какие изменения принесет Java 11
  • 90 новых фич (и API) в JDK 11
  • Java 11: новое в String
  • Java 11 / JDK 11: General Availability
  • 39 новых фич, которые будут доступны в Java 12
  • Пришло время Java 12! Обзор горячих JEP-ов
  • Новое в Java 12: The Teeing Collector
  • Только что вышла Java 13
  • В Java 13 хотят добавить "текстовые блоки"
  • Introducing Java 13: Let's dive Into JDK's New Features
  • Что нового будет в Java 14
  • Java 14 is coming
  • Java 14: Record, более лаконичный instanceof, упаковщик jpackage, switch-лямбды и текстовые блоки
  • Исследуем записи в Java 14
  • Пробуем улучшенный оператор instanceof в Java 14
  • Исследуем sealed классы в Java 15
  • Sealed classes. Semantics vs performance
  • Sealed типы в Java
  • Что нового в Java 15?
  • Вышла Java 15
  • Project Panama: как сделать Java "ближе к железу"?
  • Раздача халявы: нетормозящие треды в Java. Project Loom
  • Project Loom: виртуальные потоки в Java уже близко
  • Десять вещей, которые можно делать с GraalVM
  • Как работает Graal — JIT-компилятор JVM на Java
  • Graal: как использовать новый JIT-компилятор JVM в реальной жизни
  • Разрабатываем утилиту на GraalVM
  • Скрещиваем ужа с ежом: OpenJDK-11 + GraalVM
  • JavaScript, Java, какая теперь разница?
  • Что под капотом компиляторных оптимизаций GraalVM?
    И не только:
  • Java-альманах
  • State of Loom: часть 1 и часть 2
  • GraalVM

Посмотреть:


  • Cay Horstmann — Feature evolution in Java 13 and beyond
  • Тагир Валеев — Java 9-14: Маленькие оптимизации
  • Никита Липский — Java 9 Модули. Почему не OSGi?
  • Cay Horstmann — Java 9: the good parts (not modules)
  • Владимир Иванов — Project Panama: как сделать Java “ближе к железу”?
  • Олег Чирухин — GraalVM Всемогущий
  • Олег Чирухин — Graal, Value Types, Loom и прочие ништяки
  • Олег Шелаев — Компилируем Java ahead of time с GraalVM
  • Олег Шелаев — Суперкомпиляция, partial evaluation, проекции Футамуры и как GraalVM спасет мир
  • Project Loom и Новое в JDK 14 на letsCode
  • GOTO 2019 • Life After Java 8 • Trisha Gee
  • Dalia Abo Sheasha — Migrating beyond Java 8
  • Project Loom: Helping Write Concurrent Applications on the Java Platform by Ron Pressler

17. В какой кодировке строки в Java? Как хранятся строки внутри класса String? Как устроен String?

До Java 9 все строки имели кодировку UTF-16 (2 байта на символ) и хранились в массиве char.
С Java 9 пришло такое изменение как Compact String. Если все символы строки входят в множество символов Latin-1 (а это подавляющее большинство строк), то каждый из них может поместиться в 1 байт, поэтому в этом случае массив char избыточен. В результате было принято решение заменить массив char на массив byte, что позволяет строкам Latin-1 расходовать меньше памяти. Кодировка строки хранится в отдельном поле byte coder, значение которого представляет Latin-1 или UTF-16.


Также интересной особенностью является кеширование классом String своего hashcode.


Строки являются неизменяемыми, наследоваться от строк запрещено (final class). Все операции по изменении строки возвращают её новый экземпляр, в том числе и конкатенация строк. Компилятор умеет оптимизировать конкатенацию и превращать её в объект StringBuilder и совокупность вызовов методов append. ОДНАКО! В Java 9 вошёл JEP 280: Indify String Concatenation, который изменил эту оптимизацию и пошёл ещё дальше. Теперь вместо StringBuilder генерируется bytecode для вызова StringConcatFactory через invokedynamic, поэтому стоит расслабиться и чаще выбирать +.


Ещё можно упомянуть про String pool — это выделяемое в heap пространство, которое используется для оптимизации потребления памяти при хранении строк. Благодаря ему одинаковые строковые литералы могут ссылаться на один и тот же объект.


Стоит помнить, что с помощью [String.intern()](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/String.html#intern()) производительности особой не добиться, а можно наоборот пустить всё по миру. Лучше напишите свою реализацию. Подоробнее читайте в статье Алексея Шипилёва — JVM Anatomy Quark #10: String.intern().


Кроме того, equals и методы поиска (например indexOf) оптимизируются JIT компилятором на нативном уровне.


Посмотреть доклады Алексея Шипилёва на тему строк: Катехизис java.lang.String и The Lord of the Strings: Two Scours.


Подробнее:


  • String javadoc
  • Как обойти строчку?
  • Из чего состоит String?
  • JDK 9/JEP 280: конкатенация строк никогда больше не будет прежней на Хабре
  • Компактные строки в Java 9 на Хабре
  • Guide to Java String Pool на Baeldung
  • Compact Strings in Java 9
  • Владимир Иванов — Глубокое погружение в invokedynamic
  • Charles Nutter — Let's Talk About Invokedynamic
  • Что там с JEP-303 или изобретаем invokedynamic

18. Что такое ThreadLocal переменные?

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


Данные ThreadLocal-переменных хранятся не в них самих, а непосредственно в объектах Thread. У каждого экземпляра класса Thread есть поле ThreadLocal.ThreadLocalMap threadLocals, которое инициализируется и используется ThreadLocal. ThreadLocal.ThreadLocalMap представляет собой специализированную версию HashMap, записи которой наследуют от WeakReference<ThreadLocal<?>>, используя ключ мапы как ref field слабой ссылки. Ключами такой мапы являются ThreadLocal, а значением — Object. Если ключ записи равен null, то такая запись называется просроченной (stale) и будет удалена из мапы.
Следует обратить внимание, что ThreadLocal изолирует именно ссылки на объекты, а не копии их значений. Если изолированные внутри потоков ссылки ведут на один и тот же объект, то возможны коллизии.
Когда у ThreadLocal-переменной запрашивается её значение (например через метод get), то она получает текущий поток, извлекает из него мапу threadLocals, и получает значение из мапы, используя себя в качестве ключа. Аналогично выполняются методы изменения значения ThreadLocal.
Из этого следует, что значение ThreadLocal-переменной должно устанавливаться в том же потоке, в котором оно будет использоваться.


Подробнее:


  • Зачем используются thread local переменные?
  • О бедном ThreadLocal замолвите слово
  • Использование ThreadLocal переменных
  • An Introduction to ThreadLocal in Java
  • Утечка памяти с ThreadLocal на Хабре
  • 5 вещей, которых вы не знали о многопоточности на Хабре
  • Разбор основных концепций параллелизма на Хабре

19. Сколько в байт занимает каждый из примитивных типов в памяти? А объект?

Казалось бы:


  • byte — 1 байт
  • short — 2 байта
  • int — 4 байта
  • long — 8 байт
  • char — 2 байта
  • float — 4 байта
  • double — 8 байт

А размер boolean не упоминается в спецификации вовсе. Однако также спецификация не запрещает использовать для хранения примитива больше памяти — главное, чтобы размер был достаточным для всех значений. Конкретный объём таки зависит от реализации JVM. Не последнюю роль в этом играет выравнивание данных в памяти.


Похожая ситуация и со ссылочными типами — спецификация JVM не требует какой-то определённой структуры для объектов и отдаёт её на откуп реализации. Все тонкости и секреты занимаемой объектами памяти раскрывает Алексей Шипилёв в своей статье Java Objects Inside Out.


Подробнее:


  • The Java Virtual Machine Specification
  • Какие существуют примитивы?
  • Сколько памяти занимает объект?
  • Какие существуют примитивы?
  • Размер Java объектов на Хабре
  • Java Objects Inside Out
  • jol
  • Как JVM аллоцирует объекты? на Хабре
  • Сжатие указателей в Java на Хабре
  • Measuring Object Sizes in the JVM на Bealdung

Если вас заинтересовало представление объектов в jvm и их реализация (и вы умеете-могёте читать C++), то можно пойти посмотреть исходники openjdk. Начать, например, отсюда:


  • instanceOop.hpp
  • klass.hpp
  • instanceKlass.hpp
  • objArrayKlass.hpp
  • objArrayOop.hpp
  • oopFactory.hpp

20. Какие ссылки бывают в Java?

Типы ссылок в Java:


  • Strong reference — обычная переменная ссылочного типа в Java. Объект такой ссылки очищается GC не раньше, чем станет неиспользуемым (никто нигде на него больше не ссылается).
  • Слабые ссылки — сборщик мусора тем или иным образом не учитывает связь ссылки и объекта в куче при выявлении объектов, подлежащих удалению. Объект будет удалён даже при наличии слабой ссылки на него:
    • Soft reference — мягкая ссылка, экземпляр класса SoftReference. Объект гарантированно будет собран GC до возникновения OutOfMemoryError. Может использоваться для реализации кэшей, увеличивающихся без риска OutOfMemoryError для приложения.
    • Weak reference — слабая ссылка, экземпляр класса WeakReference. Не препятствует утилизации объекта и игнорируется GC при сборке мусора. Может использоваться для хранения некоторой связанной с объектом информации до момента его смерти. Также стоит обратить внимание на WeakHashMap.
    • Phantom reference — фантомная ссылка, экземпляр класса PhantomReference. Не препятствует утилизации объекта и игнорируется GC при сборке мусора и имеет ряд особенностей, описанных ниже. Может быть применена для получения уведомления, что объект стал неиспользуемым и можно освободить связанные с ним ресурсы (как более надёжный вариант, чем finalize(), вызов которого не гарантируется, может проводить сеансы воскрешения и вообще deprecated).

Чтобы достать объект из слабых ссылок, необходимо вызывать метод get(). Если объект недостижим, то метод вернёт null. Для фантомных ссылок всегда возвращается null.


При создании слабой ссылки в конструктор можно, а для PhantomReference необходимо, передать экземпляр ReferenceQueue — в очереди будет сообщение, когда ссылка протухнет. Для SoftReference и WeakReference это будет ДО финализации объекта, а для PhantomReference ПОСЛЕ. Однако фактическое удаление объекта фантомной ссылки из памяти не производится до момента её очистки.


Подробнее:


  • Мягкие ссылки на страже доступной памяти или как экономить память правильно на Хабре
  • Использование PhantomReferences в Java

Spring


21. Какие есть scope в Spring? Какой по умолчанию? Чем singleton отличается от prototype? Можно ли сделать свой scope и как? Плавно переходит в вопрос 'Как заинжектить prototype в singleton?'

Spring scope:


  • singleton (по умолчанию)
  • prototype
  • request
  • session
  • application
  • websocket

Про scope подробнее можно прочитать в документации, Bealdung. И, конечно же, надо посмотреть Spring-потрошитель Ч. 2.


Про prototype в singleton можно вспомнить несколько вариантов:


  • @Lookup
  • Фабрика для создания экземпляров prototype-бинов
  • ProxyMod = ScopedProxyMode.TARGET_CLASS
    Подробнее о каждом варианте есть в Bealdung и смотрим Spring-потрошитель Ч. 2.

22. Часто спрашивают о циклических зависимостях бинов в Spring. Проблема ли это или что получим в результате? Если проблема, то как её решить?

Да, это проблема — будет выброшено исключение BeanCurrentlyInCreationException (при внедрении зависимостей через конструктор).


Варианты решения:


  • Ещё раз подумать, той ли дорогой мы держим путь — может не поздно сделать редизайн и избавиться от циклических зависимостей
  • Инициализировать один из бинов лениво с помощью @Lazy
  • Внедрение зависимостей в setter-метод, а не в конструктор

Подробнее есть в документации и в Bealdung


23. Бывают вопросы про жизненный цикл бина, этапы инициализации контекста, про устройство спринга внутри, про DI и как он работает

Тут однозначно надо смотреть Spring-потрошитель часть 1 и часть 2. Также благое дело — это почитать документацию.


Также по этапам инициализации контекста есть статья с красивыми картинками на хабре.


24. Расскажите про прокси и про @Transactional. Как работает и зачем? Какие могут быть проблемы? Можно ли навесить @Transactional на приватный метод? А если вызывать метод с @Transactional внутри другого метода с @Transactional одного класса - будет работать?

Для начала, если вы не знали или случайно забыли про паттерн Proxy в общем виде, то можно освежиться здесь.


Допустим, что наш сервис MyServiceImpl имеет 2 публичных метода, аннотированных @Transactionalmethod1 и method2(он с Propagation.REQUIRES_NEW). В method1 вызываем method2.


В связи с тем, что для поддержки транзакций через аннотации используется Spring AOP, в момент вызова method1() на самом деле вызывается метод прокси объекта. Создается новая транзакция и далее происходит вызов method1() класса MyServiceImpl. А когда из method1() вызовем method2(), обращения к прокси нет, вызывается уже сразу метод нашего класса и, соответственно, никаких новых транзакций создаваться не будет.

Это цитата и краткий ответ на вопрос из статьи на Хабре, где можно ознакомиться с подробностями.


Что тут можно ещё посоветовать? Spring-потрошитель опять и снова — часть 1 и часть 2. А также документация является несомненным и любимым первоисточником информации о Proxy и управление транзакциями.


25. Где у обычного (НЕ Boot) Spring-приложения main-класс?

Старое доброе обычное Spring-приложение деплоится в контейнер сервлетов (или сервер приложений), где и расположен main-класс. При этом оно собирается в war-архив. Когда war-файл разворачивается в контейнере, контейнер обычно распаковывает его для доступа к файлам, а затем запускает приложение. Spring Boot приложение также можно собрать как war и задеплоить его таким же образом.


Подробнее:


  • Понимание WAR
  • В чём разница между jar и war?
  • Конвертация Spring Boot JAR приложения в WAR на RUS или ENG

26. Как работает Spring Boot и его стартеры?

Во-первых, благодаря spring-boot-starter-parent, у которого родителем является spring-boot-dependencies, можно особо не париться о зависимостях и их версиях — большинство версии того, что может потребоваться прописано и согласовано в dependencyManagement родительского pom. Или можно заимпортировать BOM.


Spring Boot черпает свою мощь из стартеров — наборов сконфигурированных бинов со всеми необходимыми зависимостями, готовых к использованию и доступных для тонкой настройки через properties-файлы.


Для Spring Boot приложения создаётся main-класс с аннотацией @SpringBootApplication и запуском метода run класса SpringApplication, который возвращает ApplicationContext.


@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

Аннотация @SpringBootApplication просто скрывает за собой аннотации @EnableAutoConfiguration, @ComponentScan и @Configuration.


SpringBootApplication создаёт либо WebApplicationContext (если в classpath есть Servlet и ConfigurableWebApplicationContext), либо GenericApplicationContext.


При создании стартера используется файл META-INF/spring.factories — в нём ключу org.springframework.boot.autoconfigure.EnableAutoConfiguration приравнивается список из полных имён всех классов-конфигураций (а в них бины, @ComponentScan, @Import и т.п.) стартера через запятую.


Аннотация @EnableAutoConfiguration импортирует AutoConfigurationImportSelector, который и отвечает за поиск необходимых классов конфигурации. Вызов метода getCandidateConfigurations обращается к SpringFactoriesLoader и его методу loadFactoryNames, чтобы просканировать classpath на наличие файлов META-INF/spring.factories и имен классов-конфигураций в них, а затем загрузить их в контекст.


Также у Spring boot есть модуль spring-boot-autoconfigure со своим файлом META-INF/spring.factories. Чтобы не создавать все-все бины из конфигураций, у бинов используется аннотация @Conditional (или её вариации) с каким-либо условием.


Чтобы упаковать Spring boot в jar используется spring-boot-maven-plugin. У такого jar в META-INF/MANIFEST.MF будет прописан Main-Class — org.springframework.boot.loader.JarLauncher, а в Start-Class будет уже main-класс нашего приложения. JarLauncher формирует class path (в начале в нём только org.springframework.boot), который находится в BOOT-INF(там lib с зависимостями и class с классами приложения), а затем запускает Start-Class.


Посмотреть:


  • Доклад Евгения Борисова и Кирилла Толкачёва Boot yourself, Spring is coming: часть 1, часть 2. На Хабре есть текстовая расшифровка: часть 1, часть 2.
  • Доклад Кирилла Толкачёва и Максима Гореликова Spring Boot Starter — как и зачем?
  • Доклад Кирилла Толкачёва и Александра Тарасова — Твой личный Spring Boot Starter

Почитать:


  • На Хабре: Как работает Spring Boot Auto-Configuration, Пишем свой spring-boot-starter и Использование Conditional в Spring
  • Hа Baeldung: A Comparison Between Spring and Spring Boot, Create a Custom Auto-Configuration with Spring Boot, Intro to Spring Boot Starters, Spring Boot: Configuring a Main Class
  • What is Spring Boot? Autoconfigurations In-Depth
  • Spring Boot for beginners
  • Spring Boot Documentation
  • Список готовых стартеров

27. Как выполняется http-запрос в Spring?

В современном Spring есть два подхода к построению веб-приложений:


  • Spring MVC
  • Spring WebFlux

Работа Spring MVC строится вокруг DispatcherServlet, который является обычным Servlet'ом и реализует паттерн Front Controller: принимает Http-запросы и координирует их с требуемыми обработчиками. Для своей конфигурации DispatcherServlet использует WebApplicationContext. DispatcherServlet в обработке запроса помогают несколько "специальных бинов" следующим образом:


  1. После получения HTTP-запроса DispatcherServlet перебирает доступные ему (предварительно найденные в контексте) экземпляры HandlerMapping, один из которых определит, метод какого Controller должен быть вызван. Реализации HandlerMapping, использующиеся по умолчанию: BeanNameUrlHandlerMapping и RequestMappingHandlerMapping (создаёт экземпляры RequestMappingInfo по методам аннотированным @RequestMapping в классах с аннотацией @Controller). HandlerMapping по HttpServletRequest находит соответствующий обработчик — handler-объект (например, HandlerMethod). Каждый HandlerMapping может иметь несколько реализаций HandlerInterceptor — интерфейса для кастомизации пред- и постобработки запроса. Список из HandlerInterceptor'ов и handler-объекта образуют экземпляр класса HandlerExecutionChain, который возвращается в DispatcherServlet.
  2. Для выбранного обработчика определяется соответствующий HandlerAdapter из предварительно найденных в контексте. По умолчанию используются HttpRequestHandlerAdapter (поддерживает классы, реализующие интерфейс HttpRequestHandler), SimpleControllerHandlerAdapter (поддерживает классы, реализующие интерфейс Controller) или RequestMappingHandlerAdapter (поддерживает контроллеры с аннотацией @RequestMapping).
  3. Происходит вызов метода applyPreHandle объекта HandlerExecutionChain. Если он вернёт true, то значит все HandlerInterceptor выполнили свою предобработку и можно перейти к вызову основного обработчика. false будет означать, что один из HandlerInterceptor взял обработку ответа на себя в обход основного обработчика.
  4. Выбранный HandlerAdapter извлекается из HandlerExecutionChain и с помощью метода handle принимает объекты запроса и ответа, а также найденный метод-обработчик запроса.
  5. Метод-обработчик запроса из Controller (вызванный через handle) выполняется и возвращает в DispatcherServlet ModelAndView. При помощи интерфейса ViewResolver DispatcherServlet определяет, какой View нужно использовать на основании полученного имени.
    Если мы имеем дело с REST-Controller или RESTful-методом контроллера, то вместо ModelAndView в DispatcherServlet из Controller вернётся null и, соответственно, никакой ViewResolver задействован не будет — ответ сразу будет полностью содержаться в теле HttpServletResponse после выполнения handle. Чтобы определить RESTful-методы, достаточно аннотировать их @ResponseBody либо вместо @Controller у класса поставить @RestController, если все методы котроллера будут RESTful.
  6. Перед завершением обработки запроса у объекта HandlerExecutionChain вызывается метод applyPostHandle для постобработки с помощью HandlerInterceptorов.
  7. Если в процессе обработки запроса выбрасывается исключение, то оно обрабатывается с помощью одной из реализаций интерфейса HandlerExceptionResolver. По умолчанию используются ExceptionHandlerExceptionResolver (обрабатывает исключени из методов, аннотированных @ExceptionHandler), ResponseStatusExceptionResolver (используется для отображения исключений аннотированных @ResponseStatus в коды HTTP-статусов) и DefaultHandlerExceptionResolver (отображает стандартные исключения Spring MVC в коды HTTP-статусов).
  8. В случае с классическим Controller после того, как View создан, DispatcherServlet отправляет данные в виде атрибутов в View, который в конечном итоге записывается в HttpServletResponse. Для REST-Controller ответ данная логика не вызывается, ведь ответ уже в HttpServletResponse.

Когда HTTP запрос приходит с указанным заголовком Accept, Spring MVC перебирает доступные HttpMessageConverter до тех пор, пока не найдет того, кто сможет конвертировать из типов POJO доменной модели в указанный тип заголовка Accept. HttpMessageConverter работает в обоих направлениях: тела входящих запросов конвертируются в Java объекты, а Java объекты конвертируются в тела HTTP ответов.
По умолчанию, Spring Boot определяет довольно обширный набор реализаций HttpMessageConverter, подходящие для использования широкого круга задач, но также можно добавить поддержку и для других форматов в виде собственной или сторонней реализации HttpMessageConverter или переопределить существующие.


Также стоит упомянуть, что как и в случае любого другого сервлета, к обработке запроса в Spring MVC может быть применена одна из реализаций интерфейса javax.servlet.Filter как до выполнения запроса, так и после. Sring MVC предоставляет несколько уже готовых реализаций.


Отдельного разговора заслуживает путь запроса по внутренностям Spring Security, где используется множество различных фильтров. На хабре есть статья об этом.


Подробнее:


  • Spring MVC — основные принципы на Хабре
  • Путь запроса по внутренностям Spring Security на Хабре
  • An Intro to the Spring DispatcherServlet на Bealdung
  • HandlerAdapters in Spring MVC на Bealdung
  • Quick Guide to Spring Controllers на Bealdung
  • Spring RequestMapping на Bealdung
  • Http Message Converters with the Spring Framework на Bealdung
  • How to Define a Spring Boot Filter? на Bealdung
  • Spring Professional Study Notes
  • Spring Security Architecture
  • Схематично

Документация:


  • Spring Web MVC
  • Spring MVC Auto-configuration в Spring Boot

Spring WebFlux — это реактивный веб-фреймворк, который появился в Spring Framework 5.0. Он не требует Servlet API (но может использовать Servlet 3.1+containers, хотя чаще это Netty (по умолчанию в Spring Boot) или Undertow), полностью асинхронный и неблокирующий, реализует спецификацию Reactive Streams при помощи проекта Reactor.
В Spring WebFlux используется большинство аннотаций из Spring MVC (RestController, RequestMapping и другие) для определения аннотированных контроллеров. Однако представляет новую возможность создания функциональных котроллеров, основанных на HandlerFunction.


В Spring WebFlux обработка запроса на стороне сервера строится в два уровня:


  • HttpHandler — это базовый интерфейс обработки HTTP-запросов с использованием неблокирующего I/O, Reactive Streams back pressure через адаптеры для Reactor Netty, Undertow и т.д.
  • WebHandler — интерфейс, который предоставляет верхнеуровневое API для обработки HTTP-запросов поверх аннотированных или функциональных контроллеров.

Контракт HttpHandler представляет обработку HTTP-запроса, как его прохождение через цепочку множества WebExceptionHandler, множества WebFilter и одного единственного WebHandler. Сборкой цепочки занимается WebHttpHandlerBuilder при помощи ApplicationContext.


Диспетчеризация запросов в Spring WebFlux выполняется DispatcherHandler, который является имплементацией интерфейса WebHandler и также реализует паттерн Front Controller: принимает Http-запросы и координирует их с требуемыми обработчиками. DispatcherHandler — это Spring bean, имплементирующий ApplicationContextAware для доступа к контексту, с которым он был запущен. DispatcherHandler с бин-именем webHandler обнаруживает WebHttpHandlerBuilder и помещает в цепочку в качестве WebHandler.


DispatcherHandler в ходе обработки http-запроса и ответа делегирует часть работы "специальным бинам", которые могут быть подвержены кастомизации, расширению и замене пользователем. Сам процесс обработки выглядит следующим образом:


    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        if (this.handlerMappings == null) {
            return createNotFoundError();
        }
        return Flux.fromIterable(this.handlerMappings)
                .concatMap(mapping -> mapping.getHandler(exchange))
                .next()
                .switchIfEmpty(createNotFoundError())
                .flatMap(handler -> invokeHandler(exchange, handler))
                .flatMap(result -> handleResult(exchange, result));
    }

  1. Каждый из экземпляров HandlerMapping пытается найти подобающий обработчик для данного запроса (какой-то метод, какого-то контроллера). В итоге выбирается первый найденный обработчик (handler). Основными доступными реализациями HandlerMapping являются:
    • RequestMappingHandlerMapping для методов-обработчиков, аннотированных @RequestMapping
    • RouterFunctionMapping для функциональных обработчиков
    • SimpleUrlHandlerMapping для маппинга URL-ов на бины-обработчики запросов
  2. Если обработчик найден, то для него (в invokeHandler) выбирается подходящий HandlerAdapter, вызывается его метод handle для непосредственной обработки запроса выбранным обработчиком. Результат обработки упаковывается в HandlerResult, который возвращается в DispatcherHandler. Главная задача HandlerAdapter — скрыть детали и способ непосредственного вызова метода-обработчика от DispatcherHandler. Примерами доступных реализаций HandlerAdapter являются:
    • RequestMappingHandlerAdapter — для вызова методов, аннотированных @RequestMapping
    • HandlerFunctionAdapter — для вызова HandlerFunctions
  3. Полученный HandlerResult обрабатывается (в handleResult) необходимым для него HandlerResultHandler. Здесь обработка завершается формированием ответа на запрос требуемым образом. По умолчанию доступно несколько реализаций HandlerResultHandler:
    • ResponseEntityResultHandler — обрабатывает ResponseEntity, обычно из @Controller
    • ServerResponseResultHandler — обрабатывает ServerResponse, обычно из функциональных контроллеров
    • ResponseBodyResultHandler — обрабатывает возвращаемые значения из методов, аннотированных @ResponseBody, или методов класса @RestController
    • ViewResolutionResultHandler — инкапсулирует в себе алгоритм View Resolution и обработку поддерживаемых данным алгоритмом типов результатов

Документация:


  • Spring WebFlux
  • Spring WebFlux Auto-configuration в Spring Boot

Продолжение следует


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

Источник: https://habr.com/ru/post/529210/

Интересные статьи

Интересные статьи

Приветствую, читатель! Меня зовут Сергей, я являюсь инди-разработчиком компьютерных игр. В моем портфолио имеется уже несколько инди-проектов, часть из которых была самостоятельно...
Для одних Ким Дотком, основатель скандально известного файлообменника «MegaUpload», преступник и интернет-пират, для других — несгибаемый борец за неприкосновенность персональных данных. 12 марта...
Недавно мы опубликовали перевод материала, в котором были приведены полезные советы для Python-программистов. У того материала есть продолжение, которое мы представляем вашему вниманию сегодня. ...
Продолжаем лаконичную интерпретацию официальной документации Flutter в формате «вопрос-ответ». Вот уже 3-я часть, и она в большей степени будет полезна React Native-разработчикам. В данной интерп...
Довольно часто владельцы сайтов просят поставить на свои проекты индикаторы курсов валют и их динамику. Можно воспользоваться готовыми информерами, но они не всегда позволяют должным образом настроить...