Spring + Hibernate + JPA 多租户实现的循环引用问题

Circular reference issue with Spring + Hibernate + JPA multi tenancy implementation

我在使用 Spring + JPA + Hibernate 使用单独的数据库方法为我的 Web 应用程序设置多租户支持时遇到问题。

我已经尝试使用我自己的 CurrentTenantIdentifierResolverAbstractMultiTenantConnectionProvider 实现的 Hibernate 方式,以及使用 AbstractRoutingDataSource 的 Spring 方式。我将使用我命名为 UserRoutingDataSourceAbstractRoutingDataSource 解决方案来解释我的问题。

我要实现的目标如下:

  1. 我有一个“主”数据库,其中包含有关用户及其数据库的所有数据
  2. 当用户登录时,我将他的数据库 ID 保存在名为 UserSession
  3. 的会话范围 bean 中
  4. 我的 UserRoutingDataSourcetargetDataSources 需要填充代表我的主数据库 CustomDatabases table.
  5. 的所有数据库的数据源
  6. 当需要连接时,我的 UserRoutingDataSourcedetermineCurrentLookupKey 需要从用户的 UserSession 实例中检索数据库 ID。

在这两种情况下(Hibernate 方式/Spring 方式)我都以循环引用异常结束。当我尝试在 UserRoutingDataSource.

中自动装配 CustomDatabasesDAOUserSession 时出现问题

这是我得到的异常:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userEntityManagerFactory' defined in class path resource [MyApp/webapp/WEB-INF/config/applicationContext.xml]: Cannot resolve reference to bean 'userRoutingDataSource' while setting bean property 'dataSource'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userRoutingDataSource' defined in class path resource [MyApp/webapp/WEB-INF/config/applicationContext.xml]: Unsatisfied dependency expressed through constructor argument with index 0 of type [myApp.java.data.dao.Global.CustomDatabasesDAO]: : Error creating bean with name 'customDatabasesDAO': Injection of persistence dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'userEntityManagerFactory': FactoryBean which is currently in creation returned null from getObject; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customDatabasesDAO': Injection of persistence dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'userEntityManagerFactory': FactoryBean which is currently in creation returned null from getObject
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:108)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1481)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1226)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getObject(AbstractBeanFactory.java:305)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:301)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1051)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:828)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:537)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:446)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:328)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:107)
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4728)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5162)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1409)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1399)
    at java.util.concurrent.FutureTask.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userRoutingDataSource' defined in class path resource [MyApp/webapp/WEB-INF/config/applicationContext.xml]: Unsatisfied dependency expressed through constructor argument with index 0 of type [myApp.java.data.dao.Global.CustomDatabasesDAO]: : Error creating bean with name 'customDatabasesDAO': Injection of persistence dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'userEntityManagerFactory': FactoryBean which is currently in creation returned null from getObject; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customDatabasesDAO': Injection of persistence dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'userEntityManagerFactory': FactoryBean which is currently in creation returned null from getObject
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:185)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1143)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1046)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getObject(AbstractBeanFactory.java:305)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:301)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
    ... 24 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customDatabasesDAO': Injection of persistence dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'userEntityManagerFactory': FactoryBean which is currently in creation returned null from getObject
    at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.postProcessPropertyValues(PersistenceAnnotationBeanPostProcessor.java:357)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getObject(AbstractBeanFactory.java:305)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:301)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1192)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1116)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1014)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:813)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 34 more
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'userEntityManagerFactory': FactoryBean which is currently in creation returned null from getObject
    at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:181)
    at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:127)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getObjectForBeanInstance(AbstractBeanFactory.java:1584)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:253)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.findEntityManagerFactory(EntityManagerFactoryUtils.java:130)
    at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.findNamedEntityManagerFactory(PersistenceAnnotationBeanPostProcessor.java:556)
    at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.findEntityManagerFactory(PersistenceAnnotationBeanPostProcessor.java:538)
    at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor$PersistenceElement.resolveEntityManager(PersistenceAnnotationBeanPostProcessor.java:707)
    at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor$PersistenceElement.getResourceToInject(PersistenceAnnotationBeanPostProcessor.java:680)
    at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:169)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.postProcessPropertyValues(PersistenceAnnotationBeanPostProcessor.java:354)
    ... 46 more

我不明白的是为什么当我的 CustomDatabasesDAO 没有对 userEntityManagerFactory 的单个引用时我会得到这个异常。在 CustomDatabasesDAO 中我能想到的唯一相关的事情是我对 master 数据库的 entityManager 的调用:

@PersistenceContext(unitName = "masterEntityManagerFactory")
private EntityManager masterEntityManager;

有关更多上下文,这里是我的 UserRoutingDataSource 文件和我的应用程序上下文文件中的相关部分。

UserRoutingDataSource

public class UserRoutingDataSource extends AbstractRoutingDataSource {
    
    /*private CustomDatabasesDAO customDatabasesDAO;
    
    @Autowired
    public void setCustomDatabasesDAO(final CustomDatabasesDAO customDatabasesDAO)
    {
        this.customDatabasesDAO = customDatabasesDAO;
    }*/
    
    @Autowired
    private UserSession session;

    /*@Autowired
    public void setUserSession(final UserSession session)
    {
        this.session = session;
    }*/

    @Autowired
    public UserRoutingDataSource(CustomDatabasesDAO customDatabasesDAO) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        
        for(CustomDatabases database : customDatabasesDAO.findDatabasesByDeleted(0))
        {
            // All the information necessary for the datasource will eventually be retrieved from the database variable
            DriverManagerDataSource datasource = new DriverManagerDataSource();
            datasource.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
            datasource.setUrl("jdbc:sqlserver://localhost:1433;databaseName=" + database.getCdboAliasName() + ";");
            datasource.setUsername("username");
            datasource.setPassword("password");
            
            targetDataSources.put(String.valueOf(database.getCdboDatabaseId()), datasource);
        }
        
        setTargetDataSources(targetDataSources);
    }

    
    @Override
    protected Object determineCurrentLookupKey() {
        return session.getCdboDatabaseId();
    }
}

应用程序上下文

/*<!--<bean id="currentTenantIdentifierResolverImpl" class="myApp.java.config.CurrentTenantIdentifierResolverImpl" />
    <bean id="multiTenantConnectionProvider" class="myApp.java.config.MultiTenantConnectionProvider" />-->*/
    
    <bean id="masterDataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
        <property name="url" value="jdbc:sqlserver://localhost:1433;databaseName=myDatabase;" />
        <property name="username" value="sa" />
        <property name="password" value="password" />
    </bean>

    <bean id="masterEntityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      <property name="dataSource" ref="masterDataSource" />
      <property name="packagesToScan" value="myApp.java.data.model" />
      <property name="jpaVendorAdapter">
         <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
      </property>
      <property name="jpaProperties">
         <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.SQLServerDialect</prop>
            <prop key="hibernate.show_sql">false</prop>
            <prop key="hibernate.use_outer_join">true</prop>
         </props>
      </property>
    </bean>
    
    <bean id="userRoutingDataSource" class="myApp.java.config.UserRoutingDataSource">
        <property name="targetDataSources">
            <map />
        </property>
    </bean>
    
    <bean id="userEntityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      <property name="dataSource" ref="userRoutingDataSource" />
      <property name="packagesToScan" value="myApp.java.data.model" />
      <property name="jpaVendorAdapter">
         <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
      </property>
      <property name="jpaProperties">
         <map>
            <entry key="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
            <entry key="hibernate.show_sql" value="false" />
            <entry key="hibernate.use_outer_join" value="true" />
            /*<!--<entry key="hibernate.tenant_identifier_resolver" value-ref="currentTenantIdentifierResolverImpl" />
            <entry key="hibernate.multi_tenant_connection_provider" value-ref="multiTenantConnectionProvider" />
            <entry key="hibernate.multiTenancy" value="DATABASE" />-->*/
         </map>
      </property>
   </bean>
    
    <!-- Transaction managers -->
    <tx:annotation-driven />
    
    <bean id="masterTransactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="masterEntityManagerFactory" />
        <qualifier value="master" />
    </bean>
    
    <bean id="userTransactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="userEntityManagerFactory" />
        <qualifier value="user" />
    </bean>

我已经尝试了很多东西,我不确定我是否仍然理解依赖注入是如何工作的。而且我不知道我可能会遗漏什么。

谢谢你的帮助。

解决方案:

感谢 Roman 的回答,我解决了我的问题。我也放弃了使用我的会话作用域 bean 来确定我当前查找密钥的想法,而是使用我的 Spring 安全身份验证。当用户登录时,我执行以下操作:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), authorities);
HashMap<String, Object> details = new HashMap<String, Object>();
details.put("databaseId", session.getCdboDatabaseId());
newAuth.setDetails(details);
SecurityContextHolder.getContext().setAuthentication(newAuth);

重要的部分是我用户 UsernamePasswordAuthenticationToken 上的 setDetails(details)

这是我的(工作)配置的当前状态:

UserRoutingDataSource

@Component
public class UserRoutingDataSource extends AbstractRoutingDataSource {

    @Autowired
    public UserRoutingDataSource(CustomDatabasesDAO customDatabasesDAO) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        
        for(CustomDatabases database : customDatabasesDAO.findDatabasesByDeleted(0))
        {
            DriverManagerDataSource datasource = new DriverManagerDataSource();
            datasource.setDriverClassName(database.getCdboDriverName());
            datasource.setUrl("jdbc:sqlserver://"+database.getCdboServer()+":"+database.getCdboPort()+";databaseName="+database.getCdboAliasName() + ";");
            datasource.setUsername(database.getCdboUserName());
            datasource.setPassword(database.getCdboPassword());
            
            targetDataSources.put(database.getCdboDatabaseId(), datasource);
        }
        
        /*
         * This default datasource is necessary because for some reason (Hibernate, JPA related ?) the routing datasource 
         * calls the "determineCurrentLookupKey()" on startup which returned null ("default" now) because there is no Authentication at startup yet.
         */
        targetDataSources.put("default", new DriverManagerDataSource());    
        setTargetDataSources(targetDataSources);
        afterPropertiesSet();
    }
    
    @Override
    protected Object determineCurrentLookupKey() {
        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        
        if (auth != null && auth.getDetails() instanceof Map)
        {
            HashMap<String, Object> details = (HashMap<String, Object>) auth.getDetails();
            return details.get("databaseId");
        }
        
        return "default";
    }
}

应用程序上下文

<bean id="masterDataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
        <property name="url" value="jdbc:sqlserver://localhost:1433;databaseName=myDatabase;" />
        <property name="username" value="myUser" />
        <property name="password" value="myPassword" />
    </bean>

    <bean id="masterEntityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      <property name="persistenceUnitName" value="masterEntityManagerFactory" />
      <property name="dataSource" ref="masterDataSource" />
      <property name="packagesToScan" value="myApp.java.data.model" />
      <property name="jpaVendorAdapter">
         <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
      </property>
      <property name="jpaProperties">
         <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.SQLServerDialect</prop>
            <prop key="hibernate.show_sql">false</prop>
            <prop key="hibernate.use_outer_join">true</prop>
         </props>
      </property>
    </bean>
    
    <bean id="userEntityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      <property name="persistenceUnitName" value="userEntityManagerFactory" />
      <property name="dataSource" ref="userRoutingDataSource" />
      <property name="packagesToScan" value="myApp.java.data.model" />
      <property name="jpaVendorAdapter">
         <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
      </property>
      <property name="jpaProperties">
         <map>
            <entry key="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
            <entry key="hibernate.show_sql" value="false" />
            <entry key="hibernate.use_outer_join" value="true" />
         </map>
      </property>
   </bean>
    
    <!-- Transaction managers -->
    <tx:annotation-driven />
    
    <bean id="masterTransactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="masterEntityManagerFactory" />
        <qualifier value="master" />
    </bean>
    
    <bean id="userTransactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="userEntityManagerFactory" />
        <qualifier value="user" />
    </bean>

Spring 遍历 EntityManagerFactoryUtils.findEntityManagerFactory() 内的所有 EntityManagerFactory 以找出 属性 persistenceUnitName 值等于 [=13= 的工厂]的属性unitName值。仅当未找到候选者时,才会使用名称为 unitName 值的 bean。因此 userEntityManagerFactory 在此过程中被实例化。

作为解决方法,您可以尝试在 CustomDatabasesDAO class.

中使用常规 @Autowired 而不是 @PersistenceUnit