轉自 : https://blog.csdn.net/weixin_44600430/article/details/112108902
MyBatis攔截器(自定義注解+實現多租戶查詢)
前言:
公司現有運營管理平台上的功能都要增加多租戶, 原本功能都是單租戶。
就是要做數據隔離, 登錄用戶只能看到當前登錄用戶名下數據, 關鍵數據表都加了個用戶ID字段, 之前的功能都已經寫好, 所以就在想怎么在最少改動代碼的情況下實現給之前的所有查詢增加一個查詢條件=值, 后來想到利用mybatis攔截器動態修改sql進行拼接多個查詢。
下面就開始利用來進行實現。 (技術框架<mybatis-plus.version>1.4.8</mybatis-plus.version>, 公司用的版本太低, 好像mybatis-plus在2.1版本 也增加了多租戶攔截器, 但是還是不能完全滿足我現有需求)
使用到的技術有: <mybatis-plus> , <jsqlparser>
1.0 自定義MyBatis攔截器
/** * @Author: ZhiHao * @Date: 2020/12/16 16:37 * @Description: 代理商通用sql拼接攔截器(兼容之前查詢, 僅對方法標記注解生效) * @Versions 1.0 **/ //@Component @Intercepts({@Signature( type = StatementHandler.class, //攔截構建sql語句的StatementHandler method = "prepare", //里面的prepare方法 args = { Connection.class, //方法的參數 Integer.class } )}) public class AgentSqlInterceptor implements Interceptor { private Logger logger = LoggerFactory.getLogger(getClass()); // 是否進行攔截動態修改sql private boolean required; // 表別名 private String tableAlias; public AgentSqlInterceptor(boolean required, String tableAlias) { this.required = required; this.tableAlias = tableAlias; } @Override public Object intercept(Invocation invocation) throws Throwable { // 需要修改sql語句才攔截 if (required) { StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler); // 先判斷是不是SELECT操作 MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement"); if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) { return invocation.proceed(); } BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql"); String sql = boundSql.getSql(); logger.info("之前sql語句:{}", sql); // 判斷是否符合需要增加區分代理商查詢條件 sql = this.ifAgentQuery(sql); logger.info("代理商查詢sql語句:{}", sql); // 最終將修改好的sql語句設置回去執行 metaStatementHandler.setValue("delegate.boundSql.sql", sql); } return invocation.proceed(); } @Override public Object plugin(Object target) { // 返回攔截器本身, 還是返回目標本身 return target instanceof StatementHandler ? Plugin.wrap(target, this) : target; } @Override public void setProperties(Properties properties) { } /** * 判斷是否需要進行拼接代理商查詢條件 * 有以下幾種情況: (0 運營商, 不進行拼接查詢全部) , (1 代理商並且是總倉管理員, 僅拼接代理商字段查詢) * (2 代理商並且是分倉管理員, 需拼接代理商字段與倉庫字段查詢) , (3 代理商並且是總倉管理員, 需拼接代理商字段查詢) * * @param sql * @return java.lang.String * @author: ZhiHao * @date: 2020/12/17 */ private String ifAgentQuery(String sql) { // sql解析器解析查詢語句(也只有查詢能進來) Select selectStatement = (Select) CCJSqlParserUtil.parse(sql); // 不是單表與多表直接查詢 if (!(selectStatement.getSelectBody() instanceof PlainSelect)) { return sql; } PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody(); // 僅對枚舉符合表, 並沒有代理商字段的進行拼接sql Table table = this.ifConform(selectBody); if (table != null && this.doesItExist(selectBody.getWhere())) { // 進行拼接, 判斷是否有where Expression where = selectBody.getWhere(); // 獲取查詢語句表別名(兼容之前做好功能一個方法表別名不一致) String sqlTableAlias = table.getAlias() != null ? table.getAlias().getName() : null; String queryConditionsAndValues; if (where != null) { // 獲取代理商查詢條件與值 queryConditionsAndValues = this.getQueryConditionsAndValues( table.getName(), StringUtils.isNotBlank(sqlTableAlias) ? sqlTableAlias : this.tableAlias); // 解析之前表達式, 使用弱引用 WeakReference<ExpressionDeParser> weakReference = new WeakReference<>(new ExpressionDeParser()); where.accept(weakReference.get()); // 獲取之前表達式 StringBuilder buffer = weakReference.get().getBuffer(); // 拼接sql buffer.append(queryConditionsAndValues); // 設置回去 selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(buffer.toString())); weakReference.clear(); } else { // 獲取查詢條件與值 queryConditionsAndValues = " 1 = 1 " + this.getQueryConditionsAndValues(table.getName(), StringUtils.isNotBlank(sqlTableAlias) ? sqlTableAlias : this.tableAlias); //沒有where情況 selectBody.setWhere(CCJSqlParserUtil.parseCondExpression(queryConditionsAndValues)); } sql = selectStatement.toString(); } return sql; } /** * 僅對dms_battery表查詢並沒有代理商字段的進行拼接sql, * 防止攔截器開啟時, 兼容以后自定義查詢語句包含查詢字段的進行了拼接出錯 * * @param selectBody * @return boolean * @author: ZhiHao * @date: 2020/12/18 */ private Table ifConform(PlainSelect selectBody) { // 判斷from后面是否符合需要拼接的表名 FromItem fromItem = selectBody.getFromItem(); if (fromItem instanceof Table) { Table table = (Table) fromItem; if (TenantTable.tableMap.get(table.getName()) != null) { return table; } } // 上面from后面不滿足則判斷多表情況是否包含 List<Join> joins = selectBody.getJoins(); Table table = null; if (joins != null && joins.size() > 0) { Optional<Join> any = joins.stream().filter((join) -> { FromItem rightItem = join.getRightItem(); if (rightItem instanceof Table) { return TenantTable.tableMap.get(((Table) rightItem).getName()) != null ? true : false; } return false; }).findAny(); table = any.isPresent() ? (Table) any.get().getRightItem() : null; } return table != null ? table : null; } /** * 判斷之前sql是否存在了代理商查詢字段 * * @param sql * @return boolean * @author: ZhiHao * @date: 2020/12/21 */ private boolean doesItExist(Expression sql) { String str = sql != null ? sql.toString() : null; if (StringUtils.containsIgnoreCase(str, TenantTable.AGENT_ID.getColumns(null)) || StringUtils.containsIgnoreCase(str, TenantTable.DEPOT_ID.getColumns(null))) { return false; } return true; } private final Integer AGENT = 0; //代理商 private final Integer OPERATOR = 1; //運營商 /** * 根據表名構建條件 * * @param tableName 表名 * @param tableAlias 別名 * @return java.lang.String * @author: ZhiHao * @date: 2020/12/21 */ public String getQueryConditionsAndValues(String tableName, String tableAlias) { StringBuilder builder = new StringBuilder(); // 獲取登錄用戶 IDmsUserService dmsUserService = SpringUtils.getBean(IDmsUserService.class); DmsUser dmsUser = dmsUserService.getCurrentUser(); Integer type = dmsUser.getType(); Integer terminal = dmsUser.getTerminal(); Integer agentOrOperatorId = dmsUser.getAgentOrOperatorId(); Integer depotId = dmsUser.getDepotId(); switch (TenantTable.tableMap.get(tableName)) { // 電池表 case DMS_BATTERY: // 是運營商並是總倉不做拼接可見全部 if (OPERATOR.equals(type) && TerminalEnums.WEB_ADMIN.getCode().equals(terminal)) { return ""; } // 是運營商並是分倉拼接可見分倉 if (OPERATOR.equals(type) && TerminalEnums.WEB.getCode().equals(terminal)) { builder.append(" AND ") .append(TenantTable.DEPOT_ID.getColumns(tableAlias)) .append(" = ") .append(depotId) .append(" "); return builder.toString(); } // 是代理商並是總倉拼接可見總+分倉 if (AGENT.equals(type) && TerminalEnums.WEB_ADMIN.getCode().equals(terminal)) { builder.append(" AND ") .append(TenantTable.AGENT_ID.getColumns(tableAlias)) .append(" = ") .append(agentOrOperatorId) .append(" "); return builder.toString(); } // 是代理商並是分倉拼接可見分倉 if (AGENT.equals(type) && TerminalEnums.WEB.getCode().equals(terminal)) { builder.append(" AND ") .append(TenantTable.AGENT_ID.getColumns(tableAlias)) .append(" = ") .append(agentOrOperatorId) .append(" AND ") .append(TenantTable.DEPOT_ID.getColumns(tableAlias)) .append(" = ") .append(depotId) .append(" "); return builder.toString(); } // 設備表 case DEVICE: if (AGENT.equals(type)) { builder.append(" AND ") .append(TenantTable.AGENT_ID.getColumns(tableAlias)) .append(" = ") .append(agentOrOperatorId) .append(" AND ") // 只查詢櫃子 .append(TenantTable.DEVICE_TYPE.getColumns(tableAlias)) .append(" = 1 "); return builder.toString(); } default: break; } return null; } public void setRequired(boolean required) { this.required = required; } public void setTableAlias(String tableAlias) { this.tableAlias = tableAlias; } }
2.0 利用AOP
+注解實現標記方法才進行攔截
PS: 如果查詢方法都是寫在
DAO
層接口里面的, 可以不使用AOP
(具體看擴展) , 因為使用到了mybatis-plus
很多查詢方法都是使用其提供的, 所以注解只能標記到service
層,
/** * @Author: ZhiHao * @Date: 2020/12/17 15:08 * @Description: 需要增加代理商查詢條件 agent_id = xx 的表名 * @Versions 1.0 **/ @Aspect @Component public class AgentMethodAspect { private Logger log = LoggerFactory.getLogger(getClass()); //會話工廠 @Autowired private SqlSessionFactory sqlSessionFactory; private final StampedLock lock = new StampedLock(); /** * 切入點 * * @author: ZhiHao * @date: 2020/12/17 */ @Pointcut("@annotation(com.xxx.xxx.multitenant.RequiredTenant)") public void requiredTenant() { } /** * 環繞通知 * * @param point * @return java.lang.Object * @author: ZhiHao * @date: 2020/12/17 */ @Around("requiredTenant()") public Object around(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); RequiredTenant requiredTenant = method.getAnnotation(RequiredTenant.class); String alias = requiredTenant.tableAlias(); // 添加過濾器進sql會話工廠配置 Configuration configuration = sqlSessionFactory.getConfiguration(); // 判斷是否有別名 AgentSqlInterceptor agentSqlInterceptor = (AgentSqlInterceptor) configuration.getInterceptors().stream().filter( interceptor -> interceptor instanceof AgentSqlInterceptor ? true : false ).findAny().get(); try { // 進行加鎖, 控制並發將攔截設置為取消 if (lock.tryWriteLock(30, TimeUnit.SECONDS) != 0) { // 設置攔截與別名 agentSqlInterceptor.setRequired(true); agentSqlInterceptor.setTableAlias(StringUtils.isNotBlank(alias) ? alias : null); // 繼續執行方法 return point.proceed(); } } catch (Throwable throwable) { throwable.printStackTrace(); log.info("加鎖失敗:{}",throwable.getMessage()); } finally { // 執行完畢都將其修改回未攔截標記注解其他請求 agentSqlInterceptor.setRequired(false); // 釋放鎖 lock.tryUnlockWrite(); } return null; } /** * 僅做首次添加攔截器 * * @author: ZhiHao * @date: 2020/12/24 */ @Override public void afterPropertiesSet() throws Exception { AgentSqlInterceptor agentSqlInterceptor = new AgentSqlInterceptor(false, null); sqlSessionFactory.getConfiguration().addInterceptor(agentSqlInterceptor); log.info("首次添加AgentSqlInterceptor攔截器:{}", agentSqlInterceptor); } }
3.0 注解
/** * @Author: ZhiHao * @Date: 2020/12/16 19:01 * @Description: 是否需要代理商查詢 * @Versions 1.0 **/ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequiredTenant { /** * 查詢語句表別名(按需選擇) * @return */ String tableAlias() default ""; }
4.0 枚舉
/** * @Author: ZhiHao * @Date: 2020/12/17 10:08 * @Description: 需要增加代理商查詢條件 agent_id = xx 的表名 * @Versions 1.0 **/ public enum TenantTable { DMS_BATTERY("dms_battery", null), DEVICE("device", null), // 代理商查詢字段 AGENT_ID(null, "agent_id"), // 倉庫查詢字段 DEPOT_ID(null, "depot_id"), // 櫃子類型查詢字段 DEVICE_TYPE(null, "type"), ; private String tableName; private String columns; TenantTable(String tableName, String columns) { this.tableName = tableName; this.columns = columns; } public static Map<String, TenantTable> tableMap; static { tableMap = Arrays.stream(TenantTable.values()) .collect(Collectors.toMap(tenantTable -> tenantTable.tableName, tenantTable -> tenantTable, (tenantTable, tenantTable2) -> tenantTable2)); } /** * 獲取表名 * * @return java.lang.String * @author: ZhiHao * @date: 2020/12/18 */ public String getTableName() { return tableName; } /** * 獲取查詢字段 * * @param tableAlias 表別名 * @return java.lang.String * @author: ZhiHao * @date: 2020/12/18 */ public String getColumns(String tableAlias) { if (StringUtils.isNotBlank(tableAlias)) { String str = tableAlias + "." + this.columns; return str; } return columns; } }
5.0 Service
層方法使用 (測試)
@RequiredTenant @Override public List<xxxx> getDepreciation(xxxx indexDataQueryDto) { // 自定義方法查詢 List<xxxxx> listDto = batteryDao.getDepreciation(xxx); // mybatis-plus提供方法 xxxxxx Battery = selectById(xxx); }
結果:
首次添加AgentSqlInterceptor攔截器:com.gizwits.lease.multitenant.AgentSqlInterceptor@4bd8a2c7 之前sql語句:SELECT IFNULL( db.life - TIMESTAMPDIFF(MONTH,db.initial_time,NOW()) , IFNULL(db.life - TIMESTAMPDIFF(MONTH,db.first_service_time,NOW()),IFNULL(db.life - TIMESTAMPDIFF(MONTH,db.ctime,NOW()),null))) month, count(1) number FROM dms_battery db WHERE db.status in (0,2,3,6,7,8) AND db.is_cancellation = 1 GROUP BY month 代理商查詢sql語句:SELECT IFNULL( db.life - TIMESTAMPDIFF(MONTH,db.initial_time,NOW()) , IFNULL(db.life - TIMESTAMPDIFF(MONTH,db.first_service_time,NOW()),IFNULL(db.life - TIMESTAMPDIFF(MONTH,db.ctime,NOW()),null))) month, count(1) number FROM dms_battery db WHERE 1 = 1 AND db.agent_id = 8 AND db.status in (0,2,3,6,7,8) AND db.is_cancellation = 1 GROUP BY month 之前sql語句:SELECT COUNT(1) FROM dms_battery WHERE (is_cancellation = ? AND is_deleted = ? AND status NOT IN (?,?,?)) 代理商查詢sql語句:SELECT COUNT(1) FROM dms_battery WHERE 1 = 1 AND agent_id = 8 AND (is_cancellation = ? AND is_deleted = ? AND status NOT IN (?,?,?))
擴展:
Mybatis-自定義注解加攔截器
最后加鎖控制臨界資源, 可以更換為使用ThreadLocal
1