配置多数据源有多种方式,这里使用的是AOP动态代理的方式进行动态切换的。所谓的动态数据源就是,多个数据源,可以进行切换,比如读写分离,读是一个数据源,写又是一个数据源。不过使用这种方式配置的动态数据源不适合动态扩展,当需要新增数据源的时候,只能重启服务进行重新配置,当然也可以使用nacos配置中新做动态刷新,我只是想到可以怎么做,但是具体没有做过。
动态数据源最核心的就是AbstractRoutingDataSource 这个抽象类,我们只需要继承这个抽象类,然后重写 determineCurrentLookupKey()方法给他返回一个数据源名称就可以了,到时候他会根据你返回的数据源名称去resolvedDataSources这个map集合中找,resolvedDataSources这个map集合的key就是数据源名称,value对应的就是具体的数据库连接。
@Nullable
protected abstract Object determineCurrentLookupKey();
这个是父类的方法,我们在子类进行重写。到时候父类会调用determineTargetDataSource()方法确定目标数据源,然后就调用到子类的实现,这也是人家留给我们的一个扩展,让我能动态的确定数据源。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.0</version>
</dependency>
</dependencies>
spring:
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/db1?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
username: root
password: root
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://122.132.222.110:3306/db2?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=GMT%2B8
username: root
password: root
mybatis:
type-aliases-package: com.compass.mapper
mapper-locations: classpath:mapper/*.xml
server:
port: 8080
1.定义标识使用那个数据源的注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value();
}
2.数据源配置类
import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
@MapperScan(basePackages = {"com.compass.mapper"})
public class DataSourceConfigurer {
/**
* master DataSource
*
* @return data source
* @Primary 注解用于标识默认使用的 DataSource Bean,因为有多个 DataSource Bean,该注解可用于 master
* 或 slave DataSource Bean, 但不能用于 dynamicDataSource Bean, 否则会产生循环调用
* @ConfigurationProperties 注解用于从 application.properties 或 application.yml 文件中读取配置,为 Bean 设置属性
*/
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource master() {
return new DruidDataSource();
}
/**
* slave DataSource * * @return data source
*/
@Bean("slave")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slave() {
return new DruidDataSource();
}
/**
* Dynamic data source. * * @return the data source
*/
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put("master", master());
dataSourceMap.put("slave", slave());
// 将 master 数据源作为默认指定的数据源
dynamicRoutingDataSource.setDefaultTargetDataSource(master());
// 将 master 和 slave 数据源作为指定的数据源
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
// 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
return dynamicRoutingDataSource;
}
/**
* 配置 SqlSessionFactoryBean
*
* @return the sql session factory bean
* @ConfigurationProperties 在这里是为了将 MyBatis 的 mapper 位置和持久层接口的别名设置到
* Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
*
* @ConfigurationProperties(prefix = "mybatis") 不写,Reason: No converter found capable of converting from type [java.lang.String] to type [@org.springframework.boot.context.properties.ConfigurationProperties org.mybatis.spring.SqlSessionFactoryBean]
* Github 资料表明与 SpringBoot 2.0 有关
*/
/**
* 方式1:
* @return
* @throws Exception
*/
// @Bean("sqlSessionFactoryBean")
public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 配置分页插件
setPagePlugins(sqlSessionFactoryBean);
// 配置mapper映射路径
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
return sqlSessionFactoryBean;
}
/**
* 方式2:
* @return
* @throws Exception
*/
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 配置分页插件
setPagePlugins(sqlSessionFactoryBean);
// 配置mapper映射路径
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactoryBean.setConfiguration(configuration);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
/**
* 分页插件
* @param sqlSessionFactoryBean
*/
public void setPagePlugins(SqlSessionFactoryBean sqlSessionFactoryBean){
Properties properties = new Properties();
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("pageSizeZero", "true");
PageInterceptor interceptor = new PageInterceptor();
interceptor.setProperties(properties);
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
}
/**
* 配置事务管理,如果使用到事务需要注入该 Bean,否则事务不会生效
* 在需要的地方加上 @Transactional 注解即可
*
* @return the platform transaction manager
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
3.控制动态数据源的AOP切面
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
// 该切面应当先于 @Transactional 执行
@Order(-1)
@Component
public class DynamicDataSourceAspect {
/** * Switch DataSource * * @param point * @param targetDataSource */
// @annotation:用来拦截所有被某个注解修饰的方法
@Before("@annotation(targetDataSource))")
public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) {
if (!DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value())) {
System.out.println("DataSource [{}] doesn't exist, use default DataSource [{}] " + targetDataSource.value());
} else {
// 切换数据源
DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value());
System.out.println("Switch DataSource to [{}] in Method [{}] " +
DynamicDataSourceContextHolder.getDataSourceKey() + point.getSignature());
}
}
/** * Restore DataSource * * @param point * @param targetDataSource */
@After("@annotation(targetDataSource))")
public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) {
// 将数据源置为默认数据源
DynamicDataSourceContextHolder.clearDataSourceKey();
System.out.println("Restore DataSource to [{}] in Method [{}] " +
DynamicDataSourceContextHolder.getDataSourceKey() + point.getSignature());
}
}
4.动态数据源的封装
import java.util.ArrayList;
import java.util.List;
public class DynamicDataSourceContextHolder {
/** 为每个线程维护变量,以避免影响其他线程 */
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/** 将 master 数据源的 key 作为默认数据源的 key */
@Override
protected String initialValue() {
return "master";
}
};
/** * 数据源的 key 集合,用于切换时判断数据源是否存在 */
public static List<Object> dataSourceKeys = new ArrayList<>();
/** *要切换 DataSource @param key 键 */
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/** * 获取当前DataSource @return 数据源key */
public static String getDataSourceKey() {
return contextHolder.get();
}
/** * To set DataSource as default */
public static void clearDataSourceKey() {
contextHolder.remove();
}
/** * 检查给定数据源是否在当前数据源列表中 @param key @return boolean */
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
}
5.继承AbstractRoutingDataSource返回确定的数据源
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
基本上配置就完毕了。
大致思路,不是很详细,大家可以自行debug
以上准备工作做好,我们就可以进行测试业务代码了。
public class User implements Serializable {
private int id;
private String name;
private int age;
private String email;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public User() {
}
public User(int id, String name, int age, String email) {
this.id = id;
this.name = name;
this.age = age;
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", email='" + email + '\'' +
'}';
}
}
mapper接口
@Repository
@Mapper
public interface UserMapper {
User findById(@Param("id") String id);
Integer addUser(User user);
}
mapper接口的实现(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.compass.mapper.UserMapper">
<insert id="addUser" >
insert into t_user(name, age, email) values (#{name},#{age},#{email});
</insert>
<select id="findById" resultType="com.compass.pojo.User">
select * from t_user where id=#{id};
</select>
</mapper>
service接口
public interface UserService {
User findById(@Param("id") String id);
Integer addUser(User user);
}
service实现
import com.compass.dds.TargetDataSource;
import com.compass.mapper.UserMapper;
import com.compass.pojo.User;
import com.compass.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@TargetDataSource(value = "master")
@Override
public User findById(String id) {
return userMapper.findById(id);
}
@TargetDataSource(value = "slave")
@Override
public Integer addUser(User user) {
return userMapper.addUser(user);
}
}
import com.compass.pojo.User;
import com.compass.service.UserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
public class HelloController {
@Resource
UserService userService;
@GetMapping("/getUserById/{id}")
public String getUserById(@PathVariable String id){
return userService.findById(id).toString();
}
@GetMapping("/addUser")
public String addUser(){
User user = new User(0,"李四", 25, "[email protected]");
Integer insertCount = userService.addUser(user);
return "插入的记录数:"+insertCount;
}
}
我为了方便,没有使用post提交参数,直接用2个get请求也是一样的,两个请求都是不同的数据源,一个数据源是查询,一个数据源是读取。
主启动类就跟正常的启动类一样,这里我就不贴出来了
CREATE TABLE `t_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
# 可以放在不同的数据库中,一个读库,一个写库
到这儿springBoot的多数据源配置句完毕了,感谢欣赏,可以配合MySQL主从复制也可以配合别的一些数据同步工具,来达到读写分离的目的,在下一篇文章中,我将给大家讲解,如果使用MySQL主从复制+多数据源完成读写分离。