在spring boot多数据源支持中讲了我在项目中做多数据源支持的配置方式,但是由于服务很多,每个项目都要写这些配置就重复性太多了,考虑到提出一个common的工具包给其他服务使用,又因为我们的spring boot的一大特点就是自动化配置,所以在看了一些源码和自动化配置的尝试后,尝试出了一种方式,经验有限希望不要误导大家,接下来的方式仅供参考。
明确目的
- 我要提供一个stater的jar包出来给其他服务使用。
- 我想要的想过是使用的服务导入jar包后不需要其他的操作,只通过配置就可以完成多数据源支持
- 配置的形式如下:
#连接池配置
spring:
datasource:
multi:
#name的第一个为默认数据源
name: db1,db2
db1:
filters: stat
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1
username: root
password: 123456
mapperxmlpath: classpath:mapper/db1/*.xml
mapperpath: com.example.demo.mapper.db1
configlocation: classpath:public/mybatis-configuration.xml
db2:
filters: stat
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2
username: root
password: 123456
mapperxmlpath: classpath:mapper/db2/*.xml
mapperpath: com.example.demo.mapper.db2
configlocation: classpath:public/mybatis-configuration.xml
注:配置中数据源的名字的第一个为默认数据源,这里的配置默认数据源就是db1
需要特别指出的是
-
mapperxmlpath 为spring boot多数据源支持中说到过的对应的数据源的mapper.xml文件路径
-
mapperpath为mapper.xml对应的java接口
-
configlocation为mybatis的配置文件
所以说最终希望的效果就是这样,导入stater的jar包,然后配置上面的配置就可以使用。下面详细来说一下我的实现过程。
原理思路
因为spring boot提供了自动化配置的方式,就是autoconfig,但是在spring boot version 1.5.10中尝试过使用autoconfig的方式去配置,事务支持一直没有成功。所以说这里我采用的是另外一种方式就是ApplicationContextInitializer + BeanDefinitionRegistryPostProcessor 的方式去配置的。
实现的原理其实就是在spring提供的自动化配置方案spring.factories中添加我们自己加载数据源的ApplicationContextInitializer,然后在ApplicationContextInitializer初始化的时候读取配置数据源信息,添加我们自己的BeanDefinitionRegistryPostProcessor,在spring boot启动的时候就会调用我们的BeanDefinitionRegistryPostProcessor完成我们的spring boot多数据源的自动化加载与配置。
创建一个springboot项目
- springboot 1.5.14.RELEASE
- jdk1.8
因为我们是做一个stater,所以我们没有使用springapplication,所以删掉启动类。 调整后的pom文件如下所示:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.multipledatasource</groupId>
<artifactId>multipledatasource-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>multiple-datasource</name>
<description>Demo project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<druid.version>1.1.2</druid.version>
<mybatis.version>1.3.2</mybatis.version>
<spring-boot.version>1.5.14.RELEASE</spring-boot.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<!-- <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- 支持的数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
创建配置数据源
1)创建spring.factories
springboot提供了一种自动化的配置方案就是在spring.factories中加入我们自己的initializer,如图所示
这里我们先定义好我们的initializer的路径,内容如下:
# 添加自定义bean的处理,通过initializer
org.springframework.context.ApplicationContextInitializer=\
com.multiplesource.config.MultiInitializer
2)创建MultiInitializer
public class MultiInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
Environment ev = applicationContext.getEnvironment();
}
}
我们需要在MultiInitializer初始化的时候加载配置的数据源信息,并且添加到我们自己的BeanDefinitionRegistryPostProcessor中以供使用,所以说我们需要创建我们自己的javabean以供装载我们配置的多数据源信息,我这里提供了两个类MultiDataSourceProperties和SingleDataSourceProperties,SingleDataSourceProperties就是对应于每个属于的配置信息,MultiDataSourceProperties就是维护了配置的多个数据源的信息。这里就不在贴代码了,主要看下MultiInitializer的实现吧,MultiInitializer的完整代码如下所示:
public class MultiInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
Environment ev = applicationContext.getEnvironment();
MultiDataSourceProperties pp = new MultiDataSourceProperties();
initProperties(pp,ev);
//添加我们自己定义的BeanDefinitionRegistryPostProcessor,以供创建数据源的时候使用
applicationContext.addBeanFactoryPostProcessor(new MultiDataSourceProcessor(pp));
}
/**
* 加载环境属性变量
* @param prop
* @param env
*/
private void initProperties(MultiDataSourceProperties prop,Environment env){
String datasourcenames = env.getProperty(MultiDataSourceProperties.PREFIX+".name");
if(StringUtils.isEmpty(datasourcenames)) {
return ;
}
String[] sourcenames = datasourcenames.split(",");
prop.setNames(sourcenames);
Map<String,SingleDataSourceProperties> sps = new HashMap<String,SingleDataSourceProperties>();
prop.setProperties(sps);
Field[] fields = SingleDataSourceProperties.class.getDeclaredFields();
for(int i=0;i<sourcenames.length;i++) {
SingleDataSourceProperties sp = new SingleDataSourceProperties();
sps.put(sourcenames[i], sp);
StringBuffer realnamebuf = new StringBuffer(MultiDataSourceProperties.PREFIX);
realnamebuf.append(".");
realnamebuf.append(sourcenames[i]);
realnamebuf.append(".");
for(int k=0;k<fields.length;k++) {
Field field = fields[k];
String fieldname = field.getName();
String realname = realnamebuf.toString()+fieldname;
if(env.containsProperty(realname)) {
try {
String firstLetter = fieldname.substring(0,1).toUpperCase();
String methodname_set = "set"+ firstLetter + fieldname.substring(1);
Method setMethod = SingleDataSourceProperties.class.getMethod(methodname_set, new Class[]{field.getType()});
setMethod.invoke(sp, new Object[]{env.getProperty(realname)});
}catch (Exception e) {
}
}
}
}
}
}
接下来就是我们最重要核心的一步了,创建我们自己的BeanDefinitionRegistryPostProcessor,并维护一个MultiDataSourceProperties对象
3)创建MultiDataSourceProcessor 这个对象主要实现的接口是BeanDefinitionRegistryPostProcessor,通过重写postProcessBeanFactory和postProcessBeanDefinitionRegistry,这样springboot在启动的时候初始化bean的期间就会自动调用完成datasource和mapper的创建和初始化工作。所以这个类所处理的事情有:
- 创建每一个数据源对象(并初始化,给出默认数据源标记为isPrimary=true)
- 创建事务并绑定对应的数据源
- 创建mapper扫码路径和配置文件所在的路径
下面给出MultiDataSourceProcessor的具体实现
package com.multiplesource.config;
import java.io.IOException;
import java.sql.SQLException;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.AnnotationScopeMetadataResolver;
import org.springframework.context.annotation.ScopeMetadata;
import org.springframework.context.annotation.ScopeMetadataResolver;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.util.StringUtils;
/**
* 根据数据配置,动态创建bean交给spring管理
* 并给定扫描对于的数据源的mapper
*
* @author zhuzhoutong
*
*/
public class MultiDataSourceProcessor implements BeanDefinitionRegistryPostProcessor{
/**
* 数据源bean的前缀
* 数据源的bean名称就是dataSource_+(spring.datasource.multi的name属性值)
*/
public static final String DATASOURCPREFIX = "dataSource_";
public static final String TRANSACTIONPREFIX = "transAction_";
public static final String SQLSESSIONFACTORYPREFIX = "sqlSessionFactory_";
public MultiDataSourceProperties datasourceproperties;
private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();
private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
public MultiDataSourceProcessor(MultiDataSourceProperties datasourceproperties) {
this.datasourceproperties = datasourceproperties;
}
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 这里可以设置属性
initProperties(beanFactory);
}
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
registerDataSourceBeans(registry);
}
private void registerDataSourceBeans(BeanDefinitionRegistry registry) {
String[] names = datasourceproperties.getNames();
if(names==null) {
return;
}
for(int i=0;i<names.length;i++) {
String name = names[i];
boolean isPrimary = false;
String datasourcename = DATASOURCPREFIX+name;
String transactionname = TRANSACTIONPREFIX+name;
String sqlsessionFactoryname = SQLSESSIONFACTORYPREFIX+name;
if(i==0) {
isPrimary = true;
}
//数据源
registerBean(registry, datasourcename, DruidDataSource.class,isPrimary);
//tansaction
registerBean(registry, transactionname, DataSourceTransactionManager.class,isPrimary);
//sessionfactory
registerBean(registry, sqlsessionFactoryname, SqlSessionFactoryBean.class,isPrimary);
//mapperscan
registerMapperInfo(registry,sqlsessionFactoryname,datasourceproperties.getProperties().get(name));
}
}
private void registerMapperInfo(BeanDefinitionRegistry registry,String sqlseesionfactoryname,SingleDataSourceProperties prop) {
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setSqlSessionFactoryBeanName(sqlseesionfactoryname);
String[] basepackages = prop.getMapperpath().split(",");
scanner.registerFilters();
scanner.doScan(basepackages);
}
private void registerBean(BeanDefinitionRegistry registry, String name, Class<?> beanClass,boolean isPrimary){
AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
abd.setPrimary(isPrimary);
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
abd.setScope(scopeMetadata.getScopeName());
// 可以自动生成name
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, registry));
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}
private void initProperties(ConfigurableListableBeanFactory beanFactory) {
String[] names = datasourceproperties.getNames();
if(names==null) {
return ;
}
for(int i=0;i<names.length;i++) {
String name = names[i];
SingleDataSourceProperties datasourceprop = datasourceproperties.getProperties().get(name);
String datasourcename = DATASOURCPREFIX+name;
String transactionname = TRANSACTIONPREFIX+name;
String sessionfactoryname = SQLSESSIONFACTORYPREFIX+name;
//datasource
initDataSource(datasourceprop,datasourcename,(DruidDataSource)beanFactory.getBean(datasourcename));
//transaction
BeanDefinition transactionbd = beanFactory.getBeanDefinition(transactionname);
MutablePropertyValues transactionbdpv = transactionbd.getPropertyValues();
transactionbdpv.addPropertyValue("dataSource", beanFactory.getBean(datasourcename));
//sqlsessionfactory
BeanDefinition bdd = beanFactory.getBeanDefinition(sessionfactoryname);
MutablePropertyValues mpvd = bdd.getPropertyValues();
mpvd.addPropertyValue("dataSource", beanFactory.getBean(datasourcename));
try {
mpvd.addPropertyValue("mapperLocations", new PathMatchingResourcePatternResolver().getResources(datasourceprop.getMapperxmlpath()));
} catch (IOException e) {
e.printStackTrace();
}
if(!StringUtils.isEmpty(datasourceprop.getConfiglocation())) {
mpvd.addPropertyValue("configLocation", new DefaultResourceLoader().getResource(datasourceprop.getConfiglocation()));
}
}
}
/**
* 初始化数据源
* @param properties
* @param datasourcename
* @param dataSource
*/
private void initDataSource(SingleDataSourceProperties properties,String datasourcename,DruidDataSource dataSource) {
dataSource.setName(datasourcename);
if (properties.getUrl() != null) {
dataSource.setUrl(properties.getUrl());
}
if (properties.getUsername() != null) {
dataSource.setUsername(properties.getUsername());
}
if (properties.getPassword() != null) {
dataSource.setPassword(properties.getPassword());
}
if (properties.getDriverClassName() != null) {
dataSource.setDriverClassName(properties.getDriverClassName());
}
if (properties.getInitialSize() != null) {
dataSource.setInitialSize(properties.getInitialSize());
}
if (properties.getMaxActive() != null) {
dataSource.setMaxActive(properties.getMaxActive());
}
if (properties.getMinIdle() != null) {
dataSource.setMinIdle(properties.getMinIdle());
}
if (properties.getMaxWait() != null) {
dataSource.setMaxWait(properties.getMaxWait());
}
if (properties.getPoolPreparedStatements() != null) {
dataSource.setPoolPreparedStatements(properties.getPoolPreparedStatements());
}
if (properties.getMaxOpenPreparedStatements() != null) {
dataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
}
if (properties.getMaxPoolPreparedStatementPerConnectionSize() != null) {
dataSource.setMaxPoolPreparedStatementPerConnectionSize(properties.getMaxPoolPreparedStatementPerConnectionSize());
}
if (properties.getValidationQuery() != null) {
dataSource.setValidationQuery(properties.getValidationQuery());
}
if (properties.getValidationQueryTimeout() != null) {
dataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
}
if (properties.getTestWhileIdle() != null) {
dataSource.setTestWhileIdle(properties.getTestWhileIdle());
}
if (properties.getTestOnBorrow() != null) {
dataSource.setTestOnBorrow(properties.getTestOnBorrow());
}
if (properties.getTestOnReturn() != null) {
dataSource.setTestOnReturn(properties.getTestOnReturn());
}
if (properties.getTimeBetweenEvictionRunsMillis() != null) {
dataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
}
if (properties.getMinEvictableIdleTimeMillis() != null) {
dataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
}
if (properties.getMaxEvictableIdleTimeMillis() != null) {
dataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
}
try {
if (properties.getFilters() != null) {
dataSource.setFilters(properties.getFilters());
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
数据源的名字默认为"dataSource_"+配置的数据源名字,如:dataSource_db1,dataSource_db2 默认的事务名称为"transAction_"+配置的数据源名字 到此为止我们的stater就写完了,下面我们来测试一下多数据源的支持和事务的支持。
测试
1)创建一个demo项目,并且导入我们的stater。
<!-- 自动配置多数据源starter -->
<dependency>
<groupId>com.multipledatasource</groupId>
<artifactId>multipledatasource-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
2)配置我们的配置文件
spring:
datasource:
multi:
#name的第一个为默认数据源
name: db1,db2
db1:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1
username: root
password: 123456
mapperxmlpath: classpath:mapper/db1/*.xml
mapperpath: com.example.demo.mapper.db1
configlocation: classpath:public/mybatis-configuration.xml
db2:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2
username: root
password: 123456
mapperxmlpath: classpath:mapper/db2/*.xml
mapperpath: com.example.demo.mapper.db2
configlocation: classpath:public/mybatis-configuration.xml
3)编写我们的controller和service,mapper和xml
Db1Service.java
@Component
public class Db1Service {
@Autowired
public UserMapper_db1 db1;
/**
* 查询demo
* @return
*/
public User selectuser() {
return db1.selectuser();
}
/**
* 添加demo
* @param user
* @return
*/
//默认事物可以不用写名称
//直接使用的是@Transactional
@Transactional
public Map<String,Object> insert(User user) {
db1.insert(user);
if("zhu".equals(user.getName())) {
throw new RuntimeException("回滚操作");
}
Map<String,Object> res = new HashMap<String,Object>();
res.put("status", 0);
res.put("message", "添加成功");
return res;
}
}
Db2Service.java
@Component
public class Db2Service {
@Autowired
public UserMapper_db2 db2;
/**
* 查询demo
* @return
*/
public User selectuser() {
return db2.selectuser();
}
/**
* 添加demo
* @param user
* @return
*/
//默认事物可以不用写名称
//直接使用的是@Transactional
@Transactional("transAction_db2")
public Map<String,Object> insert(User user) {
db2.insert(user);
if("zhu".equals(user.getName())) {
throw new RuntimeException("回滚操作");
}
Map<String,Object> res = new HashMap<String,Object>();
res.put("status", 0);
res.put("message", "添加成功");
return res;
}
}
注:默认数据源可以不用明确指出,其他数据源需要指出,如Db2Service中的@Transactional("transAction_db2")
UserMapper_db1.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.db1.UserMapper_db1">
<!-- 查询用户信息 -->
<select id="selectuser" resultType="com.example.demo.entity.User">
select * from usertable where id = '1111'
</select>
<insert id="insert" parameterType="com.example.demo.entity.User">
INSERT into usertable (id,name) values(#{id},#{name})
</insert>
</mapper>
UserMapper_db2.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.db2.UserMapper_db2">
<!-- 查询用户信息 -->
<select id="selectuser" resultType="com.example.demo.entity.User">
select * from usertable where id = '1111'
</select>
<insert id="insert" parameterType="com.example.demo.entity.User">
INSERT into usertable (id,name) values(#{id},#{name})
</insert>
</mapper>
最后看看我们的controller
@RestController
public class Contorller {
@Autowired
public Db1Service db1service;
@Autowired
public Db2Service db2service;
@RequestMapping("/hello1")
public User selectUser1() {
return db1service.selectuser();
}
@RequestMapping("/add1")
public Map<String,Object> add1(User user) {
return db1service.insert(user);
}
@RequestMapping("/hello2")
public User selectUser2() {
return db2service.selectuser();
}
@RequestMapping("/add2")
public Map<String,Object> add2(User user) {
return db2service.insert(user);
}
}
4)调用测试
db1查询:
db1正常添加:
db1事务回滚测试:
可以看到第二次的添加被回滚了,没有添加成功,db2也是一样的操作过程,这里就不在贴图了,至此多数据源的自动化配置以及完成。
另外如果有兴趣或者发现有问题的地方还请多多指正,代码已上传至码云
地址:[数据源自动化配置](https://gitee.com/qingyitianxia/multidatasource)