前言
我们在日常开发中,经常会用到多数据源,实现的方式有很多种,我这里分享一种通过动态数据源的方式来实现多数据源。通过自定义一个注解DS
加上AOP来动态切换数据源。我们的注解可以作用于类、方法、接口、接口方法上。优先级为:类方法>类>接口方法>接口
SpringBoot的动态数据源,实际上就是把多个数据源存储在一个Map中,当需要用到某个数据源的时候,从Map中取就好了,SpringBoot已经为我们提供了一个抽象类AbstractRoutingDataSource
来实现这个功能。我们只需要继承它,重写它的determineCurrentLookupKey
方法,并且把我们的map添加进去。
环境准备
我们项目使用Druid作为数据库连接池,Mybatis为ORM框架,因为用到了AOP需要还需要引入相关依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
实现动态数据源
包结构如下
配置文件
我们想要实现多数据源,必然需要把数据库的相关配置信息给准备好。我这里以2个数据源为例
spring:
datasource:
primary:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/daily_db?useSSL=false&serverTimezone=UTC
second:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/daily_db2?useSSL=false&serverTimezone=UTC
定义数据源标识常量
我们这里定义两个字符串常量来作为这两个数据源的标识
object DataSourceConstants {
const val DS_KEY_PRIMARY = "DS_KEY_PRIMARY"
const val DS_KEY_SECOND = "DS_KEY_SECOND"
}
动态数据源类
SpringBoot为我们提供了一个抽象类AbstractRoutingDataSource
来实现动态切换数据源,我们只需要重写determineCurrentLookupKey
方法即可。
class DynamicDataSource: AbstractRoutingDataSource() {
override fun determineCurrentLookupKey(): Any? {
return DynamicDataSourceContextHolder.getContextKey()
}
}
DynamicDataSourceContextHolder
数据源Key的上下文,通过setContextKey
跟getContextKey
来设置获取当前需要的数据源
object DynamicDataSourceContextHolder {
private val contextHolder = ThreadLocal<String>()
fun setContextKey(key:String){
contextHolder.set(key)
}
fun getContextKey():String?{
return contextHolder.get()
}
fun clear(){
contextHolder.remove()
}
}
配置数据源
我们先通过@ConfigurationProperties
来读取我们application.yml
里面的配置文件,然后我们注册一个dataSource
的bean到容器中,将我们定义的两个数据源,配置到DynamicDataSource
,并且设置默认的数据源为DataSourceConstants.DS_KEY_PRIMARY
这个,后面三个为Mybatis相关的配置。由于我们的动态数据源是返回dataSource
,所有可以应用于各种使用了DataSource
的ORM框架,Mybatis、JDBCTemplate、JPA等等。我们通过DataSource
来获取connection
的时候,会通过determineCurrentLookupKey
来获取key然后在我们配置的map中通过key来获取与之相对应的DataSource
。
@Configuration
class DynamicDataSourceConfig {
@Bean(name = [DataSourceConstants.DS_KEY_PRIMARY])
@ConfigurationProperties("spring.datasource.primary")
fun masterDataSource():DataSource{
return DruidDataSourceBuilder.create().build()
}
@Bean(name = [DataSourceConstants.DS_KEY_SECOND])
@ConfigurationProperties("spring.datasource.second")
fun secondDataSource():DataSource{
return DruidDataSourceBuilder.create().build()
}
@Bean(name = ["dataSource"])
fun dynamicDataSource(@Qualifier(DataSourceConstants.DS_KEY_PRIMARY) ds1:DataSource, @Qualifier(DataSourceConstants.DS_KEY_SECOND) ds2:DataSource):DataSource{
val map:Map<Any,Any> = mapOf(DataSourceConstants.DS_KEY_PRIMARY to ds1, DataSourceConstants.DS_KEY_SECOND to ds2)
val dynamicDataSource = DynamicDataSource()
dynamicDataSource.setTargetDataSources(map)
dynamicDataSource.setDefaultTargetDataSource(ds1)
return dynamicDataSource
}
@Bean(name = ["sqlSessionFactory"])
fun sqlSessionFactory(@Qualifier("dataSource") dataSource: DataSource):SqlSessionFactory?{
val bean = SqlSessionFactoryBean().apply {
setDataSource(dataSource)
setMapperLocations(*PathMatchingResourcePatternResolver().getResources("classpath:mappers/*.xml"))
setTypeAliasesPackage("org.loveletters.entity")
}
return bean.`object`
}
@Bean(name = ["transactionManager"])
fun transactionManager(@Qualifier("dataSource") dataSource: DataSource):DataSourceTransactionManager{
return DataSourceTransactionManager(dataSource)
}
@Bean(name = ["sqlSessionTemplate"])
fun sqlSessionTemplate(@Qualifier("sqlSessionFactory") sqlSessionFactory: SqlSessionFactory):SqlSessionTemplate{
return SqlSessionTemplate(sqlSessionFactory)
}
}
定义注解
我的目的是通过注解+AOP的形式来切换数据源,我们的注解可以设置在方法,类上面,优先级为:类方法>类>接口方法>接口。
@Target(AnnotationTarget.CLASS,AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Inherited
annotation class DS(
val value:String= DataSourceConstants.DS_KEY_PRIMARY
)
AOP切面
SpringAOP中如果通过Aspect
的方式来实现切面是没有办法对接口跟接口中的方法进行拦截的,只能对接口的实现类、以及实现类中的方法进行拦截,所以我们需要用到Advisor
来实现切面。
在Spring AOP中,Advisor和Aspect是两个不同的概念,虽然它们都是用于实现横切关注点的处理,但是它们的作用和实现方式略有不同。
- Advisor是Spring AOP框架中的一个接口,用于表示一个切面或者一个通知。它可以被看作是通知和切点的组合体,其中通知用于定义需要在目标方法执行前后或者抛出异常时执行的逻辑,而切点用于定义哪些方法需要被通知。
Spring框架提供了多种类型的Advisor,包括BeforeAdvice、AfterAdvice、AroundAdvice等,每种Advisor类型都对应着不同的通知和切点组合。
- Aspect是一个横切关注点的模块化封装,它是由切点和通知组成的。在Spring AOP中,切面类使用@Aspect注解进行标注,其中的通知方法则使用@Before、@After、@Around、@AfterReturning和@AfterThrowing等注解进行标注,用于定义在目标方法执行前、执行后、环绕执行、正常返回时和抛出异常时需要执行的逻辑。
Aspect提供了一种更加灵活的方式来定义横切关注点,可以将不同类型的通知组合在一起,形成一个切面,从而实现对目标方法的统一处理。
因此,Advisor和Aspect在实现横切关注点时的方式略有不同,Advisor是通过将通知和切点组合在一起实现的,而Aspect则是通过将不同类型的通知组合在一起形成一个切面实现的。
简单的解释一下这个代码:
-
pointCut1 是一个 AnnotationMatchingPointcut,它匹配带有 @DS 注解的方法。这里的第一个参数 DS::class.java 表示要匹配的注解类型,第二个参数默认为 false,表示只匹配方法上的注解,不包括类上的注解。
-
pointCut2 是另一个 AnnotationMatchingPointcut,它与 pointCut1 相同,但第二个参数设置为 true。这意味着它将匹配带有 @DS 注解的方法以及类上标记了 @DS 注解的方法。
-
pointCut3 是一个 AnnotationMatchingPointcut,它没有指定类类型,只指定了注解类型为 DS::class.java。这意味着它将匹配类上标记了 @DS 注解的方法。
-
pointCut4 是另一个 AnnotationMatchingPointcut,与 pointCut3 相同,但第二个参数设置为 true。这意味着它将匹配类上标记了 @DS 注解的方法以及接口方法上标记了 @DS 注解的方法。
接下来,这些切点通过 union() 方法进行组合,形成一个复合切点 pointCut。ComposablePointcut 提供了 union() 方法,用于将多个切点组合成一个逻辑上的或操作。
这样,pointCut 将匹配带有 @DS 注解的方法,无论是在方法上还是在类或接口上
before方法:通过反射获取方法上的 @DS 注解,并根据注解的值设置动态数据源的上下文。首先,尝试获取方法上的注解,如果没有,则尝试获取声明方法的类上的注解。如果仍然没有找到注解,则通过接口方法的方式查找注解。
afterReturning方法:清除动态数据源的上下文,以防止上下文的泄漏。
@Component
class DataSourceAspect {
@Bean
fun dataSourceAdvisor(): Advisor {
val pointCut1 = AnnotationMatchingPointcut(DS::class.java)
val pointCut2 = AnnotationMatchingPointcut(DS::class.java,true)
val pointCut3 = AnnotationMatchingPointcut(null,DS::class.java)
val pointCut4 = AnnotationMatchingPointcut(null,DS::class.java,true)
val pointCut = ComposablePointcut(pointCut1).union(pointCut2).union(pointCut3).union(pointCut4)
return DefaultPointcutAdvisor(pointCut, MethodAround())
}
class MethodAround : MethodBeforeAdvice ,AfterReturningAdvice{
override fun before(method: Method, args: Array<out Any>, target: Any?) {
var annotation: DS?
annotation = method.getAnnotation(DS::class.java)
if (annotation === null) {
annotation = method.declaringClass.getDeclaredAnnotation(DS::class.java)
}
if (annotation === null){
val declaringInterface = findDeclaringInterface(method)
val interfaceMethod = findInterfaceMethod(declaringInterface, method)
annotation = interfaceMethod?.getAnnotation(DS::class.java)
}
if (annotation===null){
val interfaces = method.declaringClass.interfaces
for (clazz in interfaces) {
if (clazz.getAnnotation(DS::class.java)!==null){
annotation = clazz.getAnnotation(DS::class.java)
}
}
}
val value = annotation!!.value
DynamicDataSourceContextHolder.setContextKey(value)
}
override fun afterReturning(returnValue: Any?, method: Method, args: Array<out Any>, target: Any?) {
DynamicDataSourceContextHolder.clear()
}
private fun findDeclaringInterface(method: Method): Class<*>? {
val declaringClass = method.declaringClass
for (interfaceType in declaringClass.interfaces) {
try {
interfaceType.getDeclaredMethod(method.name, *method.parameterTypes)
return interfaceType
} catch (ex: NoSuchMethodException) {
// Ignore and continue searching
}
}
return null
}
private fun findInterfaceMethod(interfaceType: Class<*>?, method: Method): Method? {
return interfaceType?.getDeclaredMethod(method.name, *method.parameterTypes)
}
}
}
自此我们动态数据源功能就已经开发完成了,我们简单测试一下功能。
测试
table
分别在两个数据库中创建一张相同的表
create table t_department
(
id bigint auto_increment
primary key,
name varchar(255) null,
location varchar(255) null
);
在daily_db中插入如下数据
INSERT INTO daily_db.t_department (id, name, location) VALUES (1, '萝卜', '武汉');
在daily_db2中插入如下数据
INSERT INTO daily_db2.t_department (id, name, location) VALUES (1, '大壮', '苏州');
entity
class Department {
var id:Long? = null
var name:String? = null
var location:String? = null
override fun toString(): String {
return "Department(id=$id, name=$name, location=$location)"
}
}
mapper
@Mapper
interface DepartmentMapper {
fun selectById(id:Long):Department?
fun insert(department: Department)
fun list():List<Department>
}
<?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="org.loveletters.mapper.DepartmentMapper">
<select id="selectById" parameterType="long" resultType="org.loveletters.entity.Department">
select * from t_department where id = #{id}
</select>
<insert id="insert" parameterType="org.loveletters.entity.Department">
insert into t_department(name,location) values (#{name},#{location})
</insert>
<select id="list" resultType="org.loveletters.entity.Department">
select * from t_department
</select>
</mapper>
service
interface IDepartmentService {
fun selectByIdPrimary(id:Long):Department?
fun selectByIdSecond(id:Long):Department?
fun insertPrimary(department: Department)
fun insertSecond(department: Department)
fun list():List<Department>
}
@Service
class DepartmentServiceImpl(
val departmentMapper: DepartmentMapper
):IDepartmentService {
override fun selectByIdPrimary(id: Long): Department? {
return departmentMapper.selectById(id)
}
override fun selectByIdSecond(id: Long): Department? {
return departmentMapper.selectById(id)
}
@Transactional
override fun insertPrimary(department: Department) {
departmentMapper.insert(department)
}
@Transactional
override fun insertSecond(department: Department) {
departmentMapper.insert(department)
}
override fun list(): List<Department> {
return departmentMapper.list()
}
}
Test Case
- 没有添加
@DS
注解查询
@SpringBootTest
class DepartmentTest {
@Resource
private lateinit var service:IDepartmentService
@Test
fun test1() {
println(service.selectByIdSecond(1))
println(service.selectByIdPrimary(1))
}
}
没有添加@DS
注解则查询默认的数据源也就是daily_db
- 只在
selectByIdSecond
方法上添加@DS(DataSourceConstants.DS_KEY_SECOND)
,可以看到只影响了selectByIdSecond
方法。
- 只在
DepartmentServiceImpl
类上添加@DS(DataSourceConstants.DS_KEY_SECOND)
,可以看到影响了所有的方法。
- 只在
IDepartmentService
接口的selectByIdSecond
方法上添加@DS(DataSourceConstants.DS_KEY_SECOND)
,可以看到只影响了selectByIdSecond
方法
- 只在
IDepartmentService
上添加@DS(DataSourceConstants.DS_KEY_SECOND)
,可以看到影响了实现类的所有方法。
- 在
DepartmentServiceImpl
类上添加@DS(DataSourceConstants.DS_KEY_SECOND)
,在selectByIdPrimary
上添加@DS
,可以看到selectByIdPrimary
上的注解优先级高于类上的,所以还是查询的daily_db库
- 在
DepartmentServiceImpl
类上添加@DS(DataSourceConstants.DS_KEY_SECOND)
,在IDepartmentService
接口的selectByIdPrimary
上添加@DS
,可以看到实现类上的注解优先级高于接口方法上的,所以方法上的注解没有生效。
- 在
IDepartmentService
上添加注解@DS(DataSourceConstants.DS_KEY_SECOND)
,在它的selectByIdPrimary
上添加@DS
注解,可以看到方法上的注解优先级高于接口上的
结束
到这里我们已经开发完成了功能,经过测试也满足我们的需求,可以作用于方法、类、接口、接口方法上,并且优先级也满足我们的需求。动态数据源的主实现是靠SpringBoot为我们提供的一个抽象类AbstractRoutingDataSource
来完成的,而其中AOP的实现,我们是通过Advisor
来实现的,这个需要注意一下。