近日应一个小学弟的要求给他讲解个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即可。


本人新博客地址