Весна. Доступ к компоненту сеанса в моем методе CustomDataSource getConnection

Кто-нибудь знает, как получить доступ к bean-компоненту с ограниченным сеансом в DataSource getConnection?

Я решил реализовать отдельную схему базы данных для каждого клиента (мультитенантность) и нашел эту очень многообещающую статью: Мультитенантность с использованием Spring и PostgreSQL, и какое-то время мы пытались реализовать то, что он говорит.

Как вы можете видеть в комментариях (и я застрял, как и другие), это не работает из-за java.lang.IllegalStateException. Также нашел этот вопрос, где вы можете увидеть то же самое, в последнем комментарии.

Ответ автора (gargii) таков:

Сообщение об ошибке, вероятно, очень хорошо описывает состояние, в котором вы находитесь. Попробуйте подумать об этом. Сосредоточьтесь на темах. Какой поток получает исходный запрос? В каком потоке возникает ошибка? Попробуйте отладить класс RequestContextHolder. Это место, где на самом деле выполняется магия сеанса (с использованием ThreadLocal).

Что ж ... Я пытался отладить, но ничего не добился, я видел десятки страниц, пытающихся решить проблему, но все еще не нашел решения.

Я всегда получаю это исключение (я скопировал полную трассировку в конце):

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.

Короче говоря, вот что у меня есть:

Фильтр безопасности, который выполняется раньше всего (applicationContext-Security.xml):

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

Фильтр:

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 имеет:

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

(Обратите внимание, что это не RooEntity или RooActiveRecord. Я просто использую @RooJavaBean и @RooToString, чтобы автоматически создавать сеттеры, геттеры и toString)

С обязательной конфигурацией в 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>

И applicationContext.xml:

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

Есть также:

@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();
}

Этот класс настроен в 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>

Если я удалю @Autowired из показанного кода TenantAwareDataSource, это не вызовет исключения, но на самом деле не работает. хахаха. tenantContext всегда имеет значение null, потому что это больше не сеансовый компонент.

Кстати. Это приложение было создано с использованием следующих версий (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>

Наконец, полное исключение:

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 `

person elysch    schedule 08.10.2013    source источник


Ответы (1)


Ваш DataSource, вероятно, создается задолго до того, как будет сделан какой-либо запрос (обратите внимание на имя потока начальной загрузки в вашем журнале - [localhost-startStop-1]). Таким образом, на самом деле нет SESSION, который Spring мог бы использовать для удовлетворения области SESSION.

Вам необходимо реализовать определение объема сеанса вручную (через RequestContextHolder) или иметь слабую связь между компонентами (например, через ApplicationContext#getBean).


В нескольких проектах мы реализовали TenantContext как переменную ThreadLocal (аналогично переменной SecurityContext Spring Security), которая устанавливается и сбрасывается специальным сервлетом Filter. Это, безусловно, лучший подход, поскольку ни один компонент не будет полагаться на факт наличия активного HTTP-запроса.

Подводя итог - для вашего конкретного случая использования было бы лучше без какого-либо bean-компонента с ограниченным сеансом.


Примечание: если вы используете Hibernate, имейте в виду, что уже существует поддержка мультитенантности. Другие фреймворки также могут его поддерживать.

person Pavel Horal    schedule 08.10.2013
comment
Спасибо за ваш ответ. Один вопрос: если экземпляр DataSource создается до того, как будет сделан какой-либо запрос, это означает, что невозможно будет использовать настраиваемый DataSource с setConnectionInitSqls. Верно? Или просто не было выполнено getConnection, так что это все еще возможно. - person elysch; 09.10.2013
comment
DataSource создается до того, как будет сделан какой-либо запрос, однако он не используется (т.е. getConnection не вызывается). Вы все еще можете использовать setConnectionInitSqls. Что касается ThreadLocal - вы даже можете поместить его как статическую переменную в некоторый служебный класс (проверьте Spring RequestContextHolder или LocaleContextHolder). - person Pavel Horal; 09.10.2013
comment
И последний вопрос. Поскольку все мои материалы по мультиарендности связаны с вошедшим в систему пользователем, следует ли мне хранить информацию о клиенте в Authentication.getDetails(), например, этот вопрос говорит, что это возможно ?. Вы видите проблемы, если я сделаю так? (надеюсь, я не задаю дурацкий вопрос) - person elysch; 09.10.2013
comment
Это очень хороший вопрос. Я всегда стараюсь сохранять независимость логики контекста арендатора. Но вы определенно можете использовать контекст безопасности для хранения информации о клиенте. Вам нужно только подумать о ситуации, когда у вас нет доступного пользователя (например, во время аутентификации) ... но это можно решить с помощью docs.spring.io/spring-security/site/docs/3.0.x/reference/ (например, путем создания временного анонимная аутентификация для конкретного клиента). - person Pavel Horal; 09.10.2013
comment
:). Я думаю, что Authentication.getDetails() здесь не работает. В моем TenantAwareDataSource.getConnectoin() SecurityContextHolder.getContext().getAuthentication() всегда равно нулю. Даже если я уже успешно вошел в систему. (Примечание: у меня есть источник данных / схема по умолчанию, когда нет информации о клиенте) - person elysch; 09.10.2013
comment
Странно ... должно работать. Либо вы не в потоке запроса, либо не за цепочкой фильтров безопасности. - person Pavel Horal; 09.10.2013
comment
Думаю, я нашел причину, по которой он не работает с SecurityContext. Даже если Аутентификация не равна нулю: Пользователь-участник (элемент SecurityContext) хранится в HTTP-сеансе. Это такая же проблема, как если бы я использовал свой сеансовый компонент. - person elysch; 09.10.2013
comment
Нет ... контекст безопасности можно сохранить в сеансе, однако SecurityContextHolder не зависит от этого. Он устанавливается и снимается SecurityContextPersistenceFilter. Вы, должно быть, делаете что-то не так. - person Pavel Horal; 09.10.2013
comment
Да ... Я что-то не так делал. То, как я проверял ценность, было неправильным. Сейчас я получаю значение с помощью переменной ThreadLocal, но я прекрасно вижу данные SecurityContext. Я думал, что все вызовы getConnection должны показывать данные, но обнаружил, что это не тот случай, когда, например, отображается форма входа в систему. Если я получу список или создаю его, я получу необходимые данные. - person elysch; 11.10.2013
comment
Кстати. Я добавил новый (как мне кажется) интересный вопрос: Выполнить оператор sql перед обычным выполнением с помощью aop. На всякий случай можете / захотите взглянуть. - person elysch; 11.10.2013