Java Mybatis Plugin插件实现分表路由规则
2015-08-03 18:37
453 查看
Mybatis Plugin插件种类
mybatis支持对于Executor、
StatementHandler、
PameterHandler、
ResultSetHandler做拦截。要想通过拦截器做分表路由可以在
Executor或
StatementHandler两个阶段进行拦截。本次的路由实现是在
StatementHandler拦截Sql在通过Rule修改Sql的表名,这样系统原有的Sql不用修改表名会自动替换成路由计算出的表名。
定义mybatis-config.xml配置文件
<plugins> <plugin interceptor="com.*.settlement.provider.bill.prepay.dao.shard.BillShardDefault"> <!-- 必须以xxStrategy结尾 --> <property name="defaultStrategy" value="com.strategy.DefaultShardStrategy"/> <!-- prepay_bill_detail_index|defaultStrategy, prepay_bill_detail_content, prepay_bill_sheet_fee --> <!-- 表名|路由策略 --> <!-- 策略必须继承 com.strategy.ShardStrategy --> <!-- 如果不指定Strategy则使用默认的策略 --> <!-- 使用了路由策略表对应的Dao method 必须包含参数 @Param("_shardParam")@NotNull ShardParam shardParam 参见: void insert( @Param("detailIndex")PrepayBillDetailIndex billDetail, @Param("_shardParam")@NotNull ShardParam shardParam); --> <property name="tableNames" value=" prepay_bill_detail_index|defaultStrategy, prepay_bill_detail_content, prepay_bill_sheet_fee" /> </plugin> </plugins>
编写自己的拦截器继承拦截器接口类Plugin
ShardStrategy 接口 & 实现类 DefaultShardStrategy
ShardStrategy 接口: /** * Shard分表策略 * Created by xueping.you on 15-7-29. */ public interface ShardStrategy{ /** * 获取表名 * @param param * @return */ String getTableName(String tableName ,ShardParam param); } DefaultShardStragy 实现: /** * Created by xueping.you on 15-7-29. */ public class DefaultShardStrategy extends ShardStrategy{ /** * @param tableName 例如:test * @param param1 例如:new ShardParam(new Date() , BizSystem.OTR) * @return 例如:test_201509 */ @Override public String getTableName(String tableName , ShardParam param1) { StringBuilder builder = new StringBuilder(tableName); builder.append("_"); builder.append(getAssShardParam(param1)); return builder.toString(); } public static String getAssShardParam(ShardParam param1){ StringBuilder builder = new StringBuilder(); if(param1.getBizSystem().equals(BizSystem.OTR)){ builder.append(BizSystem.OTR.name()); } builder.append(param1.getDivideDate().getYear() + 1900 + ""); int month = param1.getDivideDate().getMonth() + 1; if( month <10 ){ builder.append("0"+month); }else { builder.append(month); } return builder.toString(); } }
Interceptor实现
可以通过Annotation@Interceptor申明拦截器属于四中拦截器里面的哪种,并且可以指定拦截的接口方法以及方法的参数
/** * Created by xueping.you on 15-7-29. */ @Intercepts( { @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }) public class BillShardDefault implements Interceptor { private final static Logger logger = LoggerFactory.getLogger(BillShardDefault.class); private String tableName; private List<String> tableNames = Lists.newArrayList(); /** * 线程安全类,初始化常量,避免重复创建 * **/ private final static ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory(); /** * 线程安全类,初始化常量,避免重复创建 * **/ private final static ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory(); private final static String BOUNDSQL_NAME = "delegate.boundSql"; private final static String BOUNDSQL_SQL_NAME = "delegate.boundSql.sql"; private final static String SQL_PARAM_NAME = "delegate.parameterHandler.parameterObject"; private final static ShardStrategy DEFAULTSTRATEGY = new DefaultShardStrategy(); private final static String SHARDPARAM = "_shardParam"; private final static String STRATEGY_SUFFIX = "Strategy"; private Map<String , ShardStrategy> STRATEGY_CONTEXT = Maps.newHashMap(); private Map<String , ShardStrategy> TABLE_ROUTER = Maps.newHashMap(); @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); MetaObject metaObject = MetaObject.forObject( statementHandler , DEFAULT_OBJECT_FACTORY , DEFAULT_OBJECT_WRAPPER_FACTORY); BoundSql boundSql = (BoundSql)metaObject.getValue(BOUNDSQL_NAME); String executeSql = boundSql.getSql(); MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)metaObject.getValue(SQL_PARAM_NAME); ShardParam shardParam = null; if (paramMap.containsKey(SHARDPARAM)) { shardParam = (ShardParam)paramMap.get(SHARDPARAM); } /** 临时注释掉这段代码 数据迁移完成需要去掉 **/ //checkArgument(shardParam!=null , "_shardParam Param Can't Be Null"); if(shardParam!=null){ /** 临时if条件判断数据迁移完成需要去掉 **/ executeSql = decorateSql(executeSql , shardParam); metaObject.setValue(BOUNDSQL_SQL_NAME,executeSql); } return invocation.proceed(); } private String decorateSql(String executeSql, ShardParam shardParam) { for(String tableName : tableNames){ String newTaleName = TABLE_ROUTER.get(tableName).getTableName(tableName , shardParam); executeSql = executeSql.replaceAll(tableName,newTaleName); } return executeSql; } @Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target , this); }else { return target; } } /** * Don't Modify Any Code * @param properties */ @Override public void setProperties(Properties properties) { setShardStrategy(properties); setTableNames(properties); } private void setTableNames(Properties properties){ tableName = properties.getProperty("tableNames"); checkArgument(!Strings.isEmpty(tableName) , "参数[tableNames]必填!"); List<String> tempTableRouterStrList = Lists.newArrayList( Splitter.on(",").trimResults().omitEmptyStrings().split(tableName) ); for(String tempTableRouterStr : tempTableRouterStrList){ List<String> single = Lists.newArrayList( Splitter.on("|").omitEmptyStrings().trimResults().split(tempTableRouterStr) ); checkArgument(!CollectionUtils.isEmpty(single) , "Config is not correct!"); tableNames.add(single.get(0)); if(single.size()==1){ TABLE_ROUTER.put(single.get(0), DEFAULTSTRATEGY); }else { TABLE_ROUTER.put(single.get(0), STRATEGY_CONTEXT.get(single.get(1))); } } } private void setShardStrategy(Properties properties){ try { for(Map.Entry entry : properties.entrySet()){ String strategyClassNameKey = entry.getKey().toString(); if(strategyClassNameKey.indexOf(STRATEGY_SUFFIX)!=-1){ String strategyClassName = entry.getValue().toString(); Class strategyClass = Class.forName(strategyClassName); Object o = strategyClass.newInstance(); if(o instanceof ShardStrategy){ STRATEGY_CONTEXT.put(strategyClassNameKey , (ShardStrategy)o); }else { throw new IllegalStateException( "strategyClass must implement interface ShardStrategy<P>" ); } } } }catch (Exception e){ logger.error("生成ShardStrategy策略失败", e); throw new RuntimeException(e); } } }
PS说说Mybatis mybatis-config.xml
文件的解析&MybatisPlugin
代码逻辑
Mybatis通过 SqlSessionFactoryBuilder 构造 SqlSessionFactory
在mybatis中存在一个SqlSessionFactoryBuilder类用于在实例起来时构造Session工厂实例。涉及到的最终方法:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {....}该方法中会调用
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse()); 去解析配置文件,里面会涉及到解析pulgins配置、typeAliases配置、settings配置etc,最终会将配置加载到全文配置Configure中,在Executor 或者 StatementHandler中会使用
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);来执行Plugin。
解析方法链:
Created with Raphaël 2.1.0sessionFactoryBuildersessionFactoryBuilderxmlConfigBuilderxmlConfigBuilderConfigurationConfigurationInterceptorChainInterceptorChainparser.parse()builder.build()调用XmlConfigBuilder.parser()configuration.addInterceptor()XMLConfiguretion解析配置文件将Plugin实例化后加入到全文配置Configure中chain.addInterceptor()最终调用InterceptorChain的addInterceptor()方法加入掉调用链上。