闲来无事手撸了一个简单的MybatisPlus读写分离注解

382 阅读3分钟

背景

最近项目要做读写分离,然后看了下没什么好的开源框架,开源框架因为要兼顾太多的东西,所以显的很笨重,不够轻量,而我的目的很简单,就是提供一个注解,动态的切换数据源。所以看了下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);
}

关于我

觉得文章不错请扫码关注我吧

weichat