Exception Handling in Java, Spring, Hibernate #9
ctapobep
started this conversation in
Conventions & Approaches
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Types of exceptions
There are 2 types of exceptions:
Exception
).RuntimeException
). Examples: violated a constraint, invalid request body, etc. We want to handle them at the top, API-level, inRestExceptionHandler
.Translating Database exceptions
We use both Hibernate and SpringJdbc to access the database, which use different hierarchies of exceptions for the same kind of errors. If it’s the same error that happens in both situations, we want to handle it just once and in just one way. Spring has an exception-translation mechanism but it sucks, so we had to hack it. All the places that we needed to update are listed below.
Define PgExceptionTranslator
This is an additional exception translator, for the exceptions that we wanted to handle in a non-default way. This translator has higher priority than the standard one.
It had to implement both
PersistenceExceptionTranslator
for Hibernate andSQLExceptionTranslator
for JdbcTemplate/JdbcClient. We must pass the translatorJdbcTemplate
explicitly.Define multiple
PersistenceExceptionTranslationPostProcessor
’sOne of the ways to apply the exception translation is with the Bean PostProcessor. It accepts an annotation which must be present on the class/method (the default one is
@Repository
) - Spring creates proxies for such classes.We really need the exception translations mechanism only at the highest level - API. If an exception is thrown by the
Controller
orRestController
, then our RestExceptionHandler is going to handle it. So we had to define the PostProcessor for@Controller
(for HtmlController) and@RequsetMapping
(for the API).But it would be good to process similar translated errors in the tests too. So we also added another PostProcessor for
@Transactional
. Note, that for this to work in DAO tests we must mark ourXxxDao
with@Transactional
too (most of the time we wantpropagation=MANDATORY
).Implement custom ElsciHibernateTransactionManager
The PostProcessor mechanism doesn’t really work in half of the cases because Hibernate-related errors don’t happen in our code - they happen during Hibernate flush() which in turn happens before Hibernate’s modification queries or before the transaction commit. Transactions are mostly committed not by our code, but by Spring proxy. By that time the execution is already not in the
@Transactional
method, but in Spring’s TransactionManager - and it’s this class that now is responsible for the translation.If we lived in the perfect world we could pass our translation logic either to
SessionFactoryUtils.convertHibernateAccessException()
(used duringflush()
) or to the TransactionManager. But in both cases the translation is hardcoded, which proves we don't live in the perfect world. So we implemented our own TransactionManager.Explicitly invoke translation in tests
And even that is not enough - oftentimes we flush manually in tests. And when flush() is executed, we’re not in TransactionManager and not in the production code. So the row Hibernate’s PersistenceException is thrown in the tests. If we want to translate it, we must invoke
PgExceptionTranslater
manually.Justification
Both Spring and Hibernate are a mess when it comes to exception handling. Spring has multiple exception translation mechanisms:
SQLExceptionTranslator
, which we need to pass toJdbcTemplate
in order to manage some exceptions our wayPersistenceExceptionTranslator
) which we can set up as aPersistenceExceptionTranslationPostProcessor
.LocalSessionFactoryBean
also translates exceptions, but that mechanism is hardcoded and I’ve never seen it actually being used.HibernateTransactionManager
) has a hardcoded exception translation.Problems:
PersistenceExceptionTranslationPostProcessor
and mark the classes with appropriate annotations, this mechanism isn’t going to work if exception happens insideTransactionManager
- which is often the case as the flushing usually happens before transaction commit.You may think that we can override Hibernate’s own exception converter? Well, it’s not that easy, but even if we succeed - it converts exceptions to
JDBCException
, while our exception already has to extend Spring’sDataAccessException
🤦I tried to override SessionFactory & Session creation that happens in
LocalSessionFactoryBean
by wrapping it with our own proxy, however for some reason Spring/Hibernate associate the underlying proxied object with the thread instead. And at some point we get “no transaction in progress” even though our own implementation is actually associated with the thread.I’m exhausted fighting this shit, so for now if we write tests that flush and expect an error at that point - we’ll have to catch generic exceptions instead of our own pretty ones.
Alternatives
In the future it’s worth considering using AOP instead of the PreProcessor and custom TransactionManager. This way we can create a proxy around transactional proxy. So when an exception is thrown in TransactionManager, we can still handle and translate it. The order of AOP proxies matters in this case as the translation proxy must enclose other persistence-related proxies.
Beta Was this translation helpful? Give feedback.
All reactions