Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
显示更多📈 Telegram 频道 Java: fill the gaps 的分析概览
频道 Java: fill the gaps (@java_fillthegaps) 俄语 语言赛道中的 是活跃参与者。目前社区聚集了 12 552 名订阅者,在 技术与应用 类别中位列第 10 101,并在 俄罗斯 地区排名第 52 755 位。
📊 受众指标与增长动态
自 невідомо 创建以来,项目保持高速增长,吸引了 12 552 名订阅者。
根据 05 六月, 2026 的最新数据,频道保持稳定运转。过去 30 天订阅人数变化为 -49,过去 24 小时变化为 -4,整体触达仍然可观。
- 认证状态: 未认证
- 互动率 (ER): 平均受众互动率为 34.71%。内容发布后 24 小时内通常能获得 N/A% 的反应,占订阅者总量。
- 帖子覆盖: 每篇帖子平均可获得 0 次浏览,首日通常累积 0 次浏览。
- 互动与反馈: 受众积极参与,单帖平均反应数为 0。
- 主题关注点: 内容集中在 redis, hashmap, linkedhashmap, индекс, фича 等核心主题上。
📝 描述与内容策略
作者将该频道定位为表达主观观点的平台:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
凭借高频更新(最新数据采集于 07 六月, 2026),频道始终保持新鲜度与高覆盖。分析显示受众积极互动,使其成为 技术与应用 类别中的关键影响点。
String str = "Hello, " + name + "!";Этот подход использует StringBuilder, метод concat и тд. 🔸 Интерполяция — замена переменных внутри шаблона:
String name = "Jake";
String str = "Hello, ${name}!";
В чистом виде в java такого нет. Отдаленно похожи Formatter и MessageFormat, но там вместо переменных какие-то %s и %d, а переменные стоят отдельно:
String.format("%d plus %d equals %d", x, y, x + y);
А так чистокровная интерполяция выглядит в Kotlin:
"$x plus $y equals ${x + y}"
Новые String templates реализуют своеобразный вариант java интерполяции. В начале строки добавляется STR, переменные обрамляются в \{}
int x = 10, y = 20;
String str = STR."\{x} + \{y} = \{x + y}";
// "10 + 20 = 30"
❓ Зачем они добавили префикс STR? Почему нельзя сделать как в котлине?
По 2 причинам:
1️⃣ Для обратной совместимости
На джаве написано много кода и библиотек. Некоторые из них используют формат с фигурными скобками, и будет обидно, если этот код перестанет компилироваться. Чтобы этого избежать, решили явно обозначать строки для интерполяции
2️⃣ Для других обработчиков
По задумке авторов другие процессоры могут делать больше, чем просто подстановку переменных. Например, собрать и провалидировать SQL запрос:
String sql = DB."select from users where id=/{id}";
❓ Что пошло не так?
Разработчики собрали фидбэк и решили, что процессоры (STR, DB, etc) — это лишнее усложнение и очень странный API. Я с ними согласна, выглядит как работа со статическими полями, и вряд ли кому-то нужно что-то большее, чем работа со строками.
❓ Что в итоге с интерполяцией?
Brian Goetz (архитектор java) написал:
The remaining question that everyone is probably asking is: “so how do we do interpolation.” The answer there is “ordinary library methods”.
= в текущем виде фича не будет реализована, интерполяции как в других языках тоже не будет, всё остаётся как есть.
Напомню, что идея String templates появилась в 2021 году, прошлой осенью вышла preview версия. И только сейчас разработчики поняли, что фича так себе. Косяки бывают на всех проектах, это нормально:)record Point(int x, int y) { }
Point p1 = new Point(1,1);
Допустим, нам нужна точка p2, у которой Х в 2 раза больше, чем у p1:
Point p2 = new Point(p1.x()*2, p1.y());
Надо прочитать ВСЕ поля исходного объекта и передать их в конструктор с нужными модификациями. Даже в классе с двумя полями надо напрячь глаза, чтобы понять, какой параметр меняется. В реальной жизни полей больше, и получается максимально унылая простыня кода.
Вместо конструктора удобно использовать with методы:
Point p2 = p1.withX(p2.x()*2);Сразу видно параметр, которым отличаются точки🔥 With методы — отличное решение, но не идеальное. Возьмём случай, когда между полями должны соблюдаться некоторые соотношения. Например, Х всегда должен быть больше Y. With методы работают только с одним полем и не знают про дальнейшие изменения. Поэтому сложная валидация в них затруднительна. После вызова
Point p3 = p1.withX(p1.x() - 5);получаем объект со сломанным инвариантом. Мы не знаем, вызовет ли пользователь потом withY, и не можем это проконтролировать. Новый JEP работает с такими случаями и предлагает создавать объекты так:
Point p4 = p1 with {
x *= 2;
y *= 2;
};
🔸 Названия полей p1 берутся в качестве локальных переменных с теми же именами
🔸 Выполняются преобразования в скобках
🔸 После выполнения блока вызывается канонический конструктор Point, где находится валидация полей
🔸 Полученный объект присваивается объекту p4
В целом всё неплохо, но возникает вопрос целесообразности. На моей практике рекордс обычно используют как контейнер данных, который редко меняется. Если всё же меняется, то обычно это простая логика, куда бы идеально вписались with методы. Если бы records превращались в классы с готовыми with методами — было бы супер удобно.
Я редко встречала в рекордах сложную валидацию и жёсткие отношения между полями. Мне кажется, добавлять новый синтаксис для таких случаев избыточно. Если нужна проверка инвариантов, можно написать свой метод.
Поэтому вердикт новой фиче — сомнительно, но окэй. Я бы потратила человеко-часы на что-нибудь другое🙂 К слову, это уже вторая фича java 23, которая не вызвала у меня восторга. Первой были Gatherers, почитать можно тут
А как вам новый синтаксис в рекордах?
🔥 - огонь, очень полезно
👍 - не вау, но пусть будет
❤️ - просто спасибо за пост:)Optional — супер удобный инструмент, который появился в java 8. В документации его цель явно обозначена — показать, что результат вызова метода может отсутствовать.
Например, функция поиска может найти нужный элемент, а может и не найти. В этой ситуации отлично подойдёт Optional:
Optional<String> search();
С виду всё просто и понятно.
Тем не менее, на практике Optional часто используется некорректно, код получается сложным или плохо читаемым. Самые популярные ошибки:
1️⃣ Использовать Optional во входных параметрах и конструкторах
❌ void init(Optional<String> value) {…}
Приходится добавлять лишние обёртки и проверки.
✅ Лучше проверять параметры на null в начале метода:
void init (String value) {
if (value == null) …
}
2️⃣ Возвращать Optional при наличии дефолтного значения
❌ return Optional.ofNullable(value).orElse("default");
✅ return value == null ? "default" : value;
3️⃣ Обрабатывать Optional не сразу
и тащить его далеко по коду. Идеально, если обработка происходит сразу после возвращения из метода:
Optiona<String> valueOpt = …
String value = valueOpt.orElse("default");
4️⃣ Возвращать Optional для коллекций
❌ public Optional<List<Integer>> search(…)
Если элементов нет, верните пустой список:
✅ public List<Integer> search(…) {
… return List.of();
}
5️⃣ Неоптимальная работа с примитивами
Для int, long и double есть специальные классы: OptionalInt, OptionalDouble и OptionalLong.
У них нет затрат на создание объекта, а код становится чуть короче и понятнее. Методов меньше, чем в классическом Optional, но при интенсивной работе с примитивами возможен прирост производительности.
Ответ на вопрос перед постом
Входные параметры лучше проверять внутри метода:
❌ List<Student> search(Optional<String> city)
❌ void init(Optional<String> conf)
Лучше вернуть пустой список, если элементов нет:
❌ Optional<List<Student>> all()equals и hashcode. За что отвечают, как соотносятся между собой, когда переопределять, а когда не стоит.
Если хочется посмотреть, как думает человек за пределами стандартного ответа, возможен такой диалог:
— Как считается хэшкод по умолчанию?
— Это адрес объекта в памяти
— А почему так?
— Адрес каждого объекта уникален, то что надо для хэшкода
— Сборщик мусора перемещает объекты внутри памяти. Как это влияет на значения хэшей?
— 😥
Тут можно предположить, что хэшкод считается один раз и приписывается к самому объекту. Это будет логичная мысль.
А вот вычисление хэша на основе адреса в памяти — популярный миф. В этом посте разберём, как на самом считается хэшкод под умолчанию.
В разных JVM реализации могут отличаться. Рассмотрим исходный код hashcode в OpenJDK. Там 6(!) стратегий вычисления хэшкода. Стратегия задаётся опциями VM:
-XX:+UnlockExperimentalVMOptions -XX:hashCode={число}
При первом вызове хэш сохраняется внутри объекта и не меняется. Теперь к стратегиям:
🔸-XX:hashCode=0
Случайное число по алгоритму Lehmer RNG. Генератор один на всех, поэтому работает медленно
🔸-XX:hashCode=2
Чемпион по скорости, всегда возвращает 1:
java.lang.Object@1Используется как отправная точка для тестов остальных стратегий 🔸-XX:hashCode=3 Обычная возрастающая последовательность:
java.lang.Object@a4 java.lang.Object@a5 java.lang.Object@a6🔸-XX:hashCode=4 Текущий адрес в памяти. Популярный, но неправильный ответ на собеседованиях. Отчасти в этом виновата спецификация: там адрес приводится как пример реализации. Работает быстро, но не даёт равномерного распределения и должного уровня уникальности 🔸-XX:hashCode=1 Адрес объекта в памяти и немного манипуляций с битами 🔸 Стратегия по умолчанию Случайное число по алгоритму Xorshift RNG. Следующее значение вычисляется на основе предыдущего. Значения равномерно распределены. Работает быстро, тк у каждого потока свой генератор, и синхронизации между потоками нет Рейтинг стратегий по скорости: 🏆 Вернуть единицу: 184 операций за микросекунду 🥈 Вариант по умолчанию: 176 оп/мск 🥉 Адрес в памяти-1: 160 оп/мск ▪️ Растущая последовательность: 14 оп/мск ▪️ Случайное число и глобальная переменная: 10 оп/мск Интересно, что до java 8 самая медленная опция была вариантом по умолчанию. Итого ✅ Реализация хэшкода зависит от JVM и VM-флажков ✅ В OpenJDK 6 стратегий вычисления хэшкода. По умолчанию используется генератор случайных чисел в рамках одного потока ✅ Расчёт на основе адреса памяти не очень хорош по итоговым характеристикам ✅ Общие переменные в методах снижают производительность при интенсивном использовании. Яркие примеры — стратегии хэшкода с общим генератором и последовательностью
build
Всё это при умелом использовании создаёт симпатичное и лаконичное API.
Пример 1: класс StringBuilder
Гораздо интереснее, чем кажется:
▫️ Копит данные в изменяемом массиве, на выходе отдаёт неизменяемую строку
▫️ Методы append можно вызывать сколько угодно раз
▫️ Есть дополнительные методы вроде reverse или delete
В результате получаем удобный и функциональный класс. Ломбок такой билдер не соберёт, чатЖПТ такое не придумает:)
Пример 2: класс HttpClient
В самом простом варианте код выглядит так:
HttpClient client = HttpClient.newBuilder().build();
🤔 Зачем тут билдер? Почему просто не сделать new HttpClient()?
Потому что внутри build происходит такая магия:
SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
impl.start();
…
return facadeFactory.facade;
Тут прячется установка работы с сетью в 200+ строк кода и механизм по завершению работы с сетью, когда объект HttpClient станет не нужен. Хотя объект работает с ресурсами, его не надо помещать в блок try-with-resources.
Мелочь, а приятно🥰
И вторая классная особенность:
2️⃣ Билдер может сделать несколько объектов на базе переданных данных
Часто помогает при написании тестов.
Пример: тестируем работу с классом Account, в котором много полей. Можно в каждом тесте создавать тестовые объекты с нуля и копипастить километр кода. Или поступить иначе:
▫️ Сделать общий для всех тестов билдер, но build() не вызывать
▫️ В каждом тесте доставить нужные поля и построить новый объект
Получается так:
// общее поле c базовой информацией
Account.Builder accBuilder = Account.builder().INN(…).KPP(…)
// в тесте, где важен БИК:
Account acc = accBuilder.RCBIC(123).build();
// в тесте, где нужно название банка
Acccount acc = accBuilder.bankName("green").build();
Это короче, чем в каждом тесте создавать объект с нуля. Плюс сразу видно "главное" для теста поле.
Приём подходит не всегда, но иногда здорово улучшает читаемость.
Итого
✅ Метод build может содержать сложную логику: от проверки параметров до шифрований и преобразований
✅ Builder может создать несколько объектов на основе переданных данных
Возьмите эти свойства на заметку, в умелых руках паттерн Builder делает код проще и удобнее🔥Fluent API — это стиль написания кода. Грубо говоря, когда методы соединяются через точку (method chaining). С помощью Fluent API можно создать объект и проинициализировать поля.
▫️ Builder — паттерн для создания объектов, суть которого в постепенном накоплении информации. Билдер тоже использует method chaining и относится к fluent api.
В чём разница на практике?
Допустим, надо создать экземпляр класса Account с информацией о счёте. Рассмотрим 4 варианта:
1️⃣ Все параметры передаём в конструкторе:
Account acc = new Account(111, 222, 333);✅ Идеально для обязательных параметров. Компилятор не даст шанса пользователю что-то забыть ❌ Плохая читаемость — передаётся набор чисел, непонятно, что есть что ❌ Если есть необязательные параметры, нужно несколько конструкторов 2️⃣ Параметры выставляем через сеттеры:
Account acc = new Account();
acc.setINN(111);
acc.setKPP(222);
acc.setRCBIC(333);
✅ Читаемость лучше. Понятно, с какими полями работаем
❌ Нет контроля за обязательными параметрами. Пользователь должен знать, какие сеттеры вызвать обязательно, а какие нет
3️⃣ Используем Fluent API:
Account acc = Account.new()
.INN(111)
.KPP(222)
.RCBIC(333);
✅ Симпатичнее, чем сеттеры
❌ Та же проблема с обязательными параметрами, никаких проверок
❌ Не подходит для неизменяемых классов
❌ Более сложный код. Но если у вас разрешён ломбок, аннотация @Accessors(chain = true) упрощает задачу
4️⃣ Создание объекта через билдер:
Account acc = Account.builder().
.INN(111)
.KPP(222)
.RCBIC(333)
.build();
✅ В build() можно добавить нужные проверки
✅ Можно использовать для неизменяемых классов
❌ Надо написать отдельный класс Builder или использовать аннотацию @Builder из ломбока
Создание объекта через Fluent API не очень отличается от билдера по лаконичности, но проигрывает ему в функциональности. Fluent API не может проверить поля на "обязательность".
Итого
🔸 Если в классе мало полей, идём по классическому пути — обязательные параметры помещаем в конструктор, необязательные выставляем сеттерами
🔸 Если полей много или между полями сложные связи — используем билдер. Именованые методы улучшат читаемость, а метод build() проверит всё, что нужно
Fluent API обычно не даёт преимуществ в ситуациях выше, и редко используется для создания объектов. Его сильная сторона — логические блоки из нескольких операций🔥CompletableFuture.runAsync(…).thenRun(…).exceptionally(…);Fluent API не всегда уместен и не всегда реализован удачно. Список формальных условий определить сложно, на практике такой формат часто выбирается интуитивно. Поэтому рассмотрим побольше примеров! ✅ Хороший пример из AssertJ:
assertThat(str).startsWith(…).contains(…);
За раз пишем несколько проверок для строки str. Без Fluent API кода будет больше
✅ Хороший пример из Мокито:
when(mock.method()).thenReturn(…).thenReturn(…).thenThrow(…);Читается как одно предложение. Мне даже сложно представить, как это написать в “традиционном” стиле:) ✅ Прекрасный пример из Spring JDBC:
List<User > users = jdbcClient.sql(…)
.param("rating", 5, Types.INTEGER)
.query(mapper)
.list();
Почему пример прекрасен? Потому что Fluent API скрывает работу с объектом PreparedStatement. Код получается не только короче, но и проще🔥
😐 Так себе пример из Spring Data:
ExampleMatcher matcher = ExampleMatcher.matching() .withIgnorePaths(…) .withStringMatcher(StringMatcher.ENDING);Приставка with у методов лишняя, название последнего метода неудачное ❌ Плохой пример из SLF4J:
logger.atInfo().log(…);
Классический logger.info(…) короче и удобнее
❌ Плохой пример из популярного джава канала:
Person person = new Person().setName(…).setAge(…);Тоже никакой пользы от Fluent API, ни по читаемости, ни по удобству использования. Итого Fluent API подойдёт, когда работа с объектом проходит в несколько шагов, но единым логическим блоком. Основная цель — улучшить читаемость. Высший пилотаж — повысить с помощью Fluent API уровень инкапсуляции. Чтобы сделать удобно и красиво, нужен опыт, насмотренность и немножко вдохновения:) Отдельный случай — Fluent API при создании объектов. Здесь часто возникает путаница с билдером, и непонятно, что когда использовать. Этот вопрос я разберу отдельно в следующем посте🔥
var, текстовые блоки, records, pattern matching, sealed классы, string templates и так далее.
Что-то получается хорошо, что-то не очень. Где-то много пафосных разговоров про data-oriented programming. Есть странные фичи, вроде упрощения написания Hello world.
⭐️ Project Leyden
Цель: ускорить время старта Java программ
Оптимизировать загрузку классов, линковку, перенести часть процессов на этап компиляции. На энтерпрайз повлияет мало, по сравнению с работой фреймворков ускорения на уровне JVM будут мало заметны.
⭐️ Project Valhalla
Цель: оптимизировать работу с данными
Здесь так же два направления:
🔹 Создать value types — объект с полями и методами, работа с которым идёт как с примитивом:
✅ Передаётся по значению
✅ Компактно лежит в памяти
✅ Не может быть null
🔹 Создать общую схему работы с примитивами, объектами и value types, избавить разработчика от мыслей про boxing/unboxing
❓ А когда будет готово?
Плохая новость — реализации всех проектов растягиваются на десятки лет.
10 лет — не преувеличение. Лямбда-выражения в java обсуждались с 2004 года, а увидели свет только в 2014.
В случае Java медлительность — это фича и часть стратегии: смотреть, как решаются проблемы в других языках и не изобретать велосипед. Осторожно выбирать, что войдёт в язык, а что — нет, тщательно продумывать архитектуру.
На java пишут большие системы, которые работают десятки лет. Поэтому основательный подход абсолютно оправдан😌clients.stream()
.distinct(Client::getBonusType)
.map(Client::getId).forEach(…)
Но это невозможно, distinct работает только по equals. В итоге надо либо извращаться, либо переписывать на for.
В Java 22 в Stream API появится универсальный метод gather. Туда можно передать логику преобразований с любыми отношениями - 1:1, 1:N, N:1, N:N. Синтаксис сложный, но круто, что такая возможность появилась.
Второе нововведение. Для collect есть готовые статические методы в классе Collectors, а для gather появится Gatherers с новыми методами. Самое полезное — два метода по работе с окнами:
🪟 windowFixed — поделить стрим на подмножества заданного размера
[1,2,3,4,5,6] → windowFixed(3) → [1,2,3], [4,5,6]🪟 windowSliding — подмножества с пересечением одного элемента
[1,2,3,4,5,6,7] → windowSliding(3) → [1,2,3], [3,4,5], [5,6,7]Что не так c методом gather? 1️⃣ Многословность Та же ситуация, что и с collect. Чтобы разбить список на окна, придётся написать
list.stream()
.gather(Gatherers.windowFixed(2))
.collect(Collectors.toList())
Даже со статическим импортом выглядит не очень. Хочется писать без лишних слов:
list.stream()
.windowFixed(2)
.toList()
Да, исходный код Stream API сложный, и его тяжело расширять. Да, через статические методы реализация получится проще. Но не вижу ничего невозможного, чтобы сделать новые методы лаконичными, без приставки "gather(Gatherers"
2️⃣ Странные новые методы
Начнём с оконных. windowSliding делает пересечение только по одному элементу. Зачем нужно это ограничение — непонятно. Так же непонятно, зачем делать два отдельных метода windowFixed и windowSliding.
За образец можно взять Kotlin:
list.windowed(5,2)Первый параметр задаёт размер окна, второй — шаг, с которым идём по списку. Удобно и понятно. Ещё в Gatherers появятся три странных метода: fold, mapConcurrent и scan. С первого взгляда непонятно, зачем они нужны, очень уж специфичны. В целом криминала в gather/Gatherers нет, жить можно. Но важный навык разработчика — замечать слабые места в своих и чужих решениях. Этот навык нужно развивать, для этого и нужен этот пост:) Что ещё почитать: 🔥 Серия постов про коллекторы. Там же я рассказала, почему в стримах используется отдельный метод collect, а не просто toList() 🔥 Новые методы Stream API в Java 16 🔥 Критикую метод HashMap в Java 20. Хотя сам метод маленький, он показывает серьёзную ошибку проектирования API
cancel(true), и в чём разница с cancel(false).
Метод Future#cancel — наглядная иллюстрация, почему boolean параметры в public методах не ок. Потому что непонятно, что означают true и false.
Public методы определяют интерфейс. В хорошем API пользователю не нужно зарываться в доки, чтобы понять, как пользоваться классом.
Даже если у параметров хорошие имена, итоговые true и false выглядят несимпатично и неудобно.
С private методами ситуация другая. Внутренняя реализация должна быть читаемой и понятной, но требования всё же менее жёсткие, чем у public методов. В private методах флажки допустимы, но увлекаться не стоит:)
Как исправить метод с boolean параметром?
1️⃣ Сделать два метода
и зашить "особенность" флажка в название. Необязательно дублировать реализацию — public методы могут под капотом вызывать private метод с флажком.
Так сделано, например, в классе HashMap для методов put(k,v) и putIfAbsent(k,v). Оба в итоге вызывают private метод putVal с флажком ifAbsent
2️⃣ Создать enum с двумя значениями и передавать его вместо флажка
enum ReplicateStatus {NO_REPLICATION, REPLICATE};
...
saveRequest(req, ReplicateStatus.REPLICATE);
Такой вариант предлагается в книге Effective Java (Item 51).
Выглядит лучше, чем флажки, но на практике такой способ встречается редко. Если enum нужен только для обозначения true/false, быстрее и проще сделать 2 метода с разными именами.
🔥 Ответ на вопрос перед постом — "невозможно определить".
Мы не знаем точно, начала ли задача выполнение до вызова cancel. Даже если задача стартовала, успех cancel зависит от кода внутри задачи. Поэтому итог непредсказуем🤷♀️persist/save/merge, использует нужные типы данных и тд.
А вот JPA определяет только интерфейс доступа к данным. Поэтому в Spring Data JPA многие хибернейт фичи не используются.
Пример — ленивая загрузка коллекций и кэш 1 уровня. Spring Data в общем случае при каждом обращении к репозиторию создаёт новую сессию. Кэширования в итоге нет, а при загрузке коллекций ловим эксепшн.
Кэш 2 уровня и EntityGraph поправят ситуацию, но это уже продвинутый уровень:) Недостаточно пользоваться абстракцией "репозиторий", надо знать и Hibernate, и как Spring использует Hibernate.
Практический совет — если что-то читаете по хибернейту, уточняйте, как это работает в Spring Data и работает ли вообще.
Для простых сервисов Spring Data JPA существенно упрощает жизнь. Для сложных тоже, но требует больше знаний.
Spring Data JDBC
— альтернатива Spring Data JPA. Под капотом у него JDBC без посредничества Hibernate.
Интерфейс такой же — пользователь работает с репозиторием и размечает классы аннотациями типа @Id или @Column.
JDBC проще, у него нет кэшей, ленивой загрузки, каскадных операций и автоматического сохранения. Код становится предсказуемым, но многие вещи нужно делать явно.
Отдельного внимания заслуживает работа с зависимыми сущностями в DDD стиле. А в этом докладе показан наглядный пример и больше различий Spring Data JPA/JDBC.
Важный момент! Не путайте две библиотеки:
🌸 Spring JDBC упрощает работу с соединениями. Запросы, маппинг сущностей, управление транзакциями пишет разработчик
🌹 Spring Data JDBC даёт следующий уровень абстрации — репозиторий. Работа c запросами, маппингом и транзациями упрощается за счёт аннотаций
MyBatis
часто упоминается как альтернатива Hibernate. Называет себя persistence framework, а не ORM, но занимается тем же — помогает писать меньше кода по перегону данных между БД и приложением.
Основное отличие MyBatis от хибернейта — все SQL-запросы пишутся явно, и внутри можно писать if и foreach блоки.
MyBatis в целом ничего, но редко встречается. Причины просты:
❌ Нет Spring Data модуля, только Spring Boot Starter. Писать руками нужно гораздо больше
❌ В MyBatis есть аннотации, но документация и большинство статей используют XML. Выглядит несовременно👨🦳
Итого
⭐️ Spring Data * берёт на себя конфиги, работу с сессиями, генерацию некоторых запросов
⭐️ Spring Data JPA упрощает работу с Hibernate
⭐️ Spring Data JDBC предлагает похожий интерфейс, но на основе JDBC
⭐️ MyBatis для тех, кто хочет чего-то другого
Что выбрать?
Функционально Spring Data JPA/JDBC и MyBatis похожи, но со своими нюансами. Адекватных и современных бенчмарков в интернете нет. Статьи вроде "Hibernate vs MyBatis" очень поверхностные, не тратьте на них время.
На практике выбор делается почти случайно. Что затащат в проект на старте, то и используется:)
现已上线!2025 年 Telegram 研究 — 年度关键洞察 
