一直很纠结Spring都把数据源配置好了,那类似WordPress那样安装时怎么配置数据源的。想了一下,既然要通过安装步骤配置数据源,那么数据源实际上就是动态或者半动态去加载的。想要做到这一点,靠默认的那些Spring配置肯定实现不了。研究了一下,最终还是有个解决方案了。当然,这只是其中一种实现,因为dataSource的特殊性而实现的。不过,其他dataSource管理器也可以参考实现。
先上关键代码,dataSource。这个是基于Apache的BasicDataSource的,也就是本方案实现的关键:
package net.microsnow.framework.db;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
/**
* 基于Apache BasicDataSource定制的数据源管理器
* <li>因为可以实现手动触发数据源重载</li>
* @author tomtrije
*
*/
public class BasicDataSource extends org.apache.commons.dbcp.BasicDataSource {
private static boolean loadFromProperties = false;
private String path;//配置文件路径(类路径)
private String driverPropertyName;//配置文件中配置driver的属性名称
private String urlPropertyName;//配置文件中配置url的属性名称
private String usernamePropertyName;//配置文件中配置username的属性名称
private String passwordPropertyName;//配置文件中配置password的属性名称
@Override
protected synchronized DataSource createDataSource() throws SQLException {
//判断是否需要从配置文件中加载最新的数据源
if(BasicDataSource.loadFromProperties){
super.dataSource = null;//设置父类的dataSource为空时,父类在提供Connection时会重新创建,这样下面的配置更改才有效。
//加载配置
Properties prop = new Properties();
try {
prop.load(this.getClass().getClassLoader().getResourceAsStream(path));
} catch (IOException e) {
throw new SQLException(e);
}
//设置相关属性
super.driverClassName = prop.get(driverPropertyName).toString();
super.url = prop.get(urlPropertyName).toString();
super.username = prop.get(usernamePropertyName).toString();
super.password = prop.get(passwordPropertyName).toString();
//设置下次加载无需从配置文件加载
BasicDataSource.setChanged(false);
}
return super.createDataSource();
}
public void setPropertiesPath(String path){
this.path = path;
}
public String getPropertiesPath(){
return this.path;
}
public static synchronized void setChanged(boolean loadFromProperties) {
BasicDataSource.loadFromProperties = loadFromProperties;
}
public static synchronized boolean isChanged() {
return BasicDataSource.loadFromProperties;
}
public String getDriverPropertyName() {
return driverPropertyName;
}
public void setDriverPropertyName(String driverPropertyName) {
this.driverPropertyName = driverPropertyName;
}
public String getUrlPropertyName() {
return urlPropertyName;
}
public void setUrlPropertyName(String urlPropertyName) {
this.urlPropertyName = urlPropertyName;
}
public String getUsernamePropertyName() {
return usernamePropertyName;
}
public void setUsernamePropertyName(String usernamePropertyName) {
this.usernamePropertyName = usernamePropertyName;
}
public String getPasswordPropertyName() {
return passwordPropertyName;
}
public void setPasswordPropertyName(String passwordPropertyName) {
this.passwordPropertyName = passwordPropertyName;
}
}
关键处理是有了,但这只是处理了数据源的变更,那么没有数据源配置时,肯定不能正常使用,需要对请求进行拦截转发,因此需要一个Filter(或者拦截器):
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String path = request.getServletPath().substring(1);
if(StringUtils.isNotEmpty(path) && !ArrayUtils.contains(monitorFilters, path)){
for (int i = 0; i < monitorFiles.length; i++) {
String p = this.getClass().getClassLoader().getResource("").getPath() + monitorFiles[i];
File f = new File(p);
if(f == null || !f.exists()){
response.sendRedirect(installPage);
return;
}
long last = f.lastModified();
if(lastModify[i] == null || lastModify[i] != last){
lastModify[i] = last;
BasicDataSource.setChanged(true);
}
}
}
chain.doFilter(request, response);
}
该过滤器实现了对请求的过滤处理,对于不在monitorFilters(过滤)范围内的所有请求进行拦截处理,对配置文件进行判断,如果为空就跳转到对应安装页面(安装页面和安装请求务必加入monitorFilters(过滤)范围。如果配置文件存在且上次保存的修改时间与文件最新修改时间不一致,即文件已变更,则设置数据源需重新加载。如果文件存在且未变更,则同意请求,进行正常请求处理。
安装页面就不看具体实现了,来看下安装处理请求的实现,这里简单化,直接写Servlet了。以下代码在doPost方法中:
if(StringUtils.isNotEmpty(req.getParameter("save"))){
String url = req.getParameter("url");
String username = req.getParameter("username");
String password = req.getParameter("password");
String driver = req.getParameter("driver");
for (int i = 0; i < configs.length; i++) {
//将页面提交的配置通过properties写入文件。这里该配置文件只负责配置数据源</span>
//如果数据源配置与其他配置在一起,这里只需先将Properties load进来再setProperty再store即可。但filter那边的实现就不是判断是否文件存在了,而是对properties进行加载并判断相关设置是否存在
String p = this.getClass().getClassLoader().getResource("").getPath() + configs[i];
File f = new File(p);
Properties prop = new Properties();
prop.setProperty("db.url", url);
prop.setProperty("db.username", username);
prop.setProperty("db.password", password);
prop.setProperty("db.driver", driver);
prop.store(new FileOutputStream(f), "");
}
}
//以下代码为了测试数据源的获取
Connection conn = null;
ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(req.getSession().getServletContext());
try {
DataSource ds = (DataSource)context.getBean("dataSource");
conn = ds.getConnection();
} catch (SQLException e) {
throw new ServletException(e);
}
最后是一些配置:
web.xml
<filter>
<filter-name>dataSourceMonitor</filter-name>
<filter-class>net.microsnow.framework.db.DataSourceMonitorFilter</filter-class>
<init-param>
<param-name>monitorFiles</param-name>
<param-value>config.properties</param-value>
</init-param>
<init-param>
<param-name>monitorFilters</param-name>
<param-value>login.jsp,install.jsp,index.jsp,install.action</param-value>
</init-param>
<init-param>
<param-name>installPage</param-name>
<param-value>install.jsp</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>dataSourceMonitor</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Spring配置(关键):
<!-- 数据源配置 -->
<bean id="dataSource" class="net.microsnow.framework.db.BasicDataSource" destroy-method="close">
<property name="initialSize" value="5"/>
<property name="maxActive" value="10"/>
<property name="maxWait" value="60000"/>
<property name="poolPreparedStatements" value="true"/>
<!-- 配置与Apache的BasicDataSource一直,但无需配置固定的用户名密码,新增以下配置,用于对配置的变更做动态调整 -->
<property name="PropertiesPath" value="config.properties"/>
<property name="driverPropertyName" value="db.driver"/>
<property name="urlPropertyName" value="db.url"/>
<property name="usernamePropertyName" value="db.username"/>
<property name="passwordPropertyName" value="db.password"/>
</bean>