Search code examples
spring-boothibernatespring-data-jpa

Spring, Hibernate. App with custom LocalContainerEntityManagerFactoryBean fails to create custom Converter


I have a web application that should work with a single tenant or multiple tenants (using different DB schemas).

there is a custom Converter that does not have a default constructor and requires a bean to be defined:

@Converter
public class CustomConverter implements AttributeConverter<String, String> {
    public CustomConverter(EncryptionDecryptionAlgorithmService algorithmService) {
        this.algorithmService = algorithmService;
        log.info("CustomConverter CREATED");
    }
    ....
}
public interface EncryptionDecryptionAlgorithmService {
    String encrypt(String text);
    String decrypt(String text);
}

this bean is defined in a Configuration:

@Slf4j
@Configuration
public class AppConfig {
   @Value("key")
   private String appSecretKey;

   public AppConfig() {
       log.info("AppConfig initialized");
   }

   @Bean
   public EncryptionDecryptionAlgorithmService getAppEncryptionDecryption(){
        log.info("EncryptionDecryptionAlgorithmService");
        return new EncryptionDecryptionAlgorithmServiceImpl(appSecretKey);
    }
}

For a single tenant I only set up typical annotations to enable JPA and scan component, like this:

@SpringBootApplication
@EnableJpaRepositories(value = {"my.app", "my.lib"})
@EntityScan(value = {"my.app", "my.lib"})
@ComponentScan(value = {"my.app", "my.lib"})
@Import({CommonConfig.class, AppConfig.class})
public class Application extends SpringBootServletInitializer {
...

And during initial logs I can see that this bean is created before LocalContainerEntityManagerFactoryBean:

INFO 33533 --- [application] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1271 ms
INFO 33533 --- [application] [main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
INFO 33533 --- [application] [main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.4.4.Final
INFO 33533 --- [application] [main] com.zaxxer.hikari.HikariDataSource       : application-ds-pool - Starting...
INFO 33533 --- [application] [main] com.zaxxer.hikari.pool.HikariPool        : application-ds-pool - Added connection org.postgresql.jdbc.PgConnection@229c4d34
INFO 33533 --- [application] [main] com.zaxxer.hikari.HikariDataSource       : application-ds-pool - Start completed.
INFO 33533 --- [application] [main] my.app.AppConfig    : AppConfig initialized
INFO 33533 --- [application] [main] CustomConverter : CustomConverter CREATED
INFO 33533 --- [application] [main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
INFO 33533 --- [application] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'

So right after HikariDataSource log the AppConfig class is initialized and the bean is created, after that it is used to create a converter instance.

The problem I'm facing is that for multi-tenant configuration this bean is not created and the application fails due to converter can not be createed. It tries to create it with the default constructor which is not available.

Here is my configuration:

@Slf4j
@Configuration
@ConditionalOnProperty(name = "multitenancy.enabled", havingValue = "true")
public class MultitenantHibernateConfig {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            JpaVendorAdapter jpaVendorAdapter,
            DataSource dataSource,
            MultiTenantConnectionProvider multiTenantConnectionProvider,
            CurrentTenantIdentifierResolver tenantIdentifierResolver,
            TenantConfigProperties tenantConfigProperties,
            DataSourceConfigProperties dataSourceConfigProperties
    ) {

        log.info("LocalContainerEntityManagerFactoryBean");

        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan(dataSourceConfigProperties.getPackageScan());
        em.setJpaVendorAdapter(jpaVendorAdapter);
        Map<String, Object> jpaProperties = new HashMap<>(tenantConfigProperties.getJpaProperties().getProperties());
        jpaProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
        jpaProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
        jpaProperties.put(AvailableSettings.PHYSICAL_NAMING_STRATEGY, "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy");
        em.setJpaPropertyMap(jpaProperties);
        return em;
    }
}
 INFO 30943 --- [application] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1275 ms
 WARN 30943 --- [application] [main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
 INFO 30943 --- [application] [main] com.zaxxer.hikari.HikariDataSource       : application-ds-pool - Starting...
 INFO 30943 --- [application] [main] com.zaxxer.hikari.pool.HikariPool        : application-ds-pool - Added connection org.postgresql.jdbc.PgConnection@7f9d40b3
 INFO 30943 --- [application] [main] com.zaxxer.hikari.HikariDataSource       : application-ds-pool - Start completed.
 INFO 30943 --- [application] [main] my.app.MultiTenantConnectionProvider     : Master tenant: master.
 INFO 30943 --- [application] [main] my.app.TenantResolver                    : Master tenant: master.
 INFO 30943 --- [application] [main] my.app.MultitenantHibernateConfig        : LocalContainerEntityManagerFactoryBean
 INFO 30943 --- [application] [main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
 INFO 30943 --- [application] [main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.4.4.Final
 INFO 30943 --- [application] [main] my.app.MultiTenantConnectionProvider     : Using master DataSource
ERROR 30943 --- [application] [main] j.LocalContainerEntityManagerFactoryBean : Failed to initialize JPA EntityManagerFactory: Could not instantiate managed bean directly 'my.app.CustomConverter' due to: my.app.CustomConverter.<init>()

Here is a different behavior, right after HikariDataSource log there is no call of AppConfig.

In addition, multi-tenancy logic (MultitenantHibernateConfig and other classes) is in a separate library so it's not possible to know in advance which dependencies are needed.

I tried to add @Order annotation to init AppConfig first but it didn't work.

What is the way to properly create a custom LocalContainerEntityManagerFactoryBean with all required pre-steps of initializing all the necessary beans?


Solution

  • To properly initiate all the required beans it's needed to define a bean container in properties:

    jpaProperties.put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));

    @Slf4j
    @Configuration
    @ConditionalOnProperty(name = "multitenancy.enabled", havingValue = "true")
    public class MultitenantHibernateConfig {
    
        @Bean
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(
                JpaVendorAdapter jpaVendorAdapter,
                ConfigurableListableBeanFactory beanFactory,
                DataSource dataSource,
                MultiTenantConnectionProvider multiTenantConnectionProvider,
                CurrentTenantIdentifierResolver tenantIdentifierResolver,
                TenantConfigProperties tenantConfigProperties,
                DataSourceConfigProperties dataSourceConfigProperties
        ) {
    
            log.info("LocalContainerEntityManagerFactoryBean");
    
            LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
            em.setDataSource(dataSource);
            em.setPackagesToScan(dataSourceConfigProperties.getPackageScan());
            em.setJpaVendorAdapter(jpaVendorAdapter);
            Map<String, Object> jpaProperties = new HashMap<>(tenantConfigProperties.getJpaProperties().getProperties());
            jpaProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
            jpaProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifierResolver);
            jpaProperties.put(AvailableSettings.PHYSICAL_NAMING_STRATEGY, "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy");
            jpaProperties.put(AvailableSettings.BEAN_CONTAINER, new SpringBeanContainer(beanFactory));
            em.setJpaPropertyMap(jpaProperties);
            return em;
        }
    }