SSH实现动态数据源切换,事务场景下使用AOP

1上周写代码遇到了切换数据源的问题,在同一个方法中向两个不同数据源做一些操作,但是这个方法使用了事务,所以网上一般动态切换数据源的方法就失效了。框架是spirngmvc+hibernate,数据库是oracle,连接池druid。 2一般情况下,操作数据都是在DAO层进行处理。一种办法是使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,不过这样的入侵性比较明显,一般的情况下我们都是使用继承HibernateSupportDao进行封装了的处理,如果多个 3SessionFactor这样处理就是比较的麻烦了,修改的地方估计也是蛮多的。最后一个,也就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。比如我们希望对于读写分离或者其他的数据同步的业务场景。

下面来看看图片

      

  • 单数据源的场景(一般的Web项目工程这样配置进行处理,就已经比较能够满足我们的业务需求)

  • 多数据源多SessionFactory这样的场景,估计作为刚刚开始想象想处理在使用框架的情况下处理业务,配置多个SessionFactory,然后在Dao层中对于特定的请求,通过特定的SessionFactory即可处理实现这样的业务需求,不过这样的处理带来了很多的不便之处,所有很多情况下我们宁愿直接使用封装的JDBC编程,或者使用Mybatis处理这样的业务场景

  • 使用AbstractRoutingDataSource 的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource,不用修改我们编写的对于继承HibernateSupportDao的实现类的修改,这样的编写方式比较好,至于其中的实现原理,让我细细道来。我们想看看如何去应用,实现原理慢慢的说!

  • 编写AbstractRoutingDataSource的实现类,DataSourceContextHolder就是提供给我们动态选择数据源的工具类,我们这里编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以手动的设置当前的数据源,在编程的类中。

    /** *类名:DynamicDataSource.java *功能:动态数据源类 / public class DynamicDataSource extends AbstractRoutingDataSource { / * 该方法必须要重写 方法是为了根据数据库标示符取得当前的数据库 */ @Override protected Object determineCurrentLookupKey() { DataSourceType type = DataSourceContextHolder.getType(); return type; }

    1public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { 2 super.setDataSourceLookup(dataSourceLookup); 3} 4 5public void setDefaultTargetDataSource(Object defaultTargetDataSource) { 6 super.setDefaultTargetDataSource(defaultTargetDataSource); 7} 8 9public void setTargetDataSources(Map targetDataSources) { 10 super.setTargetDataSources(targetDataSources); 11}

    }

  • 设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前。

    /**

    • 获得和设置上下文环境的类,主要负责改变上下文数据源的名称,根据当前线程来选择具体的数据源 */ public class DataSourceContextHolder {    /*获取当前线程/ private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal();    /*提供给AOP去设置当前线程数据源的信息/ public static void setType(DataSourceType type) { contextHolder.set(type); }    /*提供给AbstractRoutingDataSource的实现类,通过key选择数据源/ public static DataSourceType getType() { return contextHolder.get(); }   /*使用默认的数据源/ public static void clear() { contextHolder.remove(); } }
  • 设置拦截数据源的注解,可以设置在具体的类上,或者在具体的方法上,DataSourceType是当前数据源的一个别名用于标识我们的数据源的信息(此处是一个枚举类)。

    @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DynamicSwitchDataSource { DataSourceType type(); }

    public enum DataSourceType { DATASOURCE_RDP("dataSource_rdp"), DATASOURCE_OCM("dataSource_ocm");

    1private String type; 2 3DataSourceType(String type) { 4 this.type = type; 5} 6 7public String type() { 8 return type; 9}

    }

  • AOP拦截类的实现,通过拦截上面的注解,在其执行之前处理设置当前执行SQL的数据源的信息,DataSourceContextHolder.setType(….),这里的数据源信息从我们设置的注解上面获取信息,如果没有设置就是用默认的数据源的信息。

    @Component @Aspect @Order(1) public class DataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    1//@within在类上设置 2//@annotation在方法上进行设置 3@Pointcut("@within(org.jeecgframework.core.annotation.DynamicSwitchDataSource)||@annotation(org.jeecgframework.core.annotation.DynamicSwitchDataSource)") 4public void pointcut() { 5} 6 7@Before("pointcut()") 8public void doBefore(JoinPoint joinPoint) { 9 Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); 10 DynamicSwitchDataSource annotationClass = method.getAnnotation(DynamicSwitchDataSource.class);//获取方法上的注解 11 if (annotationClass == null) { 12 annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicSwitchDataSource.class);//获取类上面的注解 13 if (annotationClass == null) return; 14 } 15 //获取注解上的数据源的值的信息 16 DataSourceType dataSourceKey = annotationClass.type(); 17 if (dataSourceKey != null) { 18 //给当前的执行SQL的操作设置特殊的数据源的信息 19 DataSourceContextHolder.setType(dataSourceKey); 20 } 21 logger.info("AOP动态切换数据源,className" + joinPoint.getTarget().getClass().getName() + "methodName" + method.getName() + ";dataSourceKey:" + dataSourceKey == "" ? "默认数据源" : dataSourceKey.type()); 22} 23 24@After("pointcut()") 25public void after(JoinPoint point) { 26 //清理掉当前设置的数据源,让默认的数据源不受影响 27 DataSourceContextHolder.clear(); 28}

    }

  •  配置数据源在Spring 核心容器中配置

    <!-- 配置数据源rdp -->
    1<bean id="dataSource_rdp" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> 2 <property name="url" value="${rdp.url}"/> 3 <property name="username" value="${rdp.username}"/> 4 <property name="password" value="${rdp.password}"/> 5 <!-- 初始化连接大小 --> 6 <property name="initialSize" value="0"/> 7 <!-- 连接池最大使用连接数量 --> 8 <property name="maxActive" value="50"/> 9 <!-- 连接池最小空闲 --> 10 <property name="minIdle" value="5"/> 11 <!-- 获取连接最大等待时间 --> 12 <property name="maxWait" value="60000"/> 13 <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> 14 <property name="timeBetweenEvictionRunsMillis" value="60000"/> 15 <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> 16 <property name="minEvictableIdleTimeMillis" value="300000"/> 17 <!--<property name="validationQuery" value="${validationQuery.sql}" />--> 18 <property name="testOnBorrow" value="false"/> 19 <property name="testOnReturn" value="false"/> 20 <property name="testWhileIdle" value="true"/> 21 <!-- 开启Druid的监控统计功能 --> 22 <property name="filters" value="stat"/> 23 <!-- 打开removeAbandoned功能 --> 24 <property name="removeAbandoned" value="true"/> 25 <!-- 1800秒,也就是30分钟 --> 26 <property name="removeAbandonedTimeout" value="3600"/> 27 <!-- 关闭abanded连接时输出错误日志 --> 28 <property name="logAbandoned" value="true"/> 29 <!-- Oracle连接是获取字段注释 --> 30 <property name="connectProperties"> 31 <props> 32 <prop key="remarksReporting">true</prop> 33 </props> 34 </property> 35</bean> 36 37<!-- 配置数据源ocm --> 38<bean id="dataSource_ocm" class="com.alibaba.druid.pool.DruidDataSource" 39 ...数据源2 40</bean>
    <!-- 数据源集合 -->
    1<bean id="dataSource" class="org.jeecgframework.core.extend.datasource.DynamicDataSource"> 2 <property name="targetDataSources"> 3 <map key-type="org.jeecgframework.core.extend.datasource.DataSourceType"> 4 <entry key="DATASOURCE_RDP" value-ref="dataSource_rdp"/> 5 <entry key="DATASOURCE_OCM" value-ref="dataSource_ocm"/> 6 </map> 7 </property> 8 <property name="defaultTargetDataSource" ref="dataSource_rdp"/> 9</bean> 10 11<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> 12 <property name="dataSource" ref="dataSource"/> 13 <property name="entityInterceptor" ref="hiberAspect"/> 14 <property name="hibernateProperties"> 15 <props> 16 <!--<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> --> 17 <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop> 18 <!--<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>--> 19 <prop key="hibernate.show_sql">true</prop> 20 <prop key="hibernate.format_sql">true</prop> 21 <prop key="hibernate.temp.use_jdbc_metadata_defaults">false</prop> 22 </props> 23 </property>
  • 配置之前我们实现的数据源选择的中间层AbstractRoutingDataSource的实现类,这里的key就是数据源信息的别名,通过这个key可以选择到数据源的信息。DynamicDataSource就是上面写的数据源选择器的实现类。

  • SessionFactory的配置还是照旧,使用以前的配置,只不过当前选择的数据源是datasource,也就是数据源选择的中间层DynamicDataSource,因为当前的中间层中实现了DataSource这个接口,所以可以看做为DataSource的是实现类啦,所以配置不会出现问题。

  • 简单的使用AOP进行测试一下,这里测试的结果时不同的,所以是生效的,使用了不同的数据源,但是底层的实现没有进行任何的修改处理。(Transactional是事务注解**)**

  • 看下AOP配置

    <?xml version="1.0" encoding="UTF-8"?>

    <beans xmlns="http://www.springframework.org/schema/beans"... > <!--设置expose-proxy属性为true,将代理暴露出来--> <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/> <context:component-scan base-package="org.jeecgframework.core.aop"/> </beans>

    @Service("cgFormFieldService") @Slf4j @Transactional public class CgFormFieldServiceImpl implements CgFormFieldServiceI{

    1@DynamicSwitchDataSource(type = DataSourceType.DATASOURCE_RDP) 2public void deleteCgForm(CgFormHeadEntity cgFormHead) { 3 this.delete(cgFormHead); 4 String sql = getTableUtil().dropTableSQL(cgFormHead.getTableName()); 5 //执行ocm中sql 6 ((CgFormFieldServiceImpl) AopContext.currentProxy()).executeOcmSql(sql); 7 } 8}

      @DynamicSwitchDataSource(type = DataSourceType.DATASOURCE_OCM) @Transactional(propagation = Propagation.REQUIRES_NEW) public void executeOcmSql(String sql) { this.executeSql(sql); }

    }

事务环境的隔离性

如果在一个方法A中已经开启事务,在这个方法A中调用另一个事务方法B,如果不作其他配置,那么方法B会沿用方法A的事务环境,而不会开启一个新的事务。如果不开启一个新的事务,当然也不会进行一系列的改变直至数据源的切换。这也证明他们同处于一个事务环境,因为hibernate的session适合transaction绑定的。由于事务环境的隔离性,所以下面一个方法中必须设置为REQUIRES_NEW。

在Service层,A方法调用B方法的时候,用了((Service)AopContext.currentProxy()).B()  作用:

原来在springAOP的用法中,只有代理的类才会被切入,我们在controller层调用service的方法的时候,是可以被切入的,但是如果我们在service层 A方法中,调用B方法,切点切的是B方法,那么这时候是不会切入的,解决办法就是如上所示,在A方法中使用((Service)AopContext.currentProxy()).B() 来调用B方法,这样一来,就能切入了!

总结

​ 以上就是所有关于动态切换数据源的内容,相关知识主要涉及到AOP,事务管理。因此只要掌握相关知识,想来处理起来不算困难。需要注意的点:AbstractRoutingDataSource 的使用、带参枚举类、事物的隔离性、AOP代理等,希望能够帮到大家。 有志者,事竟成!

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

SSH实现动态数据源切换,事务场景下使用AOP - HelloWorld