前文:在mybatis源码解析(一)中我们已经对mybatis容器初始化加载配置文件和解析mapper.xml和jdk动态代理mapper接口,简单回顾下上文解析,首先是通过SqlSessionFactoryBuilder加载全局配置文件(包括SQL映射器),这些配置都会封装在Configuration中,其中每一条SQL语句的信息都会封装在MappedStatement中。然后创建SqlSession,这时还会初始化Executor执行器。最后通过调用sqlSession.getMapper()来动态代理执行Mapper中对应的SQL语句。而当一个动态代理对象进入到了MapperMethod的execute()方法后,它经过简单地判断就进入了SqlSession的delete、update、insert、select等方法,这里是真正执行SQL语句的地方。那么这些方法是如何执行呢?
答:实际上SqlSession的执行过程是通过Executor、StatementHandler、ParameterHandler和ResultSetHandler来完成数据库操作和结果返回的,它们简称为四大对象:,本篇幅我们主要讲解一下SqlSession下的四大对象。这既是sql执行的整体逻辑也是利用mybatis做插件的空隙。那么,我们还是按照惯例来介绍一个几个接口和类。

一、介绍一下四大内置对象。

  • Executor:代表执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL,其中StatementHandler是最重要的。
  • StatementHandler:作用是使用数据库的Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。
  • ParameterHandler:是用来处理SQL参数的。
  • ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的。

二、代码执行流程分析

1、首先来到mybatis源码解析(一)的末尾。

可以看到这里获取了MappedStatement对象,并且调用了executor对象的query()方法来执行SQL。所以我们来看看Executor类。

2、Executor对象

Executor表示执行器,它是真正执行Java和数据库交互的对象,所以它十分重要,每一个SqlSession都会拥有一个Executor对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为JDBC中Statement的封装版。
Executor的关系图如下:

BaseExecutor:是一个抽象类,采用模板方法的设计模式。它实现了Executor接口,实现了执行器的基本功能。

SimpleExecutor:最简单的执行器,根据对应的SQL直接执行即可,不会做一些额外的操作;拼接完SQL之后,直接交给 StatementHandler 去执行。
BatchExecutor:批处理执行器,用于将多个SQL一次性输出到数据库,通过批量操作来优化性能。通常需要注意的是批量更新操作,由于内部有缓存的实现,使用完成后记得调用flushStatements来清除缓存。
ReuseExecutor :可重用的执行器,重用的对象是Statement,也就是说该执行器会缓存同一个sql的Statement,省去Statement的重新创建,优化性能。内部的实现是通过一个HashMap来维护Statement对象的。由于当前Map只在该session中有效,所以使用完成后记得调用flushStatements来清除Map。调用实现的四个抽象方法时会调用 prepareStatement()
CachingExecutor:启用于二级缓存时的执行器;采用静态代理;代理一个 Executor 对象。执行 update 方法前判断是否清空二级缓存;执行 query 方法前先在二级缓存中查询,命中失败再通过被代理类查询。
我们来看看Mybatis是如何创建Executor的,其实在前面已经介绍过了,它是在Configuration类中完成的,这里不看可以跳过:

Executor对象会在MyBatis加载全局配置文件时初始化,它会根据配置的类型去确定需要创建哪一种Executor,我们可以在全局配置文件settings元素中配置Executor类型,setting属性中有个defaultExecutorType,可以配置如下3个参数:

  • SIMPLE: 简易执行器,它没有什么特别的,默认执行器
  • REUSE:是一种能够执行重用预处理语句的执行器
  • BATCH:执行器重用语句和批量更新,批量专用的执行器
    默认使用SimpleExecutor。而如果开启了二级缓存,则用CachingExecutor进行包装,SqlSession会调用CachingExecutor执行器的query()方法,先从二级缓存获取数据,当无法从二级缓存获取数据时,则委托给BaseExecutor的子类进行操作。

CachingExecutor执行过程代码如下: 在queryFromDatabase()方法中调用SimpleExecutor的 doQuery() 方法(注意:这里说是调用了SimpleExecutor的方法,但是还在BaseExecutor类中是因为SimpleExecutor继承了它,所以SimpleExecutor对象中也有这个方法,而doQuery()方法在子类SimpleExecutor实现的,所以说是调用SimpleExecutor的 doQuery() 方法。),其方法代码如下:

这里显然是根据Configuration对象来构建StatementHandler,然后使用prepareStatement()方法对SQL编译和参数进行初始化。实现过程是:它调用了StatementHandler的prepare() 进行了预编译和基础的设置,然后通过StatementHandler的parameterize()来设置参数,这个parameterize()方法实际是通过ParameterHandler来对参数进行设置。最后使用 StatementHandler的query()方法,把ResultHandler传递进去,执行查询后再通过ResultSetHandler封装结果并将结果返回给调用者来完成一次查询,这样焦点又转移到了 StatementHandler 对象上。所以通过以上流程发现,MyBatis核心工作实际上是由Executor、StatementHandler、ParameterHandler和ResultSetHandler四个接口完成的,掌握这四个接口的工作原理,对理解MyBatis底层工作原理有很大帮助。

3、StatementHandler对象

StatementHandler是数据库会话器,顾名思义,数据库会话器就是专门处理数据库会话的,相当于JDBC中的Statement(PreparedStatement)。StatementHandler的关系图如下:

StatementHandler接口设计采用了适配器模式,其实现类RoutingStatementHandler根据上下文来选择适配器生成相应的StatementHandler。三个适配器分别是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。

BaseStatementHandler: 是一个抽象类,它实现了StatementHandler接口,用于简化StatementHandler接口实现的难度,采用适配器设计模式,它主要有三个实现类SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。

  • SimpleStatementHandler:
    最简单的StatementHandler,处理不带参数运行的SQL,对应JDBC的Statement

  • PreparedStatementHandler: 预处理Statement的handler,处理带参数允许的SQL,对应JDBC的PreparedStatement(预编译处理)

  • CallableStatementHandler:存储过程的Statement的handler,处理存储过程SQL,对应JDBC的CallableStatement(存储过程处理)

  • RoutingStatementHandler:RoutingStatementHandler根据上下文来选择适配器生成相应的StatementHandler。三个适配器分别是SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler。

StatementHandler的初始化过程如下(它也是在Configuration对象中完成的)

当调用到doQuery()方法时内部会通过configuration.newStatementHandler()方法来创建StatementHandler对象。

可以发现MyBatis生成StatementHandler代码中,创建的真实对象是一个RoutingStatementHandler的对象,而不是其它三个对象中的。但是RoutingStatementHandler并不是真实的服务对象,它是通过适配器模式来找到对应的StatementHandler来执行的。在初始化RoutingStatementHandler对象时,它会根据上下文环境来决定创建哪个具体的StatementHandler。RoutingStatementHandler 的构造方法如下

它内部定义了一个对象的适配器delegate,它是一个StatementHandler接口对象,然后构造方法根据配置来配置对应的StatementHandler对象。它的作用是给3个接口对象的使用提供一个统一且简单的适配器。此为对象的配适,可以使用对象配适器来尽可能地使用己有的类对外提供服务,可以根据需要对外屏蔽或者提供服务,甚至是加入新的服务。我们以常用的PreparedStatementHandler 为例,看看Mybatis是怎么执行查询的。
继续跟踪到SimpleExecutor对象中的prepareStatement()方法:

可以发现Executor 执行查询时会执行 StatementHandler 的 prepare() 和 parameterize() 方法来对SQL进行预编译和参数的设置, 其中 PreparedStatementHandler 的 prepare 方法如下:

注意:这个 prepare 方法是先调用到 StatementHandler 的实现类 RoutingStatementHandler,再由RoutingStatementHandler 调用 BaseStatementHandler 中的 prepare 方法。

通过prepare()方法,可知其中最重要的方法就是 instantiateStatement() 方法了,因为它要完成对SQL的预编译。在得到 Statement 对象的时候,会去调用 instantiateStatement() 方法,这个方法位于 BaseStatementHandler 中,是一个抽象方法由子类去实现,实际执行的是三种 StatementHandler 中的一种,我们以 PreparedStatementHandler 中的为例:


从上面的代码我们可以看到,instantiateStatement() 方法最终返回的也是Statement对象,所以经过一系列的调用会把创建好的 Statement 对象返回到 SimpleExecutor 简单执行器中,为后面设置参数的 parametersize 方法所用。也就是说,prepare 方法负责生成 Statement 实例对象,而 parameterize 方法用于处理 Statement 实例对应的参数。所以我们来看看parameterize 方法:
可以看到这里通过调用了ParameterHandler对象来设置参数,所以下面我们来介绍一下ParameterHandler对象。

4、ParameterHandler对象

ParameterHandler 是参数处理器,它的作用是完成对预编译的参数的设置,也就是负责为 PreparedStatement 的 SQL 语句参数动态赋值。
ParameterHandler对象的创建,ParameterHandler参数处理器对象是在创建 StatementHandler 对象的同时被创建的,同样也是由 Configuration 对象负责创建:

那么 ParameterHandler 如何解析SQL中的参数呢?ParameterHandler 由实现类DefaultParameterHandler执行,使用TypeHandler将参数对象类型转换成jdbcType,完成预编译SQL的参数设置,这是在setParameters()方法中完成的,setParameters()方法的实现如下:


至此,我们的参数就处理完成了。一切都准备就绪之后就肯定可以执行了呗!在SimpleExecutor 的 doQuery()方法中最后会调用query()方法来执行SQL语句。
可以看到这里执行了我们的SQL语句,然后对执行的结果进行处理,这里用到的是MyBatis 四大对象的最后一个神器也就是 ResultSetHandler,所以下面我们继续来介绍ResultSetHandler对象。

5、ResultSetHandler对象

ResultSetHandler 是结果处理器,它是用来组装结果集的。

ResultSetHandler 对象的创建,ResultSetHandler 对象是在处理查询请求时创建 StatementHandler 对象同时被创建的,同样也是由 Configuration 对象负责创建,示例如下:
Configuration对象中的newResultSetHandler()方法:

ResultSetHandler接口只有一个默认的实现类是DefaultResultSetHandler,我们通过SELECT语句执行得到的结果集由其 handleResultSets() 方法处理,方法如下:



上面涉及的主要对象有:
ResultSetWrapper:结果集的包装器,主要针对结果集进行的一层包装。这个类中的主要属性有:

  • ResultSet : Java JDBC ResultSet接口表示数据库查询的结果。
    有关查询的文本显示了如何将查询结果作为java.sql.ResultSet返回。 然后迭代此ResultSet以检查结果。
  • TypeHandlerRegistry: 类型注册器,TypeHandlerRegistry 在初始化的时候会把所有的
    Java类型和类型转换器进行注册。
  • ColumnNames: 字段的名称,也就是查询操作需要返回的字段名称
  • ClassNames: 字段的类型名称,也就是 ColumnNames 每个字段名称的类型
  • JdbcTypes: JDBC 的类型,也就是java.sql.Types 类型.

ResultMap:负责处理更复杂的映射关系。
multipleResults:用于封装处理好的结果集。其中的主要方法是 handleResultSet。

以上在 DefaultResultSetHandler 中处理完结果映射,并把上述得到的结果返回给调用的客户端,从而执行完成一条完整的SQL语句。结果集的处理就看到这里了,因为ResultSetHandler的实现非常复杂,它涉及了CGLIB或者JAVASSIST作为延迟加载,然后通过typeHandler和ObjectFactory解析组装结果在返回,由于实际工作需要改变它的几率不高加上他比较复杂,所以这里就不在论述了,有兴趣的可自行去百度信息。
8、小结
一条SQL语句在Mybatis中的执行过程小结:首先是创建Mapper的动态代理对象MapperProxy,然后将Mappe接口中的方法封装至MapperMethod对象,通过MapperMethod对象中的execute()方法来执行SQL,其本质是通过SqlSession下的方法来实现的,SQL语句的具体的执行则是通过SqlSession下的四大对象来完成。Executor先调用StatementHandler的prepare()方法预编译SQL,然后用parameterize()方法启用ParameterHandler设置参数,完成预编译,执行查询,update()也是这样的。如果是查询,MyBatis则会使用ResultSetHandler来封装结果并返回给调用者,从而完成查询。

三、拦截器讲解流程分析

1、Configuration 类包含 Mybatis 的一切配置信息,里面有 4 个非常重要的方法,也是拦截器拦截的方法

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;}

整个调用过程是这样的:newExecutor -> StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler

说了这么多还没有讲到拦截器是怎样被执行的,别急,前面这些都是铺垫,也许有细心的小伙伴已经发现,在 Configuratin 类中的那四个方法中,都有相同的一段代码:

interceptorChain.pluginAll(...)

没错,通过名字我们也猜测得到,这是拦截器的关键,interceptorChain 是 Configuration 类的成员变量,且看 InterceptorChain.java 类:

public class InterceptorChain {private final List<Interceptor> interceptors = new ArrayList<Interceptor>();public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);}public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);}}

在 pluginAll 方法中遍历所有拦截器的 plugin 方法,在自定义的拦截器中,我们重写 plugin 方法,这里以 通用分页拦截器 讲解调用拦截器过程,来看关键代码:

@Intercepts({@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 PageInterceptor implements Interceptor {@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}
}

Plugin.java 实现了 InvocationHandler 接口,看的出也是 Java 动态代理,调用其静态方法 wrap:

public static Object wrap(Object target, Interceptor interceptor) {Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);Class<?> type = target.getClass();Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}return target;}

如果 interfaces.length > 0 也就会为 target 生成代理对象,也就是说为 Configuration类 四个方法调用的 executor/parameterHandler/resultSetHandler/statementHandler 生成代理对象,这里需要单独分析里面的两个很重要的方法 getSignatureMap(interceptor) 和 getAllInterfaces(type, signatureMap)
先看 getSignatureMap(interceptor) 方法:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      }Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();for (Signature sig : sigs) {Set<Method> methods = signatureMap.get(sig.type());if (methods == null) {methods = new HashSet<Method>();signatureMap.put(sig.type(), methods);}try {Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);}}return signatureMap;}

该方法通过 Java 反射读取拦截器类上的注解信息,最终返回一个以 Type 为 key,Method 集合为 Value 的HashMap, 以上面分页拦截器为例子, key 是 org.apache.ibatis.executor.Executor, Value 是两个重载的 query 方法
再看 getAllInterfaces(type, signatureMap)

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {Set<Class<?>> interfaces = new HashSet<Class<?>>();while (type != null) {for (Class<?> c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {interfaces.add(c);}}type = type.getSuperclass();}return interfaces.toArray(new Class<?>[interfaces.size()]);
}

该方法返回根据目标实例 target 和它的父类们的接口数组,回看 Plugin.wrap 方法,如果接口数组长度大于 0,则为 target 生成代理对象
最后当在 DefaultSqlSession 中执行具体执行时,如 selectList 方法中, 此时的 executor 是刚刚生成的代理对象

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

executor 调用的方法就会执行 Plugin 重写的 invoke 方法:

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}

最终,执行自定义拦截器的 intercept 方法,拦截器就是这样被执行的.
我们发现,在 Mybatis 框架中,大量的使用了 Java 动态代理,比如只需在 Mapper 接口中定义方法,并没有具体的实现类,这一切都是应用 Java 动态代理,所以理解动态代理,能更好的理解整个执行过程。

分页插件的拦截器和多租户拦截器。我们可以参考mybatis-plus的逻辑.附上官网连接。

PaginationInnerInterceptor(分页插件拦截器)
https://mp.baomidou.com/guide/page.html
TenantLineInnerInterceptor(多租户插件拦截器)
https://mp.baomidou.com/guide/interceptor-tenant-line.html

mybatis源码解析(二)解析SqlSession下的四大对象(Executor、StatementHandler、ParameterHandler和ResultSetHandler)和插件使用相关推荐

  1. MyBatis 源码分析 - 配置文件解析过程

    文章目录 * 本文速览 1.简介 2.配置文件解析过程分析 2.1 配置文件解析入口 2.2 解析 properties 配置 2.3 解析 settings 配置 2.3.1 settings 节点 ...

  2. mybatis源码考究二(sqlsession线程安全和缓存失效)

    mybatis源码考究二 1.mybatis整合spring解决sqlsession线程安全问题 2.mybatis整合spring一级缓存失效问题 mybatis结合spring使用 1.项目依赖 ...

  3. mybatis源码阅读(二):mybatis初始化上

    转载自  mybatis源码阅读(二):mybatis初始化上 1.初始化入口 //Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与 ...

  4. mybatis源码阅读(一):SqlSession和SqlSessionFactory

    转载自  mybatis源码阅读(一):SqlSession和SqlSessionFactory 一.接口定义 听名字就知道这里使用了工厂方法模式,SqlSessionFactory负责创建SqlSe ...

  5. 手把手带你阅读Mybatis源码(二)执行篇

    点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 前言 上一篇文章提到了MyBatis是如何构建配置类的,也说了MyBatis在运行过程中主 ...

  6. Mybatis源码:@MapperScan解析过程

    目录 0.说明 1.@MapperScan 2. MapperScannerRegister 3.ClassPathMapperScanner 4. MapperFactoryBean 0.说明 my ...

  7. Mybatis源码阅读(二)

    本文主要介绍Java中,不使用XML和使用XML构建SqlSessionFactory,通过SqlSessionFactory 中获取SqlSession的方法,使用SqlsessionManager ...

  8. Mybatis源码学习二(一级缓存)

    一级缓存流程 一级缓存有效的因素 一级缓存有效测试 public class User {private Integer id;private String name;public Integer g ...

  9. Mybatis源码阅读(四):核心接口4.1——StatementHandler

    *************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如 ...

  10. MyBatis 源码解读-配置解析过程

    首先我们要清楚的是配置解析的过程全部只解析了两种文件.一个是mybatis-config.xml 全局配置文件.另外就是可能有很多个的Mapper.xml 文件,也包括在Mapper 接口类上面定义的 ...

最新文章

  1. 4.1 MyEclipse中搭建Struts2开发环境
  2. 为什么很多招聘信息都要求三年以上的工作经验?
  3. Puppet 之 模板和模块
  4. 数据驱动创新 融合引领变革 2017中国工业大数据大会·钱塘峰会今日在杭州国际博览中心顺利举行...
  5. linux里用户权限:~$,/$,~#,/#的区别与含义
  6. 窄脉冲matlab实现,[求助]如何获得与50Hz工频同步的窄脉冲信号
  7. java的socket编程---telnet客户端的实现
  8. c语言 五个学生学号 姓名 三门,有五个学生,每个学生的数据包括学号、姓名、三门课的成绩,从键盘输入五个学生的数据,要求打印三门课总平均...
  9. python怎么排名次_2019:python第3次获得TIOBE最佳年度语言排名
  10. bash shell set 命令
  11. 对称加密密码 对称加密算法
  12. opencv-python实际演练(二)军棋自动裁判(1)棋子图像采集设备DIY
  13. 微信小程序开通直播的条件
  14. Android 路由框架ARouter最佳实践
  15. Julia数据可视化:Gadfly.jl包的使用
  16. HTTP协议之GET与POST区别
  17. MongoDB not authorized for query - code 13 错误解决办法
  18. 关闭计算机防火墙命令,电脑防火墙怎么关?有哪些关闭方法
  19. vimium:全键盘操作插件 Chrome插件图文教程
  20. VBA如何清除excel单元格的内容和格式?

热门文章

  1. html怎么修改背景图片,css怎么设置背景图片?
  2. 机器学习笔记之R语言基础5(T,F检验)
  3. w ndows资源管理器无响应,windows资源管理器未响应
  4. Excel怎么转换成PDF?这两种转换方法看到就是赚到
  5. ORA-28001: the password has expired解决
  6. Android 设计模式之二:MVP模式与MVC模式 .
  7. python考拉兹猜想_考拉兹猜想的变体
  8. MySQL基本数据类型
  9. 树莓派开启SSH、VNC远程桌面、开启root账号以及换国内镜像源码等
  10. [转载]Delta Lake、Iceberg 和 Hudi 三大开源数据湖不知道如何选?那是因为你没看这篇文章