SpringBoot基于注解来动态切换数据源

LOVELETTERD·2023-05-17 16:58·1256 次阅读

SpringBoot基于注解来动态切换数据源

前言#

我们在日常开发中,经常会用到多数据源,实现的方式有很多种,我这里分享一种通过动态数据源的方式来实现多数据源。通过自定义一个注解DS加上AOP来动态切换数据源。我们的注解可以作用于类、方法、接口、接口方法上。优先级为:类方法>类>接口方法>接口

SpringBoot的动态数据源,实际上就是把多个数据源存储在一个Map中,当需要用到某个数据源的时候,从Map中取就好了,SpringBoot已经为我们提供了一个抽象类AbstractRoutingDataSource来实现这个功能。我们只需要继承它,重写它的determineCurrentLookupKey方法,并且把我们的map添加进去。

环境准备#

我们项目使用Druid作为数据库连接池,Mybatis为ORM框架,因为用到了AOP需要还需要引入相关依赖

Copy
<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个数据源为例

Copy
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

定义数据源标识常量#

我们这里定义两个字符串常量来作为这两个数据源的标识

Copy
object DataSourceConstants { const val DS_KEY_PRIMARY = "DS_KEY_PRIMARY" const val DS_KEY_SECOND = "DS_KEY_SECOND" }

动态数据源类#

SpringBoot为我们提供了一个抽象类AbstractRoutingDataSource来实现动态切换数据源,我们只需要重写determineCurrentLookupKey方法即可。

Copy
class DynamicDataSource: AbstractRoutingDataSource() { override fun determineCurrentLookupKey(): Any? { return DynamicDataSourceContextHolder.getContextKey() } }

DynamicDataSourceContextHolder#

数据源Key的上下文,通过setContextKeygetContextKey来设置获取当前需要的数据源

Copy
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

Copy
@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的形式来切换数据源,我们的注解可以设置在方法,类上面,优先级为:类方法>类>接口方法>接口。

Copy
@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是两个不同的概念,虽然它们都是用于实现横切关注点的处理,但是它们的作用和实现方式略有不同。

  1. Advisor是Spring AOP框架中的一个接口,用于表示一个切面或者一个通知。它可以被看作是通知和切点的组合体,其中通知用于定义需要在目标方法执行前后或者抛出异常时执行的逻辑,而切点用于定义哪些方法需要被通知。

Spring框架提供了多种类型的Advisor,包括BeforeAdvice、AfterAdvice、AroundAdvice等,每种Advisor类型都对应着不同的通知和切点组合。

  1. 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方法:清除动态数据源的上下文,以防止上下文的泄漏。

Copy
@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#

分别在两个数据库中创建一张相同的表

Copy
create table t_department ( id bigint auto_increment primary key, name varchar(255) null, location varchar(255) null );

在daily_db中插入如下数据

Copy
INSERT INTO daily_db.t_department (id, name, location) VALUES (1, '萝卜', '武汉');

在daily_db2中插入如下数据

Copy
INSERT INTO daily_db2.t_department (id, name, location) VALUES (1, '大壮', '苏州');

entity#

Copy
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#

Copy
@Mapper interface DepartmentMapper { fun selectById(id:Long):Department? fun insert(department: Department) fun list():List<Department> }
Copy
<?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#

Copy
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#

  1. 没有添加@DS注解查询
Copy
@SpringBootTest class DepartmentTest { @Resource private lateinit var service:IDepartmentService @Test fun test1() { println(service.selectByIdSecond(1)) println(service.selectByIdPrimary(1)) } }

没有添加@DS注解则查询默认的数据源也就是daily_db

  1. 只在selectByIdSecond方法上添加@DS(DataSourceConstants.DS_KEY_SECOND),可以看到只影响了selectByIdSecond方法。

  1. 只在DepartmentServiceImpl类上添加@DS(DataSourceConstants.DS_KEY_SECOND),可以看到影响了所有的方法。

  1. 只在IDepartmentService接口的selectByIdSecond方法上添加@DS(DataSourceConstants.DS_KEY_SECOND),可以看到只影响了selectByIdSecond方法

  1. 只在IDepartmentService上添加@DS(DataSourceConstants.DS_KEY_SECOND),可以看到影响了实现类的所有方法。

  1. DepartmentServiceImpl类上添加@DS(DataSourceConstants.DS_KEY_SECOND),在selectByIdPrimary上添加@DS,可以看到selectByIdPrimary上的注解优先级高于类上的,所以还是查询的daily_db库

  1. DepartmentServiceImpl类上添加@DS(DataSourceConstants.DS_KEY_SECOND),在IDepartmentService接口的selectByIdPrimary上添加@DS,可以看到实现类上的注解优先级高于接口方法上的,所以方法上的注解没有生效。

  1. IDepartmentService上添加注解@DS(DataSourceConstants.DS_KEY_SECOND),在它的selectByIdPrimary上添加@DS注解,可以看到方法上的注解优先级高于接口上的

结束#

到这里我们已经开发完成了功能,经过测试也满足我们的需求,可以作用于方法、类、接口、接口方法上,并且优先级也满足我们的需求。动态数据源的主实现是靠SpringBoot为我们提供的一个抽象类AbstractRoutingDataSource来完成的,而其中AOP的实现,我们是通过Advisor来实现的,这个需要注意一下。

posted @   loveletters  阅读(1273)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
目录