繁体   English   中英

动态多租户WebApp(Spring休眠)

[英]Dynamic Multi-tenant WebApp (Spring Hibernate)

我提出了一个使用以下内容的动态多租户应用程序:

  • Java 8
  • Java Servlet 3.1
  • Spring 3.0.7-RELEASE( 无法更改版本
  • Hibernate 3.6.0.Final( 无法更改版本
  • 公用dbcp2

这是我第一次必须自己实例化Spring对象,因此我想知道我是否正确完成了所有操作,或者该应用程序是否会在生产期间未指定的将来出现在我的面前。

基本上,单个DataBase模式是已知的,但是数据库详细信息将在运行时由用户指定。 他们可以自由指定任何主机名/端口/数据库名称/用户名/密码。

这是工作流程:

  • 用户登录到Web应用程序,然后从已知列表中选择一个数据库,或指定一个自定义数据库(主机名/端口/等)。
  • 如果成功建立了Hibernate SessionFactory (或在缓存中找到),则使用SourceContext#setSourceId(SourceId)将其保存在用户会话中,然后用户可以使用此数据库。
  • 如果有人选择/指定了相同的数据库,则返回相同的缓存的AnnotationSessionFactoryBean
  • 用户可以随时切换数据库。
  • 当用户离开自定义数据库(或注销)时,缓存的AnnotationSessionFactoryBean被删除/销毁。

那么以下工作将按预期进行吗? 帮助和指针是最欢迎的。

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一起进行连接,其余的一切都照常进行。 正如我所说,还有更多代码需要遵循。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM