javaspringspring-mvcmulti-tenantsession-bean

Spring. Access session Bean in my CustomDataSource getConnection method


Does anybody know how to access a session scoped bean in a DataSource getConnection?

I decided to implement a separate database schema for each client (Multi-tenancy) and I found this very promising article: Multitenancy using Spring and PostgreSQL, and have been trying to implement what it says for a while.

As you can see in the comments (and I'm stuck just like the others), it doesn't work because of a java.lang.IllegalStateException. Also found this question where you can see the same thing, in the last comment.

The answer the author (gargii) gives is this:

The error message is probably a very good desciption of the state you are in. Try to think of it. Focus on threads. Which thread gets the original request? Which thread fails with the error? Try to debug the RequestContextHolder class. This is the place where the session-magic is actually done (using ThreadLocal).

Well... I tried to debug but didn't get nowhere, I've seen dozens of pages trying to solve the problem, but still haven’t found a solution.

I always get this exception (I've copied the complete trace at the end):

2013-10-08 08:47:37,232 [localhost-startStop-1] ERROR org.springframework.web.context.ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor#0': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in file [C:\Users\ely\Documents\Desarrollo\Spring\springsource\vfabric-tc-server-developer-2.9.2.RELEASE\base-instance\wtpwebapps\TestMultiTenant\WEB-INF\classes\META-INF\spring\applicationContext.xml]: Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.tenantContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

In Short, this is what I’ve got:

A security filter that executes before anything else (applicationContext-Security.xml):

<custom-filter before="FIRST" ref="tenantFilter" />

The filter is:

public class TenantFilter implements Filter {

private TenantContext tenantContext;
private TenantRepository tenantRepository;
...

applicationContext.xml:

<bean id="tenantFilter" class="com.i4b.test1.core.TenantFilter">
    <property name="tenantContext" ref="tenantContext" />
    <property name="tenantRepository" ref="tenantRepository" />
</bean>

TenantContext has:

@Component
@RooJavaBean
@RooToString
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TenantContext {
    private Tenant tenant;
    private String idSchema;
} 

(Note it’s not a RooEntity or RooActiveRecord. I’m just using @RooJavaBean and @RooToString to have setters, getters and toString created automatically)

With it’s mandatory configuration at web.xml:

<!-- ADDED FOR MULTI TENANCY START -->
<listener>
  <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<!-- MULTI TENANCY END -->

<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

And applicationContext.xml:

<bean id="tenantContext" class="com.i4b.test1.core.TenantContext" scope="session">
    <aop:scoped-proxy />
</bean>

There is also:

@Component
public class TenantAwareDataSource extends BasicDataSource  {

@Autowired
private TenantContext tenantContext;

// [tenant,schema] to [DataSource] mapping
private final Map<String, BasicDataSource> schemaToDataSource = new HashMap<String, BasicDataSource>();

...
@Override
public Connection getConnection() throws SQLException {
    BasicDataSource determineTargetDataSource = determineTargetDataSource();
    return determineTargetDataSource.getConnection();
}

This class is configured in the applicationContext.xml:

<bean class="com.i4b.test1.core.TenantAwareDataSource" destroy-method="close" id="dataSource">
    <property name="driverClassName" value="${database.driverClassName}"/>
    <property name="url" value="${database.url}"/>
    <property name="username" value="${database.username}"/>
    <property name="password" value="${database.password}"/>
    <property name="testOnBorrow" value="true"/>
    <property name="testOnReturn" value="true"/>
    <property name="testWhileIdle" value="true"/>
    <property name="timeBetweenEvictionRunsMillis" value="1800000"/>
    <property name="numTestsPerEvictionRun" value="3"/>
    <property name="minEvictableIdleTimeMillis" value="1800000"/>
    <property name="validationQuery" value="SELECT version();"/>
</bean>

If I remove the @Autowired from "TenantAwareDataSource" shown code, it doesn't give me the exception, but doesn't really work. hahaha. tenantContext is always null because it's not the session bean anymore.

By the way. This application was created using the following versions (pom.xml):

<properties>
    <aspectj.version>1.7.2</aspectj.version>
    <java.version>7</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <roo.version>1.2.4.RELEASE</roo.version>
    <slf4j.version>1.7.5</slf4j.version>
    <spring.version>3.2.3.RELEASE</spring.version>
    <spring-security.version>3.1.0.RELEASE</spring-security.version>
</properties>

At last, the complete exception is:

2013-10-08 08:47:37,232 [localhost-startStop-1] ERROR org.springframework.web.context.ContextLoader - Context initialization failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor#0': Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in file [C:\Users\ely\Documents\Desarrollo\Spring\springsource\vfabric-tc-server-developer-2.9.2.RELEASE\base-instance\wtpwebapps\TestMultiTenant\WEB-INF\classes\META-INF\spring\applicationContext.xml]: Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.tenantContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:529)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:198)
    at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:741)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:464)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:389)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:294)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:112)
    at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4887)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5381)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:901)
    at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:877)
    at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:633)
    at org.apache.catalina.startup.HostConfig.deployDescriptor(HostConfig.java:657)
    at org.apache.catalina.startup.HostConfig$DeployDescriptor.run(HostConfig.java:1637)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
    at java.util.concurrent.FutureTask.run(FutureTask.java:166)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in file [C:\Users\ely\Documents\Desarrollo\Spring\springsource\vfabric-tc-server-developer-2.9.2.RELEASE\base-instance\wtpwebapps\TestMultiTenant\WEB-INF\classes\META-INF\spring\applicationContext.xml]: Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.tenantContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1482)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:198)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:439)
    at org.springframework.beans.factory.BeanFactoryUtils.beansOfTypeIncludingAncestors(BeanFactoryUtils.java:277)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.detectPersistenceExceptionTranslators(PersistenceExceptionTranslationInterceptor.java:139)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.<init>(PersistenceExceptionTranslationInterceptor.java:79)
    at org.springframework.dao.annotation.PersistenceExceptionTranslationAdvisor.<init>(PersistenceExceptionTranslationAdvisor.java:71)
    at org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor.setBeanFactory(PersistenceExceptionTranslationPostProcessor.java:85)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeAwareMethods(AbstractAutowireCapableBeanFactory.java:1502)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1470)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:521)
    ... 24 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.tenantContext': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:343)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:34)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.getTarget(CglibAopProxy.java:663)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:614)
    at com.i4b.test1.core.TenantContext$$EnhancerByCGLIB$$c51ab566.getTenant(<generated>)
    at com.i4b.test1.core.TenantContext_Roo_JavaBean.ajc$interMethodDispatch1$com_i4b_test1_core_TenantContext_Roo_JavaBean$com_i4b_test1_core_TenantContext$getTenant(TenantContext_Roo_JavaBean.aj)
    at com.i4b.test1.core.TenantAwareDataSource.currentLookupKey(TenantAwareDataSource.java:77)
    at com.i4b.test1.core.TenantAwareDataSource.determineTargetDataSource(TenantAwareDataSource.java:99)
    at com.i4b.test1.core.TenantAwareDataSource.getConnection(TenantAwareDataSource.java:216)
    at org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider.getConnection(InjectedDataSourceConnectionProvider.java:70)
    at org.hibernate.engine.jdbc.internal.JdbcServicesImpl$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcServicesImpl.java:242)
    at org.hibernate.engine.jdbc.internal.JdbcServicesImpl.configure(JdbcServicesImpl.java:117)
    at org.hibernate.service.internal.StandardServiceRegistryImpl.configureService(StandardServiceRegistryImpl.java:75)
    at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:159)
    at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:131)
    at org.hibernate.cfg.Configuration.buildTypeRegistrations(Configuration.java:1797)
    at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1755)
    at org.hibernate.ejb.EntityManagerFactoryImpl.<init>(EntityManagerFactoryImpl.java:96)
    at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:914)
    at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:899)
    at org.hibernate.ejb.HibernatePersistence.createContainerEntityManagerFactory(HibernatePersistence.java:76)
    at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:288)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:310)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1541)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1479)
    ... 39 more
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.request.SessionScope.get(SessionScope.java:90)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:329)
    ... 64 more `

Solution

  • Your DataSource is probably being instantiated long before any request is made (note the bootstrap thread name in your log - [localhost-startStop-1]). So there is actually no SESSION which can be used by Spring to satisfy the SESSION scope.

    You need to implement session-scoping manually (via RequestContextHolder) or have the components loosly coupled (e.g. via ApplicationContext#getBean).


    What we have done on several projects is to implement TenantContext as ThreadLocal variable (similar to Spring Security's SecurityContext), which is being set and unset by a special servlet Filter. That is by far the best approach as no component will rely on the fact that there is actually an active HTTP request.

    To sum this up - you would be better off without any session scoped bean for your particular use case.


    Sidenote: If you are using Hibernate, then be aware that there is already a support for multi-tenancy. Other frameworks might support it as well.