diff --git a/mallinkAdmin/pom.xml b/mallinkAdmin/pom.xml
index 02831b8d3..ec65f306d 100644
--- a/mallinkAdmin/pom.xml
+++ b/mallinkAdmin/pom.xml
@@ -18,7 +18,11 @@
mallinkService
1.0
-
+
+ com.iformall
+ mybatis-multi-tenancy
+ 1.0
+
diff --git a/mallinkBApi/pom.xml b/mallinkBApi/pom.xml
index 9f2f013a8..a6c7c0588 100644
--- a/mallinkBApi/pom.xml
+++ b/mallinkBApi/pom.xml
@@ -17,7 +17,11 @@
mallinkService
1.0
-
+
+ com.iformall
+ mybatis-multi-tenancy
+ 1.0
+
diff --git a/mallinkCApi/pom.xml b/mallinkCApi/pom.xml
index 7935ee17e..647618dbc 100644
--- a/mallinkCApi/pom.xml
+++ b/mallinkCApi/pom.xml
@@ -18,8 +18,9 @@
1.0
- junit
- junit
+ com.iformall
+ mybatis-multi-tenancy
+ 1.0
diff --git a/mallinkService/pom.xml b/mallinkService/pom.xml
index 5f7a6419c..2ea9abea7 100644
--- a/mallinkService/pom.xml
+++ b/mallinkService/pom.xml
@@ -10,4 +10,14 @@
4.0.0
mallinkService
+
\ No newline at end of file
diff --git a/mybatis-multi-tenancy/pom.xml b/mybatis-multi-tenancy/pom.xml
new file mode 100644
index 000000000..19c112f64
--- /dev/null
+++ b/mybatis-multi-tenancy/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+ com.iformall
+ mybatis-multi-tenancy
+ 1.0
+
+
+ com.github.jsqlparser
+ jsqlparser
+ 1.3
+ compile
+
+
+ org.mybatis
+ mybatis
+ 3.4.6
+ compile
+
+
+
+ test
+ junit
+ junit
+ 4.12
+
+
+ test
+ mysql
+ mysql-connector-java
+ 8.0.11
+
+
+ test
+ com.alibaba
+ druid
+ 1.1.11
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 2.3.2
+
+ 1.8
+ 1.8
+ utf-8
+
+
+
+ maven-source-plugin
+ 2.4
+
+
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ maven-surefire-plugin
+ 2.9
+
+
+ false
+
+ once
+ -Dfile.encoding=UTF-8
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mybatis-multi-tenancy/src/main/java/com/iformall/plugin/MultiTenancy.java b/mybatis-multi-tenancy/src/main/java/com/iformall/plugin/MultiTenancy.java
new file mode 100644
index 000000000..afef9d6fe
--- /dev/null
+++ b/mybatis-multi-tenancy/src/main/java/com/iformall/plugin/MultiTenancy.java
@@ -0,0 +1,553 @@
+package com.iformall.plugin;
+
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.BinaryExpression;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
+import net.sf.jsqlparser.statement.create.table.CreateTable;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.statement.update.Update;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import org.apache.ibatis.executor.statement.StatementHandler;
+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.reflection.MetaObject;
+import org.apache.ibatis.reflection.SystemMetaObject;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.ResultHandler;
+
+import java.sql.Connection;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ *共享数据库的多租户系统实现
+ * TODO 白名单模式
+ * TODO 黑名单模式
+ * Created by kleen@qq.com on 2016/08/13.
+ */
+@Intercepts({
+ //@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
+ //@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
+ @Signature( method = "prepare", type = StatementHandler.class,args = {Connection.class, Integer.class}),
+ @Signature(method = "query", type = StatementHandler.class, args = {java.sql.Statement.class, ResultHandler.class})
+})
+public class MultiTenancy implements Interceptor {
+
+ //获取tenantId的接口
+ private TenantInfo tenantInfo;
+ private String tenantIdColumn = "tenant_id";
+ private String dialect = "mysql";
+ //属性参数信息
+ private Properties properties;
+
+ public MultiTenancy(){
+ }
+
+ public Object intercept(Invocation invocation) throws Throwable {
+ if (getTenantInfo().getTenantId() != null) {
+ // 能从cookie或者session获取到tenantId
+ mod(invocation);
+ }
+ return invocation.proceed();
+ }
+
+ /**
+ * 更改MappedStatement为新的
+ * @param invocation
+ * @throws Throwable
+ */
+ public void mod(Invocation invocation) throws Throwable {
+ StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
+ MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
+ Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
+
+ MappedStatement ms = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
+ if (getTenantInfo().doMappedStatementFIlter(ms))
+ return;
+ SqlCommandType type = ms.getSqlCommandType();
+
+ BoundSql boundSql = statementHandler.getBoundSql();
+
+ String newSQL = addWhere(boundSql.getSql());
+ if (newSQL != null) {
+ metaStatementHandler.setValue("delegate.boundSql.sql", newSQL);
+ }
+
+ }
+
+ /**
+ * 添加租户id条件
+ * @param sql
+ * @return
+ * @throws JSQLParserException
+ */
+ private String addWhere(String sql) throws Exception {
+ Statement stmt = CCJSqlParserUtil.parse(sql);
+ if (stmt instanceof Insert) {
+ //获得Insert对象
+ Insert insert = (Insert) stmt;
+ if (getTenantInfo().doTableFilter(insert.getTable().getName()))
+ return insert.toString();
+ boolean bExist = false;
+ for(Column col:insert.getColumns()) {
+ if(col.getColumnName().equalsIgnoreCase(getTenantIdColumn()))
+ bExist = true;
+ }
+ if (!bExist) {
+ insert.getColumns().add(new Column(getTenantIdColumn()));
+
+ if (insert.getItemsList() instanceof MultiExpressionList){
+ for (ExpressionList expressionList : ((MultiExpressionList) insert.getItemsList()).getExprList()) {
+ addTenantValue(expressionList);
+ }
+ }else {
+ addTenantValue(((ExpressionList) insert.getItemsList()));
+ }
+ }
+ return insert.toString();
+ }
+
+ if (stmt instanceof Delete) {
+ //获得Delete对象
+ Delete deleteStatement = (Delete) stmt;
+ if (getTenantInfo().doTableFilter(deleteStatement.getTable().getName()))
+ return deleteStatement.toString();
+ Expression where = deleteStatement.getWhere();
+ if (where instanceof BinaryExpression) {
+ EqualsTo equalsTo = new EqualsTo();
+ equalsTo.setLeftExpression(new Column(getTenantIdColumn()));
+ equalsTo.setRightExpression(new StringValue(getTenantInfo().getTenantId()));
+ AndExpression andExpression = new AndExpression(equalsTo, where);
+ deleteStatement.setWhere(andExpression);
+ }
+ return deleteStatement.toString();
+ }
+
+ if (stmt instanceof Update) {
+ //获得Update对象
+ Update updateStatement = (Update) stmt;
+ TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
+ List tableList = tablesNamesFinder.getTableList(stmt);
+ if (tableList.size() == 0) {
+ return updateStatement.toString();
+ }
+ //获得where条件表达式
+ Expression where = updateStatement.getWhere();
+ for (String table: tableList) {
+ if (getTenantInfo().doTableFilter(table))
+ continue;
+ if (where != null) {
+ updateStatement.setWhere(addAndExpression(stmt, table, updateStatement.getWhere()));
+ } else {
+ throw new Exception("update语句不能没有where条件:" + sql + Arrays.toString(Thread.currentThread().getStackTrace()));
+ }
+ }
+ return updateStatement.toString();
+ }
+
+ if (stmt instanceof Select) {
+ //获得Select对象
+ Select select = (Select) stmt;
+ PlainSelect ps = (PlainSelect) select.getSelectBody();
+ TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
+ List tableList = tablesNamesFinder.getTableList(select);
+
+ if (tableList.size() == 0) {
+ return select.toString();
+ }
+ for (String table : tableList) {
+ if (getTenantInfo().doTableFilter(table))
+ continue;
+ if (ps.getWhere() != null) {
+ AndExpression where = addAndExpression(stmt, table, ps.getWhere());
+ // form 和 join 中加载的表
+ if (where != null) {
+ ps.setWhere(where);
+ } else {
+ //子查询中的表
+ findSubSelect(stmt, ps.getWhere());
+ }
+ } else {
+ ps.setWhere(addEqualsTo(stmt, table));
+ }
+ }
+ return select.toString();
+ }
+
+ if (stmt instanceof CreateTable) {
+ CreateTable createTable = (CreateTable) stmt;
+ ColumnDefinition columnDefinition = new ColumnDefinition();
+ columnDefinition.setColumnName(getTenantIdColumn());
+ ColDataType colDataType = new ColDataType();
+ colDataType.setDataType("varchar(50)");
+ columnDefinition.setColDataType(colDataType);
+ createTable.getColumnDefinitions().add(columnDefinition);
+ return createTable.toString();
+ }
+ throw new RuntimeException("非法sql语句,请检查"+sql);
+ }
+
+ /**
+ * 生成代理对象
+ *
+ * @param target
+ * @return
+ */
+ @Override
+ public Object plugin(Object target) {
+ if (target instanceof StatementHandler) {
+ return Plugin.wrap(target, this);
+ } else {
+ return target;
+ }
+ }
+
+ /**
+ * 获取配置信息
+ * {dialect=mysql, tenantInfo=org.xue.test.TenantInfoImpl, tenantIdColumn=tenant_id}
+ * @param properties
+ */
+ public void setProperties(Properties properties) {
+ this.properties=properties;
+ try {
+ Class onwClass=Class.forName(this.properties.getProperty("tenantInfo"));
+ this.tenantInfo = (TenantInfo)onwClass.newInstance();
+ this.tenantIdColumn = this.properties.getProperty("tenantIdColumn");
+ this.dialect = this.properties.getProperty("dialect");
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 插入数据 添加租户id
+ * @param expressionList
+ * @throws Exception
+ */
+ private void addTenantValue(ExpressionList expressionList) throws Exception {
+ expressionList.getExpressions().add(new StringValue(getTenantInfo().getTenantId()));
+ }
+
+ /**
+ * 多条件情况下,使用AndExpression给where条件加上tenantid条件
+ *
+ * @param table
+ * @param where
+ * @return
+ * @throws Exception
+ */
+ public AndExpression addAndExpression(Statement stmt, String table, Expression where) throws Exception {
+ EqualsTo equalsTo = addEqualsTo(stmt, table);
+ if (equalsTo != null) {
+ return new AndExpression(equalsTo, where);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 创建一个 EqualsTo相同判断 条件
+ *
+ * @param stmt 查询对象
+ * @param table 表名
+ * @return “A=B” 单个where条件表达式
+ * @throws Exception
+ */
+ public EqualsTo addEqualsTo(Statement stmt, String table) throws Exception {
+ EqualsTo equalsTo = new EqualsTo();
+ String aliasName;
+ aliasName = getTableAlias(stmt, table);
+ if (aliasName != null) {
+ equalsTo.setLeftExpression(new Column(aliasName + '.' + getTenantIdColumn()));
+ equalsTo.setRightExpression(new StringValue(getTenantInfo().getTenantId()));
+ return equalsTo;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 获取sql送指定表的别名你,没有别名则返回原表名 如果表名不存在返回null
+ * 【仅查询from和join 不含 IN 子查询中的表 】
+ *
+ * @param stmt
+ * @param tableName
+ * @return
+ */
+ public String getTableAlias(Statement stmt, String tableName) {
+ String alias = null;
+
+ // 插入不做处理
+ if (stmt instanceof Insert) {
+ return tableName;
+ }
+
+ if (stmt instanceof Delete) {
+ //获得Delete对象
+ Delete deleteStatement = (Delete) stmt;
+
+ if ((deleteStatement.getTable()).getName().equalsIgnoreCase(tableName)) {
+ alias = deleteStatement.getTable().getAlias() != null ? deleteStatement.getTable().getAlias().getName() : tableName;
+ }
+ }
+
+ if (stmt instanceof Update) {
+ //获得Update对象
+ Update updateStatement = (Update) stmt;
+
+ if ((updateStatement.getTables().get(0)).getName().equalsIgnoreCase(tableName)) {
+ alias = updateStatement.getTables().get(0).getAlias() != null ? updateStatement.getTables().get(0).getAlias().getName() : tableName;
+ }
+ }
+
+ if (stmt instanceof Select) {
+ Select select = (Select) stmt;
+
+ PlainSelect ps = (PlainSelect) select.getSelectBody();
+ FromItem item = ps.getFromItem();
+
+ // 判断主表的别名
+ if (item instanceof Table) {
+ if (((Table) item).getName().equalsIgnoreCase(tableName)) {
+ alias = item.getAlias() != null ? item.getAlias().getName() : tableName;
+ }
+ } else {
+
+ }
+ }
+ return alias;
+ }
+
+ /**
+ * 针对子查询中的表别名查询
+ *
+ * @param subSelect
+ * @param tableName
+ * @return
+ */
+ public String getTableAlias(SubSelect subSelect, String tableName) {
+ PlainSelect ps = (PlainSelect) subSelect.getSelectBody();
+ // 判断主表的别名
+ String alias = null;
+ FromItem item = ps.getFromItem();
+ if (item instanceof Table) {
+ if (((Table) item).getName().equalsIgnoreCase(tableName)){
+ if (item.getAlias() != null) {
+ alias = item.getAlias().getName();
+ } else {
+ alias = tableName;
+ }
+ }
+ }
+ return alias;
+ }
+
+ /**
+ * 递归处理 子查询中的tenantid-where
+ *
+ * @param stmt sql查询对象
+ * @param where 当前sql的where条件 where为AndExpression或OrExpression的实例,解析其中的rightExpression,然后检查leftExpression是否为空,
+ * 不为空则是AndExpression或OrExpression,再次解析其中的rightExpression
+ * 注意tenantid-where是加在子查询上的
+ */
+ void findSubSelect(Statement stmt, Expression where) throws Exception {
+
+ // and 表达式
+ if (where instanceof AndExpression) {
+ AndExpression andExpression = (AndExpression) where;
+ if (andExpression.getRightExpression() instanceof SubSelect) {
+ SubSelect subSelect = (SubSelect) andExpression.getRightExpression();
+ doSelect(stmt, subSelect);
+ }
+ if (andExpression.getLeftExpression() != null) {
+ findSubSelect(stmt, andExpression.getLeftExpression());
+ }
+ } else if (where instanceof OrExpression) {
+ // or表达式
+ OrExpression orExpression = (OrExpression) where;
+ if (orExpression.getRightExpression() instanceof SubSelect) {
+ SubSelect subSelect = (SubSelect) orExpression.getRightExpression();
+ doSelect(stmt, subSelect);
+ }
+ if (orExpression.getLeftExpression() != null) {
+ findSubSelect(stmt, orExpression.getLeftExpression());
+ }
+ }
+ }
+
+ /**
+ * 处理select 和 subSelect
+ *
+ * @param stmt 查询对象
+ * @param select
+ * @return
+ * @throws Exception
+ */
+ Expression doSelect(Statement stmt, Expression select) throws Exception {
+ PlainSelect ps = null;
+ boolean hasSubSelect = false;
+
+ if (select instanceof SubSelect) {
+ ps = (PlainSelect) ((SubSelect) select).getSelectBody();
+ }
+ if (select instanceof Select) {
+ ps = (PlainSelect) ((Select) select).getSelectBody();
+ }
+
+ TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
+ List tableList = tablesNamesFinder.getTableList(select);
+ if (tableList.size() == 0) {
+ return select;
+ }
+ for (String table : tableList) {
+ // sql 包含 where 条件的情况 使用 addAndExpression 连接 已有的条件和新条件
+ if (ps.getWhere() == null) {
+ AndExpression where = addAndExpression(stmt, table, ps.getWhere());
+ // form 和 join 中加载的表
+ if (where != null) {
+ ps.setWhere(where);
+ } else {
+ // 如果在Statement中不存在这个表名,则存在于子查询中
+ hasSubSelect = true;
+ }
+ } else {
+ // sql 不含 where条件 新建一个EqualsTo设置为where条件
+ EqualsTo equalsTo = addEqualsTo(stmt, table);
+ ps.setWhere(equalsTo);
+ }
+ }
+
+ if (hasSubSelect) {
+ //子查询中的表
+ findSubSelect(stmt, ps.getWhere());
+ }
+ return select;
+ }
+
+ private void processSelectBody(SelectBody selectBody) {
+ if (selectBody instanceof PlainSelect) {
+ processPlainSelect((PlainSelect) selectBody);
+ } else if (selectBody instanceof WithItem) {
+ WithItem withItem = (WithItem) selectBody;
+ if (withItem.getSelectBody() != null) {
+ processSelectBody(withItem.getSelectBody());
+ }
+ } else {
+ SetOperationList operationList = (SetOperationList) selectBody;
+ if (operationList.getOperations() != null && operationList.getSelects().size() > 0) {
+ List plainSelects = operationList.getSelects();
+ for (SelectBody sub: plainSelects) {
+ processSelectBody(sub);
+ }
+ }
+ if (!orderByHashParameters(operationList.getOrderByElements())) {
+ operationList.setOrderByElements(null);
+ }
+ }
+ }
+
+ private void processPlainSelect(PlainSelect plainSelect) {
+ if (!orderByHashParameters(plainSelect.getOrderByElements())) {
+ plainSelect.setOrderByElements(null);
+ }
+ if (plainSelect.getFromItem() != null) {
+ processFromItem(plainSelect.getFromItem());
+ }
+ if (plainSelect.getJoins() != null && plainSelect.getJoins().size() > 0) {
+ List joins = plainSelect.getJoins();
+ for (Join join : joins) {
+ if (join.getRightItem() != null) {
+ processFromItem(join.getRightItem());
+ }
+ }
+ }
+ }
+
+ public void processFromItem(FromItem fromItem) {
+ if (fromItem instanceof SubJoin) {
+ SubJoin subJoin = (SubJoin) fromItem;
+ if (subJoin.getJoinList() != null) {
+ for(Join tjoin: subJoin.getJoinList()) {
+ if (tjoin.getRightItem() != null) {
+ processFromItem(tjoin.getRightItem());
+ }
+ }
+ }
+ } else if (fromItem instanceof SubSelect) {
+ SubSelect subSelect = (SubSelect) fromItem;
+ if (subSelect.getSelectBody() != null) {
+ processSelectBody(subSelect.getSelectBody());
+ }
+ } else if (fromItem instanceof ValuesList) {
+
+ } else if (fromItem instanceof LateralSubSelect) {
+ LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
+ if (lateralSubSelect.getSubSelect() != null) {
+ SubSelect subSelect = (SubSelect) (lateralSubSelect.getSubSelect());
+ if (subSelect.getSelectBody() != null) {
+ processSelectBody(subSelect.getSelectBody());
+ }
+ }
+ }
+ //Table时不用处理
+ }
+
+ public boolean orderByHashParameters(List orderByElements) {
+ if (orderByElements == null) {
+ return false;
+ }
+ for (OrderByElement orderByElement : orderByElements) {
+ if (orderByElement.toString().toUpperCase().contains("?")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public TenantInfo getTenantInfo() {
+ return tenantInfo;
+ }
+
+ public void setTenantInfo(TenantInfo tenantInfo) {
+ this.tenantInfo = tenantInfo;
+ }
+
+ public String getTenantIdColumn() {
+ return tenantIdColumn;
+ }
+
+ public void setTenantIdColumn(String tenantIdColumn) {
+ this.tenantIdColumn = tenantIdColumn;
+ }
+
+ public String getDialect() {
+ return dialect;
+ }
+
+ public void setDialect(String dialect) {
+ this.dialect = dialect;
+ }
+
+
+}
diff --git a/mybatis-multi-tenancy/src/main/java/com/iformall/plugin/TenantInfo.java b/mybatis-multi-tenancy/src/main/java/com/iformall/plugin/TenantInfo.java
new file mode 100644
index 000000000..33c6a546e
--- /dev/null
+++ b/mybatis-multi-tenancy/src/main/java/com/iformall/plugin/TenantInfo.java
@@ -0,0 +1,15 @@
+package com.iformall.plugin;
+
+import org.apache.ibatis.mapping.MappedStatement;
+
+/**
+ * 需要实现该接口用于获取租户id
+ * Created by kleen@qq.com on 2016/08/13.
+ */
+public interface TenantInfo {
+ String getTenantId();
+
+ boolean doTableFilter(String tableName);
+
+ boolean doMappedStatementFIlter(MappedStatement ms);
+}
diff --git a/mybatis-multi-tenancy/src/test/java/com/iformall/plugin/TenantInfoImpl.java b/mybatis-multi-tenancy/src/test/java/com/iformall/plugin/TenantInfoImpl.java
new file mode 100644
index 000000000..5e7d212de
--- /dev/null
+++ b/mybatis-multi-tenancy/src/test/java/com/iformall/plugin/TenantInfoImpl.java
@@ -0,0 +1,24 @@
+package com.iformall.plugin;
+
+import org.apache.ibatis.mapping.MappedStatement;
+
+/**
+ * 自行实现TenantInfo获取
+ * Created by Meara on 2016/3/20.
+ */
+public class TenantInfoImpl implements TenantInfo{
+ @Override
+ public String getTenantId() {
+ return "2";
+ }
+
+ @Override
+ public boolean doTableFilter(String tableName) {
+ return false;
+ }
+
+ @Override
+ public boolean doMappedStatementFIlter(MappedStatement ms) {
+ return false;
+ }
+}
diff --git a/mybatis-multi-tenancy/src/test/java/com/iformall/plugin/mapper/BookMapper.java b/mybatis-multi-tenancy/src/test/java/com/iformall/plugin/mapper/BookMapper.java
new file mode 100644
index 000000000..0efa365cc
--- /dev/null
+++ b/mybatis-multi-tenancy/src/test/java/com/iformall/plugin/mapper/BookMapper.java
@@ -0,0 +1,11 @@
+package com.iformall.plugin.mapper;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by Meara on 2016/3/17.
+ */
+public interface BookMapper {
+ List