SSM下利用AOP注解的形式配置多数据源

SSM下利用AOP注解的形式配置多数据源

需求描述:

系统原先使用的SQL server数据库,druid数据源。目前又新增了postgresql数据库,需要实现在方法上加上一个注解就会将此方法执行postgresql的数据源。

实现思路:

简单的说就是:

Mybatis来调用数据源,AOP用来切入方法,注解来告诉AOP应该切换哪个。

实现过程:

首先在Mybatis.xml中添加一个数据源的bean,为了方便区分,下面用dataSource1/2代替数据源名字

spring-dao.xml配置文件

<!--原本的数据源 dataSource1为原本的数据源-->
<bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${mssql.driver}"/>

    <!-- 基本属性 url、user、password -->
    <property name="url" value="${mssql.url}"/>
    <property name="username" value="${mssql.username}"/>
    <property name="password" value="${mssql.password}"/>

    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="${mssql.initialSize}"/>
    <property name="minIdle" value="${mssql.minIdle}"/>
    <property name="maxActive" value="${mssql.maxActive}"/>

    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="60000"/>

    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>

    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <property name="minEvictableIdleTimeMillis" value="300000"/>

    <!-- <property name="validationQuery" value="${jdbc.testSql}" /> -->
    <!-- <property name="testWhileIdle" value="true" /> -->
    <property name="testOnBorrow" value="false"/>
    <property name="testOnReturn" value="false"/>

    <!-- 配置监控统计拦截的filters -->
    <property name="filters" value="stat"/>
</bean>

<!--配置多数据源 dataSource2为新增数据源-->
<bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
    <property name="driverClassName" value="${postgresql.driver}"/>
    <property name="url" value="${postgresql.url}"/>
    <property name="username" value="${postgresql.username}"/>
    <property name="password" value="${postgresql.password}"/>
</bean>

<!--配置多数据源-->
<bean id="dataSource" class="com.***.***.***.datasource.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry value-ref="dataSource1" key="dataSource1"></entry>
            <entry value-ref="dataSource2" key="dataSource2"></entry>
        </map>
    </property>
    <!--此处为默认数据源,非常重要,如果不配置后续clear掉数据源后可能会报错-->
    <property name="defaultTargetDataSource" ref="dataSource1"></property>
</bean>


<!--配置SqlSessionFactory对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!--注入数据库连接池-->
    <property name="dataSource" ref="dataSource"/>
    <!--配置mybatis全局配置文件:mybatis-config.xml-->
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <!--扫描entity包,使用别名,多个用;隔开-->
    <!--<property name="typeAliasesPackage" value="com.***.***.***.entity"/>-->
    <!--扫描sql配置文件:mapper需要的xml文件-->
    <property name="mapperLocations" value="classpath:mapper/*/*.xml"/>
</bean>

DataSource注解接口类 @interface

/**
 * 描述: 配置多数据源注解接口
 * @Author: zhuyu
 * @Date: Create in 19:01 2020/11/30
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    // 此处为注解的默认数据源,即当方法上加上接口的时候,不添加任何参数时的默认值
    String value() default "dataSource1";
}

DataSourceAop功能实现类

/**
 * @Description: 多数据源切换AOP接口
 * @Author: zhuyu
 * @Date: Create in 19:06 2020/11/30
 */
@Aspect
@Component
public class DataSourceAop {
    private final static Logger logger = LoggerFactory.getLogger(DataSourceAop.class);

    @Pointcut("@annotation(com.***.***.***.datasource.DataSource)")
    public void cut() {
    }

    /**
     * Before方法,主要功能为判断方法是否合法,如果合法则切换数据源
     * <p>
     * 在配置多数据源中,ProceedingJoinPoint不支持@before和@after,仅支持@Around。
     * 如果要使用@Before和@After,需要使用JoinPoint来代替ProceedingJoinPoint
     *
     * @param point
     * @throws Throwable
     */
    @Before(value = "cut()")
    public void cutDataSource(JoinPoint point) throws Throwable {
        try {
            //获得访问的方法名
            String methodName = point.getSignature().getName();
            logger.info("方法:{}", methodName, "正在切换数据源");
            //获取当前方法
            MethodSignature methodSignature = (MethodSignature) point.getSignature();
            Method method = methodSignature.getMethod();
            // 读取该方法的注解,得到需要切换数据源的名称
            String switchDataSourceName = method.getAnnotation(DataSource.class).value();
            // 切换数据源
            DataSourceContextHolder.setDbType(switchDataSourceName);
        } catch (Exception e) {
            DataSourceContextHolder.clearDbType();
            logger.error("切换数据源发生异常,已切换回主数据源,异常信息为:{}", e.getMessage());
        }
    }

    /**
     * 方法调用完毕,切换回主数据源
     *
     * @param point 加入点
     */
    @After(value = "cut()")
    public void afterCut(JoinPoint point) {
        try {
            // 方法结束后清理数据源
            DataSourceContextHolder.clearDbType();
        } catch (Exception e) {
            logger.error("@After清理数据源发生异常,异常信息为:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 无论如何都要执行
     *
     * @param point 加入点
     * @throws Throwable
     */
    @AfterReturning(value = "cut()")
    public void afterReturning(JoinPoint point) {
        try {
            // 获取当前数据源
            String dbType = DataSourceContextHolder.getDbType();
            // 如果当前数据源不是主数据源,则清理数据源信息切换到主数据源
            if (dbType != null && !dbType.equals("dataSource1")) DataSourceContextHolder.clearDbType();
        } catch (Exception e) {
            DataSourceContextHolder.clearDbType();
            logger.error("@AfterReturning切换数据源发生异常,已切换回默认数据源.异常信息为:{}", e.getMessage());
        }
    }

    /**
     * AOP执行过程中抛出Exception之后被调用
     *
     * @param point
     */
    @AfterThrowing(value = "cut()")
    public void afterThrowing(JoinPoint point) {
        try {
            logger.info("AOP执行过程中发送异常");
            DataSourceContextHolder.clearDbType();
        } catch (Exception e) {
            logger.error("@AfterThrowing执行过程中发送异常,异常信息为:{}", e.getMessage());
        }
    }

}

DataSourceContextHolder 切换数据源操作类

/**
 * @Description: 切换数据源操作类
 * @Author: zhuyu
 * @Date: Create in 19:06 2020/11/30
 */
public class DataSourceContextHolder {

    private final static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
    // 新建一个线程
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    /**
     * 切换数据源,set到线程中
     *
     * @param dbType 数据源名称
     */
    public static void setDbType(String dbType) {
        logger.info("已切换到数据源:{}", dbType);
        contextHolder.set(dbType);
    }

    /**
     * 获取当前线程数据源名称
     *
     * @return 当前数据源名称
     */
    public static String getDbType() {
        return (contextHolder.get());
    }

    /**
     * 清理当前线程数据源信息,清理掉以后当前数据源信息为 null
     * 清理掉数据源后mybatis会自动使用 dao.xml中的defaultTargetDataSource(默认数据源)作为当前数据源
     */
    public static void clearDbType() {
        contextHolder.remove();
        logger.info("已清理数据源");
    }

    /**
     * 设置当前线程数据源为默认数据源,一般情况下作用相当于clearDbType()
     */
    public static void setDefault() {
        contextHolder.set("dataSource1");
        logger.info("已切换到默认数据源dataSource1");
    }

}

DynamicDataSource.java

/**
 * @Description: mybatis访问该类获取数据源信息
 * @Author: zhuyu
 * @Date: Create in 19:02 2020/11/30
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * mybatis来拿数据源信息,方法获取当前数据源信息返回给mybatis(我是这样理解的,如果有更好的理解请修改注释)
     * @return      当前线程数据源信息
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDbType();
    }
}

至此,多数据源以及配置完毕,但是需要注意,我xml中并未贴出aop和mybatis的其他配置,不要忘记了

测试一下

在方法上加入 @DataSource("dataSource2"),访问方法查看日志输出

需要注意的点

  • 切换数据源的本质实际上是新建一个线程,然后对新建的线程中的数据源内容进行切换修改,方法执行完后,线程要记得remove掉。我的代码中将线程remove封装到了clearDbType中。如果后续多数据源在并发情况下导致数据源报错什么的,就自行想办法加Lock保证线程安全吧。一般流量下只要及时remove应该就没什么问题了。
  • 为了保证数据源可以顺利clear,我代码中进行了多次clear操作,其实没太大作用。
  • 如果数据源多的话,可以考虑在@Before里面加上方法名被切换数据源的判断之类的来保证万无一失,我之前加上了,但是我们系统只有两个数据源,暂时用不到就删掉了。

结语

AOP切换数据源实现过程如上,如果有问题或疑问以及不足的地方,请在评论区指出,谢谢。

posted @ 2020-12-20 23:10  zhuyu++  阅读(304)  评论(0)    收藏  举报