目录
前言
前置环境
pom
yml
Entity
Dao
枚举类
数据源
AOP
Controller
启动类
演示
前言
大多数系统中,都需要数据库来持久化数据,在大多数情况下,一个系统只需要配置一个数据源便能够完成所有业务的查询,保存操作。也存在一个系统需要多个数据源的情况,不同的数据源对应不同的业务操作,这种场景下配置多个数据源,并且在代码中维护多套dao层就可以了。
还存在一种业务场景,所有的业务操作都是一样的,只有操作的数据源的不同,如果用多套dao层来实现,由于业务操作都一样,会出现多块一模一样的代码,这样的冗余代码是我们不希望看到,不利于维护。这种业务场景就很适合用动态数据源来实现。
可以使用 AbstractRoutingDataSource + ThreadLocal + AOP 来实现动态数据源切换
前置环境
JDK8 + SringBoot2 + MySQL8
分别创建数据库 test1 test2
分别在两个数据库中创建 user 表
create table user (
id int auto_increment primary key,
username varchar(255),
password varchar(255)
);
在test1.user 表中插入数据
insert into user(username, password) values('张三', '123456');
在test2.user 表中插入数据
insert into user(username, password) values('李四', '123456');
pom
<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependencies>
yml
server:port: 8888spring:datasource:primary:jdbc-url: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driversecondary:jdbc-url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjpa:show-sql: trueproperties:hibernate:hbm2ddl:auto: updatedialect: org.hibernate.dialect.MySQL5InnoDBDialect
Entity
UserEntity
@Entity
@Table ( name = "user")
public class UserEntity {private Integer id;private String username;private String password;@Id@Column ( name = "id" )public int getId() {return id;}public void setId(Integer id) {this.id = id;}@Basic@Column ( name = "username" )public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}@Basic@Column ( name = "password" )public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
Dao
IUserDao
@Repository (value = IUserDao.DAO_BEAN_NAME )
public interface IUserDao extends JpaRepository<UserEntity, Long> {String DAO_BEAN_NAME = "userDao";
}
枚举类
枚举动态数据源,提高代码可读性
DataSourceEnums
public enum DataSourceEnums {PRIMARY,SECONDARY;static {set = Arrays.stream(values()).map(e -> e.name()).collect(Collectors.toSet());}private static Set<String> set;public static boolean isValid(String dataSource) {return set.contains(dataSource);}
}
数据源
线程切换数据源上下文,每个请求线程都维护一个自己的当前数据源变量
DynamicDataSourceContenxtHolder
public class DynamicDataSourceContextHolder {/*** 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();/*** 设置数据源变量* @param dataSourceType*/public static void setDataSourceType(String dataSourceType){System.out.printf("切换到{%s}数据源", dataSourceType);CONTEXT_HOLDER.set(dataSourceType);}/*** 获取数据源变量* @return*/public static String getDataSourceType(){return CONTEXT_HOLDER.get();}/*** 清空数据源变量*/public static void clearDataSourceType(){CONTEXT_HOLDER.remove();}
}
DynamicDataSource
public class DynamicDataSource extends AbstractRoutingDataSource {public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);// afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的super.afterPropertiesSet();}/*** 根据Key获取数据源的信息** @return*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSourceType();}
}
DynamicDatasourceConfig
记得将类中的两个包路径修改成自己项目的包路径
REPOSITORY_PACKAGE
ENTITY_PACKAGE
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories (basePackages = DynamicDatasourceConfig.REPOSITORY_PACKAGE,entityManagerFactoryRef = "dynamicEntityManagerFactory",transactionManagerRef = "dynamicTransactionManager"
)
public class DynamicDatasourceConfig {//--------------数据源配置-------------------@Bean(name="primary")@ConfigurationProperties(prefix = "spring.datasource.primary")public DataSource primaryDataSource() {return DataSourceBuilder.create().build();}@Bean(name="secondary")@ConfigurationProperties(prefix = "spring.datasource.secondary")public DataSource secondaryDataSource() {return DataSourceBuilder.create().build();}@Primary@Bean(name = "dynamicDataSource")public DynamicDataSource dataSource(@Qualifier ("primary") DataSource primaryDataSource, @Qualifier ( "secondary" )DataSource secondaryDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DataSourceEnums.PRIMARY.name(), primaryDataSource);targetDataSources.put(DataSourceEnums.SECONDARY.name(), secondaryDataSource);return new DynamicDataSource(primaryDataSource, targetDataSources);}/*** 该方法仅在需要使用JdbcTemplate对象时选用** @param dataSource 注入名为dynamicDataSource的bean* @return 数据源JdbcTemplate对象*/@Bean(name = "dynamicJdbcTemplate")public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {return new JdbcTemplate(dataSource);}//-------------jpa配置---------------static final String REPOSITORY_PACKAGE = "com.your.dao";private static final String ENTITY_PACKAGE = "com.your.entity";/*** 扫描spring.jpa.dynamic开头的配置信息** @return jpa配置信息*/@Primary@Bean (name = "dynamicJpaProperties")@ConfigurationProperties (prefix = "spring.jpa")public JpaProperties jpaProperties() {return new JpaProperties();}/*** 获取主库实体管理工厂对象** @param dynamicDataSource 注入名为dynamicDataSource的数据源* @param jpaProperties 注入名为dynamicJpaProperties的jpa配置信息* @param builder 注入EntityManagerFactoryBuilder* @return 实体管理工厂对象*/@Primary@Bean(name = "dynamicEntityManagerFactory")public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(@Qualifier ("dynamicDataSource") DataSource dynamicDataSource,@Qualifier("dynamicJpaProperties") JpaProperties jpaProperties,EntityManagerFactoryBuilder builder) {return builder// 设置数据源.dataSource(dynamicDataSource)// 设置jpa配置.properties(jpaProperties.getProperties())// 设置实体包名.packages(ENTITY_PACKAGE)// 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源.persistenceUnit("dynamicPersistenceUnit").build();}/*** 获取实体管理对象** @param factory 注入名为dynamicEntityManagerFactory的bean* @return 实体管理对象*/@Primary@Bean(name = "dynamicEntityManager")public EntityManager entityManager(@Qualifier("dynamicEntityManagerFactory") EntityManagerFactory factory) {return factory.createEntityManager();}/*** 获取主库事务管理对象** @param factory 注入名为dynamicEntityManagerFactory的bean* @return 事务管理对象*/@Primary@Bean(name = "dynamicTransactionManager")public JpaTransactionManager transactionManager(@Qualifier("dynamicEntityManagerFactory") EntityManagerFactory factory) {return new JpaTransactionManager(factory);}}
AOP
对需要动态切换数据源的请求做切面,编写切换数据源逻辑
记得将切点表达式换成自己项目的路径
@Pointcut ("execution(* com.your.controller.DynamicController.*(..))")
DataSourceAspect
@Aspect
@Component
public class DataSourceAspect {@Pointcut ("execution(* com.your.controller.DynamicController.*(..))")public void dsPointCut() {}@Around ("dsPointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {RequestAttributes ra = RequestContextHolder.getRequestAttributes();ServletRequestAttributes sra = (ServletRequestAttributes) ra;HttpServletRequest request = sra.getRequest();String dataSource = request.getParameter("database");if (dataSource != null && DataSourceEnums.isValid(dataSource)) {DynamicDataSourceContextHolder.setDataSourceType(dataSource);}try {return point.proceed();} finally {// 销毁数据源 在执行方法之后DynamicDataSourceContextHolder.clearDataSourceType();}}
}
Controller
DynamicController
@RestController
@RequestMapping(value = "/test")
public class DynamicController {@ResourceIUserDao userDao;@GetMapping(value = "/findAll")public List<UserEntity> findAll() {return userDao.findAll();}
}
启动类
由于使用的是自定义的数据源配置,在启动时需要将Spring Boot 中扫描默认数据源的配置类排除掉,注解如下所示:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
public class DynamicApplication {public static void main(String[] args) {SpringApplication.run(DynamicApplication.class, args);}
}
演示
请求 /test/findAll 或者 /test/findAll?database=PRIMARY
不传默认是 PRIMARY 数据库
请求 /test/findAll?database=SECONDARY