昨天因为项目需要,需要写一个mongoDB的多数据源,之前的项目也做过类似的东西,无非是根据配置文件配置mongoTemplate,但是最近刚好看了spring源码,想要实现一个动态的多数据源,经过大半天的时间,终于算搞定了。
思路如下:
1. 从配置文件中获取数据源的配置;
2. 利用spring的ImportBeanDefinitionRegistrar动态注册实例到spring容器中;
3. 使用时只需要根据需要注入相应的bean即可;
思路很简单,因为网上大多的例子都是配置一些注解啊,配置文件中加一个数据源,代码中就需要配置一个类,ok!开干。
项目中先引入mongo依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
先贴一个配置文件下面备用
server:
port: 10250
eureka:
client:
service-url:
defaultZone: http://filink:123456@xxx.xxx.xxx.xxx:8761/eureka/
instance:
prefer-ip-address: true
# ---------------------------------------------------
spring:
application:
name: filink-dump-server
cloud:
bus:
refresh:
enabled: true
stream:
kafka:
binder:
brokers: xxx.xxx.xxx.xxx:9092
zkNodes: xxx.xx.xxx:2181
data:
mongodb:
alarm_history:
host: xxx.xxx.xxx.xx
port: 27017
database: mongoOne
alarm_current:
host: xx.x.x.xx.x
port: 27017
database: mongoTwo
device_log:
host: xx.x.x.x.x
port: 27017
database: mongoThree
hystrix:
command:
default:
execution:
timeout:
enable: true
isolation:
thread:
timeoutInMilliseconds: 5000
# 配置feign使用熔断
feign:
hystrix:
enabled: true
# feign有超时时间的设置,要单独配置ribbon才能生效
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
#是否需要权限拉去,默认是true,如果不false就不允许你去拉取配置中心Server更新的内容
management:
security:
enabled: false
# 启动暴露url
endpoints:
web:
expose: "*"
新建开启多数据源数据,装逼装到底,跟着Spring风格走:
这个注解的主要作用是引入一个自定义注册器MongoMultiDataSourceRegistrar,同时也起到一个开关的作用,要使用咱们的东西,要先声明。
/**
* 开启mongo多数据源
* 不能用sit作为后缀,不能修改配置文件名称,只能使用bootstrap开头
* bootstrap-dev.yml
* bootstrap-pro.yml
*
* @author yuanyao@wistronits.com
* create on 2019-05-29 21:53
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({MongoMultiDataSourceRegistrar.class})
public @interface EnableMongoMultiDataSource {
}
接下来就是最重要的自定义注册器的实现 , 先贴全部代码,最烦代码不贴全的人。。。
后面再对每一对进行解释:
package com.fiberhome.filink.mongo.configuration;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.web.context.support.StandardServletEnvironment;
import java.util.HashMap;
import java.util.Map;
/**
*
* Mongo多数据源注册器
*
* @author yuanyao@wistronits.com
* create on 2019-05-29 21:55
*/
@Slf4j
public class MongoMultiDataSourceRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
public static Map<String, SimpleMongoDbFactory> mongoDbFactoryMap = new HashMap<>();
/**
* 注册MongoTemplate
*
* 目前实现注册出现问题,先使用静态map实现,后续试着改为注册Spring容器
*
* @param importingClassMetadata
* @param registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
int count = 0;
for (Map.Entry<String, SimpleMongoDbFactory> entry : mongoDbFactoryMap.entrySet()) {
SimpleMongoDbFactory value = entry.getValue();
GenericBeanDefinition definition = new GenericBeanDefinition();
// 设置bean类型
definition.setBeanClass(MongoTemplate.class);
// 设置为自定义bean
definition.setSynthetic(true);
// 设置不初始化
definition.setEnforceInitMethod(false);
if (count == 0) {
// 第一个设置为primary
definition.setPrimary(true);
}
// 构造函数参数
ConstructorArgumentValues argumentValues = new ConstructorArgumentValues();
argumentValues.addIndexedArgumentValue(0, value);
// 设置构造函数参数
definition.setConstructorArgumentValues(argumentValues);
// 注册
registry.registerBeanDefinition(entry.getKey(),definition);
count++;
}
log.info("MongoDB多数据源装载完毕,共:{} 个", count);
}
/**
* 根据配置属性获取mongoTemplate
*
* 根据配置文件获取属性还需完善
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
PropertySource<?> propertySource = ((StandardServletEnvironment) environment)
.getPropertySources().get("applicationConfig: [classpath:/bootstrap.yml]");
Map<String, String> appMap = (Map<String, String>) propertySource.getSource();
/**
* 不能用sit作为后缀,不能修改配置文件名称,只能使用bootstrap开头
*/
if (environment.getActiveProfiles().length > 0) {
PropertySource activeSource = ((StandardServletEnvironment) environment).getPropertySources()
.get("applicationConfig: [classpath:/bootstrap-" + environment.getActiveProfiles()[0] +
".yml]");
Map<String, String> activeAppMap = (Map<String, String>) activeSource.getSource();
appMap.putAll(activeAppMap);
}
// key:数据源属性 vaue:对象
Map<String, MongoConfig> configMap = new HashMap<>();
for (Map.Entry<String, String> entry : appMap.entrySet()) {
if (entry.getKey().contains("spring.data.mongodb")) {
// 分离每一个属性
String beanName = getValue(entry.getKey(),3);
String property = getValue(entry.getKey(), 4);
if (!configMap.containsKey(beanName)) {
configMap.put(beanName, new MongoConfig());
}
setValue(configMap.get(beanName),property,entry.getValue());
}
}
log.info("开始装载MongoDB多数据源");
for (Map.Entry<String, MongoConfig> entry : configMap.entrySet()) {
MongoConfig value = entry.getValue();
ServerAddress host = new ServerAddress(value.getHost(), value.getPort());
MongoClient mongoClient = new MongoClient(host);
SimpleMongoDbFactory simpleMongoDbFactory = new SimpleMongoDbFactory(mongoClient,value.getDatabase());
mongoDbFactoryMap.put(entry.getKey(), simpleMongoDbFactory);
log.info("数据源名称:{}", entry.getKey());
}
}
/**
* 获取key值
* index:3---》bean名称
* index:4---》属性
*
* @param value
* @param index
* @return
*/
public String getValue(String value,int index) {
return value.split("\\.")[index];
}
private void setValue(MongoConfig config,String key,Object value) {
if ("host".equalsIgnoreCase(key)) {
config.setHost((String) value);
} else if ("port".equalsIgnoreCase(key)) {
config.setPort((Integer) value);
} else if ("database".equalsIgnoreCase(key)) {
config.setDatabase((String) value);
}else {
throw new IllegalArgumentException("配置信息错误");
}
}
}
setEnvironment 方法
这个方法是实现EnvironmentAware重写的方法,他的参数environment可以拿到配置文件中的所有属性,但是也有比较坑的地方,这个类也可以通过@Autowired的方式获取。
/**
* 根据配置属性获取mongoTemplate
*
* 根据配置文件获取属性还需完善
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
// 打断点可以看到 配置文件的属性是一个个的PropertySources,这里和名称有关,如果用的application.properties名字也不
// 一样,也是后面需要完善的地方,不注意有可能获取不到属性,最好是打断点找到自己想到的属性,确定名称
// 我这边还使用了springcloud的配置中心,后期有没有问题还需要调整
PropertySource<?> propertySource = ((StandardServletEnvironment) environment)
.getPropertySources().get("applicationConfig: [classpath:/bootstrap.yml]");
Map<String, String> appMap = (Map<String, String>) propertySource.getSource();
/**
* 不能用sit作为后缀,不能修改配置文件名称,只能使用bootstrap开头
* 这里我测过了,bootstrap开头不是必须,如果使用bootstrap-sit.yml文件,就会获取不到配置属性,
* 通过environment.getActiveProfiles()[0]来找到配置文件指定的配置属性
* 最后保存在一个map对象中备用
*/
if (environment.getActiveProfiles().length > 0) {
PropertySource activeSource = ((StandardServletEnvironment) environment).getPropertySources()
.get("applicationConfig: [classpath:/bootstrap-" + environment.getActiveProfiles()[0] +
".yml]");
Map<String, String> activeAppMap = (Map<String, String>) activeSource.getSource();
appMap.putAll(activeAppMap);
}
// key:数据源属性 vaue:对象
// 遍历map对象,分割开配置属性
Map<String, MongoConfig> configMap = new HashMap<>();
for (Map.Entry<String, String> entry : appMap.entrySet()) {
if (entry.getKey().contains("spring.data.mongodb")) {
// 分离每一个属性
// 获取bean的名称,用这个属性区分每一个数据源
String beanName = getValue(entry.getKey(),3);
// 获取属性key 比如host / port 等
String property = getValue(entry.getKey(), 4);
// 如果map中没有就新建一个对象,这个对象在后面贴出,只是一个基本的属性设置
if (!configMap.containsKey(beanName)) {
configMap.put(beanName, new MongoConfig());
}
setValue(configMap.get(beanName),property,entry.getValue());
}
}
// 因为构建mongotemplate需要SimpleMongoDbFactory,这里就把我们配置的多数据源转换成SimpleMongoDbFactory
log.info("开始装载MongoDB多数据源");
for (Map.Entry<String, MongoConfig> entry : configMap.entrySet()) {
MongoConfig value = entry.getValue();
// 构建ServerAddress 传入host和port
ServerAddress host = new ServerAddress(value.getHost(), value.getPort());
MongoClient mongoClient = new MongoClient(host);
// 构建SimpleMongoDbFactory ,传入mongoClient和database名称,最终保存在成员变量mongoDbFactoryMap中
SimpleMongoDbFactory simpleMongoDbFactory = new SimpleMongoDbFactory(mongoClient,value.getDatabase());
mongoDbFactoryMap.put(entry.getKey(), simpleMongoDbFactory);
log.info("数据源名称:{}", entry.getKey());
}
}
MongoConfig 封装
package com.fiberhome.filink.mongo.configuration;
/**
* mongo多数据源属性配置类
*
* @author yuanyao@wistronits.com
* create on 2019-05-29 18:06
*/
public class MongoConfig {
private String host;
private Integer port;
private String database;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getDatabase() {
return database;
}
public void setDatabase(String database) {
this.database = database;
}
}
注册方法registerBeanDefinitions
这个方法是实现了ImportBeanDefinitionRegistrar的重写方法,也是我纠结了几个小时的地方。
因为看Spring源码的时候,注册bean只是简单的调用registry.registerBeanDefinition()方法,传入beanName和对象的class,
我传入beanName和mongoTemplate.class首先是报错,说找不到mongotemplate的构造函数,害得我又去研究了下spring源码。
后来想着Spring这么牛皮的东西不会不让自己注册想要的东西啊,就去看GenericBeanDefinition对象,发现里面有很多很眼熟的方法,就试着改了下面几个参数,启动项目一试,没想到ok了,激动的我立马点了根烟庆祝下。。。。
/**
* 注册MongoTemplate
*
*
* @param importingClassMetadata
* @param registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
int count = 0;
for (Map.Entry<String, SimpleMongoDbFactory> entry : mongoDbFactoryMap.entrySet()) {
SimpleMongoDbFactory value = entry.getValue();
GenericBeanDefinition definition = new GenericBeanDefinition();
// 设置bean类型
definition.setBeanClass(MongoTemplate.class);
// 设置为自定义bean
definition.setSynthetic(true);
// 设置不初始化
definition.setEnforceInitMethod(false);
if (count == 0) {
// 第一个设置为primary
definition.setPrimary(true);
}
// 构造函数参数
ConstructorArgumentValues argumentValues = new ConstructorArgumentValues();
argumentValues.addIndexedArgumentValue(0, value);
// 设置构造函数参数
definition.setConstructorArgumentValues(argumentValues);
// 注册
registry.registerBeanDefinition(entry.getKey(),definition);
count++;
}
log.info("MongoDB多数据源装载完毕,共:{} 个", count);
}
至此,配置部分算写完了,测试测试就ok!
首先主类上添加我们装逼的注解:因为引入了mongo包后会默认去连接,如果不配置就会控制台一致报错,很烦,所以就手动把自动的给排除掉exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}
/**
* 转储服务启动类
* @author yaoyuan
*/
@EnableMongoMultiDataSource
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
public class FilinkDumpServerApplication {
public static void main(String[] args) {
SpringApplication.run(FilinkDumpServerApplication.class, args);
}
}
编写测试类,就随便写个了
package com.fiberhome.filink.dump.dao;
import com.fiberhome.filink.dump.bean.TestBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author yuanyao@wistronits.com
* create on 2019-05-29 17:52
*/
@Component
public class TestMongoDao {
/**
* 因为注入了多个MongoTemplate,所以需要使用Qualifier配合Autowired找到我们想要的bean
*/
@Qualifier("device_log")
@Autowired(required = false)
private MongoTemplate device_log;
@Qualifier("alarm_current")
@Autowired(required = false)
private MongoTemplate alarm_current;
@Qualifier("alarm_history")
@Autowired(required = false)
private MongoTemplate alarm_histroy;
@PostConstruct
public void init2() {
TestBean testBean = new TestBean();
testBean.setName("张三00007777");
testBean.setAge(0);
device_log.insert(testBean);
alarm_current.insert(testBean);
alarm_histroy.insert(testBean);
}
}
至此,多数据源就ok了,我把公共的位置抽取成一个common,使用者只需要引入jar包,在主类上使用那个牛逼的注解就可以使用多数据源。向配置几个就配置几个,直接使用@Autowired注入就可以使用。。。。