spring-data-jpa icon indicating copy to clipboard operation
spring-data-jpa copied to clipboard

`PersistenceProvider.fromEntityManagerFactory(…)` throws `NullPointerException` using Hotswap Agent

Open Artur- opened this issue 6 months ago • 3 comments

This happens on startup of a test application when moving from Spring Boot 3.5.1 to Spring Boot 4.0.0-SNAPSHOT (and thus also spring-data-jpa 4.0.0-SNAPSHOT). It only happens when running with Hotswap Agent that apparently creates a proxy of some instances.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository' defined in test.vaadin.copilot.flow.testviews.service.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: null
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1813) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:604) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:526) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1659) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1606) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:912) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	... 22 common frames omitted
Caused by: java.lang.reflect.UndeclaredThrowableException: null
	at jdk.proxy2/jdk.proxy2.$Proxy179.unwrap(Unknown Source) ~[na:na]
	at org.springframework.data.jpa.provider.PersistenceProvider.fromEntityManagerFactory(PersistenceProvider.java:298) ~[spring-data-jpa-4.0.0-20250618.075653-307.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.jpa.provider.PersistenceProvider.fromEntityManager(PersistenceProvider.java:278) ~[spring-data-jpa-4.0.0-20250618.075653-307.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.<init>(JpaRepositoryFactory.java:94) ~[spring-data-jpa-4.0.0-20250618.075653-307.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.createRepositoryFactory(JpaRepositoryFactoryBean.java:191) ~[spring-data-jpa-4.0.0-20250618.075653-307.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.doCreateRepositoryFactory(JpaRepositoryFactoryBean.java:183) ~[spring-data-jpa-4.0.0-20250618.075653-307.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport.createRepositoryFactory(TransactionalRepositoryFactoryBeanSupport.java:81) ~[spring-data-commons-4.0.0-20250618.074033-267.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:320) ~[spring-data-commons-4.0.0-20250618.074033-267.jar:4.0.0-SNAPSHOT]
	at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:212) ~[spring-data-jpa-4.0.0-20250618.075653-307.jar:4.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1860) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1809) ~[spring-beans-7.0.0-20250623.173043-653.jar:7.0.0-SNAPSHOT]
	... 33 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:115) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
	at org.hotswap.agent.plugin.hibernate_jakarta.proxy.EntityManagerFactoryProxy$1.invoke(EntityManagerFactoryProxy.java:206) ~[hotswap-agent.jar:2.0.2]
	... 44 common frames omitted
Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Class.isInstance(Object)" because "type" is null
	at org.hibernate.internal.SessionFactoryImpl.unwrap(SessionFactoryImpl.java:894) ~[hibernate-core-7.0.2.Final.jar:7.0.2.Final]
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]

The problematic code in Spring Data JPA looks like

	if (Proxy.isProxyClass(unwrapped.getClass())) {
				unwrapped = unwrapped.unwrap(null);

apparently introduced here https://github.com/spring-projects/spring-data-jpa/commit/98e801cfd5f56a933cc96c26144b7f3d0244627b#diff-5dfadde09c0e52ca72301ee3409505874a939a326b7e6dcb1230ec295847573aR298

which causes an NPE in this case because unwrapped.unwrap calls unwrap in org.hibernate.internal.SessionFactoryImpl: https://github.com/hibernate/hibernate-orm/blob/74db5ed1bc2aa73c8fc4e66d35a9658b47f8be56/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java#L894

This obviously causes an NPE

Artur- avatar Jun 24 '25 11:06 Artur-

Is it perhaps supposed to be unwrapped.unwrap(EntityManagerFactory.class)?

Artur- avatar Jun 24 '25 11:06 Artur-

Apparently not that simple as there is this code in AbstractEntityManagerFactoryBean

                case "unwrap":
                    Class<?> targetClass = (Class)args[0];
                    if (targetClass == null) {
                        return this.entityManagerFactoryBean.getNativeEntityManagerFactory();
                    } else if (targetClass.isInstance(proxy)) {
                        return proxy;
                    }

and that would cause an infinite loop as unwrap here with a target class of EntityManagerFactory would just return the proxy again and again

Artur- avatar Jun 24 '25 12:06 Artur-

Thanks for looking into it. There's no consistent behavior we could rely on when unwrapping EntityManagerFactory. Using null as interface type is indeed problematic as Hibernate and EclipseLink do not expect null. However, having a JDK proxy allows us to obtain an invocation handler. We could check if the invocation handler comes from Spring and if so, we call unwrap(null), otherwise, unwrap(EntityManagerFactory.class).

FTR, version 3.5.1 if affected as well.

mp911de avatar Jun 24 '25 12:06 mp911de

That's fixed now. Our CI has published new snapshots, care to test against 3.5.2-SNAPSHOT respective 4.0.0-SNAPSHOT, available from https://repos.spring.io/snapshot/?

<dependencies>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>4.0.0-SNAPSHOT</version>
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>spring-snapshot</id>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

mp911de avatar Jun 25 '25 13:06 mp911de

Nice! Seems to work now with the new snapshot

Artur- avatar Jun 25 '25 13:06 Artur-

I am getting the same problem with 3.5.1, but not with 3.5.0; seems like a regression there.

3.5.2-SNAPSHOT works.

jojule avatar Jul 10 '25 16:07 jojule