
Когда-то я проходил серию собеседований на 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(по умолчанию)prototyperequestsessionapplicationwebsocket
Про 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) выполняется и возвращает вDispatcherServletModelAndView. При помощи интерфейса 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, базах данных, паттернах и практиках разработки, об одной популярной библиотеке, поддержке и сопровождении наших приложений, а также посмотрим на альтернативные шпаргалки и подведём итоги.
novoselov
Беспорядочный набор ссылок на все подряд (включая битые) от коротких статей до часового видео и никакой структуры.
В чем отличие от 1000 и 1 статей про «супер ответы на все вопросы с собеседований»?
frozen_coder Автор
Что ж, протухла одна ссылка пока писал. Бывает такое в интернете. Удалил. Спасибо за вашу бдительность.
Отличие в том, что всё в одном месте + много ссылок на разные материалы, разного формата, длины, подробности на любой вкус. Чтобы и пробежаться перед началом забега по собесам можно было, и покапать какие-нибудь вопросы отдельно.
Пару раз умышленно оставлял ссылки не только по теме вопроса, но и где-то около, чтобы можно было немного оглянуться по сторонам.
Во второй части статьи напихал альтернатив, если мой вариант вам не подходит.