随着业务变迁/需求变更,JavaEE 应用中会被迫连接多个数据源进行业务处理。
怎样在不影响原有项目结构的情况下,已最优雅/最简洁的方式动态切换数据源呢?
本文已一次添加数据源后动态切换实践为例,描述整个思考和实践过程,文中如有纰漏,还望指正。
1. 依赖 Spring 动态数据源实现
Spring 中提供了一个叫做 AbstractRoutingDataSource (抽象路由数据源)继承自 AbstractDataSource 并实现了 JDK DataSource 接口。
也就意味着继承 AbstractRoutingDataSource 并重写它 determineCurrentLookupKey 方法的类可以作为数据源,并个性化多数据源动态路由切换。
(如果你平时够仔细的话,现开源的数据库连接池都实现 DataSource 接口并进行了自己的个性化封装。)
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DbContextHolder.getDbType();}
}
对于一次 web 请求来说可以理解为单独的线程,将当前数据源暂存在线程当中是比较合理的做法。
public class DbContextHolder {private static final ThreadLocal contextHolder = new ThreadLocal<>();/*** 设置数据源** @param dbSourceEnum 要设置的数据库枚举名称*/public static void setDbType(DBSourceEnum dbSourceEnum) {contextHolder.set(dbSourceEnum.getValue());}/*** 取得当前数据源*/public static String getDbType() {return String.valueOf(contextHolder.get());}/*** 清除上下文数据*/public static void clearDbType() {contextHolder.remove();} }
当然为了后期的扩展和维护,以及使用的便捷性,这里数据源对象我们引入枚举类型。
这样后续其他同事编程使用枚举,改动起来也相当方便,还能进行数据源的一些自定义说明。
public enum DBSourceEnum {one("dataSource1"),two("dataSource2");private String value;DBSourceEnum(String value) {this.value = value;}public String getValue() {return value;}
}
上述的 dataSource1/dataSource2 即为 spring-context 中已加载的数据源对象 Id。
<bean name="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">......</bean>
<bean name="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">......</bean>
接下来在 context 中配置继承自 AbstractRoutingDataSource 的 DynamicDataSource。
<bean id="dataSource" class="com.rambo.spm.core.multidb.DynamicDataSource"><property name="targetDataSources"><map key-type="java.lang.String"><entry key="dataSource1" value-ref="dataSource1"/><entry key="dataSource2" value-ref="dataSource2"/></map></property><property name="defaultTargetDataSource" ref="dataSource1"/></bean>
Ok,这样在配置后续 dao 层时使用该 DynamicDataSource 即可。
2. 最优雅的切换数据源方式
完成上述工作之后,其实动态切换数据源已经实现,在业务层如下面这样编程。
DbContextHolder.setDbType(DBSourceEnum.one);List<Menu> menuList = menuService.selectList(null);DbContextHolder.setDbType(DBSourceEnum.two);List<User> userList = userService.selectList(null);
缺点很明显,连接数据源2时要进行切换/不利于扩展/切换不当时给同事埋雷的几率很大。
和团队进行交流时,讨论出用强大 aop 来拦截 dao 层对象,动态切换数据源的方案。
对于 dao 层对象来说访问数据库的哪张表是确定的,编写自定义注解与 dao 层对象进行绑定。
自定义数据源注解如下:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DataSource {DBSourceEnum value() default DBSourceEnum.one;
}
编写切面处理对象,在 dao 层对象使用前进行拦截,顺手切换数据源,如果没有数据源注解,设置为默认。
所以对于项目中原配数据源 dao 层对象,不需要进行任何修改,切面处理如下。
@Before("cut()")public void doBefore(JoinPoint joinPoint) {DataSource dataSource = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);DbContextHolder.setDbType(dataSource != null ? dataSource.value() : DBSourceEnum.one);log.info("当前数据源为:" + DbContextHolder.getDbType());}
多数项目中 dao 层错综复杂的抽象和继承关系会给你 aop 切面拦截造成一定的困难,多思考、多实践总会有办法的。
好了,就这样吧,有没有感觉 aop 拦截方式比在程序中硬编码更容易扩展、更容易编程、更容易理解,当然也更优雅。
由此可扩展数据库方面很多,比如读写分离/分库分区....具体场景具体分析吧。
代码已托管在:https://git.oschina.net/LanboEx/spmvc-mybatis.git 有兴趣的朋友,可以检到本地 run 一下。