Динамическое многопользовательское веб-приложение (весенний спящий режим)

#java #hibernate #apache-commons-dbcp #spring-orm

#java #переход в спящий режим #apache-commons-dbcp #spring-orm

Вопрос:

Я разработал работающее динамическое многопользовательское приложение, используя:

  • Java 8
  • Java-сервлет 3.1
  • Spring 3.0.7-РЕЛИЗ (невозможно изменить версию)
  • Режим гибернации 3.6.0.Окончательный (невозможно изменить версию)
  • Общий dbcp2

Это первый раз, когда мне приходится самому создавать экземпляры объектов Spring, поэтому мне интересно, все ли я сделал правильно, или приложение взорвется у меня на глазах в неуказанную будущую дату во время производства.

В принципе, единая схема базы данных известна, но детали базы данных будут указаны пользователем во время выполнения. Они могут свободно указывать любое имя хоста / порта / базы данных / имя пользователя / пароль.

Вот рабочий процесс:

  • Пользователь входит в веб-приложение, затем либо выбирает базу данных из известного списка, либо указывает пользовательскую базу данных (имя хоста / порт / и т.д.).
  • Если режим гибернации 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());
    }
}
  

Ответ №1:

В конце я расширил 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 amp; 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 и всем остальным, как обычно. Как я уже сказал, нужно следовать большему количеству кода.