上一章,Springboot管理系统数据权限过滤(三)——0业务入侵实现部门数据权限过滤数据权限实现的思路和代码实现已经了解。本节在此基础上实现支持mybatis框架的通用数据过滤插件。
实现目标:
上一章示例,使用的是mytatisplus数据持久框架,使用是的mybatisplus拦截器,本章是基于mybatis持久化框,所以仅拦截器实现上有一点点差异,但我们也是可以引入mybatisplus的依赖包,但仅使用它的对SQL的处理上,这样可以省很多事。除了这一点,其他的代码都可以沿用上一章的内容。
该项目初衷是把数据权限模板做成 自动配置 插件,对springboot版本有要求。但在springboot版本不支持的情况下,也可以通过配置创建相应bean也可以实现权限管理。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.14version>
<relativePath/>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.chengrui.toolgroupId>
<artifactId>cr-datapermissionartifactId>
<version>1.0.7version>
<packaging>jarpackaging>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<spring.boot.version>2.7.14spring.boot.version>
<maven-surefire-plugin.version>3.0.0-M5maven-surefire-plugin.version>
<maven-compiler-plugin.version>3.8.1maven-compiler-plugin.version>
<spring.boot.version>2.7.14spring.boot.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>${spring.boot.version}version>
<scope>compilescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.github.jsqlparsergroupId>
<artifactId>jsqlparserartifactId>
<version>4.3version>
<scope>compilescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.48version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.1version>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.3.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
dependencies>
project>
没有任何内容的注解定义,可以定义相关字段,以实现更精细的数据过滤,支持更多的可配置性。
package com.luo.chengrui.datapermission.annotation;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import java.lang.annotation.*;
/**
* 数据权限过滤注解
*
* @author luodz
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataPermission
{
}
这是自动配置,仅配置了注解拦截器。只要引用了该jar包则会自动配置DataPermissionAnnotationAdvisor Bean。即可拦截还有@DataPermission注解的方法了。
package com.luo.chengrui.datapermission.config;
import com.luo.chengrui.datapermission.interceptor.DataPermissionAnnotationAdvisor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* 配置mybatisPlusInterceptor
*/
@AutoConfiguration
public class DataPermissionInterceptorConfiguration {
/**
* 权限注解拦截器。
*
* @return
*/
@Bean
@Role(2)
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
return new DataPermissionAnnotationAdvisor();
}
}
spring Advisor用来配置拦截点和拦截器的实现类。
package com.luo.chengrui.datapermission.interceptor;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
/**
* {@link DataPermission} 注解的 Advisor 实现类
*/
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
private final Advice advice;
private final Pointcut pointcut;
@Override
public Advice getAdvice() {
return advice;
}
@Override
public Pointcut getPointcut() {
return pointcut;
}
public DataPermissionAnnotationAdvisor() {
this.advice = new DataScopeAnnotationInterceptor();
this.pointcut = this.buildPointcut();
}
protected Pointcut buildPointcut() {
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
return new ComposablePointcut(classPointcut).union(methodPointcut);
}
}
当方法体上有DataPermission注解时,方法执行前先执行该拦截器,解析出DataPermission的对象,并存放到线程变量中DataPermissionContextHolder。当方法执行完时,清除线程变量数据。
package com.luo.chengrui.datapermission.interceptor;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodClassKey;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* {@link DataPermission} 注解的拦截器
* 1. 在执行方法前,将 @DataPermission 注解入栈
* 2. 在执行方法后,将 @DataPermission 注解出栈
*
* @author luodz
*/
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
/**
* DataPermission 空对象,用于方法无 {@link import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;} 注解时,使用 DATA_PERMISSION_NULL 进行占位
*/
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
public Map<MethodClassKey, DataPermission> getDataPermissionCache() {
return dataPermissionCache;
}
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Logger log = LoggerFactory.getLogger(DataPermissionAnnotationInterceptor.class);
log.debug("DataPermissionAnnotationInterceptor 拦截器:" + methodInvocation.getMethod().getName());
// 入栈
DataPermission dataPermission = this.findAnnotation(methodInvocation);
if (dataPermission != null) {
DataPermissionContextHolder.add(dataPermission);
}
try {
// 执行逻辑
return methodInvocation.proceed();
} finally {
// 出栈
if (dataPermission != null) {
DataPermissionContextHolder.remove();
}
}
}
private DataPermission findAnnotation(MethodInvocation methodInvocation) {
// 1. 从缓存中获取
Method method = methodInvocation.getMethod();
Object targetObject = methodInvocation.getThis();
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
if (dataPermission != null) {
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
}
// 2.1 从方法中获取
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
// 2.2 从类上获取
if (dataPermission == null) {
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
}
// 2.3 添加到缓存中
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
return dataPermission;
}
}
用来缓存DataPermission权限注解,注解拦截器将DataPermission对象存入线程变量中,当在执行SQL拦截器时,从线程变量中获取DataPermission对象,如果能获取到,则进行数据过滤,如果没有获取到,则不做数据过滤。
package com.luo.chengrui.datapermission.interceptor;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import java.util.LinkedList;
import java.util.List;
/**
* {@link DataPermission} 注解的 Context 上下文
* 将方法上的注解对象设置到 线程变量里面,在SQL执行拦截器中获取注解对象,根据内容生成相应的权限。
* 告诉SQL执行{DataPermissionSqlParserInterceptor}拦截器,如此方法需要添加权限。
*/
public class DataPermissionContextHolder {
/**
* 使用 List 的原因,可能存在方法的嵌套调用
*/
private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
ThreadLocal.withInitial(LinkedList::new);
/**
* 获得当前的 DataPermission 注解
*
* @return DataPermission 注解
*/
public static DataPermission get() {
return DATA_PERMISSIONS.get().peekLast();
}
/**
* 入栈 DataPermission 注解
*
* @param dataPermission DataPermission 注解
*/
public static void add(DataPermission dataPermission) {
DATA_PERMISSIONS.get().addLast(dataPermission);
}
/**
* 出栈 DataPermission 注解
*
* @return DataPermission 注解
*/
public static DataPermission remove() {
DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
// 无元素时,清空 ThreadLocal
if (DATA_PERMISSIONS.get().isEmpty()) {
DATA_PERMISSIONS.remove();
}
return dataPermission;
}
/**
* 获得所有 DataPermission
*
* @return DataPermission 队列
*/
public static List<DataPermission> getAll() {
return DATA_PERMISSIONS.get();
}
/**
* 清空上下文
*
* 目前仅仅用于单测
*/
public static void clear() {
DATA_PERMISSIONS.remove();
}
}
这个是Mybatis拦截器的实现,
拦截器入口,实现了Interceptor接口中的方法:
@Override
public Object intercept(Invocation invocation) throws Throwable {
}
DataPermissionSqlParserInterceptor.java 的完整代码
package com.luo.chengrui.datapermission.interceptor;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.NotExpression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
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.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.sql.Connection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
* 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法
* 主要是在执行SQL前拦截器,在执行之前可重写SQL
*
* @author yudao,luodz
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DataPermissionSqlParserInterceptor extends JsqlParserSupport implements Interceptor {
private Logger logger = LoggerFactory.getLogger(DataPermissionSqlParserInterceptor.class);
private static final String MYSQL_ESCAPE_CHARACTER = "`";
private final List<DataPermissionRule> dataPermissionRule;
public DataPermissionSqlParserInterceptor(List<DataPermissionRule> dataPermissionRule) {
this.dataPermissionRule = dataPermissionRule;
}
private final MappedStatementCache mappedStatementCache = new MappedStatementCache();
@Override
public Object intercept(Invocation invocation) throws Throwable {
//线程变量中获取权限注解。
// 根据业务场景要求,决定是否依赖于方法上权限注解。
DataPermission saDataPermission = DataPermissionContextHolder.get();
if (saDataPermission == null) {
// 方法上未设置权限注解时,不处理SQL
return invocation.proceed();
}
Object target = invocation.getTarget();
Object[] args = invocation.getArgs();
if (target instanceof Executor) {
Executor executor = (Executor) target;
Object parameter = args[1];
boolean isUpdate = args.length == 2;
MappedStatement ms = (MappedStatement) args[0];
if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
} else {
boundSql = (BoundSql) args[5];
}
this.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
} else {
StatementHandler sh = (StatementHandler) target;
if (null == args) {
} else {
Connection connections = (Connection) args[0];
Integer transactionTimeout = (Integer) args[1];
this.beforePrepare(sh, connections, transactionTimeout);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return !(target instanceof Executor) && !(target instanceof StatementHandler) ? target : Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 获取配置文件中的属性值
}
// SELECT 场景
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 获得 Mapper 对应的数据权限的规则
if (mappedStatementCache.noRewritable(ms, dataPermissionRule)) { // 如果无需重写,则跳过
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
try {
// 初始化上下文
ContextHolder.init(dataPermissionRule);
// 处理 SQL
mpBs.sql(parserSingle(mpBs.sql(), null));
} finally {
// 添加是否需要重写的缓存
addMappedStatementCache(ms);
// 清空上下文
ContextHolder.clear();
}
}
// 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限)
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
// 获得 Mapper 对应的数据权限的规则
if (mappedStatementCache.noRewritable(ms, dataPermissionRule)) { // 如果无需重写,则跳过
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
try {
// 初始化上下文
ContextHolder.init(dataPermissionRule);
// 处理 SQL
mpBs.sql(parserMulti(mpBs.sql(), null));
} finally {
// 添加是否需要重写的缓存
addMappedStatementCache(ms);
// 清空上下文
ContextHolder.clear();
}
}
}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
processSelectBody(select.getSelectBody());
List<WithItem> withItemsList = select.getWithItemsList();
if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(this::processSelectBody);
}
}
/**
* update 语句处理
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
final Table table = update.getTable();
update.setWhere(this.builderExpression(update.getWhere(), table));
}
/**
* delete 语句处理
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
}
// ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========
protected void processSelectBody(SelectBody selectBody) {
if (selectBody == null) {
return;
}
if (selectBody instanceof PlainSelect) {
processPlainSelect((PlainSelect) selectBody);
} else if (selectBody instanceof WithItem) {
WithItem withItem = (WithItem) selectBody;
processSelectBody(withItem.getSubSelect().getSelectBody());
} else {
SetOperationList operationList = (SetOperationList) selectBody;
List<SelectBody> selectBodyList = operationList.getSelects();
if (!CollectionUtils.isEmpty(selectBodyList)) {
selectBodyList.forEach(this::processSelectBody);
}
}
}
/**
* 处理 PlainSelect
*/
protected void processPlainSelect(PlainSelect plainSelect) {
//#3087 github
List<SelectItem> selectItems = plainSelect.getSelectItems();
if (!CollectionUtils.isEmpty(selectItems)) {
selectItems.forEach(this::processSelectItem);
}
// 处理 where 中的子查询
Expression where = plainSelect.getWhere();
processWhereSubSelect(where);
// 处理 fromItem
FromItem fromItem = plainSelect.getFromItem();
List<Table> list = processFromItem(fromItem);
List<Table> mainTables = new ArrayList<>(list);
// 处理 join
List<Join> joins = plainSelect.getJoins();
if (!CollectionUtils.isEmpty(joins)) {
mainTables = processJoins(mainTables, joins);
}
// 当有 mainTable 时,进行 where 条件追加
if (!CollectionUtils.isEmpty(mainTables)) {
plainSelect.setWhere(builderExpression(where, mainTables));
}
}
private List<Table> processFromItem(FromItem fromItem) {
// 处理括号括起来的表达式
while (fromItem instanceof ParenthesisFromItem) {
fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
}
List<Table> mainTables = new ArrayList<>();
// 无 join 时的处理逻辑
if (fromItem instanceof Table) {
Table fromTable = (Table) fromItem;
mainTables.add(fromTable);
} else if (fromItem instanceof SubJoin) {
// SubJoin 类型则还需要添加上 where 条件
List<Table> tables = processSubJoin((SubJoin) fromItem);
mainTables.addAll(tables);
} else {
// 处理下 fromItem
processOtherFromItem(fromItem);
}
return mainTables;
}
/**
* 处理where条件内的子查询
*
* 支持如下:
* 1. in
* 2. =
* 3. >
* 4. <
* 5. >=
* 6. <=
* 7. <>
* 8. EXISTS
* 9. NOT EXISTS
*
* 前提条件:
* 1. 子查询必须放在小括号中
* 2. 子查询一般放在比较操作符的右边
*
* @param where where 条件
*/
protected void processWhereSubSelect(Expression where) {
if (where == null) {
return;
}
if (where instanceof FromItem) {
processOtherFromItem((FromItem) where);
return;
}
if (where.toString().indexOf("SELECT") > 0) {
// 有子查询
if (where instanceof net.sf.jsqlparser.expression.BinaryExpression) {
// 比较符号 , and , or , 等等
net.sf.jsqlparser.expression.BinaryExpression expression = (net.sf.jsqlparser.expression.BinaryExpression) where;
processWhereSubSelect(expression.getLeftExpression());
processWhereSubSelect(expression.getRightExpression());
} else if (where instanceof InExpression) {
// in
InExpression expression = (InExpression) where;
Expression inExpression = expression.getRightExpression();
if (inExpression instanceof SubSelect) {
processSelectBody(((SubSelect) inExpression).getSelectBody());
}
} else if (where instanceof ExistsExpression) {
// exists
ExistsExpression expression = (ExistsExpression) where;
processWhereSubSelect(expression.getRightExpression());
} else if (where instanceof NotExpression) {
// not exists
NotExpression expression = (NotExpression) where;
processWhereSubSelect(expression.getExpression());
} else if (where instanceof Parenthesis) {
Parenthesis expression = (Parenthesis) where;
processWhereSubSelect(expression.getExpression());
}
}
}
protected void processSelectItem(SelectItem selectItem) {
if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
if (selectExpressionItem.getExpression() instanceof SubSelect) {
processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
} else if (selectExpressionItem.getExpression() instanceof Function) {
processFunction((Function) selectExpressionItem.getExpression());
}
}
}
/**
* 处理函数
* 支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)
*
fixed gitee pulls/141
*
* @param function
*/
protected void processFunction(Function function) {
ExpressionList parameters = function.getParameters();
if (parameters != null) {
parameters.getExpressions().forEach(expression -> {
if (expression instanceof SubSelect) {
processSelectBody(((SubSelect) expression).getSelectBody());
} else if (expression instanceof Function) {
processFunction((Function) expression);
}
});
}
}
/**
* 处理子查询等
*/
protected void processOtherFromItem(FromItem fromItem) {
// 去除括号
while (fromItem instanceof ParenthesisFromItem) {
fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
}
if (fromItem instanceof SubSelect) {
SubSelect subSelect = (SubSelect) fromItem;
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
} else if (fromItem instanceof ValuesList) {
logger.debug("Perform a subQuery, if you do not give us feedback");
} else if (fromItem instanceof LateralSubSelect) {
LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
if (lateralSubSelect.getSubSelect() != null) {
SubSelect subSelect = lateralSubSelect.getSubSelect();
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody());
}
}
}
}
/**
* 处理 sub join
*
* @param subJoin subJoin
* @return Table subJoin 中的主表
*/
private List<Table> processSubJoin(SubJoin subJoin) {
List<Table> mainTables = new ArrayList<>();
if (subJoin.getJoinList() != null) {
List<Table> list = processFromItem(subJoin.getLeft());
mainTables.addAll(list);
mainTables = processJoins(mainTables, subJoin.getJoinList());
}
return mainTables;
}
/**
* 处理 joins
*
* @param mainTables 可以为 null
* @param joins join 集合
* @return List