记录一次AOP+mybatis多数据源实战

自嗨

对自己问一声好!第一次在线记录问题,不期望能帮到别人,只希望下次碰到类似问题的时候能在这里找到思路,查到答案。

问题开始

问题发生在测试平台开发期间,平台的一大功能点是测试检查,当贷款流程走完之后,我们通过贷款件KB码,去测试数据库里检查测试数据落库是否准确,校验字段内容及状态,涉及到oracle数据库,此为数据库A。而平台自身需要保存每一次测试检查记录,此为数据库B。

多数据源初始版本

原先已完成mybatis多数据源配置:

  1. 在applicationContent.xml里配置sqlSession;
    <!-- spring和MyBatis整合 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- mybatis配置文件,设置mybatis配置相关 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 自动扫描Mapper.xml文件 -->
        <property name="mapperLocations"
                  value="classpath*:dao/xml/*Mapper.xml"/>
    </bean>
  1. 配置dao目录,自动扫描下面的mapper类,上面的mapper操作都对应于这些类方法;
    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="dao.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
  1. 配置统一的mybatis属性配置,用以多数据源继承;
<!-- 父类数据源属性 -->
<bean id="parentDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <!-- Connection Pooling Info -->
        <property name="maxActive" value="20"/>
        <property name="maxIdle" value="20"/>
        <property name="minIdle" value="5"/>
        <property name="defaultAutoCommit" value="false"/>
        <property name="maxWait" value="30000"/>

        <!-- 每2分钟运行一次空闲连接回收器 -->
        <property name="timeBetweenEvictionRunsMillis" value="120000"/>
        <!--池中的连接空闲10分钟后被回收,默认值是30分钟。 -->
        <property name="minEvictableIdleTimeMillis" value="600000"/>

        <!--优化 :验证连接是否可用,使用的SQL语句 -->
        <property name="validationQuery" value="SELECT 1 FROM DUAL"/>
        <!-- 是否自动回收超时连接 -->
        <property name="removeAbandoned" value="true"/>
        <!-- 清除无用连接的等待时间(以秒数为单位) -->
        <property name="removeAbandonedTimeout" value="10"/>
        <!-- 指定在从连接池中拿连接时,要检查连接是否有效,若无效会将连接从连接池中移除掉 -->
        <property name="testOnBorrow" value="true"/>
    </bean>
  1. 配置多数据源,继承之统一父类属性;
<!-- 数据源配置, 使用应用中的apache.DBCP数据库连接池 -->
   <bean parent="parentDataSource" id="aaa">
       <!-- Connection Info -->
       <property name="driverClassName" value="${tank.driver}"/>
       <property name="url" value="${tank.url}"/>
       <property name="username" value="${tank.username}"/>
       <property name="password" value="${tank.password}"/>
   </bean>
   <bean parent="parentDataSource" id="bbb">
       <!-- Connection Info -->
       <property name="driverClassName" value="${s2bloan.driver}"/>
       <property name="url" value="${s2bloan.url}"/>
       <property name="username" value="${s2bloan.username}"/>
       <property name="password" value="${s2bloan.password}"/>
   </bean>
  1. 设置中转,保存多个key值对应多个数据源,并设置默认数据源,class在后面创建,见后续类设置;
 <!--中转-->
    <bean class="global.DynamicDataSource" id="dataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry value-ref="aaa" key="tank"></entry>
                <entry value-ref="bbb" key="s2bloan"></entry>
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="aaa"></property>
    </bean>
  1. spring相关配置,这一块有mvc一些配置,还需要待了解,只做记录;
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.1.xsd
                        http://www.springframework.org/schema/mvc
                        http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
                        http://www.springframework.org/schema/task
                        http://www.springframework.org/schema/task/spring-task.xsd
                        http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:aspectj-autoproxy/>

    <!--自动扫描,注解控制器 -->
    <context:component-scan base-package="service.Other.impl controller"/>
    <context:component-scan base-package="service.Flow controller"/>
    <context:component-scan base-package="service.TestCase controller"/>
    <context:component-scan base-package="testCase"/>
    <context:component-scan base-package="global"/>
    <context:component-scan base-package="Task"/>
    <!-- 启动注解驱动的Spring MVC功能,注册请求url和注解POJO类方法的映射-->
    <!-- 指定自己定义的validator -->
    <mvc:annotation-driven validator="validator"/>


    <!-- 以下 validator ConversionService 在使用 mvc:annotation-driven 会 自动注册 -->
    <bean id="validator"
          class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
        <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    </bean>
    <!--注册方法验证的后处理器-->
    <bean
class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
    <!--注解驱动 -->
    <mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <!-- @ResponseBody乱码问题,将StringHttpMessageConverter的默认编码设为UTF-8 -->
            <beans:bean
class="org.springframework.http.converter.StringHttpMessageConverter">
                <beans:constructor-arg value="UTF-8"/>
            </beans:bean>
            <!-- 配置Fastjson支持 -->
            <beans:bean
 class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <beans:property name="charset" value="UTF-8"/>
                <beans:property name="supportedMediaTypes">
                    <beans:list>
                        <beans:value>application/json</beans:value>
                        <beans:value>text/html;charset=UTF-8</beans:value>
                    </beans:list>
                </beans:property>
                <beans:property name="features">
                    <beans:list>
                        <!-- 是否输出值为null的字段,默认为false -->
                        <!--<beans:value>WriteMapNullValue</beans:value> -->
                        <beans:value>QuoteFieldNames</beans:value>
                        <beans:value>WriteDateUseDateFormat</beans:value>
                        <beans:value>WriteEnumUsingToString</beans:value>
                    </beans:list>
                </beans:property>
            </beans:bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:application.properties</value>
            </list>
        </property>
    </bean>
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
        <property name="properties" ref="configProperties"/>
    </bean>
    <task:annotation-driven/>
</beans>
  1. 顺便记录下变量的配置文件读取:
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location" value="classpath:application.properties"/>
        <property name="fileEncoding" value="UTF-8"/>
    </bean>

还有mybatis配置文件;

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 注意configuration中各个属性配置的顺序应为:properties,
    settings,typeAliases,typeHandlers,
    objectFactory,objectWrapperFactory,reflectorFactory,
    plugins,environments,databaseIdProvider,mappers)-->
    <!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
    <settings>
        <setting name="jdbcTypeForNull" value="VARCHAR"/>
        <!--<setting name="logImpl" value="LOG4J"/>-->
        <!-- 全局映射器启用缓存 -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 查询时,关闭关联对象即时加载以提高性能 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
        <setting name="multipleResultSetsEnabled" value="true"/>
        <!-- 允许使用列标签代替列名 -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 不允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
        <setting name="useGeneratedKeys" value="false"/>
        <!-- 给予被嵌套的resultMap以字段-属性的映射支持 FULL,PARTIAL -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        <!-- 对于批量更新操作缓存SQL以提高性能 BATCH,SIMPLE -->
        <!-- <setting name="defaultExecutorType" value="BATCH" /> -->
        <!-- 数据库超过25000秒仍未响应则超时 -->
        <!-- <setting name="defaultStatementTimeout" value="25000" /> -->
        <!-- Allows using RowBounds on nested statements -->
        <setting name="safeRowBoundsEnabled" value="false"/>
        <!-- Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn. -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>

        <!-- MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT
            local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. -->
        <setting name="localCacheScope" value="SESSION"/>
        <!-- Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers require specifying the column JDBC type but others work with generic values
            like NULL, VARCHAR or OTHER. -->
        <setting name="jdbcTypeForNull" value="OTHER"/>
        <!-- Specifies which Object's methods trigger a lazy load -->
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
        <!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指 定),不会加载关联表的所有字段,以提高性能 -->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
</configuration>

以上就是XML里的配置,具体使用还需要完善数据源切换类;

  1. 数据源切换方法类,使用静态方法全局可以随意使用;
package global;

public abstract class AbstractCustomerContextHolder {
    public final static String TANK = "tank";
    public final static String S2BLOAN = "s2bloan";
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();

    public static void setContextType(String contextType) {
            CONTEXT_HOLDER.set(contextType);
    }

    public static String getContextType() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearContextType() {
        CONTEXT_HOLDER.remove();
    }
}
  1. 匹配多数据源中转配置,查看上面中转配置,设置成该类;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 在进行DAO操作前,通过上下文环境变量,获得数据源的类型
        return AbstractCustomerContextHolder.getContextType();
    }
}
  1. 数据源需要切换时;
 AbstractCustomerContextHolder.setContextType(AbstractCustomerContextHolder.TANK);
 testCaseExecuteMapper.insert(testCaseExecute);

使用起来也还方便,只时流程渐多时,需要频繁切换,一不小心就加个步骤但忘切换数据源报错,只得再加个切换语句来解决,影响代码可读性;

注解版本改良

好的,看黑板,看我是怎么变形的;

  1. 一定一定,要加上这个扫描自动注解;
    <aop:aspectj-autoproxy/>

对应的其他属性idea工具会自动加上xmlns:aop=“http://www.springframework.org/schema/aop”
以及
xsi:schemaLocation=“http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd”
2. 增加自定义注解;

import java.lang.annotation.*;
/**
 * 增加多数据源注解
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "";
}
  1. 设置切面类;
package global;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import util.SendEmail;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

@Aspect
@Component
public class DynamicDataSourceIntercepter {

    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceIntercepter.class);

    private static List<String> evns = new ArrayList<>();
    /**
     * 拦截目标方法,获取由@DataSource指定的数据源标识,设置到线程存储中以便切换数据源
     */
    @Before("exec()")
    public void intercept(JoinPoint point) throws Exception {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        resolveDataSource(target, signature.getMethod());
    }

    // 使用完当前数据源后,切回之前数据源
    @After("exec()")
    public void switchBack(){
        for(String s:evns)
            logger.info("已有环境:"+s);
        evns.remove(evns.size()-1);
        if(evns.size()>0) {
            logger.info("切回之前库:"+evns.get(evns.size() - 1));
            AbstractCustomerContextHolder.setContextType(evns.get(evns.size() - 1));
        }
    }
    
    // 拦截Datasource的注解,做为切入点
    @Pointcut("@annotation(DataSource)")
    public void exec(){
    }

    /**
     * 提取目标对象方法注解和类型注解中的数据源标识
     * @param clazz
     * @param method
     */
    private void resolveDataSource(Class<?> clazz, Method method) {
        try {
            Class<?>[] types = method.getParameterTypes();
            logger.info("进入数据源注解"+method.getName());
            if (clazz.isAnnotationPresent(DataSource.class)) {
                DataSource source = clazz.getAnnotation(DataSource.class);
                AbstractCustomerContextHolder.setContextType(source.value());
            }
            Method m = clazz.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                DataSource source = m.getAnnotation(DataSource.class);
                AbstractCustomerContextHolder.setContextType(source.value());
            }
            evns.add(AbstractCustomerContextHolder.getContextType());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 注解使用,直接通过DataSource带需要切换的库配置即可,该方法或者类下对应的数据源自动切换;
    @DataSource("tank")
    public void start() {
        try {
            testCaseExecute = testCaseExecuteMapper.selectWithStatus("1");
            if (testCaseExecute != null) {
                logger.info("有用例正在执行,排队中..." + testCaseExecute.getEmail());
                return;
            }
          }
        }

其他问题

  1. 注解只支持public方法,本来想直接放置在mapper类里,确保最后一层调用数据源是准确的,可是mapper接口类加注解并不能生效,参考:接口注解不生效
  2. 注解也是要加扫描路径的,有需要自定义DataSource注解的类也要放在配置里
    <context:component-scan base-package="testCase"/>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值