package z.farrell.framework.core.datasource;
/**
*
* 读/写动态数据库 决策者
* 根据DataSourceType是write/read 来决定是使用读/写数据库
* 通过ThreadLocal绑定实现选择功能
*
* Created by farrell on 2017/1/6.
*/
public class ReadWriteDataSourceDecision {
private static final ThreadLocal holder = new ThreadLocal<>();
public enum DataSourceType {
write, read;
}
/**
* 设置为写数据库
*/
public static void markWrite() {
holder.set(DataSourceType.write);
}
/**
* 设置为读数据库
*/
public static void markRead() {
holder.set(DataSourceType.read);
}
/**
* 重置
*/
public static void reset() {
holder.set(null);
}
/**
* 判断是否没有选择数据源
*
* @return
*/
public static boolean isChoiceNone() {
return null == holder.get();
}
/**
* 判断是否选择的写数据库
*
* @return true是 false不是
*/
public static boolean isChoiceWrite() {
return DataSourceType.write == holder.get();
}
/**
* 判断是否选择的读数据库
*
* @return true是 false不是
*/
public static boolean isChoiceRead() {
return DataSourceType.read == holder.get();
}
}
package z.farrell.framework.core.datasource;
import org.springframework.core.NestedRuntimeException;
/**
* Created by farrell on 2017/1/6.
*/
public class ReadWriteDataSourceTransactionException extends NestedRuntimeException{
private static final long serialVersionUID = 6021269017524827565L;
/**
* Construct a {@code NestedRuntimeException} with the specified detail message
* and nested exception.
*
* @param msg the detail message
* @param cause the nested exception
*/
public ReadWriteDataSourceTransactionException(String msg, Throwable cause) {
super(msg, cause);
}
}
package z.farrell.framework.core.datasource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
*
*
* 此类实现了两个职责(为了减少类的数量将两个功能合并到一起了):
* 读/写动态数据库选择处理器
* 通过AOP切面实现读/写选择
*
*
* ★★读/写动态数据库选择处理器★★
* 1、首先读取事务属性配置
*
* 2、对于所有读方法设置 read-only="true" 表示读取操作(以此来判断是选择读还是写库),其他操作都是走写库
* 如
*
* 3、 forceChoiceReadWhenWrite用于确定在如果目前是写(即开启了事务),下一步如果是读,
* 是直接参与到写库进行读,还是强制从读库读
* forceChoiceReadWhenWrite:true 表示目前是写,下一步如果是读,强制参与到写事务(即从写库读)
* 这样可以避免写的时候从读库读不到数据
*
* 通过设置事务传播行为:SUPPORTS实现
*
* forceChoiceReadWhenWrite:false 表示不管当前事务是写/读,都强制从读库获取数据
* 通过设置事务传播行为:NOT_SUPPORTS实现(连接是尽快释放)
* 『此处借助了 NOT_SUPPORTS会挂起之前的事务进行操作 然后再恢复之前事务完成的』
* 4、配置方式
*
*
*
*
* 5、目前只适用于情况 , 支持@Transactional注解事务
*
*
*
* ★★通过AOP切面实现读/写库选择★★
*
* 1、首先将当前方法 与 根据之前【读/写动态数据库选择处理器】 提取的读库方法 进行匹配
*
* 2、如果匹配,说明是读取数据:
* 2.1、如果forceChoiceReadWhenWrite:true,即强制走读库
* 2.2、如果之前是写操作且forceChoiceReadWhenWrite:false,将从写库进行读取
* 2.3、否则,到读库进行读取数据
*
* 3、如果不匹配,说明默认将使用写库进行操作
*
* 4、配置方式
*
*
*
* 4.1、此处order = Integer.MIN_VALUE 即最高的优先级(请参考http://jinnianshilongnian.iteye.com/blog/1423489)
* 4.2、切入点:txPointcut 和 实施事务的切入点一样
* 4.3、determineReadOrWriteDB方法用于决策是走读/写库的,请参考
* @ z.farrell.framework.core.datasource.ReadWriteDataSourceDecision
* @ z.farrell.framework.core.datasource.ReadWriteDataSource
*
*
* Created by farrell on 2017/1/6.
*/
public class ReadWriteDataSourceProcessor implements BeanPostProcessor {
/**
* log
*/
private final Logger logger = LoggerFactory.getLogger(ReadWriteDataSourceProcessor.class);
/**
* 是否强制转换为读库true即强制走读库,false将从写库进行读取,默认为false
*/
private boolean forceChoiceReadWhenWrite = false;
private Map readMethodMap = new HashMap<>();
/**
* Apply this BeanPostProcessor to the given new bean instance before any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method). The bean will already be populated with property values.
* The returned bean instance may be a wrapper around the original.
*
* @param bean the new bean instance
* @param beanName the name of the bean
* @return the bean instance to use, either the original or a wrapped one;
* if {@code null}, no subsequent BeanPostProcessors will be invoked
* @throws BeansException in case of errors
* @see InitializingBean#afterPropertiesSet
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
/**
* Apply this BeanPostProcessor to the given new bean instance after any bean
* initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
* or a custom init-method). The bean will already be populated with property values.
* The returned bean instance may be a wrapper around the original.
* In case of a FactoryBean, this callback will be invoked for both the FactoryBean
* instance and the objects created by the FactoryBean (as of Spring 2.0). The
* post-processor can decide whether to apply to either the FactoryBean or created
* objects or both through corresponding {@code bean instanceof FactoryBean} checks.
*
This callback will also be invoked after a short-circuiting triggered by a
* {@link InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation} method,
* in contrast to all other BeanPostProcessor callbacks.
*
* @param bean the new bean instance
* @param beanName the name of the bean
* @return the bean instance to use, either the original or a wrapped one;
* if {@code null}, no subsequent BeanPostProcessors will be invoked
* @throws BeansException in case of errors
* @see InitializingBean#afterPropertiesSet
* @see FactoryBean
*/
@SuppressWarnings("unchecked")
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof NameMatchTransactionAttributeSource))
return bean;
NameMatchTransactionAttributeSource transactionAttributeSource = (NameMatchTransactionAttributeSource) bean;
Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class,"nameMap");
nameMapField.setAccessible(true);
try {
Map nameMap = (Map) nameMapField.get(transactionAttributeSource);
for (Map.Entry entry:nameMap.entrySet()){
RuleBasedTransactionAttribute attr = (RuleBasedTransactionAttribute) entry.getValue();
//仅对read-only的处理
if (!attr.isReadOnly())
continue;
String methodName = entry.getKey();
Boolean isForceChoiceRead = Boolean.FALSE;
if (forceChoiceReadWhenWrite){
// 不管之前操作是写,默认强制从读库读 (设置为NOT_SUPPORTED即可)
// NOT_SUPPORTED会挂起之前的事务
attr.setPropagationBehavior(Propagation.NOT_SUPPORTED.value());
isForceChoiceRead = Boolean.TRUE;
}else{
// 否则 设置为SUPPORTS(这样可以参与到写事务)
attr.setPropagationBehavior(Propagation.SUPPORTS.value());
}
logger.debug("read/write transaction process method:{} force read:{}", methodName, isForceChoiceRead);
readMethodMap.put(methodName, isForceChoiceRead);
}
} catch (IllegalAccessException e) {
throw new ReadWriteDataSourceTransactionException("process read/write transaction error", e);
}
return bean;
}
/**
* 选择读库?写库?
* @param pjp
* @return
*/
public Object determineReadOrWriteDB(ProceedingJoinPoint pjp) throws Throwable {
if (isChoiceReadDB(pjp.getSignature().getName()))
ReadWriteDataSourceDecision.markRead();
else
ReadWriteDataSourceDecision.markWrite();
try{
return pjp.proceed();
}finally {
ReadWriteDataSourceDecision.reset();
}
}
/**
* 是否选择读库
*
* @param methodName
* @return true 选择读库
* false 选择写库
*/
private boolean isChoiceReadDB(String methodName) {
String bestNameMatch = null;
for (String mappedName : readMethodMap.keySet()) {
if (isMatch(methodName, mappedName)) {
bestNameMatch = mappedName;
break;
}
}
Boolean isForceChoiceRead = readMethodMap.get(bestNameMatch);
//表示强制选择读库
if (isForceChoiceRead == Boolean.TRUE)
return true;
//如果之前选择了写库,现在还选择写库
if (ReadWriteDataSourceDecision.isChoiceWrite())
return false;
//表示应该选择读库
if (isForceChoiceRead != null)
return true;
//默认选择写库
return false;
}
/**
* 判断方法名称与springAOP拦截名称是否匹配
*
* @param methodName 方法名
* @param mappedName 拦截名
* @return
*/
private boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
}