春节将至,今天放假了,在此祝小伙伴们新春大吉,身体健康,思路清晰,永远无BUG!
一句话归纳:参数化变动源意思是根据参数动态添加数据源以及切换数据源,解决不肯定数据源的问题。
通过前面两篇文章对于 Spring Boot 处理多个数据库的策略讲解,相信你们已经对多数据源和动态数据源有了比较好的了解。如需回顾,请见:html
在前面文章中,留了一个思考题,不管是多套源仍是动态数据源,相对来讲仍是固定的数据源(如一主一从,一主多从等),即在编码时已经肯定的数据库数量,只是在具体使用哪个时进行动态处理。若是数据源自己并不肯定,或者说须要根据用户输入来链接数据库,这时,如何处理呢?能够想象如今咱们有一个需求,须要对数据库进行链接管理,用户能够输入对应的数据库链接信息,而后能够查看数据库有哪些表。这就跟平时使用的数据库管理软件有点相似了,如 MySQL Workbench、Navicat、SQLyog,下图是SQLyog截图:java
本文基于前面的示例,添加一个功能,根据用户输入的数据库链接信息,链接数据库,并返回数据库的表信息。内容包括动态添加数据源、动态代理简化数据源操做等。mysql
本文所涉及到的示例代码:https://github.com/mianshenglee/my-example/tree/master/multi-datasource
,读者可结合一块儿看。git
Spring Boot 的动态数据源,本质上是把多个数据源存储在一个 Map 中,当须要使用某个数据源时,从 Map 中获取此数据源进行处理。在动态数据源处理时,经过继承抽象类 AbstractRoutingDataSource
可实现此功能。既然是 Map ,若是有新的数据源,把新的数据源添加到此 Map 中就能够了。这就是整个解决思路。github
可是,查看 AbstractRoutingDataSource
源码,能够发现,存放数据源的 Map targetDataSources
是 private 的,并且并无提供对此 Map 自己的操做,它提供的是两个关键操做:setTargetDataSources
及 afterPropertiesSet
。其中 setTargetDataSources
设置整个 Map 目标数据源,afterPropertiesSet
则是对 Map 目标数据源进行解析,造成最终使用的 resolvedDataSources
,可见如下源码:spring
this.resolvedDataSources = new HashMap(this.targetDataSources.size()); this.targetDataSources.forEach((key, value) -> { Object lookupKey = this.resolveSpecifiedLookupKey(key); DataSource dataSource = this.resolveSpecifiedDataSource(value); this.resolvedDataSources.put(lookupKey, dataSource); });
所以,为实现动态添加数据源到 Map 的功能,咱们能够根据这两个关键操做进行处理。sql
说明,下面的操做基于以前文章的示例,基本的工程搭建及配置再也不重复说明,有须要可参考文章。
为了能够动态添加数据源到 Map ,咱们须要对动态数据源进行改造。以下:数据库
public class DynamicDataSource extends AbstractRoutingDataSource { private Map<Object, Object> backupTargetDataSources; /** * 自定义构造函数 */ public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSource){ backupTargetDataSources = targetDataSource; super.setDefaultTargetDataSource(defaultDataSource); super.setTargetDataSources(backupTargetDataSources); super.afterPropertiesSet(); } /** * 添加新数据源 */ public void addDataSource(String key, DataSource dataSource){ this.backupTargetDataSources.put(key,dataSource); super.setTargetDataSources(this.backupTargetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getContextKey(); } }
- 添加了自定义的
backupTargetDataSources
做为原targetDataSources
的拷贝- 自定义构造函数,把须要保存的目标数据源拷贝到自定义的 Map 中
- 添加新数据源时,依然使用
setTargetDataSources
及afterPropertiesSet
完成新数据源添加。- 注意:
afterPropertiesSet
的做用很重要,它负责解析成可用的目标数据源。
原来在建立动态数据源时,使用的是无参数构造函数,通过前面改造后,使用有参构造函数,以下:apache
@Bean @Primary public DataSource dynamicDataSource() { Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource()); dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource()); //有参构造函数 return new DynamicDataSource(masterDataSource(), dataSourceMap); }
在Spring Boot 使用过程当中,常常会用到 Spring 的上下文,常见的就是从 Spring 的 IOC 中获取 bean 来进行操做。因为 Spring 使用的 IOC 基本上把 bean 都注入到容器中,所以须要 Spring 上下文来获取。咱们在 context 包下添加 SpringContextHolder
,以下:后端
@Component public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextHolder.applicationContext = applicationContext; } /** * 返回上下文 */ public static ApplicationContext getContext(){ return SpringContextHolder.applicationContext; } }
经过 getContext
就能够获取上下文,进而操做。
经过参数添加数据源,须要根据参数构造数据源,而后添加到前面说的 Map 中。以下:
public class DataSourceUtil { /** * 建立新的数据源,注意:此处只针对 MySQL 数据库 */ public static DataSource makeNewDataSource(DbInfo dbInfo){ String url = "jdbc:mysql://"+dbInfo.getIp() + ":"+dbInfo.getPort()+"/"+dbInfo.getDbName() +"?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8"; String driveClassName = StringUtils.isEmpty(dbInfo.getDriveClassName())? "com.mysql.cj.jdbc.Driver":dbInfo.getDriveClassName(); return DataSourceBuilder.create().url(url) .driverClassName(driveClassName) .username(dbInfo.getUsername()) .password(dbInfo.getPassword()) .build(); } /** * 添加数据源到动态源中 */ public static void addDataSourceToDynamic(String key, DataSource dataSource){ DynamicDataSource dynamicDataSource = SpringContextHolder.getContext().getBean(DynamicDataSource.class); dynamicDataSource.addDataSource(key,dataSource); } /** * 根据数据库链接信息添加数据源到动态源中 * @param key * @param dbInfo */ public static void addDataSourceToDynamic(String key, DbInfo dbInfo){ DataSource dataSource = makeNewDataSource(dbInfo); addDataSourceToDynamic(key,dataSource); } }
- 经过
DataSourceBuilder
及相应的参数来构造数据源,注意此处只针对 MySQL 做处理,其它数据库的话,对应的 url 及 DriveClassName 需做相应的变动。- 添加数据源时,经过 Spring 上下文获取动态数据源的 bean,而后添加。
前面两步已实现添加数据源,下面咱们根据需求(根据用户输入的数据库链接信息,链接数据库,并返回数据库的表信息),看看如何使用它。
经过 MySQL 的 information_schema
能够获取表信息。
@Repository public interface TableMapper extends BaseMapper<TestUser> { /** * 查询表信息 */ @Select("select table_name, table_comment, create_time, update_time " + " from information_schema.tables " + " where table_schema = (select database())") List<Map<String,Object>> selectTableList(); }
把数据库链接信息经过一个类进行封装。
@Data public class DbInfo { private String ip; private String port; private String dbName; private String driveClassName; private String username; private String password; }
在 controller 层,咱们定义一个查询表信息的接口,根据传入的参数,链接数据源,返回表信息:
/** * 根据数据库链接信息获取表信息 */ @GetMapping("table") public Object findWithDbInfo(DbInfo dbInfo) throws Exception { //数据源key String newDsKey = System.currentTimeMillis()+""; //添加数据源 DataSourceUtil.addDataSourceToDynamic(newDsKey,dbInfo); DynamicDataSourceContextHolder.setContextKey(newDsKey); //查询表信息 List<Map<String, Object>> tables = tableMapper.selectTableList(); DynamicDataSourceContextHolder.removeContextKey(); return ResponseResult.success(tables); }
- 访问地址
http://localhost:8080/dd/table?ip=localhost&port=3310&dbName=mytest&username=root&password=111111
,对应数据库链接参数。- 此处数据源的 key 是无心义的,建议根据实际场景设置有意义的值
前面已经完成了参数化切换数据源功能,但还有一点就是有模板代码,如添加数据源、切换数据源、对此数据源进行CURD操做、释放数据源,若是每一个地方都这样作,就很繁琐,这个时候,就须要用到动态代理了,可参数我以前的文章:java开发必学知识:动态代理。此处,使用 JDK 自带的动态代理,实现参数化变动数据源的功能,消除模板代码。
添加 proxy 包,添加 JdkParamDsMethodProxy
类,实现 InvocationHandler
接口,在 invoke
中编写参数化切换数据源的逻辑便可。以下:
public class JdkParamDsMethodProxy implements InvocationHandler { // 代理对象及相应参数 private String dataSourceKey; private DbInfo dbInfo; private Object targetObject; public JdkParamDsMethodProxy(Object targetObject, String dataSourceKey, DbInfo dbInfo) { this.targetObject = targetObject; this.dataSourceKey = dataSourceKey; this.dbInfo = dbInfo; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //切换数据源 DataSourceUtil.addDataSourceToDynamic(dataSourceKey, dbInfo); DynamicDataSourceContextHolder.setContextKey(dataSourceKey); //调用方法 Object result = method.invoke(targetObject, args); DynamicDataSourceContextHolder.removeContextKey(); return result; } /** * 建立代理 */ public static Object createProxyInstance(Object targetObject, String dataSourceKey, DbInfo dbInfo) throws Exception { return Proxy.newProxyInstance(targetObject.getClass().getClassLoader() , targetObject.getClass().getInterfaces(), new JdkParamDsMethodProxy(targetObject, dataSourceKey, dbInfo)); } }
- 代码中,须要使用的参数经过构造函数传入
- 经过
Proxy.newProxyInstance
建立代理,在方法执行时(invoke
) 进行数据源添加、切换、数据库操做、清除等
有了代理,在添加和切换数据源时就能够擦除模板代码,前面的业务代码就变成:
@GetMapping("table") public Object findWithDbInfo(DbInfo dbInfo) throws Exception { //数据源key String newDsKey = System.currentTimeMillis()+""; //使用代理切换数据源 TableMapper tableMapperProxy = (TableMapper)JdkParamDsMethodProxy.createProxyInstance(tableMapper, newDsKey, dbInfo); List<Map<String, Object>> tables = tableMapperProxy.selectTableList(); return ResponseResult.success(tables); }
经过代理,代码就简洁多了。
本文基于动态数据源,对参数化变动数据源及应用场景进行了说明,提出链接数据库,查询表信息的功能需求做为示例,实现根据参数构建数据源,动态添加数据源功能,对参数化变动数据源的使用进行讲解,最后使用动态代理简化操做。本篇文章偏重代码实现,小伙伴们能够新手实践来加深认知。
本文配套的示例,示例代码,有兴趣的能够运行示例来感觉一下。
https://www.liaoxuefeng.com/article/1182502273240832
https://juejin.im/post/5cb0023d5188250df17d4ffc
个人公众号(搜索Mason技术记录
),获取更多技术记录: