背景
最近项目要做读写分离,然后看了下没什么好的开源框架,开源框架因为要兼顾太多的东西,所以显的很笨重,不够轻量,而我的目的很简单,就是提供一个注解,动态的切换数据源。所以看了下https://github.com/baomidou/dynamic-datasource-spring-boot-starter源码自己手撸了一个简易版本,项目无需引入任何多余三方依赖,很轻量
定义数据源常量名
/**
* @author : wh
* @date : 2021/12/13 13:48
* @description: 配置常量
*/
public interface ConfigConstant {
/**
* 只读库
*/
String READ = "read";
/**
* 读写库
*/
String WRITE = "write";
}
定义注解
WHDS
- 数据源切换基注解
/**
* @author : wh
* @date : 2021/12/13 14:25
* @description:
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WHDS {
String value();
}
WHRead
- 切换只读数据源注解
/**
* @author : wh
* @date : 2021/12/13 14:25
* @description:
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@WHDS(ConfigConstant.READ)
public @interface WHRead {
}
WHWrite
- 读写数据源
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@WHDS(ConfigConstant.WRITE)
public @interface WHWrite {
}
设置数据源切换工具类
/**
* 核心基于ThreadLocal的切换数据源工具类
* @author : wh
* @date : 2021/12/13 13:55
* @description:
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = ThreadLocal.withInitial(String::new);
/**
* 设置数据源
* @param dbType
*/
public static void setDbType(String dbType) {
contextHolder.set(dbType);
}
/**
* 获取数据源
* @return
*/
public static String getDbType() {
return contextHolder.get();
}
/**
* 清除上下文数据
*/
public static void clearDbType() {
contextHolder.remove();
}
}
基于抽象类DynamicRoutingDataSource选择数据源
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDbType();
}
}
基于AOP拦截注解设置数据源(基于MethodInterceptor接口)
这里我们使用的AOP切面方式不是传统的@Aspect 而是基于MethodInterceptor接口,使用
MethodInterceptor更方便更灵活
- DynamicDataSourceAnnotationInterceptor
/**
* @author : wh
* @date : 2021/12/13 14:20
* @description:
*/
@Slf4j
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String dsKey = determineDatasourceKey(invocation);
if (log.isDebugEnabled()) {
log.debug("当前数据源 {}", dsKey);
}
DynamicDataSourceContextHolder.setDbType(dsKey);
try {
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.clearDbType();
}
}
private String determineDatasourceKey(MethodInvocation invocation) {
return findDataSourceAttribute(invocation.getMethod());
}
/**
* 获取接口上面的注解
* @param ae
* @return
*/
private String findDataSourceAttribute(AnnotatedElement ae) {
AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, WHDS.class);
if (attributes != null) {
return attributes.getString("value");
}
return null;
}
}
- DynamicDataSourceAnnotationAdvisor
/**
* @author : wh
* @date : 2021/12/13 16:25
* @description:
*/
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private final Advice advice;
private final Pointcut pointcut;
private final Class<? extends Annotation> annotation;
public DynamicDataSourceAnnotationAdvisor(@NonNull MethodInterceptor advice,
@NonNull Class<? extends Annotation> annotation) {
this.advice = advice;
this.annotation = annotation;
this.pointcut = buildPointcut();
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.advice;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
}
private Pointcut buildPointcut() {
Pointcut cpc = new AnnotationMatchingPointcut(annotation, true);
Pointcut mpc = new AnnotationMethodPoint(annotation);
return new ComposablePointcut(cpc).union(mpc);
}
/**
* In order to be compatible with the spring lower than 5.0
*/
private static class AnnotationMethodPoint implements Pointcut {
private final Class<? extends Annotation> annotationType;
public AnnotationMethodPoint(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
this.annotationType = annotationType;
}
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new AnnotationMethodMatcher(annotationType);
}
private static class AnnotationMethodMatcher extends StaticMethodMatcher {
private final Class<? extends Annotation> annotationType;
public AnnotationMethodMatcher(Class<? extends Annotation> annotationType) {
this.annotationType = annotationType;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
if (matchesMethod(method)) {
return true;
}
// Proxy classes never have annotations on their redeclared methods.
if (Proxy.isProxyClass(targetClass)) {
return false;
}
// The method may be on an interface, so let's check on the target class as well.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
return (specificMethod != method && matchesMethod(specificMethod));
}
private boolean matchesMethod(Method method) {
return AnnotatedElementUtils.hasAnnotation(method, this.annotationType);
}
}
}
}
- DynamicDataSourceAutoConfiguration
/**
* @author : wh
* @date : 2021/12/13 16:33
* @description:
*/
@Configuration
public class DynamicDataSourceAutoConfiguration {
@Bean
public Advisor dynamicDatasourceAnnotationAdvisor() {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor();
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, WHDS.class);
advisor.setOrder(Integer.MIN_VALUE);
return advisor;
}
}
至此数据源切换就完成了,接下来我们再配置写一下多数据
配置MybatisPlus
- MybatisPlusConfig
@Configuration
@MapperScan("com.weihubeats.*.mapper")
public class MybatisPlusConfig {
/**
* 动态数据源配置
* @param write
* @param read
* @return
*/
@Bean
public DataSource multipleDataSource(@Qualifier(ConfigConstant.WRITE) DataSource write,
@Qualifier(ConfigConstant.READ) DataSource read) {
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(ConfigConstant.WRITE, write);
targetDataSources.put(ConfigConstant.READ, read);
dynamicDataSource.setTargetDataSources(targetDataSources);
// 设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(write);
return dynamicDataSource;
}
@Bean
@Primary
public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean () throws Exception{
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(multipleDataSource(write(), read()));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:/mybatis/**/*.xml"));
factoryBean.setTypeHandlersPackage("com.weihubeats.config");
// 分页插件暂时不定
MybatisConfiguration configuration = new MybatisConfiguration();
//开启下划线转驼峰
configuration.setMapUnderscoreToCamelCase(true);
configuration.setJdbcTypeForNull(JdbcType.NULL);
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setBanner(false);
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
factoryBean.setGlobalConfig(globalConfig);
factoryBean.setConfiguration(configuration);
return factoryBean;
}
@Bean(name = ConfigConstant.WRITE)
@ConfigurationProperties(prefix = "db.pg.write")
public DataSource write() {
return DataSourceBuilder.create().build();
}
@Bean(name = ConfigConstant.READ)
@ConfigurationProperties(prefix = "db.pg.read")
public DataSource read() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSourceTransactionManager transactionManager () {
return new DataSourceTransactionManager( multipleDataSource(write(), read()) );
}
}
使用
使用方式很简单,我们在需要切换数据源的方法上面添加注解即可
比如
@Mapper
public interface UserDAO {
@WHRead
List<UserDO> getUserDOByIds(@Param("ids") List<Long> ids);
}
关于我
觉得文章不错请扫码关注我吧