近日应一个小学弟的要求给他讲解个Spring+Hibernate下多数据源动态切换的功能,他的需求就是在登录时候,选择(或者填写)自己的数据库名称,然后登录完毕后,会话就是基于这个数据源连接的,在会话期间允许改变自己的数据库连接。正好之前也没做过类似的实验,于是就写了个小小的示例测试下。
一种最简单的方法是在Spring的配置文件上配置多个数据源,但这种方式其实和单个数据源的使用没什么差别,都是使用预先定义好了的数据源。所以这种方法并没有解决我所遇到的问题。
然后我就想通过在需要切换数据源时构造Configuration来配置数据源,但是仔细想想这种代价应当是比较高的。也就果断放弃了。
后来想想,在第一种方案中,问题主要是出在数据源是预先定义的,那么我把预先定义的数据源抽取出来,采用一个工厂方式或者缓存方式来产生数据源,这样不就可以满足需求了么?
这个问题的关键点是
1.当需要数据源的切换操作时 应当是在什么时候进行数据源更改的设置,
2 如果更改了数据连接设置,应当怎样反映到数据源上。
3 保证书数据源的更改对数据操作是透明的
针对第一个问题,我采用的是一个Filter的方式来解决的。在每次请求中过滤Session中的dataBase参数,当第一次会话请求时,这时候Session还没有存在,此时采用默认的数据库,(默认数据库的作用是用来存储登录用的用户名和密码),并把默认的数据库名称附加到当前线程中,等待datasource中的读取。如果不是第一次的请求,这时候session是存在的了,那么就应当能从session中取database参数,然后同样附加到当前线程,等到datasource的读取。这样的话,就是当在页面上更改了数据库,然后点击提交,在提交这个动作中仍然连接的是当前的数据源,但是提交动作完成后(既经过filter处理后),下一次再有数据库操作时,就会用上新的数据源了。下面是Filter的代码
package cn.swjtu.multisource.tools; import java.io.IOException; import javax.faces.context.FacesContext; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import com.apusic.web.session.ContextSession; public class MyFilter implements Filter { @Override public void destroy() { // TODO Auto-generated method stub } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String database = null; if (FacesContext.getCurrentInstance() != null) { ContextSession session = (ContextSession) FacesContext .getCurrentInstance().getExternalContext().getSession(true); database = (String) session.getAttribute(ConstArgs.KEY_DATABASE); } if (database == null && "".equals(database)) { database = ConstArgs.DEFAULT_DATABASE; } ThreadLocalHolder.set(database); chain.doFilter(request, response); ThreadLocalHolder.remove(); } @Override public void init(FilterConfig filterConfig) throws ServletException { // TODO Auto-generated method stub } }
第二个问题的解决方式 是自己实现个datasource,并在该datasource中进行数据源缓存,这样就可以不必为之前用到的数据源在进行新的创建操作的,但是数据源的混存应当是有个最大值的(保证不占用太多的数据库连接资源),我在这个示例中还没有设置。然后当是新连接时就创建个数据源并缓存,把对该datasource的所有操作方都转发到新建并缓存的这个数据源上,如果访问的是已经存在数据源就从缓存中取出,同样也是把对该datasource的所有操作方都转发到缓存的这个数据源上。下面是实现的datasource的代码:
package cn.swjtu.multisource.tools; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; import javax.faces.context.FacesContext; import javax.sql.DataSource; import org.springframework.jdbc.datasource.SingleConnectionDataSource; import com.apusic.web.session.ContextSession; public class DataSourceWithCache implements DataSource { private static Map<String, DataSource> sources = new HashMap<String, DataSource>(); // private DataSource source; private DataSource getDataSource() { String name = ThreadLocalHolder.get(); if (name == null || "".equals(name)) { if(FacesContext.getCurrentInstance()!=null){ name = (String) ((ContextSession) FacesContext.getCurrentInstance() .getExternalContext().getSession(true)) .getAttribute(ConstArgs.KEY_DATABASE); } if (name == null || "".equals(name)) { name = ConstArgs.DEFAULT_DATABASE; } } DataSource source = sources.get(name); if (source == null) { source = createSource(name); sources.put(name, source); } return source; } private DataSource createSource(String name) { SingleConnectionDataSource source = new SingleConnectionDataSource(); source.setDriverClassName(ConstArgs.DRIVER_CLASS); source.setUrl(ConstArgs.SERVER_URL + name); source.setUsername(ConstArgs.USER); source.setPassword(ConstArgs.PASSWORD); source.setSuppressClose(true); return source; } @Override public Connection getConnection() throws SQLException { return getDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return getDataSource().getConnection(username, password); } @Override public PrintWriter getLogWriter() throws SQLException { return getDataSource().getLogWriter(); } @Override public int getLoginTimeout() throws SQLException { return getDataSource().getLoginTimeout(); } @Override public void setLogWriter(PrintWriter out) throws SQLException { getDataSource().setLogWriter(out); } @Override public void setLoginTimeout(int seconds) throws SQLException { getDataSource().setLoginTimeout(seconds); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return getDataSource().isWrapperFor(iface); } @Override public <T> T unwrap(Class<T> iface) throws SQLException { return getDataSource().unwrap(iface); } }
这样所有的更改并没有涉及数据库操作层的代码,可以完全保证对数据库操作层的透明性。
该工程采用的Operamasks开源社区的提供的集成开发工具,这里只能发2M的文档,所以我把示例工程发这里 点击下载
PS:由于采用的是金蝶的开发工具,使用了其中的com.apusic.web.session.ContextSession类,导致很多人下载后出错,这里只需要把该类修改为Servlet中得Context即可。
转载于:https://blog.51cto.com/westerly/638818