Skip to content

Scoped EntityManager bean cannot get unwrapped to Session (with Hibernate 5.2) [SPR-15010] #19577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
spring-projects-issues opened this issue Dec 13, 2016 · 7 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@spring-projects-issues
Copy link
Collaborator

spring-projects-issues commented Dec 13, 2016

Rodolfo Labsch opened SPR-15010 and commented

Cannot cast spring created proxy to a different object interface.
e.g.:
Given the bean:
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public EntityManager myEntityManager(EntityManagerFactory emf) {
return emf.createEntityManager();
}

If I try to cast it or maybe unwrap the instance, we get a class cast exception:
@Autowired
EntityManager em;

Session s = em.unwrap(Session.class) -> ClassCastException
Session s = ((Session) em) -> ClassCastException


Affects: 4.2.8

Issue Links:

Referenced from: commits 6d1cae2, d15df34

@spring-projects-issues
Copy link
Collaborator Author

Juergen Hoeller commented

As far as I can see, you'll get a proxy for your declared return type EntityManager here. A hard cast to Session in certainly not going to work. However, I'm wondering why an unwrap call wouldn't work since it should be delegated to the actual target EntityManager in this case... What's the stacktrace there?

You could consider declaring a raw scoped object without a proxy mode here. As long as you're only injecting it into request-scoped application beans, this is going to work fine. For a JPA EntityManager in particular, this is the recommended way of scoping it, without any AOP proxy involved.

Finally, as of Hibernate 5.2, the Session interface extends EntityManager directly. You could declare the return type of your factory method as Session, performing a downcast or unwrap call in the factory method itself, allowing for direct injection of Session references. An unwrap call in the factory method would even work for older Hibernate versions.

@spring-projects-issues
Copy link
Collaborator Author

Rodolfo Labsch commented

"However, I'm wondering why an unwrap call wouldn't work since it should be delegated to the actual target EntityManager in this case... What's the stacktrace there?"
This is absolutely correct, the proxy unfortunately wraps this call, once the method actually returns and a cast exception is thrown. Please check this code:
spring-aop-4.2.8.RELEASE-sources.jar!/org/springframework/aop/framework/JdkDynamicAopProxy.java:213

if (retVal != null && retVal == target && returnType.isInstance(proxy) &&
					!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
				// Special case: it returned "this" and the return type of the method
				// is type-compatible. Note that we can't help if the target sets
				// a reference to itself in another returned object.
				retVal = proxy;
			}

"You could consider declaring a raw scoped object without a proxy mode here".
This is certainly an option, still if spring creates a new Entity manager for me on every request, I don't see why I should do it myself ;)

"Finally, as of Hibernate 5.2, the Session interface extends EntityManager directly."
This is currently what I'm doing right now, although this is unfortunately a workaround, since I definitely do not want Hibernate dependencies in my spring configuration.

@spring-projects-issues
Copy link
Collaborator Author

Rodolfo Labsch commented

This looks like a bug to me, although I am not certain this could be implemented with pure jdk proxies.
cglib proxies maybe?

Just my two cents here:
spring-aop-4.2.8.RELEASE-sources.jar!/org/springframework/aop/scope/ScopedProxyFactoryBean.java:100
Makes a call to ```
ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader())

Which uses EntityManager for beanType.

Since the bean is just created on the first request, it seems to me that this "cast logic" should be moved to the first time the bean is created.

@spring-projects-issues
Copy link
Collaborator Author

Juergen Hoeller commented

This seems to be caused by unwrap's generic return type where the AOP proxy assumes that the proxy is compatible while the return type is actually being downcast through a type variable. I'll see what we can do here.

Ironically, this is probably caused exactly by Hibernate 5.2's merging of the EntityManager and Session interfaces: Previously, an unwrapped Session was a different object and therefore never replaced by the proxy to begin with. Now in Hibernate 5.2, the EntityManager returns itself as the Session directly, accidentally triggering the proxy replacement mechanism.

@spring-projects-issues
Copy link
Collaborator Author

Rodolfo Labsch commented

Thank you!

@spring-projects-issues
Copy link
Collaborator Author

Juergen Hoeller commented

In terms of proxy creation time, there is not much we can do beyond introspecting the declared return type of the factory method. Technically, we have to create and inject this shared proxy before we ever call the factory method for any particular request-scoped instance. We can't amend a proxy's implemented interfaces after creation time - not with JDK proxies and not with CGLIB either -, so if you keep your return type to the generic EntityManager, that's all we know about at that time.

As a consequence, for a setup scenario like yours, we'll have to make unwrap work. Straight downcasting won't be an option.

As an alternative to a custom request-scoped EntityManager, you could consider a regular transaction-scoped EntityManager via @PersistenceContext in combination with either a transaction per handler method or with an OpenEntityManagerInView setup (effectively providing a request-scoped EntityManager for all persistence context operations). Such Spring-provided JPA handles are proxies as well but don't apply any generic replacement of return values, so unwrapping is going to work fine here as-is.

@spring-projects-issues
Copy link
Collaborator Author

Juergen Hoeller commented

I've revised our proxy replacement mechanism to not apply for generic return type signatures or plain Object return type declarations. This covers EntityManager.unwrap(T) and EntityManager.getDelegate() as well. To be backported to 4.3.5; please note that we only officially support Hibernate ORM 5.2 as of Spring Framework 4.3.

@spring-projects-issues spring-projects-issues added type: enhancement A general enhancement in: core Issues in core modules (aop, beans, core, context, expression) labels Jan 11, 2019
@spring-projects-issues spring-projects-issues added this to the 4.3.5 milestone Jan 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

2 participants