SpringBoot多数据源配置的全过程记录
前言
多数据源的核心就是向 ioc 容器注入 abstractroutingdatasource 和如何切换数据源。注入的方式可以是注册 beandefinition 或者是构建好的 bean,切换数据源的方式可以是方法参数或者是注解切换(其他的没想象出来),具体由需求决定。
我的需求是统计多个库的数据,将结果写入另一个数据库,统计的数据库数量是不定的,无法通过 @bean 直接注入,又是统计任务,dao 层注解切换无法满足,因此选择注册(abstractroutingdatasource 的)beandefinition 和方法参数切换来实现。下面以统计统计中日韩用户到结果库为例。
配置文件
master 为结果库,其他为被统计的数据库(china、japan 可以用枚举唯一标识,当然也可以用 string):
dynamic: datasources: master: driver-class-name: com.mysql.cj.jdbc.driver url: jdbc:mysql://localhost:3306/result?useunicode=true&characterencoding=utf8xxxxxxxx username: root password: 123456 china: driver-class-name: com.mysql.cj.jdbc.driver url: jdbc:mysql://localhost:3306/china?useunicode=true&characterencoding=utf8xxxxxxxx username: root password: 123456 japan: driver-class-name: com.mysql.cj.jdbc.driver url: jdbc:mysql://localhost:3306/japan?useunicode=true&characterencoding=utf8xxxxxxxx username: root password: 123456 korea: driver-class-name: com.mysql.cj.jdbc.driver url: jdbc:mysql://localhost:3306/korea?useunicode=true&characterencoding=utf8xxxxxxxx username: root password: 123456
对应的配置类:
package com.statistics.dynamicds.core.config; import com.statistics.dynamicds.core.country; import lombok.data; import org.springframework.boot.context.properties.configurationproperties; import org.springframework.context.annotation.configuration; import java.util.map; import static com.statistics.dynamicds.core.config.multidatasourceproperties.prefix; @data @configuration @configurationproperties(prefix = prefix) public class multidatasourceproperties { public static final string prefix = "dynamic"; private map<country, datasourceproperties> datasources; @data public static class datasourceproperties { private string driverclassname; private string url; private string username; private string password; } } package com.statistics.dynamicds.core; public enum country { master("master", 0), china("china", 86), japan("japan", 81), korea("korea", 82), // 其他国家省略 private final string name; private final int id; country(string name, int id) { this.name = name; this.id = id; } public int getid() { return id; } public string getname() { return name; } }
依赖
orm 用的 jpa,springboot 版本为 2.3.7.release,通过 lombok 简化 getset。
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency> <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <version>1.18.22</version> <scope>provided</scope> </dependency>
构建 abstractroutingdatasource
spring 的动态数据源需要注入 abstractroutingdatasource,因为配置文件中被统计数据源不是固定的,所以不能通过 @bean 注解注入,需要手动构建。
要在启动类加上 @import(multidatasourceimportbeandefinitionregistrar.class)。
要在启动类加上 @import(multidatasourceimportbeandefinitionregistrar.class)。
要在启动类加上 @import(multidatasourceimportbeandefinitionregistrar.class),重要的事情写三行。
package com.statistics.dynamicds.autoconfig; import com.statistics.dynamicds.core.dynamicdatasourcerouter; import com.statistics.dynamicds.core.country; import com.statistics.dynamicds.core.config.multidatasourceproperties; import com.zaxxer.hikari.hikaridatasource; import org.springframework.beans.factory.support.abstractbeandefinition; import org.springframework.beans.factory.support.beandefinitionbuilder; import org.springframework.beans.factory.support.beandefinitionregistry; import org.springframework.boot.context.properties.bind.binder; import org.springframework.boot.jdbc.datasourcebuilder; import org.springframework.context.environmentaware; import org.springframework.context.annotation.importbeandefinitionregistrar; import org.springframework.core.env.environment; import org.springframework.core.type.annotationmetadata; import javax.annotation.nonnull; import java.util.map; import java.util.stream.collectors; import static com.statistics.dynamicds.core.config.multidatasourceproperties.prefix; public class multidatasourceimportbeandefinitionregistrar implements importbeandefinitionregistrar, environmentaware { public static final string datasource_beanname = "dynamicdatasourcerouter"; private environment environment; @override public void registerbeandefinitions(@nonnull annotationmetadata importingclassmetadata, beandefinitionregistry registry) { multidatasourceproperties multidatasourceproperties = binder.get(environment) .bind(prefix, multidatasourceproperties.class) .orelsethrow(() -> new runtimeexception("no found dynamicds config")); final hikaridatasource[] defaulttargetdatasource = {null}; map<country, hikaridatasource> targetdatasources = multidatasourceproperties.getdatasources().entryset().stream() .collect(collectors.tomap( map.entry::getkey, entry -> { multidatasourceproperties.datasourceproperties datasourceproperties = entry.getvalue(); hikaridatasource datasource = datasourcebuilder.create() .type(hikaridatasource.class) .driverclassname(datasourceproperties.getdriverclassname()) .url(datasourceproperties.geturl()) .username(datasourceproperties.getusername()) .password(datasourceproperties.getpassword()) .build(); datasource.setpoolname("hikaripool-" + entry.getkey()); if (country.master == entry.getkey()) { defaulttargetdatasource[0] = datasource; } return datasource; })); abstractbeandefinition beandefinition = beandefinitionbuilder.genericbeandefinition(dynamicdatasourcerouter.class) .addconstructorargvalue(defaulttargetdatasource[0]) .addconstructorargvalue(targetdatasources) .getbeandefinition(); registry.registerbeandefinition(datasource_beanname, beandefinition); } @override public void setenvironment(@nonnull environment environment) { this.environment = environment; } }
上面代码中 multidatasourceproperties 不是由 @resource 或者 @autowired 获取的是因为 importbeandefinitionregistrar 执行的很早,此时 @configurationproperties 的配置参数类还没有注入,因此要手动获取(加 @configurationproperties 注解是为了使 ioc 容器中其他 bean 能获取配置的 country,以此来切换数据源)。
下面是 abstractroutingdatasource 的实现类 dynamicdatasourcerouter:
package com.statistics.dynamicds.core; import org.springframework.jdbc.datasource.lookup.abstractroutingdatasource; import java.util.map; public class dynamicdatasourcerouter extends abstractroutingdatasource { public dynamicdatasourcerouter(object defaulttargetdatasource, map<object, object> targetdatasources) { this.setdefaulttargetdatasource(defaulttargetdatasource); this.settargetdatasources(targetdatasources); } @override protected object determinecurrentlookupkey() { return datasourcecontextholder.getlookupkey(); } }
数据源切换
数据源的切换由 datasourcecontextholder 和切面 dynamicdatasourceaspect 控制:
package com.statistics.dynamicds.core; public class datasourcecontextholder { private static final threadlocal<country> holder = threadlocal.withinitial(() -> country.master); public static void setlookupkey(country lookupkey) { holder.set(lookupkey); } public static country getlookupkey() { return holder.get(); } public static void clear() { holder.remove(); } } package com.statistics.dynamicds.core; import org.aspectj.lang.proceedingjoinpoint; import org.aspectj.lang.annotation.around; import org.aspectj.lang.annotation.aspect; import org.aspectj.lang.annotation.pointcut; import org.springframework.stereotype.component; @aspect @component public class dynamicdatasourceaspect { @pointcut("execution(* com.statistics.dao..*.*(..))") void aspect() { } @around("aspect()") public object around(proceedingjoinpoint joinpoint) throws throwable { for (object arg : joinpoint.getargs()) { if (arg instanceof country) { datasourcecontextholder.setlookupkey((country) arg); break; } } try { return joinpoint.proceed(); }finally { datasourcecontextholder.clear(); } } }
目录
.
└─com
└─statistics
│ statisticsapplication.java
│
├─dao
│ userdao.java
│
├─dynamicds
│ ├─autoconfig
│ │ multidatasourceimportbeandefinitionregistrar.java
│ │
│ └─core
│ │ datasourcecontextholder.java
│ │ dynamicdatasourceaspect.java
│ │ dynamicdatasourcerouter.java
│ │ province.java
│ │
│ └─config
│ multidatasourceproperties.java
总结
以上就完成了多数据源配置,使用时只需要按照在 dao 层的方法参数中加一个 country 枚举就可以了。
如果无法用枚举标识数据源也可以换成 string,关于这个数据源的其他信息在内部类 datasourceproperties 加一个 map 即可,总之就是按照自己的需求扩展。
上一篇: Java线程死锁代码详解