mybatis+spring 实现多数据源(注解版)

需求

  1. 系统架构是spring framework +各种组件
  2. 项目重构 打算使用mybatis 作为orm框架
  3. 项目依赖多个数据源、多个数据源需要读写分离
  4. 抛弃xml配置,完全注解化

问题

  1. 注解扫mapper过程中,一个mapper只能被一个sqlsessiontemplate装载 ,若想实现读写分离需要对每个库表定义不同的读写mapper,然后通过@MapperScan(basePackages = "mybatisSpring.mapper" sqlSessionTemplateRef = "") 分别指定数据源 。类似图中样式,basePackages分别指定namespace 与testmapper 然后再指定对应的sqlSessionTemplate。


    image.png

    上述方式实现和逻辑比较简单,但是太粗糙,写的代码会很多。

  2. 理想使用方式 mapper层按库表逻辑划分,与业务逻辑完全剥离。

实现方案

  1. 使用AbstractRoutingDataSource进行多数据源管理
  2. 使用切面完成数据源的动态切换
  3. 直接引用sqlSessionTemplate完成读写操作

show me the code

  1. 代码结构


    代码结构
  • annotation 自定义注解
  • conf java配置文件
  • dao 封装的dao层服务
  • mapper mybatis的接口mapper类
  • model DO层
  1. 具体代码
  • AbstractRoutingDataSource 的实现类,管理项目的所有数据源。
public class MomentRoutingDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<DataSourceType> dataSourceHolder = new ThreadLocal<>();

    public static void setDataSource(DataSourceType dataSourceType) {
        dataSourceHolder.set(dataSourceType);
    }

    public static DataSourceType getDataSource() {
        return dataSourceHolder.get();
    }

    public static void clear() {
        dataSourceHolder.remove();
    }

    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }


}
  • spring 配置文件
@Configuration
@MapperScan(basePackages = "mybatisSpring.mapper" )
@ComponentScan(basePackages = "mybatisSpring")
// 开启切面
@EnableAspectJAutoProxy
public class SqlSessionTemplateConf {

    /**
     * 初始化dataSource
     *
     * @return
     * @throws Exception
     */
    private HikariDataSource getDataSource(String dbname) throws Exception {

        HikariDataSource dataSource = new HikariDataSource();
        String jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
                , "localhost", 3306, "mpaper_cms2");
        dataSource.setJdbcUrl(jdbcUrl);
        dataSource.setUsername("*******");
        dataSource.setPassword("******");
        dataSource.setMinimumIdle(10);
        dataSource.setMaximumPoolSize(20);
        //等待获取连接时间-默认30s,超过配置阈值抛异常
        dataSource.setConnectionTimeout(500);
        dataSource.setPoolName(dbname);
        return dataSource;
    }


    @Bean
    public DataSource momentRoutingDataSource() throws Exception {
        MomentRoutingDataSource momentRoutingDataSource = new MomentRoutingDataSource();
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.CMS_RO, getDataSource("cms_ro"));
        targetDataSource.put(DataSourceType.CMS_RW, getDataSource("cms_rw"));
        momentRoutingDataSource.setTargetDataSources(targetDataSource);
       return momentRoutingDataSource;
    }

    @Bean("momentSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(DataSource momentRoutingDataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(momentRoutingDataSource);
        return new SqlSessionTemplate(factoryBean.getObject());
    }

}
  • 切面实现& 定义注解
@Aspect
@Component
@Order(0)
public class CustomDataSourceAspect {
    // @annotation(mybatisSpring.annotation.DataSource) &&
    @Pointcut(value = "execution( * mybatisSpring.dao..*.*(..))")
    public void pointCut() {

    }

    @Before(value = "pointCut()")
    public void changeDataSource(JoinPoint joinPoint) throws NoSuchMethodException {
        Class<?> aClass = joinPoint.getTarget().getClass();
        //拦截mapper方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (method != null && method.isAnnotationPresent(DataSource.class)) {
            DataSource dataSource = method.getAnnotation(DataSource.class);
            MomentRoutingDataSource.setDataSource(dataSource.type());
            System.out.println("Service Class 数据源切换至--->" + dataSource.type().getValue());
            return;
        }
        //拦截mapper类
        if (aClass.isAnnotationPresent(DataSource.class)) {
            DataSource dataSource = aClass.getAnnotation(DataSource.class);
            MomentRoutingDataSource.setDataSource(dataSource.type());
            System.out.println("Service Class 数据源切换至--->" + dataSource.type().getValue());
            return;
        }

    }

    /**
     * 方法结束后
     */
    @After(value = "pointCut()")
    public void afterReturning() throws Throwable {
        try {
            MomentRoutingDataSource.clear();
            System.out.println("数据源已移除!");
        } catch ( Exception e ) {
            e.printStackTrace();
            System.out.println("数据源移除报错!");
        }

    }
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    DataSourceType type() default DataSourceType.CMS_RO;
}

-dao 层代码

@Service
@DataSource(type = DataSourceType.CMS_RO)
public class NewsTvInfoDao {
    @Autowired
    private SqlSessionTemplate momentSqlSessionTemplate;

    public NewsTvInfo getNewsTvInfo(int id) {
        NewsTvInfoMapper mapper = momentSqlSessionTemplate.getMapper(NewsTvInfoMapper.class);
        return mapper.findByVid(id);
    }

    @DataSource(type = DataSourceType.CMS_RW)
    public NewsTvInfo getNewsTvInfoRw(int id) {
        ActivityMapper mapper = momentSqlSessionTemplate.getMapper(ActivityMapper.class);
        return mapper.findByVid(id);
    }
}

  • main 入口
public class MybatieSpringMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SqlSessionTemplateConf.class);
        // 方法前未添加注解,取类的注解注入的数据源
        NewsTvInfoDao dao = context.getBean(NewsTvInfoDao.class);
        NewsTvInfo newsTvInfo = dao.getNewsTvInfo(9102910);
        System.out.println(newsTvInfo.getBigPic());
         // 方法添加注解,取方法的注解注入的数据源
        NewsTvInfo newsTvInfoRw = dao.getNewsTvInfoRw(9102910);
        System.out.println(newsTvInfoRw.getBigPic());

    }
}

说明

  1. 代码未列完整,依赖mybatis ,mybatis-spring 依赖包
  2. 切面逻辑中,会先扫描方法后扫描类。因此方法添加的注解的优先级大于类的优先级,因此可以在dao层类的上方添加默认数据源。
  3. @EnableAspectJAutoProxy需要 添加此注解
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350