Что, если ваш валидатор стал бы в 3 раза быстрее и потреблял бы вдвое меньше памяти — без единой правки бизнес-логики? Именно это случилось с Hibernate Validator 9.1: ушли тяжёлые коллекции, пришёл умный стек. Каскадная валидация теперь летает, даже при циклах в графе объектов.
Плюс бонус: меньше мусора в памяти, меньше аллокаций, быстрее интерполяция сообщений. В бенчмарках — просто космос. Все это – в новом переводе от команды Spring АйО.
Комментарий Поливаха Михаила: Несмотря на то, что с валидацией мы напрямую работаем не часто, имейте в виду, что Spring Boot и ваши @RestController-ы под капотом всё равно используют hibernate-validator. Поэтому почитайте, не поленитесь.
Hibernate Validator — это рефернсная реализация спецификации Bean Validation. Дополнительную информацию вы можете найти на сайте hibernate.org в разделе Hibernate Validator.
Комментарий от Михаила Поливаха
spring-boot-starter для validation использует рефернсную реализацию, то есть как раз hibernate-validator. В большинстве веб приложений на Spring можно увидеть подобного рода код:
@PostMapping
ResponseEntity<String> addComment(@RequestBody @Valid AddCommentPayload addCommentPayload) {
// code
}
где сами DTO имеют внутри себя jakarta.validation-api
аннотации, такие как @NotNull, @NotBlank и др.
import jakarta.validation.constraints.NotBlank;
public record AddCommentPayload(@NotBlank String featureCode, @NotBlank String content) {}
И вот spring-web, конечно, сам эту валидацию не реализовывает, а делегирует эту работу (по-умолчанию) hibernate-validator-у.
Недавно был анонсирован выпуск Hibernate Validator 9.1.0.Alpha2, новая версия делает упор на повышение производительности.
Давайте разберёмся, что изменилось, и взглянем на цифры — в конце концов, всем хочется увидеть эти диаграммы!
Что изменилось?
Наша первоначальная цель заключалась в повышении эффективности каскадной валидации графов объектов с большим количеством узлов в Hibernate Validator. Известная основная проблема, вызывавшая снижение производительности, заключалась в отслеживании уже обработанных бинов.
Поскольку валидатор должен определять тип валидируемого экземпляра узла во время выполнения, ему необходимо отслеживать уже встречавшиеся бины, чтобы выявлять циклы в графе объектов и корректно прекращать его обход. Это отслеживание также должно учитывать преобразование групп: допустимо проходить через один и тот же экземпляр объекта несколько раз, если это происходит для группы валидации, которая ещё не была обработана для данного экземпляра.
Комментарий от Михаила Поливаха
Я просто на всякий случай уточню, что под "beans"/бинами здесь имеются в виду простые Java объекты (pojo-s), которые нужно провалидировать.
Под "группой" автор имеет в виду концепцию validation groups, которая уже достаточно давно существует в Jakarta Validation API.
Тут про неё можно почитать подробнее: https://www.baeldung.com/javax-validation-groups

Прямоугольники обозначают узлы бинов.
Стрелки показывают, как связаны между собой бины.
Красные стрелки указывают на путь, вызывающий цикл валидации.
Изначально для отслеживания обработанных бинов использовалось несколько коллекций, которые постепенно заполнялись по мере обхода графа объектов. Такой подход был достаточно эффективен, пока граф объектов не содержал слишком много узлов. В приведённом выше примере, в зависимости от порядка обхода, мы могли начать с валидации узла 0, затем перейти к коллекции и пройтись по всем её узлам, после чего дойти до узла 1001, откуда последовательно перейти к 1002, 1003, 1004 и в итоге замкнуть цикл валидации, вернувшись к 1001. После завершения обработки узла мы добавляли его в коллекцию отслеживания. А перед переходом к следующему узлу необходимо было проверять, был ли он уже обработан для текущей группы валидации, что означало постоянную проверку против всё увеличивающейся коллекции.
Мы подошли к решению этой проблемы с нескольких сторон. Во-первых, если можно определить, что конкретный узел не может быть частью циклического пути валидации, его отслеживание можно полностью пропустить. Идея заключается в том, что бин не может образовать цикл, если он не содержит каскадных узлов или если содержащиеся в нём узлы сами по себе не могут вызвать цикл.
Комментарий от Михаила Поливаха
Вот например такой Java объект не содержит каскадных узлов:
class Person {
@NotBlank
private String name;
@NotNull
private Address address;
}
По-умолчанию Jakarta Validation API не будет валидировать референсные внутренние поля bean-а, кроме тех случаев, если это самое поле аннотировано @Valid. В примере выше поле address хоть и является референсом, но оно не аннотировано @Valid и, соот-но, не является каскадным узлом.
Во-вторых, если мы можем отбросить бины, которые больше не имеют значения для отслеживания, то сможем удерживать лишь разумное количество бинов для проверки, что делает саму проверку более эффективной. Вместо коллекции обработанных путей мы перешли к стековому подходу, в котором хранятся только узлы текущего пути валидации.
С учётом этих идей, возвращаясь к примеру выше, мы можем пропустить проверки отслеживания для узлов в коллекции (1..1000), а также для узла 1002, поскольку они не содержат дальнейшей каскадной валидации. А в случае цикла валидации 1001 → 1003 → 1004 → 1001 нам нужно проверять только эти три узла.
Изменение подхода к отслеживанию бинов открыло возможность переработки реализации пути валидации, что давно было у нас на радаре. Ранее для отслеживания бинов требовались пути, и внутренняя реализация пути была неизменяемой, что приводило к большому количеству выделений памяти и операций копирования. С переходом на стековую модель отслеживания путь валидации больше не должен быть неизменяемым, чем мы и воспользовались.
Как это часто бывает при работе над производительностью, в процессе тестирования мы также выявили и устранили несколько «низковисящих фруктов»:
Мы внесли изменения в область интерполяции сообщений валидации. Хотя сама интерполяция сообщений — задача далеко ��е тривиальная, нам удалось найти способ сократить количество лишних операций со строками и копирования коллекций, и мы воспользовались этой возможностью.
Ещё одно интересное наблюдение — переход к неизменяемым коллекциям (т.е. тем, что лежат в основе List.of/Set.of/Map.of). Hibernate Validator часто должен итерироваться по коллекциям с различными метаданными валидации. Хотя замена Collections.unmodifiable*(..) не дала значительного прироста, мы заметили, что в некоторых случаях итерация действительно стала быстрее — возможно, просто за счёт избавления от делегирующего итератора, который создаётся при использовании Collections.unmodifiable*(..).
Комментарий от Михаила Поливаха
Методы Collections.unmodifiableи правда создают делегирующий итератор, что при итерации по большим коллекциям может отразиться на перформансе.
В этой же области мы добавили возможность совместного использования данных при инициализации ограничений, что позволило переиспользовать экземпляры Pattern для аннотации @Pattern в случаях, когда совпадают регулярные выражения и флаги. Это приводит к созданию меньшего количества объектов, если приложение использует одни и те же шаблоны валидации для разных свойств.
Также мы удалили некоторые ненужные создания и копирования коллекций и внесли ещё ряд более мелких правок, которые в совокупности дали ощутимый прирост производительности.
Большинство соответствующих изменений можно найти в следующих наборах diff-файлов:
Введение нового подхода к отслеживанию обработанных бинов и улучшения интерполяции сообщений.
Дальнейшее использование возможности мутации внутреннего пути валидации.
Протестированные версии
Мы решили сосредоточить наш бенчмарк на следующих трёх версиях Hibernate Validator:
Hibernate Validator 9.1.0.Alpha2 (выпущена 19 сентября 2025 года)
Hibernate Validator 9.0.1.Final (выпущена 13 июня 2025 года)
Hibernate Validator 6.0.23.Final (выпущена 9 февраля 2022 года)
Почему именно такой выбор? Нам, безусловно, важно сравнить последнюю стабильную ветку 9.0 с новой, находящейся в разработке 9.1, чтобы оценить эффект от внесённых улучшений.
Что касается версии 6.0 — именно для неё мы ранее публиковали результаты производительности, и нам интересно посмотреть, насколько мы продвинулись с тех пор.
Проведённые бенчмарки
JMH-бенчмарки Hibernate Validator
В Hibernate Validator мы поддерживаем набор JMH-бенчмарков, каждый из которых направлен на тестирование конкретной области или проблемы. Эти бенчмарки можно запускать на разных версиях Hibernate Validator, что позволяет легко сравнивать результаты производительности.
Поскольку изменения в версии 9.1 касаются отслеживания обработанных бинов, реализации пути валидации и интерполяции сообщений, мы решили представить результаты по следующим бенчмаркам:
— SimpleSingleElementValidation — простой бенчмарк, валидирующий один простой бин с несколькими ограничениями и выполняющий три сценария:
когда валидация проходит успешно (для измерения производительности «положительного сценария»);
когда валидация приводит к нарушениям ограничений;
когда валидация приводит к нарушениям, но без необходимости интерполяции сообщения.
— CascadedWithLotsOfItemsValidation — бенчмарк, валидирующий бин с контейнером и с каскадной валидацией его элементов. Это тот случай, где необходимо отслеживание обработанных бинов.
— CascadedWithLotsOfItemsAndCyclesValidation — бенчмарк, аналогичный предыдущему, но с добавлением циклов в модель данных, то есть каскадная валидация должна корректно завершаться при повторной встрече уже обработанного бина.
Бенчмарк Bean Validation
Хотя бенчмарк Bean Validation основан на ��озможностях спецификации Bean Validation 1.1, он представляет собой гораздо более сложные сценарии тестирования, которые можно считать ближе к реальному использованию в приложениях. Этот набор включает два бенчмарка:
ParsingBeansSpeedBenchmark — оценивает фазу построения метаданных.
RawValidationSpeedBenchmark — оценивает непосредственно производительность валидации.
Методология
Выбирайте систему для тестов с умом
Рабочая станция или сервер, на котором вы собираетесь запускать бенчмарки, должны соответствовать ряду общих требований. В частности, следует избегать конфигураций с «всплесковыми» (burstable) CPU или ноутбуков, которые могут быстро начать троттлинг под нагрузкой. Эти бенчмарки интенсивно используют процессор, поэтому лучше всего запускать их на системе с хорошим охлаждением.
Для получения результатов, приведённых в этом материале, мы использовали рабочую станцию с Fedora 42 (6.16.7-200.fc42.x86_64), процессором Intel® Core™ i9 и 128 ГБ оперативной памяти. Бенчмарки запускались на системе, свободной от других ресурсоёмких процессов, то есть только с ОС и открытым терминалом.
Тесты выполнялись с использованием следующей версии JDK:
openjdk version "21.0.8" 2025-07-15 LTS
OpenJDK Runtime Environment Temurin-21.0.8+9 (build 21.0.8+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.8+9 (build 21.0.8+9-LTS, mixed mode, sharing)
Запускайте, запускайте и запускайте снова
Эти бенчмарки выполнялись многократно в процессе разработки, а также при подготовке этого материала. Несмотря на то, что все бенчмарки уже настроены с заданным количеством итераций прогрева и измерений, мы рекомендуем увеличить число итераций прогрева и дать бенчмарку поработать подольше. Это позволит вам наблюдать и сравнивать результаты между итерациями и поможет определить, в какой момент производительность стабилизируется на вашей тестовой системе.
Шаги по запуску этих бенчмарков и сбору результатов полностью автоматизированы и описаны в README-файлах для Hibernate Validator и Bean Validation бенчмарков. Это позволяет подготовить исполняемые бенчмарки для интересующих вас версий и запускать их последовательно одной командой.
Результаты бенчмарка
SimpleSingleElementValidation (валидный объект) бенчмарк JMH
Показатели выражены в операциях в миллисекунду (ops/ms): чем выше значение, тем лучше производительность.

SimpleSingleElementValidation (невалидный объект) бенчмарк JMH
Показатели выражены в операциях в миллисекунду (ops/ms): чем выше значение, тем лучше производительность.

SimpleSingleElementValidation (невалидный объект без интерполяции сообщений) бенчмарк JMH
Показатели выражены в операциях в миллисекунду (ops/ms): чем выше значение, тем лучше производительность.

CascadedWithLotsOfItemsValidation бенчмарк JMH
Показатели выражены в операциях в секунду (ops/s): чем выше значение, тем лучше производительность.

CascadedWithLotsOfItemsAndCyclesValidation бенчмарк JMH
Показатели выражены в операциях в секунду (ops/s): чем выше значение, тем лучше производительность.

Bean Validation (разбор) бенчмарк JMH
Показатели выражены в операциях в миллисекунду (ops/ms): чем выше значение, тем лучше производительность.

Bean Validation (валидация) бенчмарк JMH
Показатели выражены в операциях в секунду (ops/s): чем выше значение, тем лучше производительность.

Скорость выделения памяти
Мы также запустили эти бенчмарки с профилировщиком сборщика мусора (GC profiler), чтобы проанализировать скорость выделения памяти и операции сборки мусора. Показатели выражаются в байтах на операцию (B/operation): чем ниже значение, тем лучше.
Сначала рассмотрим простые сценарии валидации из SimpleSingleElementValidation:

За��ем перейдём к бенчмаркам каскадной валидации: CascadedWithLotsOfItemsAndCyclesValidation и CascadedWithLotsOfItemsValidation:

И, наконец, взглянем на результаты из бенчмарка Bean Validation:

Анализ результатов
Из представленных результатов бенчмарков видно, что производительность версии 9.1 значительно улучшилась в большинстве сценариев.
Рост пропускной способности
В следующей таблице показано, как Hibernate Validator 9.1 сравнивается с другими протестированными версиями по показателю пропускной способности (операций в секунду). Улучшение выражено в процентах и рассчитывается по формуле:
улучшение = ( [результат 9.1] - [результат предыдущей версии] ) / [результат предыдущей версии] × 100
Таблица 1. Улучшение пропускной способности (ops/sec) в разных версиях Hibernate Validator
Название бенчмарка |
От 6.0 до 9.1 |
От 9.0 до 9.1 |
CascadedWithLotsOfItemsAndCyclesValidation |
+188,186% |
+189,573% |
CascadedWithLotsOfItemsAndMoreConstraintsValidation |
+239,120% |
+239,821% |
SimpleSingleElementValidation (невалидный объект, без интерполяции) |
+210,317% |
+114,337% |
SimpleSingleElementValidation (невалидный объект) |
+149,530% |
+87,120% |
SimpleSingleElementValidation (валидный объект) |
+281,159% |
+196,709% |
Bean Validation (валидация) |
+161,098% |
+130,989% |
Улучшение по выделению памяти
Следующая таблица показывает, как Hibernate Validator 9.1 сравнивается с другими протестированными версиями по объёму выделяемой памяти (в байтах на операцию — B/operation). Улучшение выражено в процентах и рассчитывается по формуле:
улучшение = ( [результат 9.1] - [результат предыдущей версии] ) / [результат предыдущей версии] × 100
(отрицательное значение указывает на сокращение объёма выделений — это хорошо)
Таблица 2. Выделение памяти (B/operation) в разных версиях Hibernate Validator
Название бенчмарка |
От 6.0 до 9.1 |
От 9.0 до 9.1 |
CascadedWithLotsOfItemsAndCyclesValidation |
–63,542% |
–63,541% |
CascadedWithLotsOfItemsAndMoreConstraintsValidation |
–69,371% |
–69,370% |
SimpleSingleElementValidation (невалидный объект, без интерполяции) |
–68,077% |
–53,501% |
SimpleSingleElementValidation (невалидный объект) |
–60,343% |
–46,535% |
SimpleSingleElementValidation (валидный объект) |
–73,883% |
–66,222% |
Bean Validation (валидация) |
–67,843% |
–63,193% |
Единственный бенчмарк, показавший практически незаметное снижение пропускной способности, — это разбор метаданных. Хотя это можно списать на погрешность измерения, стоит отметить, что теперь при построении метаданных действительно выполняется несколько дополнительных операций. Однако, учитывая, что фаза разбора метаданных выполняется только один раз, это никак не влияет на производительность работающего приложения и на фактическую скорость валидации.
Что касается бенчмарков, измеряющих производительность валидации, здесь наблюдаются значительные улучшения во всех сценариях — в некоторых случаях пропускная способность выросла более чем в 2 раза, а объём выделяемой памяти сократился до 70%.
При анализе профилировочных данных для сценария SimpleSingleElementValidation (валидный объект) выяснилось, что почти половина времени тратится на рефлексию для получения значений свойств. Это отличный результат, так как он показывает, что в сценариях с «положительным исходом» (happy path) накладные расходы минимальны.

Сценарий с нарушением ограничений (SimpleSingleElementValidation (невалидный объект)) работает медленнее из-за сравнительно ресурсоёмкого процесса создания нарушения ограничения, в частности, из-за интерполяции сообщения (шаблон сообщения из аннотации должен быть преобразован в итоговое текстовое сообщение, включаемое в описание нарушения).
Эффект от интерполяции сообщений хорошо заметен при сравнении двух сценариев:
SimpleSingleElementValidation (невалидный объект) и
SimpleSingleElementValidation (невалидный объект без интерполяции сообщений) — последний работает быстрее именно потому, что исключена тяжёлая операция интерполяции.
Если сравнивать каскадные сценарии — CascadedWithLotsOfItemsValidation и CascadedWithLotsOfItemsAndCyclesValidation, — то второй демонстрирует ожидаемо более низкую пропускную способность. Это связано с тем, что при наличии цикла выполняется дополнительная операция: нужно попытаться провести каскадную валидацию для бина, который замыкает цикл. Прежде чем определить, что бин уже был обработан, необходимо получить его значение через рефлексию, а затем выполнить проверку на его повторное появление.
Взгляд в будущее
Как это часто бывает при работе над производительностью, в процессе анализа и профилирования уже применённых изменений обнаруживаются новые потенциальные точки оптимизации или появляются свежие идеи для решения известных проблем. Одной из таких перспективных областей остаётся интерполяция сообщений — здесь ещё есть пространство для дальнейших улучшений.
Как воспроизвести эти результаты
Все бенчмарки, представленные в этом материале, доступны по следующим ссылкам:
Из-за особенностей выполнения этих тестов, результаты на вашей системе могут отличаться, однако пропорциональные улучшения будут хорошо заметны независимо от конкретных чисел.
Выводы
Новая версия 9.1 демонстрирует более чем двукратный рост пропускной способности почти во всех проведённых бенчмарках.
Объём выделяемой памяти также значительно снижен — в большинстве случаев на 50–70% меньше байт на операцию.
Благодаря меньшему количеству ненужных аллокаций, уменьшается нагрузка на сборщик мусора, что дополнительно повышает общую эффективность.

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