ManagedBeanRegistryImpl.getBean in 7.2.0 throwing exception

Upgraded recently to 7.1.11 and things worked fine. Now on 7.2.0 getting an error because of this

public <T> ManagedBean<T> getBean(Class<T> beanClass, BeanInstanceProducer fallbackBeanInstanceProducer) {
		final String beanClassName = beanClass.getName();
		final var existing = registrations.get( beanClassName );
		if ( existing != null ) {
			if ( !beanClass.equals( existing.getBeanClass() ) ) {
				throw new AssertionFailure( "Wrong type of bean: " + beanClassName );
			}

in ManagedBeanRegistryImpl. So, in our case beanClass is an interface and existing.getBeanClass() returns interface implementation class and then class names don’t match and we get an exception. Can this be changed to see if existing.getBeanClass extends beanClass?

Thanks

I guess that’s fine, but please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

Hm, not so simple to create reproducer since it is part of the framework and we just started getting this error with 7.20.0. And I think suggested change (don’t check class equality but whether it is assignable) is reasonable.

Let’s see, I added a comment to the commit. Maybe @gavinking can elaborate why he did this changed in the first place.

not so simple to create reproducer since it is part of the framework

Statements like this trigger my spidey sense that the user is probably doing something a bit wrong somewhere. If you can’t create a reproducer, then for sure you don’t properly understand the problem.

Hi ,

Here is simplified test case that shows what is the issue we are having.
If it makes sense, I can attach it and create issue, if not then I won’t bother creating issue since I have added workaround in our code.

package org.hibernate.bugs;

import org.hibernate.resource.beans.container.spi.BeanContainer;
import org.hibernate.resource.beans.container.spi.ContainedBean;
import org.hibernate.resource.beans.internal.ManagedBeanRegistryImpl;
import org.hibernate.resource.beans.spi.BeanInstanceProducer;
import org.hibernate.resource.beans.spi.ManagedBean;
import org.junit.jupiter.api.Test;

import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Test showing issue with ManagedBeanRegistryImpl when bean type is interface and actual bean
 * an implementation of interface.
 */
class ManagedBeanRegistryTest {

    @Test
    void getByNameAndContract_withCustomBeanContainer() {

       DummyApplicationContext dummyApplicationContext = new DummyApplicationContext();
       dummyApplicationContext.registerBean(Greeter.class, new GreeterImpl("dummy"));

       final TestBeanContainer container = new TestBeanContainer(dummyApplicationContext);

       final ManagedBeanRegistryImpl registry = new ManagedBeanRegistryImpl(container);
       assertThat(registry.getBeanContainer()).isSameAs(container);

       // Lookup by type - the registry delegates to the container
       final ManagedBean<? extends Greeter> managedBean = registry.getBean(Greeter.class);
       assertThat(managedBean).isNotNull();
       assertThat(managedBean.getBeanInstance().greet()).isEqualTo("dummy");

       // Another lookup by type will throw an error because of
       /*if ( !beanClass.equals( existing.getBeanClass() ) ) {
          throw new AssertionFailure( "Wrong type of bean: " + beanClassName );
       }*/
       // in ManagedBeanRegistryImpl
       final ManagedBean<Greeter> sameByBeanType = registry.getBean(Greeter.class);
       assertThat(sameByBeanType.getBeanInstance()).isSameAs(managedBean.getBeanInstance());
    }

    public interface Greeter {
       String greet();
    }

    public static class GreeterImpl implements Greeter {
       private final String msg;

       public GreeterImpl(String msg) {
          this.msg = msg;
       }

       @Override
       public String greet() {
          return msg;
       }
    }

    /**
     * Simplified application context that is used to register and retrieve beans.
     */
    public static class DummyApplicationContext {

       private static final String DEFAULT_NAME = "default";

       Map<Class<?>, Map<String, Object>> instances = new HashMap<>();

       <T> void registerBean(Class<T> beanType, T instance) {
          registerBean(beanType, DEFAULT_NAME, instance);
       }

       <T> void registerBean(Class<T> beanType, String name, T instance) {
          Map<String, Object> beanInstanceByName = instances.get(beanType);
          if (beanInstanceByName == null) {
             beanInstanceByName = new HashMap<>();
          }
          if (beanInstanceByName.containsKey(name)) {
             if (DEFAULT_NAME.equals(name)) {
                throw new IllegalStateException("Bean instance of type " + beanType + " already exists");
             }
             throw new IllegalStateException("Bean instance with name of type " + beanType + " named " + name + " already exists");
          }
          beanInstanceByName.put(name, instance);
          instances.put(beanType, beanInstanceByName);
       }

       <T> boolean containsBean(Class<T> beanType) {
          return containsBean(beanType, DEFAULT_NAME);
       }

       <T> boolean containsBean(Class<T> beanType, String name) {
          if (!instances.containsKey(beanType)) {
             return false;
          }
          Map<String, Object> beanInstanceByName = instances.get(beanType);
          return beanInstanceByName.containsKey(name);
       }

       <T> T getBean(Class<T> beanType) {
          return getBean(beanType, DEFAULT_NAME);
       }

       <T> T getBean(Class<T> beanType, String name) {
          Map<String, Object> beanInstanceByName = instances.get(beanType);
          if (beanInstanceByName == null) {
             throw new IllegalStateException("Bean instance of type " + beanType + " not found");
          }
          T instance = (T) beanInstanceByName.get(name);
          if (instance == null) {
             if (DEFAULT_NAME.equals(name)) {
                throw new IllegalStateException("Bean instance of type " + beanType + " not found");
             }
             throw new IllegalStateException("Bean instance of type " + beanType + " named " + name + " not found");
          }
          return instance;
       }
    }

    /**
     * Minimal BeanContainer to simulate "registration" of beans by type or by name.
     * It returns a simple ContainedBean wrapper around pre-registered instances,
     * or falls back to the given BeanInstanceProducer.
     */
    public static class TestBeanContainer implements BeanContainer {
       private final DummyApplicationContext applicationContext;

       public TestBeanContainer(DummyApplicationContext applicationContext) {
          this.applicationContext = applicationContext;
       }

       @Override
       public <B> ContainedBean<B> getBean(
             Class<B> beanType,
             LifecycleOptions lifecycleOptions,
             BeanInstanceProducer fallbackProducer) {
          final Object instance = applicationContext.containsBean(beanType)
                ? applicationContext.getBean(beanType)
                : fallbackProducer.produceBeanInstance(beanType);
          return new SimpleContainedBean<>(beanType.cast(instance));
       }

       @Override
       public <B> ContainedBean<B> getBean(
             String name,
             Class<B> beanType,
             LifecycleOptions lifecycleOptions,
             BeanInstanceProducer fallbackProducer) {
          final Object instance = applicationContext.containsBean(beanType, name)
                ? applicationContext.getBean(beanType, name)
                : fallbackProducer.produceBeanInstance(name, beanType);
          return new SimpleContainedBean<>(beanType.cast(instance));
       }

       @Override
       public void stop() {
          // Does nothing
       }
    }

    /**
     * Simple ContainedBean/ManagedBean wrapper used by TestBeanContainer.
     */
    public static class SimpleContainedBean<B> implements ContainedBean<B> {
       private final B instance;

       public SimpleContainedBean(B instance) {
          this.instance = instance;
       }

       @Override
       public Class<B> getBeanClass() {
          return (Class<B>) instance.getClass();
       }

       @Override
       public B getBeanInstance() {
          return instance;
       }
    }
}

Thanks for your attention

My understanding is that SimpleContainedBean should capture the bean class and report that for getBeanClass instead of the instance class. Any reason why you can’t do that?

Yes, I guess we could do that.

1 Like

In addition in Spring-managed beans that use CGLIB proxies are rejected on the second lookup because the current type-equality check compares the user-supplied interface/implementation class against the dynamically generated proxy class name.

Environment

  • Spring Boot 4
  • Java 21
  • Default proxy mode: CGLIB

Impact

  • getBean(Class<T>, BeanInstanceProducer) works once and then always throws for the same logical type

Root cause
The cache key is the raw ClassName supplied by the caller, but the stored bean’s getBeanClass() returns the CGLIB-generated subclass (com.example.Foo$$SpringCGLIB$$0).
The subsequent lookup therefore fails the equality check:

registrations.get(beanClassName) != null          // key is Foo
bean.getBeanClass().equals(beanClass)       // bean.getBeanClass() == Foo$$SpringCGLIB$$0.class

Proposed fix
Replace the exact-class equality check with org.springframework.util.ClassUtils.isAssignable(beanClass, bean.getBeanClass()) so that the proxy subclass is accepted.

Workaround until fix is released
Disable CGLIB proxies for the affected beans or annotate the bean with @Scope(proxyMode = ScopedProxyMode.INTERFACES) when an interface exists.

You will have to give us way more details and at least a stack trace that shows how this is invoked. Hibernate ORM code AFAICT will only ever pass class names or class objects of user types i.e. never proxy classes, so I have a very hard time imagining how this can be a problem.

Sure, no problem!
I’ve created a minimal reproducible example GitHub - lbsekr/hibernate_example
When you try to start the application you should see a stacktrace like this

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘entityManagerFactory’ defined in class path resource [org/springframework/boot/hibernate/autoconfigure/HibernateJpaConfiguration.class]: Unable to build Hibernate SessionFactory [persistence unit: default] ; nested exception is org.hibernate.AssertionFailure: Wrong type of bean: com.example.demo.FooBarListener
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1817) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:603) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:976) ~[spring-context-7.0.2.jar:7.0.2]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:620) ~[spring-context-7.0.2.jar:7.0.2]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756) ~[spring-boot-4.0.1.jar:4.0.1]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445) ~[spring-boot-4.0.1.jar:4.0.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) ~[spring-boot-4.0.1.jar:4.0.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1365) ~[spring-boot-4.0.1.jar:4.0.1]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-4.0.1.jar:4.0.1]
at com.example.demo.DemoApplicationKt.main(DemoApplication.kt:13) ~[main/:na]
Caused by: jakarta.persistence.PersistenceException: Unable to build Hibernate SessionFactory [persistence unit: default] ; nested exception is org.hibernate.AssertionFailure: Wrong type of bean: com.example.demo.FooBarListener
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:428) ~[spring-orm-7.0.2.jar:7.0.2]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-7.0.2.jar:7.0.2]
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:409) ~[spring-orm-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1864) ~[spring-beans-7.0.2.jar:7.0.2]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1813) ~[spring-beans-7.0.2.jar:7.0.2]
… 14 common frames omitted
Caused by: org.hibernate.AssertionFailure: Wrong type of bean: com.example.demo.FooBarListener
at org.hibernate.resource.beans.internal.ManagedBeanRegistryImpl.getBean(ManagedBeanRegistryImpl.java:58) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.resource.beans.internal.ManagedBeanRegistryImpl.getBean(ManagedBeanRegistryImpl.java:49) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.jpa.event.internal.ListenerCallback$Definition.createCallback(ListenerCallback.java:40) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.jpa.event.internal.CallbacksFactory.buildCallbacks(CallbacksFactory.java:89) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.jpa.event.internal.CallbacksFactory.registerAllCallbacks(CallbacksFactory.java:73) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.jpa.event.internal.CallbacksFactory.buildCallbackRegistry(CallbacksFactory.java:58) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.event.spi.EventEngine.(EventEngine.java:48) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:219) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.internal.SessionFactoryRegistry.instantiateSessionFactory(SessionFactoryRegistry.java:64) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:437) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1456) ~[hibernate-core-7.2.0.Final.jar:7.2.0.Final]
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:66) ~[spring-orm-7.0.2.jar:7.0.2]
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:433) ~[spring-orm-7.0.2.jar:7.0.2]
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:416) ~[spring-orm-7.0.2.jar:7.0.2]
… 18 common frames omitted

When a ManagedBean needs a proxy, in this example because of the @Transcational Springboot will generate a ProxyClass which inherits from the target-class. This ProxyClass is returned when getBean is called but because the logic checks for equality it fails

Does Spring implement the org.hibernate.resource.beans.container.spi.BeanContainer contract? I assume it does so in a wrong way unfortunately, because ManagedBean#getBeanClass must return the type under which a bean was created, and not the class of the instance.

Yeah, so clearly this is a bug in Spring ORM, not Hibernate ORM: spring-framework/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringBeanContainer.java at 169465cce17d285b16dacd8b5aa9a6badfc4511a · spring-projects/spring-framework · GitHub

Interesting I guess the Hibernate Team will not file such issue in Springboot? I must say though, from my viewpoint the specification of ManagedBean leaves room for interpretation.
But as you said the issue lays in springs implementation if it doesn’t follow the spirit of the definition :+1:

We are Hibernate ORM developers here, not Spring developers, so no, we will not spend time on a report or anything.
If you consider that BeanContainer#getBean(Class<B>, ...) returns a ContainedBean<B> it should be clear to everyone that in turn ManagedBean#getBeanClass can’t return something that is Class<? extends B>.

I understand. Thank you for the clarification!