让数据库连接池灵活配置

针对 Smart 2.3-SNAPSHOT 版本

Smart 对技术选型是非常谨慎的,选择的技术一定是业界最牛逼的,对于数据库连接池也不例外。所以,当初经过百般纠结之后,选择了 Apache Commons 的 DBCP。一点不假,它是一个强大的数据库连接池,稳定且高效,很少有人没听说过它。

当然,市面上并非只有 DBCP 这一款优秀的连接池,比较不错的还有国外的 C3P0,以及国内的 Druid(由阿里巴巴开源)。

目前,Smart 选择的是 DBCP 连接池,而且强耦合在框架中了,若要更换成其它连接池,则必须更改源代码,这严重违反了 开闭原则,所以,非常有必要对这部分代码进行重构。

我的想法是这样的:

让 DBCP 作为 Smart 的默认连接池,当开发者需要使用其它连接池时,只需开发一个 Plugin,并实现 Smart 提供的相关接口,通过配置的方式即可覆盖默认的连接池实现。

也就是说,Smart 仅提供一个默认的实现,第三方可以提供其它的实现,但每种实现都基于相同的接口。

我们不妨先看看重构之前的代码:

<!-- lang: java -->
/**
 * 封装数据库相关操作
 *
 * @author huangyong
 * @since 1.0
 */
public class DatabaseHelper {

    ...

    public static DataSource getDataSource() {
        // 从配置文件中读取 JDBC 配置项
        String driver = ConfigHelper.getConfigString("smart.framework.jdbc.driver");
        String url = ConfigHelper.getConfigString("smart.framework.jdbc.url");
        String username = ConfigHelper.getConfigString("smart.framework.jdbc.username");
        String password = ConfigHelper.getConfigString("smart.framework.jdbc.password");
        // 创建 DBCP 数据源
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        ds.setValidationQuery("select 1 from dual");
        return ds;
    }

    ...

}

我们通过 DatabaseHelper 类的 getDataSource 方法即可获取 DataSource 对象。可见,目前确实是基于 DBCP 的实现,要用别的实现,只能修改框架内核代码了。

下面来对这段代码做一个整容手术,让它彻底改变原有的屌丝形象,瞬间变得白富美!

第一步:提供一个接口

通过 工厂模式 来创建 DataSource 对象,这种做法将创建对象的细节隐藏在具体的工厂实现中,手段非常高明,不用这种模式,都会觉得自己不够专业。

<!-- lang: java -->
/**
 * 数据源工厂
 *
 * @author huangyong
 * @since 2.3
 */
public interface DataSourceFactory {

    /**
     * 获取数据源
     */
    DataSource getDataSource();
}

以上 DataSourceFactory 接口仅提供一个 getDataSource 方法,就是要用来获取 DataSource 的,其它的事情一律不做,这就是传说中的 单一职责原则

下面要做的就是如何来实现这个接口了,参考现在基于 DBCP 的代码,这是很容易实现,但要注意的是,我们一定要做得抽象!因为不能与 DBCP 绑死了,那么应该如何来实现呢?

第二步:提供抽象实现

既然要抽象,就要学会“透过现象看本质”的功力。根据重构前的代码,我们不难发现,创建一个 DataSource 对象必须做以下几件事情:

  1. 从配置文件中读取 JDBC 配置项
  2. 创建数据源对象
  3. 设置数据源属性(包括基础属性与高级属性)

所以,我们的抽象实现应该这样写:

<!-- lang: java -->
/**
 * 默认数据源工厂
 * <br/>
 * 基于 Apache Commons DBCP 实现
 *
 * @author huangyong
 * @since 2.3
 */
public abstract class AbstractDataSourceFactory<T extends DataSource> implements DataSourceFactory {

    protected final String driver = ConfigHelper.getString("smart.framework.jdbc.driver");
    protected final String url = ConfigHelper.getString("smart.framework.jdbc.url");
    protected final String username = ConfigHelper.getString("smart.framework.jdbc.username");
    protected final String password = ConfigHelper.getString("smart.framework.jdbc.password");

    @Override
    public final T getDataSource() {
        // 创建数据源
        T ds = createDataSource();
        // 设置基础属性
        setDriver(ds, driver);
        setUrl(ds, url);
        setUsername(ds, username);
        setPassword(ds, password);
        // 设置高级属性
        setAdvancedConfig(ds);
        return ds;
    }

    public abstract T createDataSource();

    public abstract void setDriver(T ds, String driver);

    public abstract void setUrl(T ds, String url);

    public abstract void setUsername(T ds, String username);

    public abstract void setPassword(T ds, String password);

    public abstract void setAdvancedConfig(T ds);
}

以上提供了一个基于泛型的 AbstractDataSourceFactory,其中实现了 DataSourceFactory 接口的 getDataSource 方法。

注意,此时的返回值是一个泛型,此泛型必须扩展 DataSource,即泛型表示一个实现了 DataSource 接口的类型。除了必须实现的方法以外,还抽出了若干模板方法。这是什么设计模式?当然是 模板方法模式 了,看来该模式真是无处不在啊!大神们一般都会用。

大家知道,抽象类是不能创建对象的,我们必须对这个抽象类进行具体化才行,也就是再写一个实现类,让它去继承这个抽象类,这样才能实例化。不妨先写一个基于 DBCP 的具体实现吧。

第三步:提供默认实现(基于 DBCP 的实现)

既然是针对 DBCP 的具体实现,那么一定要与 DBCP 紧密耦合,代码写起来应该是非常容易的。

<!-- lang: java -->
/**
 * 默认数据源工厂
 *
 * @author huangyong
 * @since 2.3
 */
public class DefaultDataSourceFactory extends AbstractDataSourceFactory<BasicDataSource> {

    @Override
    public BasicDataSource createDataSource() {
        return new BasicDataSource();
    }

    @Override
    public void setDriver(BasicDataSource ds, String driver) {
        ds.setDriverClassName(driver);
    }

    @Override
    public void setUrl(BasicDataSource ds, String url) {
        ds.setUrl(url);
    }

    @Override
    public void setUsername(BasicDataSource ds, String username) {
        ds.setUsername(username);
    }

    @Override
    public void setPassword(BasicDataSource ds, String password) {
        ds.setPassword(password);
    }

    @Override
    public void setAdvancedConfig(BasicDataSource ds) {
        ds.setValidationQuery("select 1 from dual");
    }
}

当我们编写 DefaultDataSourceFactory 时,让它继承 AbstractDataSourceFactory,此时必须为指定一个泛型的具体类,也就是这里的 BasicDataSource 了,它是 DBCP 的核心类,它扩展了 DataSource 类,所以,下面重写的方法都是基于 BasicDataSource 的,而我们要做的事情就是对方法进行“填空”!

现在的结构看起来是这样的:

结构1

我们只需面向 DataSourceFactory 接口即可获取特定的 DataSource 对象,当然,这取决于 DataSourceFactory 接口选择哪种具体实现了。

与此类似,我们也可以提供基于 C3P0 或 Durid 的 DataSourceFactory 实现。

第四步:提供其它实现

打算通过 Plugin 的方式来提供其它实现,因为要把市面上所有的连接池都用一遍,这是一件不可思议的事情,不妨先使用 C3P0 与 Durid 做一个示例吧,也算是抛砖引玉了。

1. 基于 C3P0 的实现

<!-- lang: java -->
/**
 * 基于 C3P0 的数据源工厂
 *
 * @author huangyong
 * @since 2.3
 */
public class C3P0DataSourceFactory extends AbstractDataSourceFactory<ComboPooledDataSource> {

    private static final Logger logger = LoggerFactory.getLogger(C3P0DataSourceFactory.class);

    @Override
    public ComboPooledDataSource createDataSource() {
        return new ComboPooledDataSource();
    }

    @Override
    public void setDriver(ComboPooledDataSource ds, String driver) {
        try {
            ds.setDriverClass(driver);
        } catch (PropertyVetoException e) {
            logger.error("错误:初始化 JDBC Driver Class 失败!", e);
        }
    }

    @Override
    public void setUrl(ComboPooledDataSource ds, String url) {
        ds.setJdbcUrl(url);
    }

    @Override
    public void setUsername(ComboPooledDataSource ds, String username) {
        ds.setUser(username);
    }

    @Override
    public void setPassword(ComboPooledDataSource ds, String password) {
        ds.setPassword(password);
    }

    @Override
    public void setAdvancedConfig(ComboPooledDataSource ds) {
    }
}

与 DBCP 类似,C3P0 是使用自己的 ComboPooledDataSource 来创建 DataSource 的。

2. 基于 Durid 的实现

<!-- lang: java -->
/**
 * 基于 Durid 的数据源工厂
 *
 * @author huangyong
 * @since 2.3
 */
public class DruidDataSourceFactory extends AbstractDataSourceFactory<DruidDataSource> {

    private static final Logger logger = LoggerFactory.getLogger(DruidDataSourceFactory.class);

    @Override
    public DruidDataSource createDataSource() {
        return new DruidDataSource();
    }

    @Override
    public void setDriver(DruidDataSource ds, String driver) {
        ds.setDriverClassName(driver);
    }

    @Override
    public void setUrl(DruidDataSource ds, String url) {
        ds.setUrl(url);
    }

    @Override
    public void setUsername(DruidDataSource ds, String username) {
        ds.setUsername(username);
    }

    @Override
    public void setPassword(DruidDataSource ds, String password) {
        ds.setPassword(password);
    }

    @Override
    public void setAdvancedConfig(DruidDataSource ds) {
        try {
            ds.setFilters("stat");
        } catch (SQLException e) {
            logger.error("错误:设置 Stat Filter 失败!", e);
        }
    }
}

使用 Durid 创建 DataSource 也一样,只不过我们可以在 setAdvancedConfig 方法中提供一些高级特性,比如 Durid 的统计功能,可以在浏览器中看到连接池的相关统计数据,这个很赞哦!

想使用 Durid 的统计功能,需要在 web.xml 中配置一个 Servlet,如果不想手工配置的话,完全可以使用 Smart 牛逼的插件机制来完成这件事情,就像下面的代码这样:

<!-- lang: java -->
/**
 * 用于注册 Druid 的相关 Servlet
 *
 * @author huangyong
 * @since 2.3
 */
public class DruidPlugin extends WebPlugin {

    private static final String DRUID_STAT_URL = "/druid/*";

    @Override
    public void register(ServletContext servletContext) {
        // 添加 StatViewServlet
        ServletRegistration.Dynamic druidServlet = servletContext.addServlet("DruidStatView", StatViewServlet.class);
        // 从配置文件中获取该 Servlet 的 URL 路径
        String druidStatUrl = ConfigHelper.getString("druid.stat.url");
        // 若该 URL 路径不存在,则使用默认值
        if (StringUtil.isEmpty(druidStatUrl)) {
            druidStatUrl = DRUID_STAT_URL;
        }
        // 向 Servlet 中添加 URL 路径
        druidServlet.addMapping(druidStatUrl);
    }
}

上面使用了 Servlet 3.0 API 完成 Servlet 的注册,这样一来 web.xml 才是所谓的“零配置”。当发送 /druid/ 请求时就能看到 Druid 提供的统计页面了。

现在的代码结构看起来是不是更加丰满了呢?

结构2

既然已经有好几个 DataSourceFactory 的具体实现了,那么如何确保让 Smart 默认使用 DefaultDataSourceFactory,而通过配置的方式就能替换为第三方插件所提供的 DataSourceFactory 实现呢?

最后一步:提供定制特性

实现这个功能并不难,无非就是先从配置文件中读取实现类(类的字符串),然后通过反射去尝试实例化一个对象,若该对象不存在,则使用默认的实现去实例化。

Smart 提供了一个名为“实例工厂”的类 InstanceFactory

<!-- lang: java -->
/**
 * 实例工厂
 *
 * @author huangyong
 * @since 2.3
 */
public class InstanceFactory {

    /**
     * 用于缓存对应的实例
     */
    private static final Map<String, Object> cache = new ConcurrentHashMap<String, Object>();

    /**
     * ClassScanner
     */
    private static final String CLASS_SCANNER = "smart.framework.custom.class_scanner";

    /**
     * DataSourceFactory
     */
    private static final String DS_FACTORY = "smart.framework.custom.ds_factory";

    /**
     * 获取 ClassScanner
     */
    public static ClassScanner getClassScanner() {
        return getInstance(CLASS_SCANNER, DefaultClassScanner.class);
    }

    /**
     * 获取 DataSourceFactory
     */
    public static DataSourceFactory getDataSourceFactory() {
        return getInstance(DS_FACTORY, DefaultDataSourceFactory.class);
    }

    @SuppressWarnings("unchecked")
    public static <T> T getInstance(String cacheKey, Class<T> defaultImplClass) {
        // 若缓存中存在对应的实例,则返回该实例
        if (cache.containsKey(cacheKey)) {
            return (T) cache.get(cacheKey);
        }
        // 从配置文件中获取相应的接口实现类配置
        String implClassName = ConfigHelper.getString(cacheKey);
        // 若实现类配置不存在,则使用默认实现类
        if (StringUtil.isEmpty(implClassName)) {
            implClassName = defaultImplClass.getName();
        }
        // 通过反射创建该实现类对应的实例
        T instance = ObjectUtil.newInstance(implClassName);
        // 若该实例不为空,则将其放入缓存
        if (instance != null) {
            cache.put(cacheKey, instance);
        }
        // 返回该实例
        return instance;
    }
}

以上代码不仅对之前提到的 ClassScanner 做了实例化,而且也对现在的 DataSourceFactory 进行了实例化。使用了同一段逻辑完成了这个实例化过程(见 getInstance 方法),此外,还使用了一个基于 Map 的 cache 来缓存已创建的实例,这样下次可直接从 cache 中获取实例,而无需再次实例化了。

这样我们只需在 smart.properties 文件中稍作配置,即可使用第三方实现,同时禁用默认实现。

<!-- lang: java -->
smart.framework.custom.ds_factory=org.smart4j.plugin.druid.DruidDataSourceFactory

此时,DruidDataSourceFactory 生效了,DefaultDataSourceFactory 失效了。

将来这个 InstanceFactory 还会干更多的活,后续我会与大家继续分享。


欢迎下载 Smart 源码:

http://git.oschina.net/huangyong/smart

欢迎阅读 Smart 博文:

http://my.oschina.net/huangyong/blog/158380

转载于:https://my.oschina.net/huangyong/blog/265015

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值