如果你的后台结构是spring+mybatis,可以通过spring的AbstractRoutingDataSource和mybatis Plugin拦截器实现非常友好的读写分离,原有代码不需要任何改变。推荐第四种方案。
一、通过Spring+MyBatis实现数据库读写分离的实现来引入MyBatis的Intercepter
上面网站的第四种方案非常的奈斯,只要引入方案中的五个类,然后在spring配置文件里做点配置就能完美的实现读写分离。先来介绍一下实现的原理。
实现的重点是下面这两个:
1、org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
spring提供的这个类能够让我们实现运行时多数据源的动态切换,但是数据源是需要事先配置好的,无法动态的增加数据源。但是对于小项目来说已经足够了。
2、MyBatis提供的@Intercepts、@Signature注解和org.apache.ibatis.plugin.Interceptor接口。
另外还有一个就是ThreadLocal类,用于保存每个线程正在使用的数据源。ThreadLocal的原理之前已经分析过了。
运行流程差不多是这样的:
1、我们自己写的MyBatis的Interceptor按照@Signature的规则拦截下Executor.class的update和query方法
2、判断是读还是写方法,然后在一个Holder类中的静态不可变的ThreadLocal里保存一个读或者写数据源对应的key,每个线程持有自己的变量。
3、线程再根据这个变量作为key,借助SPRING提供的AbstractRoutingDataSource重写determineCurrentLookupKey()方法,从我们配置好的全局静态的HashMap中取出当前要用的读或者写数据源
4、返回对应数据源的connection去做相应的数据库操作
二、MyBatis的Intercepter(Plugin)
1、做上面的读写分离的时候实现Intercepter的类加了如下的注解
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) })
public class DynamicPlugin implements Interceptor {
@Intercepts 在实现Interceptor接口的类声明,使该类注册成为拦截器
Signature[] value //定义需要拦截哪些类的哪些方法
@Signature 要拦截的类(如下四种),拦截方法,方法对应参数
Class<?> type() //ParameterHandler,ResultSetHandler,StatementHandler,Executor
String method()
Class<?>[] args()
实现Interceptor
每一个拦截器都必须实现上面的三个方法,其中:
1、 Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。
2、Object plugin(Object target) 就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this) 来完成的,把目标target和拦截器this传给了包装函数。
3、setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里。
注解里描述的是指定拦截方法的签名 [type,method,args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。
在结合spring的项目中要将我们写的拦截器注入SqlSessionFactoryBean中
因为在创建ParameterHandler,ResultSetHandler,StatementHandler,Executor这四个类的实现类的时候,我们配置的拦截器链会先判断是否要进行拦截,需要拦截的话返回代理包装后的对象,否则直接返回原对象。
因此可以在实现plugin方法时判断一下目标类型,是本插件要拦截的类才执行Plugin.wrap方法,否则直接返回目标本身。
虽然在Plugin.wrap生成代理对象的方法中有做了判断,如果有包含我们要拦截的接口就进行包装代理,否则返回原对象。但是提前自己进行判断就不用在进入wrap方法。
总结
Mybatis的拦截器实现机制,使用的是JDK的InvocationHandler.
当我们调用ParameterHandler,ResultSetHandler,StatementHandler,Executor的对象的时候,
实际上使用的是Plugin这个代理类的对象,这个类实现了InvocationHandler接口.
接下来我们就知道了,在调用上述被代理类的方法的时候,就会执行Plugin的invoke方法.
Plugin在invoke方法中根据@Intercepts的配置信息(方法名,参数等)动态判断是否需要拦截该方法.
再然后使用需要拦截的方法Method封装成Invocation,并调用Interceptor的proceed方法.
这样我们就达到了拦截目标方法的结果.
例如Executor的执行大概是这样的流程:
拦截器代理类对象->拦截器->目标方法
Executor->Plugin->Interceptor->Invocation
Executor.Method->Plugin.invoke->Interceptor.intercept->Invocation.proceed->method.invoke
http://blog.csdn.net/hupanfeng/article/details/9247379
/**
* 每一个拦截器对目标类都进行一次代理
* @paramtarget
* @return 层层代理后的对象
*/
public Object pluginAll(Object target) {
for(Interceptor interceptor : interceptors) {
target= interceptor.plugin(target);
}
returntarget;
}
每一个拦截器对目标类都进行一次代理,原对象如果是X,那么第一个拦截器代理后为P(X),第二个代理后P(P(X))......最后返回这样的多重代理对象并执行。所以先配置的拦截器会后执行,因为先配置的先被包装成代理对象。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if(methods != null && methods.contains(method)) {
//调用代理类所属拦截器的intercept方法,
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch(Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
最后在调用真实对象方法的时候,实际上是调用多重代理的invoke方法,当符合拦截条件的时候执行我们编写的interceptor.intercept,intercept方法最后必定是调用invocation.proceed,proceed也是一个method.invoke,促使拦截链往下进行,不符合拦截条件的时候直接调用method.invoke,即不执行我们的拦截方法,继续拦截链。