三、SqlSession的创建以及执行流程

news2024/11/15 17:18:21

简介

SqlSession接口提供了查询,插入,更新,删除方法,Mybatis中所有的数据库交互都由SqlSession来完成。SqlSession 对象完全包含以数据库为背景的所有执行 SQL 操作的方法,它的底层封装了 JDBC 连接,可以用 SqlSession 实例来直接执行已映射的 SQL 语句。每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不能被共享,也是线程不安全的,使用完成后需要及时关闭

SqlSession的创建

SqlSession 的创建需要借助于 SqlSessionFactory,SqlSessionFactory 是 Mybatis 的关键对象,每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心。创建代码示例如下:

SqlSession sqlSession = sqlSessionFactory.openSession();

SqlSessionFactory创建SqlSession的列表如下:

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

直接分析默认使用的openSession()无参的方法,在DefaultSqlSessionFactory#openSession中,可以看到其调用的是openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit)这个方法:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      //1. 从配置中获取对应的环境
      final Environment environment = configuration.getEnvironment();
      // 获取事务工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //2. 根据数据源,事物隔离级别,是否自动提交创建事物管理器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //3. 创建Executor执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      //4. 返回DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

该方法首先是根据事物工厂以及数据源工厂构造了一个事物管理器,之后创建一个Executor对象存入到DefaultSqlSession,后续DefaultSqlSession执行的时候便是通过Executor对象实例执行的。

Executor执行器的创建

Executor 接口定义了数据库操作的基本方法,其中 query*() 方法、update() 方法、flushStatement() 方法是执行 SQL 语句的基础方法,commit() 方法、rollback() 方法以及 getTransaction() 方法与事务的提交/回滚相关。Executor的是根据传入的executorType来创建的,有多个 Executor 接口的实现类,如下图所示:

在这里插入图片描述

创建Executor的代码如下:

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;
  }

SqlSession的执行流程

创建Mapper

在获取到SqlSession后,根据SqlSession获取对应Mapper

UserMapper mapper = sqlSession.getMapper(UserMapper.class);

SqlSession创建的Mapper是从Configuration中的mapperRegistry获取的:

// Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}
// MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

根据MapperProxyFactory创建代理对象

MapperProxyFactory 的核心功能就是创建 Mapper 接口的代理对象,在 MapperRegistry 中会依赖 MapperProxyFactory 的 newInstance() 方法创建代理对象,底层则是通过 JDK 动态代理的方式生成代理对象的

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

Mapper的执行

由上面的流程可知,通过SqlSession获取到的mapper对象是一个代理对象,其执行逻辑在InvocationHandler的invoke方法中

MapperProxy

通过分析 MapperProxyFactory 这个工厂类,我们可以清晰地看到MapperProxy 是生成 Mapper 接口代理对象的关键,它实现了 InvocationHandler 接口,接下来分析一下它的invoke方法

public class MapperProxy<T> implements InvocationHandler, Serializable {

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果是Object的方法,直接调用
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else {
                return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

    private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
        try {
            // 尝试从methodCache缓存中查询方法对应的MapperMethodInvoker
            MapperMethodInvoker invoker = methodCache.get(method);
            if (invoker != null) {
                return invoker;
            }
            // 如果方法在缓存中没有对应的MapperMethodInvoker,则进行创建
            return methodCache.computeIfAbsent(method, m -> {
                if (m.isDefault()) {//针对默认方法
                    try {
                        // 这里根据JDK版本的不同,获取方法对应的MethodHandle的方式也有所不同
                        // 在JDK 8中使用的是lookupConstructor字段,而在JDK 9中使用的是
                        // privateLookupInMethod字段。获取到MethodHandle之后,会使用
                        // DefaultMethodInvoker进行封装
                        if (privateLookupInMethod == null) {
                            return new DefaultMethodInvoker(getMethodHandleJava8(method));
                        } else {
                            return new DefaultMethodInvoker(getMethodHandleJava9(method));
                        }
                    } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                             | NoSuchMethodException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    // 对于其他方法,会创建MapperMethod并使用PlainMethodInvoker封装
                    return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
                }
            });
        } catch (RuntimeException re) {
            Throwable cause = re.getCause();
            throw cause == null ? re : cause;
        }
    }
}

MapperMethod

通过对 MapperProxy的invoke分析我们知道,MapperMethod 是最终执行 SQL 语句的地方,同时也记录了 Mapper 接口中的对应方法,其核心字段也围绕这两方面的内容展开

MapperMethod 的第一个核心字段是 command(SqlCommand 类型),其中维护了关联 SQL 语句的相关信息。在 MapperMethod$SqlCommand 这个内部类中,通过 name 字段记录了关联 SQL 语句的唯一标识,通过 type 字段(SqlCommandType 类型)维护了 SQL 语句的操作类型,这里 SQL 语句的操作类型分为 INSERT、UPDATE、DELETE、SELECT 和 FLUSH 五种。

SqlCommand
public static class SqlCommand {
    //记录了关联 SQL 语句的唯一标识
    private final String name;
    //维护了 SQL 语句的操作类型
    private final SqlCommandType type;

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
         获取Mapper接口中对应的方法名称
        final String methodName = method.getName();
        // 获取Mapper接口的类型
        final Class<?> declaringClass = method.getDeclaringClass();
        // 将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识,
        // 到Configuration这个全局配置对象中查找SQL语句
        // MappedStatement对象就是Mapper.xml配置文件中一条SQL语句解析之后得到的对象
        MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
                                                    configuration);
        if (ms == null) {
            // 针对@Flush注解的处理
            if (method.getAnnotation(Flush.class) != null) {
                name = null;
                type = SqlCommandType.FLUSH;
            } else {
                throw new BindingException("Invalid bound statement (not found): "
                                           + mapperInterface.getName() + "." + methodName);
            }
        } else {
            // 记录SQL语句唯一标识
            name = ms.getId();
            // 记录SQL语句的操作类型
            type = ms.getSqlCommandType();
            if (type == SqlCommandType.UNKNOWN) {
                throw new BindingException("Unknown execution method for: " + name);
            }
        }
    }

    public String getName() {
        return name;
    }

    public SqlCommandType getType() {
        return type;
    }

    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                                   Class<?> declaringClass, Configuration configuration) {
        //将Mapper接口名称和方法名称拼接起来作为SQL语句唯一标识
        String statementId = mapperInterface.getName() + "." + methodName;
        if (configuration.hasStatement(statementId)) {
            return configuration.getMappedStatement(statementId);
            // 如果方法就定义在当前接口中,则证明没有对应的SQL语句,返回null
        } else if (mapperInterface.equals(declaringClass)) {
            return null;
        }
        // 如果当前检查的Mapper接口(mapperInterface)中不是定义该方法的接口(declaringClass),
        // 则会从mapperInterface开始,沿着继承关系向上查找递归每个接口,
        // 查找该方法对应的MappedStatement对象
        for (Class<?> superInterface : mapperInterface.getInterfaces()) {
            if (declaringClass.isAssignableFrom(superInterface)) {
                MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                                                            declaringClass, configuration);
                if (ms != null) {
                    return ms;
                }
            }
        }
        return null;
    }
}
MethodSignature

MapperMethod 的第二个核心字段是 method 字段(MethodSignature 类型),其中维护了 Mapper 接口中方法的相关信息。

public static class MethodSignature {
    //表示方法返回值是否为 Collection 集合或数组、Map 集合、void、Cursor、Optional 类型
    private final boolean returnsMany;
    private final boolean returnsMap;
    private final boolean returnsVoid;
    private final boolean returnsCursor;
    private final boolean returnsOptional;
    //方法返回值的具体类型
    private final Class<?> returnType;
    //如果方法的返回值为 Map 集合,则通过 mapKey 字段记录了作为 key 的列名。mapKey 字段的值是通过解析方法上的 @MapKey 注解得到的。
    private final String mapKey;
    //记录了 Mapper 接口方法的参数列表中 ResultHandler 类型参数的位置。
    private final Integer resultHandlerIndex;
    //记录了 Mapper 接口方法的参数列表中 RowBounds 类型参数的位置。
    private final Integer rowBoundsIndex;
    //用来解析方法参数列表的工具类
    private final ParamNameResolver paramNameResolver;

    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
     // 根据返回值类型,初始化returnsVoid、returnsMany、returnsCursor、
     // returnsMap、returnsOptional这五个与方法返回值类型相关的字段
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.returnsOptional = Optional.class.equals(this.returnType);
      // 如果返回值为Map类型,则从方法的@MapKey注解中获取Map中为key的字段名称
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;
      // 解析方法中RowBounds类型参数以及ResultHandler类型参数的下标索引位置,
      // 初始化rowBoundsIndex和resultHandlerIndex字段
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      // 创建ParamNameResolver工具对象,在创建ParamNameResolver对象的时候,
     // 会解析方法的参数列表信息
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }
  }
MapperMethod#execute

MapperMethod实例拥有了SqlCommand,MethodSignature对象之后,就可以开始执行Sql的逻辑了;根据SqlCommand不同的执行类型以及MethodSignature返回值类型调用SqlSession执行Sql语句获取到执行结果

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

SqlSession#selectList

在MapperMethod中调用SqlSession的查询方法selectList,在SqlSession中最终是通过Executor执行sql逻辑,代码如下:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
     
      MappedStatement ms = configuration.getMappedStatement(statement);
      //executor执行
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

总结

  1. SqlSession是通过SqlSessionFactory创建的,封装了Execcutor对象
  2. 获取Mapper接口是通过动态代理完成的,使用MapperProxyFactory创建Mapper代理对象,执行的时候通过MapperMethod中封装的SqlCommand获取绑定的sql,通过MethodSignature确定接口的返回值,最终统一调用Execcutor的逻辑完成整个数据库的操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/185028.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微服务链路追踪SkyWalking学习笔记

目录 1、skywalking是什么 1.2 链路追踪框架对比 1.3 性能对比 1.4 Skywalking主要功能特性 2、 SkyWalking 环境搭建部署 2.1 下载 SkyWalking 2.2 搭建SkyWalking OAP 服务 2.3 SkyWalking中三个概念 3、 SkyWalking 接入微服务 3.1 linux环境—通过jar包方式接入 …

SAP ADM100-2.5 系统启动:日志文件

本节将介绍SAP ABAP系统启动时最重要的log文件和Trce文件,以掌握通过使用系统启动log文件和trace文件分析系统问题。 1、记录系统启动过程 启动过程是一个特别重要的阶段,因此该过程将被操作系统、SAP系统、数据库记录。如果SAP系统没有启动,那么你将在log日志文件中发现相…

C#使用IronPython调用Python

一、前言以下摘自百度百科&#xff1a;IronPython 是一种在 NET 和 Mono 上实现的 Python 语言&#xff0c;由 Jim Hugunin&#xff08;同时也是 Jython 创造者&#xff09;所创造。1.0 版于2006年9月5日发布。随后&#xff0c;在 2007 年&#xff0c;开发者决定改写架构&#…

音视频xxxx

文章目录前言编解码硬件解码(高级)软解码(低级)软、硬解码对比视频解码有四个步骤Android 系统中编解码器的命名方式查看当前设备支持的硬解码基础知识RGB色彩空间常见的格式对比YUV索引格式分离RGB24像素数据中的R、G、B分量BMP 文件格式格式组成像素排列顺序RGB24格式像素数据…

Apache Solr 9.1-(三)集群模式下通过Http API操作Apache Solr

Apache Solr 9.1-&#xff08;三&#xff09;集群模式下通过Http API操作Apache Solr Solr是一个基于Apache Lucene的搜索服务器&#xff0c;Apache Lucene是开源的、基于Java的信息检索库&#xff0c;Solr能为用户提供无论在任何时候都可以根据用户的查询请求返回结果&#xf…

网络原理(TCP/IP)(3)

4)滑动窗口 1)咱们滑动窗口的效果就是说在我们尽可能地保证可靠性的情况下&#xff0c;尽可能的提高传输效率2)况且咱们进行发送滑动窗口的本质就是说进行批量的发送数据&#xff0c;咱们尽可能说是把等待ACK的时间总体进行缩短&#xff0c;咱们可以把等待一份ACK的时间变成等待…

凸优化学习:PART1凸集

凸优化学习PART1 一、引言&#xff1a;优化问题简介 优化问题的定义 凸优化是优化的一种&#xff0c;是优化中比较容易的问题。在讲解优化问题前&#xff0c;首先说明什么是优化/数学规划&#xff08;Optimization/Mathematical Planning&#xff09;。 优化&#xff1a;从一…

搭建electron开发环境

electron是使用js,html,css构建桌面端应用程序的框架&#xff0c;可以使用electron开发Windows和Mac端应用。 安装nodejs,npm,cnpm 首先需要安装nodejs,npm和cnpm&#xff0c;安装后在命令行输入 node -v 和npm -v&#xff0c;如果输出了版本号&#xff0c;说明已经正常安装。…

数据仓库-数据模型建设方法总结(全)

一、大数据领域建模综述 1.1 为什么需要数据建模 有结构地分类组织和存储是我们面临的一个挑战。 数据模型强调从业务、数据存取和使用角度合理存储数据。 数据模型方法&#xff0c;以便在性能、成本、效率之间取得最佳平衡 成本&#xff1a;良好的数据模型能极大地减少不必…

MyBatis:批量添加记录

MyBatis&#xff0c;一款优秀的ORM映射框架&#xff0c;可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO&#xff08;Plain Old Java Objects&#xff0c;普通老式 Java 对象&#xff09;为数据库中的记录。同时&#xff0c;MyBatis提供了动态SQL特性&#x…

梦熊杯-十二月月赛-白银组题解-C.永恒

C. Problem C.永恒&#xff08;eternity.cpp&#xff09; 内存限制&#xff1a;256 MiB 时间限制&#xff1a;1000 ms 标准输入输出 题目类型&#xff1a;传统 评测方式&#xff1a;文本比较 题目描述: 「稻妻」是「永恒」的国度。 巴尔泽布认为&#xff0c;如果一个数…

感知机与门电路

前言&#xff1a;简述单层感知机特征及三种表示方式&#xff0c;并用单层感知机描述门电路&#xff0c;借由单层感知机无法处理非线性空间的问题&#xff0c;引出多层感知机。 单层感知机 感知机&#xff08;preceptron&#xff09;接收多个输入信号&#xff0c;输出一个信号…

【Kubernetes 企业项目实战】05、基于云原生分布式存储 Ceph 实现 K8s 数据持久化(上)

目录 一、分布式存储 Ceph 基本介绍 1.1 块存储&#xff08;rbd&#xff09; 1.2 文件系统 cephfs 1.3 对象存储 1.4 分布式存储的优点 二、Ceph 核心组件介绍 三、准备安装 Ceph 高可用集群的实验环境 3.1 机器配置 3.2 初始化环境 3.3 配置互信 3.4 配置 Ceph 安…

【精选博客】反爬过程中 x-ca-nonce、x-ca-signature 参数的解密过程

本篇博客在 请求头 x-ca-key、x-ca-nonce、x-ca-signature 加密分析第一篇 的基础上继续编写&#xff0c;大家学习时可以从上一篇入手。 文章目录x-ca-nonce 代码实现python 实现 uuidx-ca-signature代码实现在上一篇博客我们已经捕获了参数的JS代码&#xff0c;这篇博客重点要…

Java设计模式-策略模式Strategy

介绍 策略模式&#xff08;Strategy Pattern&#xff09;中&#xff0c;定义算法族&#xff08;策略组&#xff09;&#xff0c;分别封装起来&#xff0c;让他们之间可以互相替换&#xff0c;此模式让算法的变化独立于使用算法的客户。这算法体现了几个设计原则&#xff0c;第…

这些学习技巧学起来

技巧一&#xff1a;组合多个对象 在PPT页面插入多个图形后&#xff0c;想要移动这些元素时&#xff0c;很多小伙伴会挨个拖动进行位置调整。其实&#xff0c;我们大可以使用快捷键【CtrlG】将多个同类的元素进行组合&#xff0c;使其成为一个图形元素&#xff0c;这样就可以方…

springboot整合mybatis-plus及mybatis-plus分页插件的使用

springboot整合mybatis-plus及mybatis-plus分页插件的使用1. mybatis-plus?2. 引入依赖3. 编写配置文件4. 编写sql表5. mapper层5.1 mybatis-plus做了什么&#xff1f;及创建mapper接口5.2 baseMapper源码6. service层及controller层6.1 service层6.2 controller层6.2.1 page对…

【html】超链接样式

超链接样式超链接样式超链接样式 根据超链接的类型&#xff0c;显示不同图片的前缀 根据 <!doctype html> <html> <head> <meta charset"utf-8"> <title></title> <style type"text/css"> body {background: …

C# 托管堆遭破坏问题溯源分析

一&#xff1a;背景 1. 讲故事 年前遇到了好几例托管堆被损坏的案例&#xff0c;有些运气好一些&#xff0c;从被破坏的托管堆内存现场能观测出大概是什么问题&#xff0c;但更多的情况下是无法做出准确判断的,原因就在于生成的dump是第二现场&#xff0c;借用之前文章的一张…

Exynos4412 移植针对Samsung的Linux-6.1(四)NandFlash卡驱动

系列文章目录 Exynos4412 移植针对Samsung的Linux-6.1&#xff08;一&#xff09;下载、配置、编译Linux-6.1Exynos4412 移植针对Samsung的Linux-6.1&#xff08;二&#xff09;SD卡驱动——解决无法挂载SD卡的根文件系统Exynos4412 移植针对Samsung的Linux-6.1&#xff08;三…