Java Mybatis Plugin插件實現分表路由規則

Mybatis Plugin插件種類

mybatis支持對於ExecutorStatementHandlerPameterHandlerResultSetHandler做攔截。要想通過攔截器做分表路由可以在ExecutorStatementHandler兩個階段進行攔截。本次的路由實現是在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()方法加入掉調用鏈上。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章