Спринговая аннотация
@Transactional по умолчанию работает следующим образом - любой RuntimeException или Error приводит к роллбеку, но Exception - нет. Такое поведение - прямое наследие EJB (Enterprise JavaBeans), где checked-исключения (наследники Exception) считались ошибками бизнес-логики, а unchecked (наследники RuntimeException) - неожиданными системными/техническими ошибками. Поэтому ожидаемые бизнесовые ошибки предлагалось обрабатывать явно и не отказывать транзакцию автоматически.В современном мире такое соглашение редко соблюдается, к тому же даже в стандартной библиотеке полно checked-исключений, которые нельзя назвать бизнесовыми (вроде
IOException). Разумеется, в Spring это настраивается, у @Transactional есть гибкие способы точечно подтюнить это поведение с помощью параметров rollbackFor/rollbackForClassName/noRollbackFor/noRollbackForClassName. Но даже в Java многие не раз попадались на этот нюанс, или не зная о нем, или забыв.А в Kotlin всё становится еще интереснее, ведь в Kotlin нет такого понятия как checked-exception. Да, код на Kotlin работает с кодовой базой Java и может взаимодействовать с наследниками Exception, но компилятор не заставит обработать такое исключение, как произошло бы в Java. Как так? А всё потому что checked-исключения - это фича именно языка Java и его компилятора, а не самой JVM. На уровне байткода между этими исключениями нет никакой разницы, и в рантайме они ведут себя одинаково. Только компилятор Java заставляет как-то по-особенному обрабатывать checked-исключения. А вот компилятор Kotlin нет. И в случае
@Transactional это большой подвох, потому что разработчик на Kotlin уже понятия не имеет, является ли его исключение наследником Exception или RuntimeException. За исключением работы с @Transactional нет никакой разницы, поэтому и нет смысла задумываться о различиях.Разумеется, разработчик, который всё-таки знает о таком поведении (а скорее, уже однажды попавшийся на ситуацию с не откатившейся транзакцией), просто напишет
@Transactional(rollbackOn = Exception::class.java). Но блин, это же нужно делать везде. Где-то наверняка забудем. К тому же, выглядит уже грязновато.И до недавних пор адекватного способа решить эту проблему глобально - не было. Но в Spring 6.2 (и Spring Boot 3.4, соответственно) наконец-то появилось решение, закрыв собой ишью аж от 2019 года. Теперь можно задать глобальное поведение по умолчанию вот таким образом -
@EnableTransactionManagement(rollbackOn=ALL_EXCEPTIONS), а какие-то дополнительные глобальные тюнинги можно выполнять, настраивая бин AnnotationTransactionAttributeSource. Разумеется, команда Spring не может сделать это новым дефолтом, потому что это сломает обратную совместимость, так что фича исключительно opt-in. Но они рекомендуют самостоятельно включить такой режим, и в Java, и, тем более, в Kotlin-приложениях.