动态多租户 WebApp(Spring Hibernate)
Dynamic Multi-tenant WebApp (Spring Hibernate)
我想出了一个有效的动态多租户应用程序,使用:
- Java 8
- Java 小服务程序 3.1
- Spring 3.0.7-RELEASE(无法更改版本)
- Hibernate 3.6.0.Final(无法更改版本)
- Commons dbcp2
这是我第一次必须自己实例化 Spring 对象,所以我想知道我是否已正确完成所有操作,或者该应用程序是否会在未指定的未来日期炸毁我的脸在制作过程中。
基本上,单个数据库模式是已知的,但数据库详细信息将由用户在运行时指定。他们可以自由指定任何 hostname/port/DB name/username/password.
工作流程如下:
- 用户登录 Web 应用程序,然后从已知列表中选择一个数据库,或指定一个自定义数据库 (hostname/port/etc)。
- 如果 Hibernate
SessionFactory
构建成功(或在缓存中找到),则使用 SourceContext#setSourceId(SourceId)
为用户会话保留它,然后用户可以使用此数据库。
- 如果任何人 choses/specifies 相同的数据库,则返回相同的缓存
AnnotationSessionFactoryBean
- 用户可以随时切换数据库。
- 当用户离开自定义数据库(或注销)时,缓存的
AnnotationSessionFactoryBean
是 removed/destroyed
那么下面的工作是否会按预期进行?非常欢迎帮助和指点。
web.xml
<web-app version="3.1" ...>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener> <!-- Needed for SourceContext -->
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<web-app>
applicationContext.xml
<beans ...>
<tx:annotation-driven />
<util:properties id="db" location="classpath:db.properties" /> <!-- driver/url prefix -->
<context:component-scan base-package="com.example.basepackage" />
</beans>
UserDao.java
@Service
public class UserDao implements UserDaoImpl {
@Autowired
private TemplateFactory templateFactory;
@Override
public void addTask() {
final HibernateTemplate template = templateFactory.getHibernateTemplate();
final User user = (User) DataAccessUtils.uniqueResult(
template.find("select distinct u from User u left join fetch u.tasks where u.id = ?", 1)
);
final Task task = new Task("Do something");
user.getTasks().add(task);
TransactionTemplate txTemplate = templateFactory.getTxTemplate(template);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.save(task);
template.update(user);
}
});
}
}
TemplateFactory.java
@Service
public class TemplateFactory {
@Autowired
private SourceSessionFactory factory;
@Resource(name = "SourceContext")
private SourceContext srcCtx; // session scope, proxied bean
@Override
public HibernateTemplate getHibernateTemplate() {
LocalSessionFactoryBean sessionFactory = factory.getSessionFactory(srcCtx.getSourceId());
return new HibernateTemplate(sessionFactory.getObject());
}
@Override
public TransactionTemplate getTxTemplate(HibernateTemplate template) {
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(template.getSessionFactory());
return new TransactionTemplate(txManager);
}
}
SourceContext.java
@Component("SourceContext")
@Scope(value="session", proxyMode = ScopedProxyMode.INTERFACES)
public class SourceContext {
private static final long serialVersionUID = -124875L;
private SourceId id;
@Override
public SourceId getSourceId() {
return id;
}
@Override
public void setSourceId(SourceId id) {
this.id = id;
}
}
SourceId.java
public interface SourceId {
String getHostname();
int getPort();
String getSID();
String getUsername();
String getPassword();
// concrete class has proper hashCode/equals/toString methods
// which use all of the SourceIds properties above
}
SourceSessionFactory.java
@Service
public class SourceSessionFactory {
private static Map<SourceId, AnnotationSessionFactoryBean> cache = new HashMap<SourceId, AnnotationSessionFactoryBean>();
@Resource(name = "db")
private Properties db;
@Override
public LocalSessionFactoryBean getSessionFactory(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.get(id);
if (sessionFactory == null) {
return createSessionFactory(id);
}
else {
return sessionFactory;
}
}
}
private AnnotationSessionFactoryBean createSessionFactory(SourceId id) {
AnnotationSessionFactoryBean sessionFactory = new AnnotationSessionFactoryBean();
sessionFactory.setDataSource(new CutomDataSource(id, db));
sessionFactory.setPackagesToScan(new String[] { "com.example.basepackage" });
try {
sessionFactory.afterPropertiesSet();
}
catch (Exception e) {
throw new SourceException("Unable to build SessionFactory for:" + id, e);
}
cache.put(id, sessionFactory);
return sessionFactory;
}
public void destroy(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.remove(id);
if (sessionFactory != null) {
if (LOG.isInfoEnabled()) {
LOG.info("Releasing SessionFactory for: " + id);
}
try {
sessionFactory.destroy();
}
catch (HibernateException e) {
LOG.error("Unable to destroy SessionFactory for: " + id);
e.printStackTrace(System.err);
}
}
}
}
}
CustomDataSource.java
public class CutomDataSource extends BasicDataSource { // commons-dbcp2
public CutomDataSource(SourceId id, Properties db) {
setDriverClassName(db.getProperty("driverClassName"));
setUrl(db.getProperty("url") + id.getHostname() + ":" + id.getPort() + ":" + id.getSID());
setUsername(id.getUsername());
setPassword(id.getPassword());
}
}
最后,我扩展了 Spring 的 AbstractRoutingDataSource
以便能够即时动态创建数据源。一旦一切正常,我将使用完整代码更新此答案。我还有几件事要解决,但问题的症结如下:
@Service
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
// this is pretty much the same as the above SourceSessionFactory
// but with a map of CustomDataSources instead of
// AnnotationSessionFactoryBeans
@Autowired
private DynamicDataSourceFactory dataSourceFactory;
// This is the sticky part. I currently have a workaround instead.
// Hibernate needs an actual connection upon spring startup & there's
// also no session in place during spring initialization. TBC.
// @Resource(name = "UserContext") // scope session, proxy bean
private UserContext userCtx; // something that returns the DB config
@Override
protected SourceId determineCurrentLookupKey() {
return userCtx.getSourceId();
}
@Override
protected CustomDataSource determineTargetDataSource() {
SourceId id = determineCurrentLookupKey();
return dataSourceFactory.getDataSource(id);
}
@Override
public void afterPropertiesSet() {
// we don't need to resolve any data sources
}
// Inherited methods copied here to show what's going on
// @Override
// public Connection getConnection() throws SQLException {
// return determineTargetDataSource().getConnection();
// }
//
// @Override
// public Connection getConnection(String username, String password)
// throws SQLException {
// return determineTargetDataSource().getConnection(username, password);
// }
}
所以我只是将 DynamicRoutingDataSource 作为 Spring 的 SessionFactoryBean 的数据源连同 TransactionManager 以及所有其他的一样。正如我所说,还有更多代码需要遵循。
我想出了一个有效的动态多租户应用程序,使用:
- Java 8
- Java 小服务程序 3.1
- Spring 3.0.7-RELEASE(无法更改版本)
- Hibernate 3.6.0.Final(无法更改版本)
- Commons dbcp2
这是我第一次必须自己实例化 Spring 对象,所以我想知道我是否已正确完成所有操作,或者该应用程序是否会在未指定的未来日期炸毁我的脸在制作过程中。
基本上,单个数据库模式是已知的,但数据库详细信息将由用户在运行时指定。他们可以自由指定任何 hostname/port/DB name/username/password.
工作流程如下:
- 用户登录 Web 应用程序,然后从已知列表中选择一个数据库,或指定一个自定义数据库 (hostname/port/etc)。
- 如果 Hibernate
SessionFactory
构建成功(或在缓存中找到),则使用SourceContext#setSourceId(SourceId)
为用户会话保留它,然后用户可以使用此数据库。 - 如果任何人 choses/specifies 相同的数据库,则返回相同的缓存
AnnotationSessionFactoryBean
- 用户可以随时切换数据库。
- 当用户离开自定义数据库(或注销)时,缓存的
AnnotationSessionFactoryBean
是 removed/destroyed
那么下面的工作是否会按预期进行?非常欢迎帮助和指点。
web.xml
<web-app version="3.1" ...>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener> <!-- Needed for SourceContext -->
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<web-app>
applicationContext.xml
<beans ...>
<tx:annotation-driven />
<util:properties id="db" location="classpath:db.properties" /> <!-- driver/url prefix -->
<context:component-scan base-package="com.example.basepackage" />
</beans>
UserDao.java
@Service
public class UserDao implements UserDaoImpl {
@Autowired
private TemplateFactory templateFactory;
@Override
public void addTask() {
final HibernateTemplate template = templateFactory.getHibernateTemplate();
final User user = (User) DataAccessUtils.uniqueResult(
template.find("select distinct u from User u left join fetch u.tasks where u.id = ?", 1)
);
final Task task = new Task("Do something");
user.getTasks().add(task);
TransactionTemplate txTemplate = templateFactory.getTxTemplate(template);
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
template.save(task);
template.update(user);
}
});
}
}
TemplateFactory.java
@Service
public class TemplateFactory {
@Autowired
private SourceSessionFactory factory;
@Resource(name = "SourceContext")
private SourceContext srcCtx; // session scope, proxied bean
@Override
public HibernateTemplate getHibernateTemplate() {
LocalSessionFactoryBean sessionFactory = factory.getSessionFactory(srcCtx.getSourceId());
return new HibernateTemplate(sessionFactory.getObject());
}
@Override
public TransactionTemplate getTxTemplate(HibernateTemplate template) {
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(template.getSessionFactory());
return new TransactionTemplate(txManager);
}
}
SourceContext.java
@Component("SourceContext")
@Scope(value="session", proxyMode = ScopedProxyMode.INTERFACES)
public class SourceContext {
private static final long serialVersionUID = -124875L;
private SourceId id;
@Override
public SourceId getSourceId() {
return id;
}
@Override
public void setSourceId(SourceId id) {
this.id = id;
}
}
SourceId.java
public interface SourceId {
String getHostname();
int getPort();
String getSID();
String getUsername();
String getPassword();
// concrete class has proper hashCode/equals/toString methods
// which use all of the SourceIds properties above
}
SourceSessionFactory.java
@Service
public class SourceSessionFactory {
private static Map<SourceId, AnnotationSessionFactoryBean> cache = new HashMap<SourceId, AnnotationSessionFactoryBean>();
@Resource(name = "db")
private Properties db;
@Override
public LocalSessionFactoryBean getSessionFactory(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.get(id);
if (sessionFactory == null) {
return createSessionFactory(id);
}
else {
return sessionFactory;
}
}
}
private AnnotationSessionFactoryBean createSessionFactory(SourceId id) {
AnnotationSessionFactoryBean sessionFactory = new AnnotationSessionFactoryBean();
sessionFactory.setDataSource(new CutomDataSource(id, db));
sessionFactory.setPackagesToScan(new String[] { "com.example.basepackage" });
try {
sessionFactory.afterPropertiesSet();
}
catch (Exception e) {
throw new SourceException("Unable to build SessionFactory for:" + id, e);
}
cache.put(id, sessionFactory);
return sessionFactory;
}
public void destroy(SourceId id) {
synchronized (cache) {
AnnotationSessionFactoryBean sessionFactory = cache.remove(id);
if (sessionFactory != null) {
if (LOG.isInfoEnabled()) {
LOG.info("Releasing SessionFactory for: " + id);
}
try {
sessionFactory.destroy();
}
catch (HibernateException e) {
LOG.error("Unable to destroy SessionFactory for: " + id);
e.printStackTrace(System.err);
}
}
}
}
}
CustomDataSource.java
public class CutomDataSource extends BasicDataSource { // commons-dbcp2
public CutomDataSource(SourceId id, Properties db) {
setDriverClassName(db.getProperty("driverClassName"));
setUrl(db.getProperty("url") + id.getHostname() + ":" + id.getPort() + ":" + id.getSID());
setUsername(id.getUsername());
setPassword(id.getPassword());
}
}
最后,我扩展了 Spring 的 AbstractRoutingDataSource
以便能够即时动态创建数据源。一旦一切正常,我将使用完整代码更新此答案。我还有几件事要解决,但问题的症结如下:
@Service
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
// this is pretty much the same as the above SourceSessionFactory
// but with a map of CustomDataSources instead of
// AnnotationSessionFactoryBeans
@Autowired
private DynamicDataSourceFactory dataSourceFactory;
// This is the sticky part. I currently have a workaround instead.
// Hibernate needs an actual connection upon spring startup & there's
// also no session in place during spring initialization. TBC.
// @Resource(name = "UserContext") // scope session, proxy bean
private UserContext userCtx; // something that returns the DB config
@Override
protected SourceId determineCurrentLookupKey() {
return userCtx.getSourceId();
}
@Override
protected CustomDataSource determineTargetDataSource() {
SourceId id = determineCurrentLookupKey();
return dataSourceFactory.getDataSource(id);
}
@Override
public void afterPropertiesSet() {
// we don't need to resolve any data sources
}
// Inherited methods copied here to show what's going on
// @Override
// public Connection getConnection() throws SQLException {
// return determineTargetDataSource().getConnection();
// }
//
// @Override
// public Connection getConnection(String username, String password)
// throws SQLException {
// return determineTargetDataSource().getConnection(username, password);
// }
}
所以我只是将 DynamicRoutingDataSource 作为 Spring 的 SessionFactoryBean 的数据源连同 TransactionManager 以及所有其他的一样。正如我所说,还有更多代码需要遵循。