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" />