After upgrading from 2.3.5.RELEASE to 2.4.0 instantiating a FullTextEntityManager
(hibernate search) from an injected EntityManager
is not possible anymore. The same code worked in the past and is derived from the official documentation or baeldung.
Code to reproduce: https://github.com/knoobie/spring-2-4-entitymanager
How to check: - run the test; test fails on service#load - you can verify that it worked, if you just change the version to 2.3.5.RELEASE again and start the test (it doesn't fail)
Stacktrace:
java.lang.ClassCastException: com.sun.proxy.$Proxy98 cannot be cast to org.hibernate.engine.spi.SessionImplementor
at org.hibernate.search.impl.FullTextSessionImpl.<init>(FullTextSessionImpl.java:62)
at org.hibernate.search.impl.ImplementationFactory.createFullTextSession(ImplementationFactory.java:35)
at org.hibernate.search.Search.getFullTextSession(Search.java:45)
at org.hibernate.search.jpa.Search.getFullTextEntityManager(Search.java:49)
at com.example.demo.search.MyService.load(MyService.java:51)
at com.example.demo.search.MyService$$FastClassBySpringCGLIB$$195495fd.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:134)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at com.example.demo.search.MyService$$EnhancerBySpringCGLIB$$9e31f121.load(<generated>)
at com.example.demo.DemoApplicationTests.contextLoads(DemoApplicationTests.java:20)
Comment From: wilkinsona
Thanks for the sample. I believe this change in behaviour is due to this change in Spring Framework 5.3.
The entityManagerInterface
has changed from org.hibernate.jpa.HibernateEntityManager
to org.hibernate.Session
. This has a knock-on effect when Hibernate Search attempts to unwrap the EntityManager
as a Session
:
private static Session getSession(EntityManager em) {
try {
return em.unwrap( Session.class );
}
catch (PersistenceException e) {
throw new SearchException(
"Trying to use Hibernate Search with a non-Hibernate EntityManager", e
);
}
}
The EntityManager
(a Spring Framework created proxy) now implements Session
so the proxy is returned as-is. The proxy does not implement SessionImplementor
so the subsequent cast fails. In Spring Framework 5.2 the proxy wasn't an instance of Session
so a different code path was taken.
We'll transfer this to the Spring Framework team so that they can take a look.
Comment From: knoobie
@wilkinsona thanks for you detailed analysis!
Comment From: jhoeller
This is arguably a mismatch in Hibernate Search where it calls EntityManager.unwrap(Session.class)
but subsequently casts the result to SessionImplementor
. It should really rather call EntityManager.unwrap(SessionImplementor.class)
upfront if that's what it ultimately expects.
That said, I do see the point that previous unwrap(Session.class)
calls did result in the native Session
and could therefore be cast to the implementor interface at any point. Maybe we should simply use SessionImplementor
as our EntityManager interface in HibernateJpaVendorAdapter
...
Comment From: jhoeller
We're exposing SessionFactoryImplementor
and SessionImplementor
by default now, for all JPA proxies backed by HibernateJpaVendorAdapter
. This will be available in the upcoming 5.3.2 snapshot, could you please give it a try?
Comment From: knoobie
@jhoeller I've updated my example to use the latest 5.3.2-SNAPSHOT from master. Now the exception changes to the following:
java.lang.ClassCastException: com.sun.proxy.$Proxy97 cannot be cast to org.hibernate.event.spi.EventSource
at org.hibernate.search.impl.FullTextSessionImpl.<init>(FullTextSessionImpl.java:66)
at org.hibernate.search.impl.ImplementationFactory.createFullTextSession(ImplementationFactory.java:35)
at org.hibernate.search.Search.getFullTextSession(Search.java:45)
at org.hibernate.search.jpa.Search.getFullTextEntityManager(Search.java:49)
at com.example.demo.search.MyService.load(MyService.java:51)
at com.example.demo.search.MyService$$FastClassBySpringCGLIB$$195495fd.invoke(<generated>)
Coming from the other cast inside hibernate search.
Edit: Changing the HibernateJpaVendorAdapter
to use org.hibernate.event.spi.EventSource
instead of SessionImplementor
worked for me. But I'm no hibernate expert to say if that's a good solution.
Comment From: jhoeller
Argh, another unguarded cast there! EventSource
is even worse since it's too internal for HibernateJpaVendorAdapter
to use it as a default EntityManager interface. I'll rather change our strategy then, reverting to plain Session
as default EntityManager interface but handling unwrap
calls differently, leaning towards returning the underlying native instance more eagerly.
Thanks for the quick turnaround, in any case! I'll try the other approach right away, will ping you once it's ready in a shapshot.
Comment From: jhoeller
This is a quite subtle issue. Our differentiated handling of unwrap
calls has use cases where the proxy needs to be returned, and Session
really is Hibernate's EntityManager
extension now (they deprecated their old HibernateEntityManager
interface), so it seems sensible to expose it at the proxy level as far as possible.
Frankly, Hibernate Search should call unwrap(SessionImplementor.class)
there if it knows that it applies further downcasts later on. As of Hibernate 5.2+ where Session
is an EntityManager
extension, their old unwrap(Session.class)
is arguably not semantically sufficient anymore. This is worth reporting to them.
I'll keep trying further what we can do about it from our end. Ideally we'd also remain compatible with older Hibernate Search versions, even if this gets refined in the Search implementation.
Comment From: yrodiere
I'll try to replace the casts with calls to unwrap
in Search 5, and I think we already did in Search 6.
One important detail: can you confirm that calling .unwrap(Session.class)
used to unproxy the entity manager, and that calling .unwrap(SessionImplementor.class)
will do the same, both in Spring 2.3 and 2.4? I'd rather not change the behavior for existing users...
Comment From: jhoeller
Hey @yrodiere, thanks for chiming in! Indeed, unwrap(SessionImplementor.class)
is going to do exactly the same against older versions of Spring since we're using an instanceof
check for our proxy there, and the proxy neither implemented Session
nor SessionImplementor
back then, so we'd always delegate to the native EntityManager.unwrap
method underneath.
Comment From: yrodiere
@jhoeller Thanks for confirming this.
For the record, here are the relevant PRs in Hibernate Search:
- Search 5.11: https://github.com/hibernate/hibernate-search/pull/2443
- Search 6.0: https://github.com/hibernate/hibernate-search/pull/2444 . There's a bit more work here as I tried to make sure that a
SearchSession
targeting aSession
proxy can safely be used from multiple threads, which isn't the case withFullTextSession
in Hibernate Search 5.
I'll try to release 5.11 early next week.
Comment From: jhoeller
Great news, @yrodiere - I'll leave our unwrap handling as-is then, updating our 5.3 migration notes accordingly.
Comment From: baron1405
FYI I had experienced this issue and just updated to Hibernate Search 5.11.6. Happy to report that Spring and Hibernate Search are once again happy with each other. Thanks to all who worked on identifying and fixing the issue!
Comment From: knoobie
I can confirm it as well - my application is running with Hibernate Search 5.11.6 and Spring Boot 2.4.0 without any issues. Thanks for your help!
Comment From: Toan1606
@knoobie I copy your code in github link and still has exception
java.lang.ClassCastException: class jdk.proxy4.$Proxy100 cannot be cast to class org.hibernate.engine.spi.SessionImplementor (jdk.proxy4.$Proxy100 is in module jdk.proxy4 of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @3ed2eee9; org.hibernate.engine.spi.SessionImplementor is in unnamed module of loader 'app')
at org.hibernate.search.impl.FullTextSessionImpl.
Can you let me know how to fix this code ?
Comment From: knoobie
@Toan1606 if you update your dependencies, the problem is gone. Never had any problems since this was fixed. All Spring Framework / Boot and Hibernate Search versions afterwards work perfectly fine.
Comment From: Toan1606
it must be done in springboot version 2.4 right?
Comment From: Toan1606
I am testing it in spring boot version 2.7.1
Comment From: knoobie
Please check the Hibernate Search
version. You are using an outdated and unsupported version. The fix was introduced in Hibernate Search 5.11.6
. If you are fresh starting a project, I would recommend to start with Hibernate Search 6 as well.
Comment From: Toan1606
When I change HIbernate Search to 5.11.6
Comment From: Toan1606
My project is fixed in Hibernate Search 5
Comment From: Toan1606
Is the code in your github link sure it was running before?