Когда-то я проходил серию собеседований на Backend-Java-разработчика и записывал вопросы себе на будущее, чтобы потом можно было пробежаться и освежить память. Подумалось, что, вероятно, данный сборник будет полезен не только мне, поэтому сдул с него пыль, набросал ответов и делюсь с сообществом. На оригинальность и исключительность не претендую: подобные статьи уже были и на Хабре, и много где ещё — в конце (во второй части) приведу список ссылок, чтобы шпаргалка была максимально полной.
Точно установить сложность всех вопросов не берусь — на разном уровне их потребуется раскрыть с различной степенью подробности. Я написал ответы где-то на плюс-минус middle, щедро приправив ссылками для дальнейших изысканий. На самые популярные вопросы сразу перенаправляю в источники с готовыми ответами. Заодно посмотрим по ссылкам в статье, насколько Хабр может помочь в подготовке к собесам.
Текста получилось много, поэтому пришлось разбить на две части. В первой поговорим про Java и Spring, а обо всём остальном — во второй. Вторая часть тут
GitHub-репозиторий с полной шпаргалкой тут, а Хабр всё ещё торт.
Вопросы
Java
Опишите Контракт. Далее разговор переходит к устройству HashMap
. Как устроена внутри? А происходит в случае возникновения коллизии? Назовите алгоритмические сложности поиска, чтения, удаления из элемента мапы. А что если ключ — это массив байтов? А может быть так, что мы положим элемент в мапу, а потом не найдем? Обсасывают бедную мапу со всех сторон. Самая популярная тема для обсуждения. Спрашивают все. Абсолютно все.
Контракт equals
и hashcode
:
- Для одного и того же объекта хэшкоды одинаковые.
- Если объекты равны по equals, то и хэшкоды одинаковые.
- Если же хэшкоды равны, то объекты могут быть не равны по equals (коллизия).
- Если хэшкоды разные, то и объекты разные.
В статье на Хабре это подробно разобрано, если кому-то покажется мало.
Про HashMap
и вопросы по ним есть несколько отличных статей на Хабре (в картинках, с дополнениями из Java 8, а тут вопросы-ответы про коллекциям). Кроме того, можно посмотреть исходный код в вашей любимой IDE. Можете сделать себе конспект и повесить на стену :)
По сути это вопрос про ArrayList
vs LinkedList
. Опять же, заезженная пластинка, разобранная на Хабре — вопросы-ответы про коллекциям, ArrayList в картинках, LinkedList в картинках, Что «под капотом» у LinkedList. Посмотреть исходники тоже полезно. Например, можно понтануться тем, что вставка в середину в ArrayList
выполняется с помощью нативно реализованной функции System.arraycopy
, поэтому не всё так плохо, как могло бы быть в этом случае.
Этот вопрос далее перетекает либо в обсуждение HashMap
, либо в основы многопоточного программирования на Java.
Чтобы вы вдруг внезапно не забыли каких-то методов (как это сделал я :D), привожу вам список и ссылку на JavaDoc:
- clone
- equals
- finalize (Deprecated)
- getClass
- hashCode
- toString
- notify
- notifyAll
- wait
Также можно почитать, что там вообще есть в исходниках Object
в статье на Хабре.
В принципе, статьи на Baeldung должно хватить. Лучше, конечно, пописать код с использованием wait
, notify
, notifyAll
и synchronized
руками. Также можно почитать официальный туториал от Oracle по Concurrency в Java.
Но если хотите пойти глубже, то хаброписатели опять спешат на помощь — тут. А также Java Language Specification, раздел 17.1 и 17.2.
Не знаю как у вас, но у меня при упоминании JMM молниеносно всплывает в голове Алексей Шипилёв и его доклады — раз, два, три. Если вы больше чтец, чем смотрец, то Алексея можно и почитать — ать, два.
Кроме того, абсолютно не будет лишним посмотреть доклад Романа Елизарова по теоретическому минимуму JMM.
Если совсем нет времени, то можно пробежаться по небольшой статейке по JMM. Если есть время и интерес, тогда углубляемся в тему через статью на Хабре. А ещё на Хабре есть неплохой перевод статьи "Многопоточность. Java-модель памяти": часть 1 и часть 2.
Несомненным источником истины является Java Language Specification, раздел 17.4.
Также ответ на этот вопрос можно прочитать на itsobes.ru.
Не лишним будет ознакомиться с вопросом на JVM-уровне в статье How ‘volatile’ works on JVM level? на Medium.
Память в 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 делится на два поколения:
- Young Generation
- Eden
- Survivor 0 и Survivor 1
- Old Generation
- 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. Это просто клад, за который будут воевать пришельцы на постапокалиптическую Землю, чтобы разгадать, как работает эта чёртва шайтан-виртуал-машина и промышленный код почивших человеков.
Создавать и убивать потоки — дорого. Давайте создадим N потоков (Thread pool) и будем их переиспользовать. А давайте. Вот тут описано развёрнуто.
Executor (void execute(Runnable command)
— вот и весь интерфейс) и ExecutorService (уже покруче, может запускать Callable и не только) — грубо говоря, интерфейсы выполняторов параллельных задач. А реализуют их различные выполняторы на пулах потоков. Экземпляры готовых конкретных выполняторов можно получить с помощью класса Executors. Если смелый-умелый и зачем-то надо, то можно и самому реализовать, конечно.
Также подробнее можно почитать:
Могут. Профилировать. Снимать 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
По умолчанию 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?
- Как работают параллельные стримы?
Есть 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
Тут речь пойдёт про 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
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.
JVM стартует с Xms
количеством выделенной под heap памяти и максимально может увеличить её до значения Xmx
.
Xss
флаг определяет размер выделенной под стек памяти.
Общий вид:
java -Xmx<количество><единица измерения>
Можно использовать различные единицы измерения, например килобайты (k
), мегабайты (m
) или гигабайты (g
).
Пример:
java -jar my.jar -Xms256m -Xmx2048m
Подробнее:
Атомарная операция — это операция, которая выполняется полностью или не выполняется совсем, частичное выполнение невозможно.
Атомики — это классы, которые выполняют операции изменения своего значения атомарно, т.о. они поддерживают 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
на вход и делают одну попытку записи через CASset-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
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
- Красно-черные деревья: коротко и ясно на Хабре
- Балансировка красно-чёрных деревьев — Три случая на Хабре
- Красно-чёрное дерево
- Визуализация красно-чёрного дерева. И ещё. А вот исходники
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
До 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
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
-переменной должно устанавливаться в том же потоке, в котором оно будет использоваться.
Подробнее:
Казалось бы:
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. Начать, например, отсюда:
Типы ссылок в 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
ПОСЛЕ. Однако фактическое удаление объекта фантомной ссылки из памяти не производится до момента её очистки.
Подробнее:
Spring
Spring scope:
singleton
(по умолчанию)prototype
request
session
application
websocket
Про scope
подробнее можно прочитать в документации, Bealdung. И, конечно же, надо посмотреть Spring-потрошитель Ч. 2.
Про prototype
в singleton
можно вспомнить несколько вариантов:
@Lookup
- Фабрика для создания экземпляров
prototype
-бинов ProxyMod = ScopedProxyMode.TARGET_CLASS
Подробнее о каждом варианте есть в Bealdung и смотрим Spring-потрошитель Ч. 2.
Да, это проблема — будет выброшено исключение BeanCurrentlyInCreationException
(при внедрении зависимостей через конструктор).
Варианты решения:
- Ещё раз подумать, той ли дорогой мы держим путь — может не поздно сделать редизайн и избавиться от циклических зависимостей
- Инициализировать один из бинов лениво с помощью
@Lazy
- Внедрение зависимостей в setter-метод, а не в конструктор
Подробнее есть в документации и в Bealdung
Тут однозначно надо смотреть Spring-потрошитель часть 1 и часть 2. Также благое дело — это почитать документацию.
Также по этапам инициализации контекста есть статья с красивыми картинками на хабре.
Для начала, если вы не знали или случайно забыли про паттерн Proxy в общем виде, то можно освежиться здесь.
Допустим, что наш сервис MyServiceImpl
имеет 2 публичных метода, аннотированных @Transactional
— method1
и method2
(он с Propagation.REQUIRES_NEW
). В method1
вызываем method2
.
В связи с тем, что для поддержки транзакций через аннотации используется Spring AOP, в момент вызоваmethod1()
на самом деле вызывается метод прокси объекта. Создается новая транзакция и далее происходит вызовmethod1()
классаMyServiceImpl
. А когда изmethod1()
вызовемmethod2()
, обращения к прокси нет, вызывается уже сразу метод нашего класса и, соответственно, никаких новых транзакций создаваться не будет.
Это цитата и краткий ответ на вопрос из статьи на Хабре, где можно ознакомиться с подробностями.
Что тут можно ещё посоветовать? Spring-потрошитель опять и снова — часть 1 и часть 2. А также документация является несомненным и любимым первоисточником информации о Proxy и управление транзакциями.
Старое доброе обычное Spring-приложение деплоится в контейнер сервлетов (или сервер приложений), где и расположен main-класс. При этом оно собирается в war-архив. Когда war-файл разворачивается в контейнере, контейнер обычно распаковывает его для доступа к файлам, а затем запускает приложение. Spring Boot приложение также можно собрать как war и задеплоить его таким же образом.
Подробнее:
- Понимание WAR
- В чём разница между jar и war?
- Конвертация Spring Boot JAR приложения в WAR на RUS или ENG
Во-первых, благодаря 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
- Список готовых стартеров
В современном Spring есть два подхода к построению веб-приложений:
- Spring MVC
- Spring WebFlux
Работа Spring MVC строится вокруг DispatcherServlet, который является обычным Servlet
'ом и реализует паттерн Front Controller: принимает Http-запросы и координирует их с требуемыми обработчиками. Для своей конфигурации DispatcherServlet
использует WebApplicationContext. DispatcherServlet
в обработке запроса помогают несколько "специальных бинов" следующим образом:
- После получения HTTP-запроса
DispatcherServlet
перебирает доступные ему (предварительно найденные в контексте) экземпляры HandlerMapping, один из которых определит, метод какогоController
должен быть вызван. РеализацииHandlerMapping
, использующиеся по умолчанию: BeanNameUrlHandlerMapping и RequestMappingHandlerMapping (создаёт экземпляры RequestMappingInfo по методам аннотированным @RequestMapping в классах с аннотацией @Controller).HandlerMapping
по HttpServletRequest находит соответствующий обработчик — handler-объект (например, HandlerMethod). КаждыйHandlerMapping
может иметь несколько реализаций HandlerInterceptor — интерфейса для кастомизации пред- и постобработки запроса. Список изHandlerInterceptor
'ов и handler-объекта образуют экземпляр класса HandlerExecutionChain, который возвращается вDispatcherServlet
. - Для выбранного обработчика определяется соответствующий HandlerAdapter из предварительно найденных в контексте. По умолчанию используются HttpRequestHandlerAdapter (поддерживает классы, реализующие интерфейс HttpRequestHandler), SimpleControllerHandlerAdapter (поддерживает классы, реализующие интерфейс Controller) или RequestMappingHandlerAdapter (поддерживает контроллеры с аннотацией
@RequestMapping
). - Происходит вызов метода
applyPreHandle
объектаHandlerExecutionChain
. Если он вернётtrue
, то значит всеHandlerInterceptor
выполнили свою предобработку и можно перейти к вызову основного обработчика.false
будет означать, что один изHandlerInterceptor
взял обработку ответа на себя в обход основного обработчика. - Выбранный
HandlerAdapter
извлекается изHandlerExecutionChain
и с помощью методаhandle
принимает объекты запроса и ответа, а также найденный метод-обработчик запроса. - Метод-обработчик запроса из
Controller
(вызванный черезhandle
) выполняется и возвращает вDispatcherServlet
ModelAndView. При помощи интерфейса ViewResolverDispatcherServlet
определяет, какой View нужно использовать на основании полученного имени.
Если мы имеем дело с REST-Controller или RESTful-методом контроллера, то вместоModelAndView
вDispatcherServlet
изController
вернётсяnull
и, соответственно, никакойViewResolver
задействован не будет — ответ сразу будет полностью содержаться в теле HttpServletResponse после выполненияhandle
. Чтобы определить RESTful-методы, достаточно аннотировать их @ResponseBody либо вместо@Controller
у класса поставить @RestController, если все методы котроллера будут RESTful. - Перед завершением обработки запроса у объекта
HandlerExecutionChain
вызывается методapplyPostHandle
для постобработки с помощьюHandlerInterceptor
ов. - Если в процессе обработки запроса выбрасывается исключение, то оно обрабатывается с помощью одной из реализаций интерфейса HandlerExceptionResolver. По умолчанию используются ExceptionHandlerExceptionResolver (обрабатывает исключени из методов, аннотированных @ExceptionHandler), ResponseStatusExceptionResolver (используется для отображения исключений аннотированных @ResponseStatus в коды HTTP-статусов) и DefaultHandlerExceptionResolver (отображает стандартные исключения Spring MVC в коды HTTP-статусов).
- В случае с классическим
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 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));
}
- Каждый из экземпляров HandlerMapping пытается найти подобающий обработчик для данного запроса (какой-то метод, какого-то контроллера). В итоге выбирается первый найденный обработчик (
handler
). Основными доступными реализациямиHandlerMapping
являются:
- RequestMappingHandlerMapping для методов-обработчиков, аннотированных
@RequestMapping
- RouterFunctionMapping для функциональных обработчиков
- SimpleUrlHandlerMapping для маппинга URL-ов на бины-обработчики запросов
- RequestMappingHandlerMapping для методов-обработчиков, аннотированных
- Если обработчик найден, то для него (в
invokeHandler
) выбирается подходящий HandlerAdapter, вызывается его методhandle
для непосредственной обработки запроса выбранным обработчиком. Результат обработки упаковывается в HandlerResult, который возвращается вDispatcherHandler
. Главная задачаHandlerAdapter
— скрыть детали и способ непосредственного вызова метода-обработчика отDispatcherHandler
. Примерами доступных реализацийHandlerAdapter
являются:
- RequestMappingHandlerAdapter — для вызова методов, аннотированных
@RequestMapping
- HandlerFunctionAdapter — для вызова HandlerFunctions
- RequestMappingHandlerAdapter — для вызова методов, аннотированных
- Полученный
HandlerResult
обрабатывается (вhandleResult
) необходимым для него HandlerResultHandler. Здесь обработка завершается формированием ответа на запрос требуемым образом. По умолчанию доступно несколько реализацийHandlerResultHandler
:
- ResponseEntityResultHandler — обрабатывает ResponseEntity, обычно из
@Controller
- ServerResponseResultHandler — обрабатывает ServerResponse, обычно из функциональных контроллеров
- ResponseBodyResultHandler — обрабатывает возвращаемые значения из методов, аннотированных
@ResponseBody
, или методов класса@RestController
- ViewResolutionResultHandler — инкапсулирует в себе алгоритм View Resolution и обработку поддерживаемых данным алгоритмом типов результатов
- ResponseEntityResultHandler — обрабатывает ResponseEntity, обычно из
Документация:
Продолжение следует
Во второй части поговорим о Hibernate, базах данных, паттернах и практиках разработки, об одной популярной библиотеке, поддержке и сопровождении наших приложений, а также посмотрим на альтернативные шпаргалки и подведём итоги.