spring aop实现读写分离:

spring aop实现读写分离:

原理分析:

通过配置多个数据源,通过AOP在执行service之前将数据源切换掉。spring提供了相关的类。

AbstractRoutingDataSource 类

public Connection getConnection() throws SQLException {
    return this.determineTargetDataSource().getConnection();
}

通过 determineTargetDataSource方法获取连接

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = this.determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);

通过 determineCurrentLookupKey获取key进而获取数据源

private Map<Object, Object> targetDataSources;

这里定义的多数据源是map形式,因此我们只需要实现 determineCurrentLookupKey方法获取key即可。

功能实现:

配置多数据源:

jdbc.properties

#主库
jdbc.master.driver=com.mysql.jdbc.Driver
jdbc.master.url=jdbc:mysql://localhost:3306/drink?useUnicode=true&characterEncoding=utf-8
jdbc.master.username=root
jdbc.master.password=123456

#读库1
jdbc.slave1.driver=com.mysql.jdbc.Driver
jdbc.slave1.url=jdbc:mysql://192.168.1.111:3306/drink?useUnicode=true&characterEncoding=utf-8
jdbc.slave1.username=root
jdbc.slave1.password=123456

#读库2
jdbc.slave2.driver=com.mysql.jdbc.Driver
jdbc.slave2.url=jdbc:mysql://192.168.1.112:3306/drink?useUnicode=true&characterEncoding=utf-8
jdbc.slave2.username=root
jdbc.slave2.password=123456

spring-context-datasource.xml

  <!-- 定义多个连接池-->
<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.master.url}" />
    <property name="username" value="${jdbc.master.username}" />
    <property name="password" value="${jdbc.master.password}" />
    <!-- 配置监控统计拦截的filters -->
    <property name="filters" value="stat" />
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="5" />
    <property name="minIdle" value="5" />
    <property name="maxActive" value="20" />
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="50000" />
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="3000" />
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <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" />
    <property name="poolPreparedStatements" value="true" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
</bean>
<bean id="slave1DataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.slave1.url}" />
    <property name="username" value="${jdbc.slave1.username}" />
    <property name="password" value="${jdbc.slave1.password}" />
    <!-- 配置监控统计拦截的filters -->
    <property name="filters" value="stat" />
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="5" />
    <property name="minIdle" value="5" />
    <property name="maxActive" value="20" />
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="50000" />
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="3000" />
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <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" />
    <property name="poolPreparedStatements" value="true" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
</bean>
<bean id="slave2DataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.slave2.url}" />
    <property name="username" value="${jdbc.slave2.username}" />
    <property name="password" value="${jdbc.slave2.password}" />
    <!-- 配置监控统计拦截的filters -->
    <property name="filters" value="stat" />
    <!-- 配置初始化大小、最小、最大 -->
    <property name="initialSize" value="5" />
    <property name="minIdle" value="5" />
    <property name="maxActive" value="20" />
    <!-- 配置获取连接等待超时的时间 -->
    <property name="maxWait" value="50000" />
    <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
    <property name="timeBetweenEvictionRunsMillis" value="3000" />
    <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
    <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" />
    <property name="poolPreparedStatements" value="true" />
    <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
</bean>
<!-- 定义数据源,使用自己实现的数据源 -->
<bean id="dataSource" class="com.drink.common.datasource2.ChooseDataSource">
    <!-- 设置多个数据源 -->
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!-- 这个key需要和程序中的key一致 -->
            <entry key="master" value-ref="masterDataSource"/>
            <entry key="slave" value-ref="slave1DataSource"/>
            <entry key="slave" value-ref="slave2DataSource"/>
        </map>
    </property>
    <!-- 设置默认的数据源,这里默认走写库 -->
    <property name="defaultTargetDataSource" ref="masterDataSource"/>
</bean>

自定义数据源:

public class DynamicDataSource extends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey() {
    System.out.println("determineCurrentLookupKey:"+DynamicDataSourceHolder.getDataSourceKey());
    return DynamicDataSourceHolder.getDataSourceKey();
    }
}

动态数据源持有者:

public class DynamicDataSourceHolder{
/** 写库对应的数据源key */
public static final String MASTER = "master";
/** 读库对应的数据源key */
public static final String SLAVE = "slave";
/** 使用ThreadLocal记录当前线程的数据源key */
private static final ThreadLocal<String> holder = new ThreadLocal<String>();

/**
 * 设置数据源的key
 * @param key 数据源的key值
 */
public static void putDataSourceKey(String key) {
    holder.set(key);
}
/**
 * 获取数据源key
 * @return 获取的key
 */
public static String getDataSourceKey() {
    return holder.get();
}

/**
 * 清除
 */
public static void remove(){holder.remove();};

/**
 * 标记写库
 */
public static void markMaster(){
    putDataSourceKey(MASTER);
}

/**
 * 标记读库
 */
public static void markSlave(){ putDataSourceKey(SLAVE); }

/**
 * 判断是否为读库
 *
 * @param methodName 方法名称
 * @return 是否是读库
 */
public static Boolean isSlave(String methodName) {
    // 方法名以query、find、get开头的方法名走从库
    return StringUtils.startsWithAny(methodName, "query", "find", "get");
}
}

方案一,通过service的名称来决定数据源的类型,约定型的配置,不需要侵入代码:

@Aspect
@Component
@Order(-2147483648)
public class DataSourceAspect{

@Pointcut("execution(* com.drink..service.*.*(..))")
public void pointcut(){}

/**
 * 在进入Service方法之前执行
 *
 * @param point 切面对象
 */
@Before("pointcut()")
public void before(JoinPoint point) {
    System.out.println("===============before===============");
    // 获取到当前执行的方法名
    String methodName = point.getSignature().getName();
    System.out.println("方法名为:"+methodName);
    if (isSlave(methodName)) {
        // 标记为读库
        System.out.println("标记为读库");
        DynamicDataSourceHolder.markSlave();
    } else {
        // 标记为写库
        System.out.println("标记为写库");
        DynamicDataSourceHolder.markMaster();
    }
    System.out.println("执行了");
}

@AfterReturning("pointcut()")
public void after(){
    System.out.println("关闭");
    DynamicDataSourceHolder.remove();
}

/**
 * 判断是否为读库
 *
 * @param methodName
 * @return
 */
private Boolean isSlave(String methodName) {
    // 方法名以query、find、get开头的方法名走从库
    return StringUtils.startsWithAny(methodName, "query", "find", "get");
}
}

方案二,自定义注解,在需要执行的mapper或者service上标记,在aop切点里获取配置好的数据源类型来切换数据源。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
    String value();
}

@Component
@Aspect
@Order(-2147483648)
public class DataSourceAspect {

    @Pointcut("execution(* com.drink..service.*.*(..))")
    public void pointcut(){}

    @Before("pointcut()")
    public void before(JoinPoint point){
        Object target = point.getTarget();
        System.out.println(target.toString());
        String method = point.getSignature().getName();
        System.out.println(method);
        Class<?>[] classz = target.getClass().getInterfaces();
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
                .getMethod().getParameterTypes();
        try {
            Method m = classz[0].getMethod(method, parameterTypes);
            System.out.println(m.getName());
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                DataSource data = m.getAnnotation(DataSource.class);
                System.out.println("datasource:"+data.value());
                HandleDataSource.putDataSource(data.value());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @AfterReturning("pointcut()")
    public void after(){
        System.out.println("关闭");
        DynamicDataSourceHolder.remove();
    }    
}

使用的时候在相关的service接口的方法上标注:

例如:

 public interface SysUserService {
    @DataSource("master")
    public SysUser findByUsername(String username);
}

这里注意几个坑:

1、上面的aop必须用注解,使用配置的话一直报错,暂时没找到原因。

2、aop配置的时候一定要加上order注解,因为事务的aop或许比数据源切换的aop高,切换数据源的aop要在事务之前。

3、aop代理用的是CGLIB

JDK动态代理和CGLIB字节码生成的区别?

  • JDK动态代理只能对实现了接口的类生成代理,而不能针对类
  • CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
    因为是继承,所以该类或方法最好不要声明成final
      <aop:aspectj-autoproxy proxy-target-class="true"/>

4、在controller中的aop,很多人扫描包的时候都是扫描最顶层的包,这样的话不仅将controller扫到还扫描了service等包,

spring-mvc.xml与applicationContext.xml不是同时加载,如果不进行这样的设置,那么,spring就会将所有带@Service注解的类都扫描到容器中,

等到加载applicationContext.xml的时候,会因为容器已经存在Service类,使得cglib将不对Service进行代理,直接导致的结果就是在applicationContext 中的事务配置不起作用,

发生异常时,无法对数据进行回滚。以上就是原因所在。

因此在mvc扫描包的时候不要扫其他包,在applicationContext扫描的时候不要包含controller包。

<context:component-scan base-package="com.drink.modules.sys.controller,com.drink.modules.test.controller,com.drink.modules.activemq.controller"></context:component-scan>

或者

    <!-- 扫描@Controller注解 ,如何不配置use-default-filters="false"依然会扫描其他包-->
    <context:component-scan base-package="com.drink" use-default-filters="false">
        <context:include-filter type="annotation"
            expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

<context:include-filter>引入   <context:exclude-filter>排除、过滤  

相应的@Service、@Repository、@Controller扫描

<context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository" />
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值