# mybatis-3-master **Repository Path**: SoranS/mybatis-3-master ## Basic Information - **Project Name**: mybatis-3-master - **Description**: 基于3版本的手写 Mybatis 框架。Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。 - **Primary Language**: Unknown - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-05-15 - **Last Updated**: 2024-05-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 基于3版本的Mybatis框架 ![输入图片说明](image.png) ## 项目分析文档 手写 Mybatis 框架的项目分析文档地址:[项目分析文档](https://blog.csdn.net/weixin_50999696/article/details/132031257) ## 1、Mybatis 核心流程 mybatis 核心业务流程: 1. 1. 解析 mybatis 系统配置文件,封装成 Java 的 Configuration 配置类 2. 解析 mapper.xml 封装成 MapperStatement 供后面使用 3. 具体的业务代码 调用 dao 的代理类 4. 参数的映射和处理 5. 根据入参 和 MapperStatement 动态生成可执行的 sql 语句 6. 通过 resultHandle 处理结果集合映射,并返回结果集 **总体执行活动图** ![输入图片说明](image2.png) **Mybatis四大对象** 1. **ParameterHandler** 2. **SqlSource** 3. **Executor** 4. **ResultSetHandler** ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1658022763108-a1b9ab55-6594-4e52-b6e6-ff98e93bf20f.jpeg) ## 2、流程源码分析 1. 读取mybatis主配置文件 通过流来获取指定路径的文件 1. 1. InputStream is = Resources.getResourceAsStream("MybatisConfig.xml"); 1. 创建一个SqlSessionFactoryBuilder 1. 1. SqlSessionFactoryBuilder builder = new SQLSessionFactoryBuilder(); 1. 通过**构建者模式**来创建工厂 1. 1. SqlSessionFactory factory = builder.build(is); 1. 通过**工厂模式**创建sqlSession 1. 1. SqlSession sqlSession = factory.openSession(); 1. 通过动态代理获取对应的mapper 1. 1. UserMapper mapper = sqlSession.getMapper(UserMapper.class); 1. 调用方法 1. 1. List users = mapper.getUserList(); 1. 返回结果 ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1656507643253-7e91d28f-57ed-43b2-a26a-7ccf3fa68cac.jpeg) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1656508829341-aa26cb65-8dcd-443e-9762-fd87528abafc.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1656554817862-a9007b74-f9aa-4184-8f9b-a79ff4c5ca6a.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1656606426680-4fd26649-26dc-4cc4-a8f2-bff3ece555e5.png) ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1657592772720-45034bda-e996-421f-a8fb-086e104835a6.jpeg) ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1657531190752-3cfb3c0a-c511-45bb-ad03-ad87a8a858f1.jpeg) **二级缓存** ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1657591052791-6a186adb-5648-4225-8a8d-c9d82c54c9c4.jpeg) **mybatis执行流程** ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1657534338375-d8201d31-fe82-42d7-9fea-5159150efd26.jpeg) **StatementHandler执行流程** ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1657593622037-cd5c0be2-925d-4720-8f5d-653999ddc4f0.jpeg) 测试类 ```java @Before public void init() throws Exception { // 指定全局配置文件路径 String resource = "mybatis-config.xml"; // 加载资源文件(全局配置文件和映射文件) InputStream inputStream = Resources.getResourceAsStream(resource); // 还有构建者模式,去创建SqlSessionFactory对象 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } @Test public void test1() throws Exception { // 指定全局配置文件的类路径 SqlSession sqlSession = sqlSessionFactory.openSession(); Configuration configuration = sqlSession.getConfiguration(); TransactionFactory transactionFactory = configuration.getEnvironment().getTransactionFactory(); Connection connection = configuration.getEnvironment().getDataSource().getConnection(); } ``` ### 2.1、mybatis-config.xml全局配置解析 #### 2.1.1、使用建造者模式来创建对象工厂 **作用:**负责加载并且解析配置文件 **返回SqlSessionFactory** ```java public class SqlSessionFactoryBuilder { public SqlSessionFactory build(InputStream inputStream) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream); return build(parser.parse()); } catch (Exception e) { e.printStackTrace(); } return null; } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); } } ``` **说明**:使用建造者模式创建工厂 这里使用重载实现多种组合初始化 可以学习这种思想创建 和 使用 分离 ,同时: 建造类之间 相互独立 , 在 一定程度上解耦 需要一个输出流 将配置文件的内容读取出来 ```java InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { for (ClassLoader cl : classLoader) { if (null != cl) { // try to find the resource as passed InputStream returnValue = cl.getResourceAsStream(resource); if (null == returnValue) { returnValue = cl.getResourceAsStream("/" + resource); } if (null != returnValue) { return returnValue; } } } return null; } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658023121570-a89f9c21-72e1-4540-b530-67f45dd06788.png) **说明**:通过实现多个构造器来实现不同传入参数的组合。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658023377242-33fcc52c-d85c-45e8-bfda-fe56d1b3e820.png) #### 2.1.2、解析XML Config配置的类 解析mybatis-config.xml 解析类 的建造者模式 的基类 ```java public abstract class BaseBuilder { // Configuration是MyBatis初始化过程的核心对象, MyBatis中几乎全部的配置信息会保存到Configuration对象中 // Configuration对象是在MyBatis初始化过程中创建且是全局唯一的, protected final Configuration configuration; // 在mybatis-config xml配置文件中可以使用<typeAliases>标签定义别名,这些定义的别名都会记录在TypeAliasRegistry对象中 protected final TypeAliasRegistry typeAliasRegistry; // mybatis-config.xml 配置文件中可以使用<typeHandlers>标签添加自定义 TypeHandler器, // 完成指定数据库类型与 Java 类型的转换,这些 TypeHandler 都会记录在 TypeHandlerRegistry protected final TypeHandlerRegistry typeHandlerRegistry; public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); } public Configuration getConfiguration() { return configuration; } } ``` **说明:**这里的BaseBuilder抽象类就扮演着建造者模式的Builder角色。 通过及基类来实现控制属性 包括Configuration 全局配置类、TypeAliasRegistry 别名注册类、TypeHandlerRegistry 自定义处理器 #### 2.1.3、解析mybatis-config.xml 的总解析类XMLConfigBuilder ```java public class XMLConfigBuilder extends BaseBuilder { /* 标记是否已经解析过配置文件 */ private boolean parsed; /* 解析xml文件的解析器 */ private final XPathParser parser; /** * 数据源,SqlSessionFactoryBuilder.build(InputStream in, String environment, Properties properties) * 不指定为空 */ private String environment; public XMLConfigBuilder(XPathParser parser, String environment, Properties props) { /* 初始化 Configuration */ super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); /* 设置格外的属性 */ this.configuration.setVariables(props); /* 标记初始化为 false */ this.parsed = false; this.environment = environment; this.parser = parser; } } ``` 上面为解析全局配置文件的解析类 将config的信息全部解析解析 存储到对应的类中 **XPathParser**:是属于一个XML文件的解析类 结合XNode 来实现对配置文件解析通过Properties文件格式返回 #### 2.1.4、全局配置的配置类Configuration Configuration 类配置了需要配置的属性 ```java /** * 全局配置文件的实体类 */ public class Configuration { /** * Mybatis配置类。初始化Mybatix的配置和工厂类,一些工具类的别名,语言驱动 */ public Configuration() { //事务工厂 //JDBC事务工厂 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); //ManagedTransaction:MyBatis自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理 //生成MangaedTransaction工厂 typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); //数据源工厂 //生产具有简单,同步,线程安全数据库连接池的数据源工厂 typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); //生产无连接池数据源(即每次获取请求都简单的打开和关闭连接)工厂 typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); //缓存策略 //永久缓存 Cache接口实现类,里面就是维护着一个HashMap typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); //使用先进先出缓存策略的缓存装饰类 typeAliasRegistry.registerAlias("FIFO", FifoCache.class); //使用最近最少使用的缓存策略的缓存装饰类 typeAliasRegistry.registerAlias("LRU", LruCache.class); // 软引用回收策略 缓存装饰类 typeAliasRegistry.registerAlias("SOFT", SoftCache.class); // 弱引用回收策略 缓存装饰类 typeAliasRegistry.registerAlias("WEAK", WeakCache.class); /* * XML语言驱动 *

* Mybatis默认XML驱动类为XMLLanguageDriver,其主要作用于解析select|update|insert|delete节点为完整的SQL语句。 *

*/ typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); //这是一个简单的语言驱动,只能针对静态SQL的处里,如果出现动态SQL标签如 typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); //下面是日志别名 typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); //动态代理工厂,针对懒加载的 //CGLIB代理工厂 typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); //Javassist代理工厂 typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); //设置默认的语言驱动为XMLLanguagerDriver languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); //注册RawLanguageDriver语言驱动 languageRegistry.register(RawLanguageDriver.class); } } ``` **说明**:配置类中的属性比较多 就不一一列举 源码已注释,就是将初始化全局配置文件中的参数存储到Configuration类中备用或者设置,Configuration包含了该项目配置的全部参数。 #### 2.1.5、配置解析流程 目前框架实现配置 1. properties配置 2. settings 配置 3. TypeAliases 配置 4. environments 配置 5. typeHandlers 配置 6. mappers 配置 对这些配置进行解析的流程: **第一步**: 通过建造者模式将配置文件的输出流传入返回一个sqlSessionFactory工厂 来获取SqlSession。 ```java // 1. 获取到了SqlSessionFactory传入properties, properties设置配置key和value SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, properties); // 2. 调用方法: build(InputStream inputStream, String environment, Properties properties), 行为类是重载方法方式 public SqlSessionFactory build(InputStream inputStream, Properties properties) { return build(inputStream, null, properties); } public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { // XMLConfigBuilder:用来解析XML全局配置文件 // 使用构建者模式 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // parser.parse():使用XPATH解析XML配置文件,将配置文件封装为Configuration对象 // 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息) return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } public SqlSessionFactory build(Configuration config) { // 创建SqlSessionFactory接口的默认实现类 return new DefaultSqlSessionFactory(config); } XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // parser.parse():使用XPATH解析XML配置文件,将配置文件封装为Configuration对象 // 返回DefaultSqlSessionFactory对象,该对象拥有Configuration对象(封装配置文件信息) return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } public SqlSessionFactory build(Configuration config) { // 创建SqlSessionFactory接口的默认实现类 return new DefaultSqlSessionFactory(config); } } public class XMLConfigBuilder extends BaseBuilder { private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); // 方法传入的参数存放入Configuration的variables this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } } ``` **说明**: 1. 获取SqlSessionFactoryBuilder 通过建造者模式来builder一个SqlSessionFactory 通过工厂模式来实现生产SqlSession 接口 来维护一条Sql操作。 2. builder 方法中实现了实例化XMLConfigBuilder 对象 解析Config配置文件 初始化项目配置。 3. 调用XMLConfigBuilder中的解析方法对mybatis-config.xml 的全局配置文件解析解析。 使用到的工具类 ```java public Properties getChildrenAsProperties() { Properties properties = new Properties(); //获取并遍历子节点 for (XNode child : getChildren()) { //获取property节点的name和value属性 String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); if (name != null && value != null) { //设置属性到属性对象中 properties.setProperty(name, value); } } return properties; } public List getChildren() { List children = new ArrayList<>(); //获取子节点列表 NodeList nodeList = node.getChildNodes(); if (nodeList != null) { for (int i = 0, n = nodeList.getLength(); i < n; i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { // 将节点对象封装到 XNode 中,并将 XNode 对象放入 children 列表中 children.add(new XNode(xpathParser, node, variables)); } } } return children; } ``` **第二步**:实例化一个**XMLConfigBuilder** 对象 传入InputStream参数 进行全局配置文件的解析 调用parse 2.1.2 那里介绍了XMLConfigBuilder 的基本参数 我们下面会使用到一个基本的解析xml的类 1. 首先衔接一下上面XMLConfigBuilder类中的调用parse()方法来实现解析 XMLConfigBuilder中的parse解析xml配置文件调用类 ```java // 是否解析过 只能解析一次配置集文件 private boolean parsed; // XML 解析工具类 private final XPathParser parser; // 默认数据环境配置 private String environment; private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory(); /** * 解析XML配置文件 * @return */ public Configuration parse() { // 这里作用是只能加载一次 if (parsed) { throw new BuilderException("XMLConfigBuilder只能创建一次!!"); } parsed = true; // parser.evalNode("/configuration"):通过XPATH解析器,解析configuration根节点 // 从configuration根节点开始解析,最终将解析出的内容封装到Configuration对象中 // configuration 这个父标签开始解析 parseConfiguration(parser.evalNode("/configuration")); return configuration; } ``` **说明**:解析xml配置文件只需要解析一次 同时 将解析配置文件到Configuration 对象中时先需要将 根标签下的子标签全部通过 XPathParser 解析器中的Xpath语法获取指定的节点存储 到 XNode 节点中(w3.dom 包下的XNode ) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658024681929-6af9b837-4c29-4d44-b806-bb6041358bed.png) ```java private Object evaluate(String expression, Object root, QName returnType) { try { return xpath.evaluate(expression, root, returnType); } catch (Exception e) { throw new BuilderException("Error evaluating XPath. Cause: " + e, e); } } ``` xpath 调用执行器来实现表达式获取指定标签的内容 返回一个Node 对象 最后将返回的Node对象 传参到XNode 节点中 返回一个XNode 对象。 这里为什么使用org.w3c.dom.Node 的Node呢? DOM中每个XML的节点都是一个Node(org.w3c.dom.Node),Node通常跟XPath配合使用,提供了解析节点元素名、属性名、属性值、节点文本内容、嵌套节点等功能, XNode 的基本属性: 1. ***node***:被包装的org.w3c.dom.Node对象 2. ***name***:节点名 3. ***body***:节点内容 4. ***attributes***:节点属性集合 5. ***variables***:mybatis-config.xml配置文件中节点下引入或定义的键值对 6. ***xpathParser***:封装了XPath解析器,XNode对象由XPathParser对象生成,并提供了解析 这里举一个栗子 ```xml ``` 以节点为例子,生成一个对应的XNode节点后, name:值为"typeAlias",由于没有文本内容也没有子节点所以body为空, attributes:值为{alias=role, type=com.learn.ssm.chapter4.pojo.Role}的一个Properties对象, variables:为{database.driver=com.mysql.jdbc.Driver, database.url=jdbc:mysql://localhost:3306/ssm?useSSL=false, database.username=root, database.password=root}, xpathParser:是构造函数传进来的一个参数,只需要知道它提供了解析XPath表达式的功能即可。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658025723234-c6c6bdb6-e553-4cc1-a763-de0d4fcb8d52.png) 所以这里使用的Node 的实现 然后框架包装了Node 为 XNode 作为一个实体类 1. 然后再了解XPathParser类的作用 XPathParser 解析类 ```java /** * XML解析器 */ public class XPathParser { // 要解析的xml文件被转化成的Document对象。 private final Document document; // 获取document时是否要开启校验,开启校验的话会根据xml配置文件中定义的dtd文件校验xml格式,默认不开启校验。 private boolean validation; // mybatis-config.xml配置文件中,节点引入或定义的属性。 private Properties variables; // 封装的XPath对象,用来解析表达式。 private XPath xpath; /*** * 重载的构造器 可以学习 进行组合 * @param xml */ public XPathParser(String xml) { commonConstructor(false, null); this.document = createDocument(new InputSource(new StringReader(xml))); } public XPathParser(InputStream inputStream) { commonConstructor(false, null); this.document = createDocument(new InputSource(inputStream)); } public XPathParser(Document document) { commonConstructor(false, null); this.document = document; } public XPathParser(String xml, boolean validation) { commonConstructor(validation, null); this.document = createDocument(new InputSource(new StringReader(xml))); } public XPathParser(InputStream inputStream, boolean validation) { commonConstructor(validation, null); this.document = createDocument(new InputSource(inputStream)); } public XPathParser(Document document, boolean validation) { commonConstructor(validation, null); this.document = document; } public XPathParser(String xml, boolean validation, Properties variables) { commonConstructor(validation, variables); this.document = createDocument(new InputSource(new StringReader(xml))); } public XPathParser(Document document, boolean validation, Properties variables) { commonConstructor(validation, variables); this.document = document; } public XPathParser(InputStream inputStream, boolean validation, Properties variables) { commonConstructor(validation, variables); // 解析XML文档为Document对象 this.document = createDocument(new InputSource(inputStream)); } } ``` XPathParser的 属性值 和 构造方法 上面的工作方法都使用一个共同的构造方法 创建一个XPathParser 实例 ```java /** * 通用的构造方法 * 使用外部传入的参数,初始化成员变量; * 构造XPathFactory对象,获得Xpath的对象 * @param validation 设置解析xml时是否对它进行校验。 * @param variables */ private void commonConstructor(boolean validation, Properties variables){ this.validation = validation; this.variables = variables; XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); } ``` 初始化 属性的参数后 将XNode 的参数解析 为 Document 返回 通过一个createDocument来实现创建Document对象 ```java private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 若true解析器将DTD验证被解析的文档 factory.setValidating(validation); // 若true解析器将提供对 XML 名称空间的支持 factory.setNamespaceAware(false); // 若true忽略注释 factory.setIgnoringComments(true); // 若true,解析器在解析 XML 文档时,必须删除元素内容中的空格 factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); // 若true,的解析器将扩展实体引用节点 factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setErrorHandler(new ErrorHandler() { @Override public void error(SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError(SAXParseException exception) throws SAXException { throw exception; } @Override public void warning(SAXParseException exception) throws SAXException { } }); // 通过dom解析,获取Document对象 return builder.parse(inputSource); } catch (Exception e) { throw new BuilderException("不能正确创建Document文件,原因是: " + e, e); } } ``` **说明**: 1. 通过调用 实例化XMLConfigBuilder 对象时 实例化了一个XPathParser 对象传入这个XMLConfigBuilder对象中 构造方法进行初始化操作 2. 调用commonConstructor 方法实现XPathParser 对象的属性初始化 。 3. 将输入的流包装成InputSource 对象传入createDocument 方法中解析XNode 的数据 保存到该对象的document属性中,这个操作就是解析之前将configuration标签下的内容全部解析成Document类中保存数据。createDocument方法中创建Document是通过工厂模式来创建出一个Builder方法 然后再通过设置该工厂参数传入输出流InputStream来创建一个Document对象。 4. 实例化XPathParser 对象解析完之后 XMLConfigBuilder 类中的parse() 解析方法 调用了parser.evalNode("/configuration") 方法将configuration 标签下的所有子标签通过表达式将XPathParser中的Document 属性中的数据 全部解析 成XNode 然后返回。 5. evalNode 方法中实现了对该父标签解析 将该document对象的子标签全部解析到了包装类的XNode 中 存储。同时将properties配置标签的数据存储到variables 属性中保存 。XPath 是实现对指定标签名进行解析到Node 节点的然后将Node 的节点全部包装到XNode 节点。 6. 最后将XPath表达式解析的Node 对象 、XPathParser 和 variables 的properties参数包装成XNode 对象返回到 XMLConfigBuilder 解析方法中通过XNode解析。 7. 最后将封装好的XNode对象返回到XMLConfigBuilder解析类。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658026193839-b06820b1-477e-45b3-9eca-68dbb049d853.png) **第三步**:对全局配置文件中的标签单独解析 配置 配置文件解析 分节点解析 ```java /** * 只提供 properties 标签 * settings 标签 * typeAliases标签 * environments标签 * typeHandlers 标签 * mappers 标签 * @param root */ private void parseConfiguration(XNode root) { try { // 解析标签 propertiesElement(root.evalNode("properties")); // 解析标签 /*2、对标签中的标签中的内容进行解析,这里解析的是 * VFS是mybatis中用来表示虚拟文件系统的一个抽象类,用来查找指定路径下的资源。 * 上面的key为vfsImpl的value可以是VFS的具体实现,必须 * 是权限类名,多个使用逗号隔开,如果存在则设置到configuration中的vfsImpl属性中,如果存在多个, * 则设置到configuration中的仅是最后一个 * */ Properties settings = settingsAsProperties(root.evalNode("settings")); // loadCustomVfs(settings); // 解析标签 typeAliasesElement(root.evalNode("typeAliases")); settingsElement(settings); // 解析标签 environmentsElement(root.evalNode("environments")); // 解析标签 typeHandlerElement(root.evalNode("typeHandlers")); // 解析标签 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("解析Configuration配置文件出现异常,原因为: " + e, e); } } ``` root.evalNode("typeAliases") 解析该节点的方法是一样的 都是包装成XNode节点之后返回 **解析配置文件中的所有标签** 解析properties 标签的配置 ```java /** * 解析标签 * 我们这个框架只需要实现resource就行 * @param context * @throws Exception */ private void propertiesElement(XNode context) throws Exception { if (context != null){ // 获取子标签标签的内容 Properties defaults = context.getChildrenAsProperties(); // 获取标签标签resource属性的值 String resource = context.getStringAttribute("resource"); // 如果resource或者url的值不为空,则加载对应的资源信息,然后合并到defaults中 if (resource != null) { // 将通过resource导入进全局配置文件的properties defaults.putAll(Resources.getResourceAsProperties(resource)); } // 获取配置文件中 的需要替换的需要动态配置的属性值 Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } // 将配置文件中的properties数据全替换掉 parser.setVariables(defaults); configuration.setVariables(defaults); } } ``` **说明**: 1. 获取properties标签的子标签的properties 配置信息 2. 获取resource属性的参数 信息 如果resource不为空 则将该路径的位置传入工具类返回Properties文件存入defaults 中 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658026431221-7f59d04c-4624-45eb-9c69-27257a88aa18.png) 1. 获取Configuration 对象的配置文件 再将当前解析的properties 文件加入全局配置类。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658026514113-e29697d3-02d6-494e-a43f-67a87726ea0e.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658026622387-e9cb0ee5-94fe-4eed-8ba2-b80d682e1e80.png) settings 配置标签解析 ```java /** * 解析setting文件 * 将settings中的标签内容解析到Properties对象中 一样的形式储存 * @param context * @return */ private Properties settingsAsProperties(XNode context) { // 说明setting已经配置过了 if (context == null) { return new Properties(); } // 把标签解析为Properties对象 Properties props = context.getChildrenAsProperties(); // 校验每个属性,在 Configuration 中,有相应的 setter 方法,否则抛出 BuilderException 异常 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); // 如果获取的配置的信息,name不在metaConfig中,则会抛出异常 // 这里metaConfig中的信息是从Configuration类中解析出来的,包含set方法的属性 // 所以在配置标签的时候,其name值可以参考configuration类中的属性,配置为小写 for (Object key : props.keySet()) { // 从metaConfig的relector中的setMethods中判断是否存在该属性,setMethods中存储的是可写的属性, // 所以这里要到setMethods中进行判断 if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } } return props; } ``` **说明**: 1. 将 settings 标签的子标签以 properties 文件格式解析返回。 2. 传入 Configuration 的 class 文件 和 localReflectorFactory 参数传入 返回一个 Configuration 类继续的文件 包含set方法的属性 在使用 hasSetter 方法来判断 configuration 对象中是否存在该配置类的属性同时是否给予修改方法。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658027056802-29673a01-217f-4a2a-bb2e-53054f80a4fb.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658027251948-f55f24ca-d0d5-4210-8401-7325c446206b.png) 1. 如果解析的全部 settings 标签在 configuration 配置中全部 能找到属性 则返回这个 properties 文件。 ```java private void loadCustomVfs(Properties props) throws ClassNotFoundException { String value = props.getProperty("vfsImpl"); if (value != null) { String[] clazzes = value.split(","); for (String clazz : clazzes) { if (!clazz.isEmpty()) { Class vfsImpl = (Class)Resources.classForName(clazz); configuration.setVfsImpl(vfsImpl); } } } } ``` **说明**:我们根据标签里面的标签,生成了一个抽象类VFS 的子类,并且赋值到Configuration中。 获取系统的本地文件资源。 将这些配置设置到VfsImpl 属性当中 **上面用到元信息对象创建** 元信息类MetaClass的构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass方法进行创建: ```java public class MetaClass { private final ReflectorFactory reflectorFactory; private final Reflector reflector; private MetaClass(Class type, ReflectorFactory reflectorFactory) { this.reflectorFactory = reflectorFactory; //根据类型创建Reflector this.reflector = reflectorFactory.findForClass(type); } public static MetaClass forClass(Class type, ReflectorFactory reflectorFactory) { //调用构造方法 return new MetaClass(type, reflectorFactory); } //... } ``` 上面代码出现了两个新的类ReflectorFactory和Reflector,MetaClass通过引入这些新类帮助它完成功能。关于这些类,可以先看下hasSetter方法 ```java public boolean hasSetter(String name) { //属性分词器,用于解析属性名 PropertyTokenizer prop = new PropertyTokenizer(name); //hasNext返回true,则表明name是一个复合属性 if (prop.hasNext()) { if (reflector.hasSetter(prop.getName())) { //为属性创建创建MetaClass MetaClass metaProp = metaClassForProperty(prop.getName()); //再次调用hasSetter return metaProp.hasSetter(prop.getChildren()); } else { return false; } } else { // 调用 reflector 的 hasSetter 方法 return reflector.hasSetter(prop.getName()); } } ``` typeAliases 别名标签 解析 ```java /** * typeAliases * @param parent */ private void typeAliasesElement(XNode parent) { // 如果parent 不为空 if (parent != null) { for (XNode child : parent.getChildren()) { // 如果是包 if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { // 解析typeAlias标签 String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); try { Class clazz = Resources.classForName(type); if (alias == null) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } } ``` **说明**: 1. 这个方法是解析别名标签 将参数别名 和 目的对象注册到 configuration 对象的别名列表中。 2. 这里有一个优先级问题:包 > 其他 (如果存在包路径 ) 3. 如果是包路径则将包路径传入 registerAliases 注册别名方法中里面实现了通过反射。 ```java /** * 通过包名 来将包下所有的 java bean 来注册 别名信息 * * @param packageName * @param superType */ public void registerAliases(String packageName, Class superType){ ResolverUtil> resolverUtil = new ResolverUtil<>(); //把该包下的所有类进行加载,把其Class对象放到resolverUtil的matches中 resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set>> typeSet = resolverUtil.getClasses(); for(Class type : typeSet){ if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { // 将其全部解析注册 registerAlias(type); } } } ``` 上面那个方法就是将传入的包路径,传入到resolverUtil工具类中 进行所有类进行加载,然后将加载的Class对象存放到resolverUtil的matchs中 替换返回所有的Class对象解析判断每个类是否为接口、Anonymous类、member类。如果不是则注册到aliase集合中。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658027810947-e8162b8d-ffe7-4a5d-bcc3-526ce8cf9d19.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658027953405-6d8702e7-91b1-471e-a371-f32d5756b678.png) 最后在注册方法中获取TypeAlisaRegistry 类中将注册信息存入TYPE_ALIASES属性中。 将setting解析后的Properties对象取值存入configuration对象中 ```java /** * 将上一个解析的Properties文件的key-value值进行设置 * 如果没有配置则设置为默认值 * @param props */ private void settingsElement(Properties props) { configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE"))); configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory"))); configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false)); configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false)); configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true)); configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true)); configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false)); configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE"))); configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null)); configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null)); configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false)); configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false)); configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION"))); configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER"))); configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString")); configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true)); // configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage"))); // configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler"))); configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true)); configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); // configuration.setLogPrefix(props.getProperty("logPrefix")); configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658028093864-ab001d4f-47c3-40b8-a8e2-10dfa0d0e88d.png) environments 环境标签解析 ```java /** * 解析标签, * 这个标签下面全部的数据 * XNode 会默认将${}解析掉 然后再获取值 * @param context * @throws Exception */ private void environmentsElement(XNode context) throws Exception { if (context != null){ if (environment == null){ // 获取默认的环境配置 environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); // 判断当前id 是否为默认的是否为空 if (isSpecifiedEnvironment(id)){ // 解析事务 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); // 解析数据源 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); // 返回数据源 DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } } ``` **说明**: 1. 解析 environments 标签 配置环境包括**事务**的类别和**数据源**的类别 2. 事务主要是包括 JDBC 和 MANAGED 3. 数据源主要实现了 POOLED 和 UNPOOLED 池化 和 非池化 4. 将解析的事务类型和数据源通过建造者模式将 Environment 的实例化出该建造者 然后再创建出Environment的对象。 **解析事务的方法** ```java /** * 解析事务的处理方法 * @param context * @return * @throws Exception */ private TransactionFactory transactionManagerElement(XNode context) throws Exception { if (context != null){ // 获取事务的类别 String type = context.getStringAttribute("type"); // 以properties格式返回子标签内容 Properties props = context.getChildrenAsProperties(); // 将事务的类型传入解析别名返回对应的Class 文件 再反射一个对象 TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("配置文件需要配置一个事务类型."); } ``` **说明**: 1. 解析事务类型 获取这个标签的type参数中的数据 然后返回标签的所有内容 通过别名来反射当前别名的class 对象 然后再调用 newInstance 来返回一个事务的工厂。(别名列表注册了key value 的别名:class 类) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658028385153-a1196305-7923-4427-bb23-b3419cbc318c.png)![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658028400432-af8f050a-e1cb-4586-82e7-fdf42b623b68.png) 1. 将反射处理的工厂的实例返回一个事务工厂父类 将子标签设置如工厂 **通过宿务工厂返回设置别名的事务工厂** ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658028543777-25ff336e-ca4f-44a8-9b8a-2aa72ec55bf3.png) 这里为什么可以直接resolveAlias 到事务的类型? 原因是我们实例化Configuration类的时候已经将别名注册 硬编码注册了一些Class类 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658028623939-f01b341e-55d2-4ab6-9095-ab2dc495d07e.png) **解析数据源的解析** ```java private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null){ // 获取DataSource标签的一个类型 String type = context.getStringAttribute("type"); // 将子标签的数据读取出来 Properties props = context.getChildrenAsProperties(); // 实现池化和非池化两种连接方式 返回一个实例 DataSourceFactory factory = (DataSourceFactory) resolveAlias(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory."); } ``` 解析数据源方法也是一样的只是factory.setProperties 方法的实现不一样 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658035155343-0a3cb4b3-09bf-4a8e-b91f-c978c91bb3fe.png) ```java /** * 非池化设置Properties参数 * @param properties */ @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); // 将DataSource传入 解析成一个对象 参数对象 MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); for (Object key : properties.keySet()) { String propertyName = (String) key; // 驱动需要另外设置参数 if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { String value = properties.getProperty(propertyName); driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { String value = (String) properties.get(propertyName); Object convertedValue = convertValue(metaDataSource, propertyName, value); metaDataSource.setValue(propertyName, convertedValue); } else { String value = (String) properties.get(propertyName); Object convertedValue = convertValue(metaDataSource, propertyName, value); metaDataSource.setValue(propertyName, convertedValue); throw new DataSourceException("Unknown DataSource property: " + propertyName); } } if (driverProperties.size() > 0) { metaDataSource.setValue("driverProperties", driverProperties); } } ``` **说明**: 1. 使用MetaDataSource方法实现判断数据池中是否存在这个属性并且开放了设置该属性的setter 只有存在该属性才能赋值 不存在该属性值是不能赋值 metaObject 的作用大致就是这样了 2. 这个有一个新的学习思路 就是我们给某个类存参数 需要先判断是否存在set方法 如果存在才能存储,如果不存在就需要抛出异常。 目前获取到了DataSource对象和Transaction对象 需要包装到Environment类中 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658035505429-1ce61b71-800e-438d-b4c8-ebd37ad59bb5.png) 解析typeHandlers 类处理器 什么叫typeHandlers 类处理器? 1. 类处理器就是映射jdbc 中的数据类型 和 java中的数据类型 从而我们要有一个类型转换 同时也是类型映射 比如 插入数据的时候 Date ==> time 类型 就是映射作用 思想两个语言端的数据类型转换。 ```java /** * 解析类型处理器 * typeHandlers类型处理器的作用是在设置参数或取结果集时,映射DB和java类型。 * mybatis为我们提供了大多常见的默认类型处理器。 * @param parent */ private void typeHandlerElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { // 子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler if ("package".equals(child.getName())) { String typeHandlerPackage = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else { // 子节点为typeHandler时, 可以指定javaType属性, 也可以指定jdbcType, 也可两者都指定 // javaType 是指定java类型 // jdbcType 是指定jdbc类型(数据库类型: 如varchar) String javaTypeName = child.getStringAttribute("javaType"); String jdbcTypeName = child.getStringAttribute("jdbcType"); // handler就是我们配置的typeHandler String handlerTypeName = child.getStringAttribute("handler"); // resolveClass方法就是我们上篇文章所讲的TypeAliasRegistry里面处理别名的方法 Class javaTypeClass = resolveClass(javaTypeName); // JdbcType是一个枚举类型,resolveJdbcType方法是在获取枚举类型的值 JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class typeHandlerClass = resolveClass(handlerTypeName); // 注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理 if (javaTypeClass != null) { if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } } ``` **说明**: 1. 数据类型处理器 解析也有一个优先级 package > 其他 2. 获取标签中的每个节点的数据类型 同时系统会默认将基本的数据类型 进行转换 用户可以使用自定义的 数据类型 进行转换映射。 将其注册到typeHandlersRegistry 映射集中。 这个TypeHandlersRegistry 和 TypeAliasRegistry 是相似的 先再type包下 创建需要转换的类转换 我这里就展示一个比较常见的转换 ```java public class IntegerTypeHandler extends BaseTypeHandler { @Override public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter); } @Override public Integer getNullableResult(ResultSet rs, String columnName) throws SQLException { int result = rs.getInt(columnName); return result == 0 && rs.wasNull() ? null : result; } @Override public Integer getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int result = rs.getInt(columnIndex); return result == 0 && rs.wasNull() ? null : result; } @Override public Integer getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int result = cs.getInt(columnIndex); return result == 0 && cs.wasNull() ? null : result; } } ``` 常见转换的JDBC和Java之间的转换 类型处理器 Java 类型 JDBC 类型 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658035867080-2f6ff2db-2925-41af-9e0d-93f24d66f2b4.png) 实际注册方法 ```java private void register(Type javaType, JdbcType jdbcType, TypeHandler handler) { if (javaType != null) { Map> map = TYPE_HANDLER_MAP.get(javaType); if (map == null) { map = new HashMap>(); TYPE_HANDLER_MAP.put(javaType, map); } map.put(jdbcType, handler); } ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); } public void register(Class typeHandlerClass) { boolean mappedTypeFound = false; MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class javaTypeClass : mappedTypes.value()) { register(javaTypeClass, typeHandlerClass); mappedTypeFound = true; } } if (!mappedTypeFound) { register(getInstance(null, typeHandlerClass)); } } /** * 都配置了 * @param javaType * @param jdbcType * @param handler */ private void register(Type javaType, JdbcType jdbcType, TypeHandler handler) { if (javaType != null) { Map> map = TYPE_HANDLER_MAP.get(javaType); if (map == null) { map = new HashMap>(); TYPE_HANDLER_MAP.put(javaType, map); } map.put(jdbcType, handler); } ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); } ``` 一个JDBCType对应一个TypeHandler类型 可以看到对于一个Java类型是可以有多个JDBC类型相对应的,所以会存在多个TypeHandler,在这种情况下需要根据传入参数的javaType以及其在数据库中对应JdbcType一起来选定一个TypeHandler进行处理(mybatis如何知道每个字段对应的数据库字段类型的?).下面来看一个具体的TypeHandler的实现:实现Java中Date类型和jdbc中JdbcType.Time类型转换的TimeOnlyTypeHandler.JDBC中的Time类型只记录时分秒,所以如果我们传入一个代表2017-11-21 11:55:59的Date对象,那么数据库中存储的时间是11:55:59. ```java public class TimeOnlyTypeHandler extends BaseTypeHandler { @Override public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { ps.setTime(i, new Time(parameter.getTime())); } @Override public Date getNullableResult(ResultSet rs, String columnName) throws SQLException { java.sql.Time sqlTime = rs.getTime(columnName); if (sqlTime != null) { return new Date(sqlTime.getTime()); } return null; } @Override public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException { java.sql.Time sqlTime = rs.getTime(columnIndex); if (sqlTime != null) { return new Date(sqlTime.getTime()); } return null; } @Override public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { java.sql.Time sqlTime = cs.getTime(columnIndex); if (sqlTime != null) { return new Date(sqlTime.getTime()); } return null; } } ``` 这就实现了类型转换 。 Mapper 标签 使用mybatis的xml方式,我们需要在mybatis.xml的mappers标签下配置告诉mybatis映射的SqlMapper的xml文件的哪里,所以我们需要注册 mappers和Mapper的映射表 ```java /** * 解析标签 * 使用mybatis的xml方式,我们需要在mybatis.xml的mappers标签下配置告诉mybatis映射的Sql的那里 * mappers有四种方式来加载映射的sql。 * 1.url * 2.resource * 3.class * 4.package * 优先级 package、resource、url、class * @param parent mappers标签对应的XNode对象 * @throws Exception */ private void mapperElement(XNode parent) throws Exception { if (parent != null) { // 获得全部标签 for (XNode child : parent.getChildren()) { // 加载package的标签 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { // 下面只会使用一个的方式来解析 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // resource 和 url 需要使用解析类 来实现解析 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); // 通过mapperClass 的方法直接将该类直接返回Class对象 } else if (resource == null && url == null && mapperClass != null) { Class mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } } ``` **说明**:这里需要Mapper的xml解析器XMLMapperBuilder 这里获取配置的路径 实例化一个XMLMapperBuilder 对象 来解析 mapper标签的 调用XMLMapperBuilder方法中的parse方法来实现解析mapper映射文件。解析Mapper文件我们下一个阶段解析。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658037288949-89fdd2a2-f399-42ec-9593-269f280a547d.png) 这里我们只看resource的方式 来实现 为什么package的优先级比较高 实现是一个包 本身级别就高 再者我们项目里面的Mapper.xml文件就比较多。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658037420350-af98ff11-21a6-4e2f-8340-d33f244c8b6b.png) mybatis-config.xml 配置文件解析的主要流程 ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1657246351334-2c2d1df8-8cb5-456f-bba8-fdcec8544b7c.jpeg) #### 2.1.6、解析全局配置文件图 mybatis-config.xml 的标签详细介绍流程 ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1658066585713-5dc9cd91-0758-4cc7-9471-1aecd0b19708.jpeg) ### 2.2、mapper.xml文件解析 映射文件的解析过程是配置文件解析过程的一部分,MyBatis会在解析配置文件的过程中对映射文件进行解析。解析逻辑封装在XMLConfigBuilder中的mapperElement方法中 代码的主要逻辑是遍历mappers的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以XML为载体的配置称为映射文件。 **映射方式**: 1. 从文件系统中加载映射文件; 2. 通过URL的方式加载映射文件; 3. 通过mapper接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中; 4. 通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。 #### 2.2.1、XMLMapperBuilder 类解析映射文件 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1657281049850-1b83ebe5-61dc-414d-88ef-bf4a1edd94d1.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1657283636493-e40cbfe4-77fa-4c32-9513-679f902e02bd.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658037879403-530b7e89-ca17-480d-87d3-6e9c5f2d51c9.png) ```java public void parse() { //检测映射文件是否已经被解析过 if (!configuration.isResourceLoaded(resource)) { //解析mapper节点 configurationElement(parser.evalNode("/mapper")); //添加资源路径到“已解析资源集合”中 将当前资源增加到已解析的集合中避免多次解析 configuration.addLoadedResource(resource); //通过命名空间绑定Mapper接口 bindMapperForNamespace(); } //处理未完成解析的节点 parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658038012343-6651e506-dcc5-492e-a33a-53c04c14fb42.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658038139989-b984b2ce-94d7-4ab0-a851-1372c417ee4e.png) **说明**: 1. 解析Mapper.xml 文件的第一步就是获取命名空间判断是否匹配当前的命名空间 如果匹配才会进行解析 2. 然后解析cache-ref 标签 3. 解析cache 标签 4. 解析resultMap 结果集 5. 解析sql的语句片段 6. 解析CURD的4种方式 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658038624914-ff36e087-7d9a-4e80-9791-e8feea699cb7.png) ##### 1、解析命名空间 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658038703735-96753aa8-76b5-4dc6-8692-b330382ebc79.png) MapperBuilderAssistant 类中的方法 ```java public void setCurrentNamespace(String currentNamespace) { // 如果命名空间为空 if (currentNamespace == null) { throw new BuilderException("这个映射集 Mapper的命名空间为空!!"); } if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) { throw new BuilderException("错误的命名空间! '" + this.currentNamespace + "' but found '" + currentNamespace + "'."); } this.currentNamespace = currentNamespace; } ``` ##### 2、解析cache-ref标签 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658039003475-af11bf43-492c-4f1d-bfc6-2871901bbdd9.png) **说明**: 1. CacheRefElement解析cache-ref标签 2. 获取当前命名空间 和命名空间的value 路径参数存储到configuration中的CacheRefMap中。 3. 通过builderAssistant助手和当前命名空间来创建CacheRefResolver 对象来实现resolveCacheRef 解析 4. 调用resolveCacheRef方法 - > userCahceRef 方法 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658039727821-ae0a8da1-f1f4-4d9c-9e51-270f39b4d295.png) 通过命名空间来获取当前的 二级缓存Cache 然后将其设置为当前使用的缓存,并且将解析bool设置为false。 ##### 3、解析cache标签 ```java /** * 解析cache 标签的实现方法 * * 一级缓存默认开启 二级缓存配置在映射文件中 使用者需要先开启二级缓存 * 同时可以不需要配置 * @param context */ private void cacheElement(XNode context) { if (context != null) { // 获取各种属性 String type = context.getStringAttribute("type", "PERPETUAL"); Class typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); // 获取子节点配置 Properties props = context.getChildrenAsProperties(); // 构建缓存对象 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658040618935-6739da1a-0257-4d6b-ab83-597329d0c56d.png) **说明**: 1. 通过builderAssistant 对象调用useNewCache 方法来使用一个新的缓存对象 该方法是通过当前命名空间 来建造出一新的Cache对象 同时设置参数 并且build方法来将cache包装 **一级缓存**: 一级缓存是**Session**级别,一般一个**SqlSession**对象会使用一个**Executor**对象来完成会话操作,**Executor**对象会维护一个**Cache**缓存,以提高查询性能,减少对DB的压力。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658040929422-21191258-0264-4540-8958-6e024a2624e0.png) **二级缓存**: 如果用户配置了cacheEnabled=true,那么在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者 CachingExecutor,这时SqlSession使用CachingExecutor对象完成操作请求。 CachingExecutor对查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果 - 有查询结果 - 直接返回缓存结果 - 缓存未命中 再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后再返回给用户 MyBatis二级缓存设计灵活,可以使用: - MyBatis自己定义的二级缓存实现 - 实现org.apache.ibatis.cache.Cache接口自定义缓存 - 使用第三方内存缓存库,如Memcached ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658040983378-f56540d7-c4f3-48de-a1d9-d9543d7be63f.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658041042752-6eefc90b-09c3-4183-9435-e850af7f0817.png) 缓存是通过装饰器模式来实现的 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658041130123-43aeed08-8672-4eaa-bf43-fb38557919eb.png) 通过delegate 来实现一个装饰者 Cache:Cache接口是缓存模块的核心接口,定义了缓存的基本操作。 ```java public interface Cache { String getId();//缓存实现类的id void putObject(Object key, Object value);//往缓存中添加数据,key一般是CacheKey对象 Object getObject(Object key);//根据指定的key从缓存获取数据 Object removeObject(Object key);//根据指定的key从缓存删除数据 void clear();//清空缓存 int getSize();//获取缓存的个数 ReadWriteLock getReadWriteLock();//获取读写锁 } ``` PerpetualCache:缓存的基础实现类,使用HashMap来实现缓存功能。 ```java public class PerpetualCache implements Cache { private final String id; private Map cache = new HashMap<>(); ... ``` BlockingCache:阻塞版本的装饰器,保证只有一个线程到数据库去查指定key的数据。 ```java public class BlockingCache implements Cache { //阻塞的超时时长 private long timeout; //被装饰的底层对象,一般是PerpetualCache private final Cache delegate; //锁对象集,粒度到key值 private final ConcurrentHashMap locks; public BlockingCache(Cache delegate) { this.delegate = delegate; this.locks = new ConcurrentHashMap<>(); } @Override public Object getObject(Object key) { acquireLock(key);//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试 Object value = delegate.getObject(key); if (value != null) {//获取数据成功的,要释放锁 releaseLock(key); } return value; } private ReentrantLock getLockForKey(Object key) { ReentrantLock lock = new ReentrantLock();//创建锁 ReentrantLock previous = locks.putIfAbsent(key, lock);//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁 return previous == null ? lock : previous; } //根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试 private void acquireLock(Object key) { //获得锁对象 Lock lock = getLockForKey(key); if (timeout > 0) {//使用带超时时间的锁 try { boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); if (!acquired) {//如果超时抛出异常 throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); } } catch (InterruptedException e) { throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); } } else {//使用不带超时时间的锁 lock.lock(); } } private void releaseLock(Object key) { ReentrantLock lock = locks.get(key); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } ... ``` private final Cache delegate; ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658041438968-9b458f14-60b4-4b1c-bbb8-ae6daef7d5b3.png) 如果我们设置的是默认的cache缓存 就直接将缓存设置 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658041582242-cdcb7d5d-c86a-421b-bb01-edafec28440d.png) 最后返回cache 将该装饰者模式包装好的cache设置到configuration对象中 返回的cache对象就是头结点。 上面就是实现装饰者模式 :饰者是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象扩展功能,即插即用。 ##### 4、解析resultMap标签 ```java /** * 解析结果集的参数 集合 * * * * * @param list * @throws Exception */ private void resultMapElements(List list) throws Exception { for (XNode resultMapNode : list) { try { resultMapElement(resultMapNode); } catch (IncompleteElementException e) { // ignore, it will be retried } } } ``` 一个一个进行解析 真正实现解析resultMap的方法 resultMapElement 方法 ```java /** * 解析 resultMap 方法 * * * * * * * * * @param resultMapNode * @return */ private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception{ ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); // 查找是否能 找到 不能则设置为默认 // 获取type属性 String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); String extend = resultMapNode.getStringAttribute("extends"); Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); // 解析type属性对应的类型 Class typeClass = resolveClass(type); if (typeClass == null) { typeClass = inheritEnclosingType(resultMapNode, enclosingType); } // 结果集 的列表 List resultMappings = new ArrayList<>(); // 将以前的结果集全部添加进来 resultMappings.addAll(additionalResultMappings); List resultChildren = resultMapNode.getChildren(); for (XNode resultChild : resultChildren) { if ("constructor".equals(resultChild.getName())) { // 解析constructor节点,并生成相应的 ResultMapping // 目前未实现该标签 // processConstructorElement(resultChild, typeClass, resultMappings); throw new BuilderException("目前未实现constructor 构造器标签"); } else if ("discriminator".equals(resultChild.getName())) { // 解析discriminator节点 // discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); throw new BuilderException("目前未实现discriminator 鉴别器标签"); } else { List flags = new ArrayList<>(); if ("id".equals(resultChild.getName())) { //添加ID到flags集合中 flags.add(ResultFlag.ID); } // 解析id和property节点,并生成相应的ResultMapping resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, resultMappings, autoMapping); try { return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } } ``` **说明**: 1. 获取resultMap的Id 默认拼装所有父节点的id 或 value 或 property ```java // 获取 ID , 默认值会拼装所有父节点的 id 或 value 或 property。 String id=resultMapNode.getStringAttribute("id",resultMapNode.getValueBasedIdentifier()); ``` 这里涉及到 XNode 对象中的两个函数:getStringAttribute()以及getValueBasedIdentifier() ```java /** * 生成元素节点的基础 id * @return */ public String getValueBasedIdentifier() { StringBuilder builder = new StringBuilder(); XNode current = this; // 当前的节点不为空 while (current != null) { // 如果节点不等于 this, 则在0之前插入 _ 符号, 因为是不断的获取父节点的, 因此是插在前面 if (current != this) { builder.insert(0, "_"); } // 获取 id, id不存在则获取value, value不存在则获取 property。 String value = current.getStringAttribute("id", current.getStringAttribute("value", current.getStringAttribute("property", null))); // value 非空, 则将.替换为_, 并将value的值加上 [] if (value != null) { value = value.replace('.', '_'); builder.insert(0, "]"); builder.insert(0, value); builder.insert(0, "["); } // 不管 value 是否存在, 前面都添加上节点的名称 builder.insert(0, current.getName()); // 获取父节点 current = current.getParent(); } return builder.toString(); } ``` 1. 解析结果集中的类型,结果集的类型, 对应的是一个 JavaBean 对象。通过反射来获得该类型。 ```java // 获取type, type 不存在则获取 ofType, ofType // 不存在则获取 resultType, resultType 不存在则获取 javaType String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); // ... ... // 获取 type 对应的 Class 对象 Class typeClass = resolveClass(type); ``` 1. 获取继承结果集和自动映射 ```java String extend = resultMapNode.getStringAttribute("extends"); Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); ``` 这两个属性在配置XML的时候,显得有些可有可无。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658042771872-49a3b443-2eff-41b1-88f6-bd1a9d1655c2.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658043106946-9fefaef3-d923-4d13-be7f-328975ed1760.png) ##### 5、解析sql语句片段 ```java /** * 如果有显式声明 databaseId ,那只有符合当前全局 databaseId 的 SQL 片段会提取; * 如果没有声明 databaseId ,则会全部提取。 * 解析了两遍 SQL 片段,而且在每一次循环解析中,都会判断一次 SQL 片段是否匹配当前 databaseId , * 匹配的话就会放到一个 sqlFragments 的 Map 中 * @param list * @param requiredDatabaseId */ private void sqlElement(List list, String requiredDatabaseId) { for (XNode context : list) { String databaseId = context.getStringAttribute("databaseId"); String id = context.getStringAttribute("id"); id = builderAssistant.applyCurrentNamespace(id, false); // 鉴别当前SQL片段是否匹配 if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { sqlFragments.put(id, context); } } } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658043790552-99253b24-3220-4be5-ba19-fb0ede43d7de.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658043894106-c22d4615-5458-40f9-a7ea-f5c244f29551.png) ##### 7、XMLMapperBuilder总体流程 XMLMapperBuilder标签解析总体流程 ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1658193350603-b489decb-4459-4c8a-bb95-991e6ac4f4fc.jpeg) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658276861429-e0346553-52cd-4972-8ced-6b9e922e9ca5.png) #### 2.2.2、XMLStatementBuilder类解析 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658044155595-a50d159a-3ae2-4002-b39c-f0578527eda6.png) **XMLStatementBuilder创建MappedStatement的大致流程**: 1. 获取SQL节点的属性值 2. 使用XMLIncludeTransformer解析include节点 3. 创建MappedStatement实例,保存到Configuration#mappedStatements中 4. 默认使用XMLLanguageDriver解析SQL节点得到SqlSource,使用各种类型的NodeHandler、SqlNode解析SQL节点。SqlSource有两种:**DynamicSqlSource** SQL语句有${param0}(文本替换,可能是一段SQL)或者使用了if/where(运行时决定是否拼接SQL片段)等节点需要运行时才能解析出要执行的SQL。**RawSqlSource** 使用了#{param1}占位符或者没有使用,就是一个完整SQL,不需要运行时解析得到SQL。 5. 创建MappedStatement,保存到Configuration#mappedStatements中 ```java /** * 解析 * select * , * from some_table t1 * cross join some_table t2 * * @param source * @param variablesContext * @param included */ // 递归处理include标签中的内容 private void applyIncludes(Node source, final Properties variablesContext, boolean included) { // 获取结点为include的节点 if (source.getNodeName().equals("include")) { // 查找refid对应的sql片段,并返回sql片段对象的深拷贝 Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); // 合并include中配置的局部变量和mybatis配置文件中生命的全局变量 Properties toIncludeContext = getVariablesContext(source, variablesContext); applyIncludes(toInclude, toIncludeContext, true); if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { toInclude = source.getOwnerDocument().importNode(toInclude, true); } // 将include替换为sql source.getParentNode().replaceChild(toInclude, source); while (toInclude.hasChildNodes()) { toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); } // 移除include标签 toInclude.getParentNode().removeChild(toInclude); } else if (source.getNodeType() == Node.ELEMENT_NODE) {// 替换标签属性的占位符 if (included && !variablesContext.isEmpty()) { // 用真实值替换节点属性中的占位符 NamedNodeMap attributes = source.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Node attr = attributes.item(i); attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); } } // 递归处理标签下所有子标签的占位符 NodeList children = source.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { applyIncludes(children.item(i), variablesContext, included); } } else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) { // 用真实值替换文本或CDATA区中的占位符 source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); } } ``` **说明**: ```xml ${alias}.id,${alias}.username,${alias}.password ``` 1. 获取到configuration对象存储的properties标签中的数据 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658194335391-2e324adb-78c2-4e0a-9e60-b2c70a78b5d9.png) 1. 判断标签中的数据是否为空,如果不为null则复制全部数据待后面使用 2. 判断当前节点是include标签还是element标签还是文本标签 3. 如果是include标签则获取到refid属性的参数 解析到里面的参数 存在占位符则替换掉 并且拼接命名空间 到configuration对象存储的SQLFragments的Map中通过refid查找XNode节点 通过深拷贝返回Node节点、因为一个sql片段会被多次引用 所有一定是原始对象的拷贝。 4. 获取include中的参数属性 name和value的属性获取 然后和configuration对象中的数据合并返回 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658194967318-ded36b7d-72fd-4d43-9f7f-dd903e1e6396.png) 1. 递归处理include标签中的数据防止 include的标签中嵌套了标签 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658195133500-deec89f9-7a2e-4083-8182-feaf10625d41.png) 替换sql的意思是将refid的节点替换include中的数据 通过获取的sql片段来替换 1. 将inclue标签移除 保留sql片段 2. 如果*替换标签属性的占位符 因为引用sql标签的深克隆是会保留占位符的所以需要进行替换* ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658195786423-4cf17851-7520-4a4f-8564-74781d6ba9eb.png) 1. *获取结点下的全部节点 通过遍历Node节点来替换节点属性中的占位符 同时处理完递归处理所有子标签的占位符* - 替换SQL标签 ```xml ``` - 将sql的内容插入到select节点中对应的位置 ```xml ``` - 移除sql多余的节点标签 ```xml ``` **执行流程**: 1. 解析include元素 2. 递归解析include元素 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658277952979-63d03a36-c336-4ef1-957b-6dd661a4f25a.png) 1. 查找SQL片段 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658278048260-72e08659-cc55-44c0-9afc-b943b555dd22.png) 1. include子节点属性值替换占位符 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658278241009-0353fb46-9595-4767-ae04-8c4ca6746207.png) ##### 2、SelectKey标签解析 解析selectKey标签 目前还未实现解析selectKey标签 ##### 3、LanguageDriver 创建 通过LanguageDriver驱动来创建SqlSource动态SQL ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658196317677-4edfc8b3-3533-4e49-8c50-ed3bb622f546.png) ```java /** * 解析动态sql语句 标签 * 解析select\insert\ update\delete标签中的SQL语句, * 最终将解析到的SqlNode封装到MixedSqlNode中的List集合中 * 将带有${}号的SQL信息封装到TextSqlNode * 将带有#{}号的SQL信息封装到StaticTextSqlNode * 将动态SQL标签中的SQL信息分别封装到不同的SqlNode中 * @return */ public SqlSource parseScriptNode() { //解析DML标签下的子节点,封装成MixedSqlNode MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { //只创建DynamicSqlSource对象。完整可执行的sql,需要在调用getBoundSql(Parameter)方法时才能组装完成。 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { /** * 调用 SqlSourceBuilder类将"#{xxx}“ 替换为占位符”?",并绑定ParameterMapping, * 最后返回的RawSqlSource中持有一个由SqlSourceBuilder构建的SqlSource对象。 */ sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } ``` **说明**: 1. 最终将解析到的SqlNode封装到MixedSqlNode中的List集合中 所以先判断是否为动态语句 通过parseDynamicTags方法判断 同时返回一个MixedSqlNode对象最后的结果对象 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658209077311-fd5ad3a2-9b8b-4464-b638-2cec5df0e3a5.png) 1. 获取该节点下的全部节点遍历 获取子节点下的全部标签解析存储到指定的列表中存储 判断是处理文本节点还是元素节点 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658209302913-a28253af-d6fb-40bd-ac35-5eee6f7ccdbd.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658209311365-55c9e972-224e-4cbe-9b30-c665b90c2846.png) 如果是文本节点 获取文本的内容存储到TextSQLNode对象中保存标签将该标签下的文本对象add到contents的List中存储 统一返回 如果是元素节点则通过节点名来获取SQL标签的处理器 同时设置动态标签是dynamic的 将List列表传入MIE的SQLNode对象存储并且返回。 MixedSqlNode对象是存储SqlNode结合的 就是存储哪些SQLNode集合 contents可能包含 StaticTextSqlNode和IfSqlNode或者WhereSqlNode或者其他。 当sql语句中存在${}的话就是dynamic 的 通过TextSQLNode来判断是否为动态标签 如果存在${}则加入到contents动态标签中 以TextSQLNode对象存储到contents中 如果不存在${}的话以StaticTextSQLNode对象存储到contents 中 记住存在${}就是dynamic 的需要存储到TextSQLNode类中 如果是除${}的话就是static的sql语句只需要使用apply 字符串追加操作 - StaticTextSqlNode 最简单的SQLNode 功能只能拼接到context上下文 - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658212982056-bc3a286a-ba25-423b-be20-1460a620584c.png) - TextSQLNode 表示包含${}占位符的动态SQL节点 里面会使用GenericTokenParser解析“${}”占位符,并直接替换成传入的实际参数值。实际参数值获取逻辑在内部BindingTokenParser.handleToken中。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658213410780-92217062-d0f6-464f-9185-2a506a68e0dd.png) - IfSqlNode 看传入的参数值是否有满足if test里的表达式,满足将SQL片段合并到context上下文中。若不满足test则过滤掉这一部分SQL片段,不添加到context中。![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658213669288-841557c7-87e0-41b6-ad58-5bbd3cc4d748.png) - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658213750100-8678f818-a21b-4fcb-bfd5-3779902f8d88.png) - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658214033713-2118bcab-baad-4a37-87e1-0bb8fd646e3e.png) 如果存在 等标签 需要执行多次 存储到contents中 通过里面的IfHandler处理器来对if语法的处理 是否满足if 里的内容 满足则将if标签以IfSqlNode 对象put到contents 中 - WhereSqlNode 、SetSqlSet 节点 标签的 SqlNode 实现类,继承至WhereSqlNode。 标签的 SqlNode 实现类,继承至SetSqlNode。WhereSqlNode会传入prefix=WHERE和prefixesToOverride=["AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"] - TrimSqlNode 标签的 SqlNode 实现类 - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658212343685-7b6d490e-2369-4682-a410-167ef81d9f2a.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658209740592-b186cf86-f438-48e6-8528-658be69394b5.png) 如果是动态标签就add到系统的contents的List中然后存储到MixedSqlNode对象中 以备后面使用。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658211576225-0b183b27-c00d-4215-8b17-a6c32fdfd2d8.png) 1. 通过判断是否为动态标签解析 如果是动态标签则实例化DynamicSQLSource 对象存储sql 调用getBoundSql才会使用 2. 获取到MixedSqlNode 对象 判断当前节点是为dynamic节点还是#{}节点 如果是dynamic节点则需要都要DynamicSQLSource对象 #{}节点只需要调用RawSqlSource节点维护。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658214408961-e48ba6cb-345b-4cf4-affe-ad3e448879ce.png) 1. 调用MapperBuilderAssistant构造助手创建MappedStatement 对象 使用建造者模式创建一个MappedStatement对象 同时获取ParameterMap对象设置到Map配对Statement对象中 最后在将创建的MappedStatement对象存储到configuration对象中。 2. 将resource 添加到configuration对象中 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658214773834-9a2b37b8-0413-40f8-a309-b9ca33128d52.png) bindMapperForNamespace 绑定Mapper接口 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658214893667-0deb1919-4498-4aec-be58-dd503226c344.png) ```java parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); ``` 这几个方法就是解析还未解析的方法 内容不太多 重新解析 这里就已经将Mapper解析完成 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658215772068-9cf97018-3cb9-4242-8a9a-c795699f7dbd.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658215822604-f706b04f-40c9-4477-8dc7-351042e6661b.png) ##### 4、XMLScriptBuilder类的流程 ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1658283332280-2e2c0dc7-10b1-42b3-85db-7a329892cd40.jpeg) ##### 5、XMLStatementBuilder总体流程 ![img](https://cdn.nlark.com/yuque/0/2022/jpeg/25484710/1658282598103-f1ffdf81-b983-47a7-b157-3f217097ed8a.jpeg) ### 2.3、SqlSession会话获取 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658216174316-d867f6c3-edcb-41a3-b2a9-4182bbc0ccda.png) ```java private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 获取数据源环境信息 final Environment environment = configuration.getEnvironment(); // 获取事务工厂 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 获取JdbcTransaction或者ManagedTransaction 设置参数 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 创建Executor执行器 final Executor executor = configuration.newExecutor(tx, execType); // 创建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(); } } ``` **说明**: 1. openSessionFromDataSource 方法需要获取数据源、事务工厂、JdbcTransaction管理 、创建Executor执行器 最后将configuration对象和执行器 是否自动提交传入DefaultSqlSession类中创建对象 2. 返回事务 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658216804928-c871e3c2-585e-4594-9d90-c42867f6586b.png) 通过事务工厂 返回一Transaction 事务实例 1. **configuration**.newExecutor(tx, execType); 创建Executor执行器。 2. 最后创建一个DefaultSqlSession 实例对象,出现异常则需要关闭事务。 3. 放回SqlSession 对象 ### 2.4、Mapper代理对象获取 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658217339022-246ef697-abc1-4e7a-814e-a17b6ac54336.png) ```java @SuppressWarnings("unchecked") // getMapper方法返回mapper的动态代理生成的对象 public T getMapper(Class type, SqlSession sqlSession) { // 根据Mapper接口的类型,从Map集合中获取Mapper代理对象工厂 存放了mapper标签解析的反射类 final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // 通过MapperProxyFactory生产MapperProxy,通过MapperProxy产生Mapper代理对象 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } ``` **说明**: 1. 调用MapperRegistry类中 getMapper方法来;返回mapper的动态代理生成的对象, 该类存放了代理对象的mapper 首先通过knowMappers 集合中通过class对象获取代理工厂 如果工厂为空则抛出异常 通过MapperProxyFactory来生成MapperProxy 2. 通过MapperProxyFactory来生产MapperProxy ```java @SuppressWarnings("unchecked") protected T newInstance(MapperProxy mapperProxy) { // 使用JDK动态代理方式,生成代理对象 使用代理反射 mapperProxy实现invoke类(实现了InvocationHandler接口的类) return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { // 创建基于JDK实现的Mapper代理对象 final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); // 创建实例对象 return newInstance(mapperProxy); } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658217707390-106e791a-61dc-4a51-853c-49926edc43b3.png) 1. Proxy.newProxyInstance 通过传入MapperInterface接口的反射类(UserMapper.class)来实例化代理对象 ### 2.5、执行方法 #### 1、动态代理的实现类 invoke方法 ```java @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 是Interface接口还是Object类 // 如果方法是Object类自带的方法,比如没有被重写的equals toString, hashcode 等,还是执行原来的方法 不需要拦截 // getDeclaringClass()返回表示声明由此 Method 对象表示的方法的类或接口的 Class 对象。 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); // 调用接口的默认方法 } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 如果不是object的自带方法,先去 Map methodCache中找是否已经存在这个method了, // 没有就将method封装成MapperMethod存进methodCache中然后返回MapperMethod。 final MapperMethod mapperMethod = cachedMapperMethod(method); // 执行sqlSession的方法 return mapperMethod.execute(sqlSession, args); } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658217997470-04300416-f0c4-40ff-bde7-11de4e0b4e91.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658218101954-23facd1f-77fd-47b0-8978-0bf84696f569.png) ```java public Object execute(SqlSession sqlSession, Object[] args) { Object result = null; // 很明显我们的SqlCommandType是SELECT类型 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 { 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()); } //是否返回类型是void类型并且Method参数列表中包含resultHandler,具体的判断在文末分析 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; } ``` **说明**: 1. 获取command.getType() 的类别通过Switch 来分支 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658218420986-028e4696-a425-48d7-b9eb-2bb46e56a5a6.png) 1. 解析insert ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658218792308-0afce13a-9795-4afe-97f9-da1eed6435fe.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658218841757-c3791992-4746-4a54-87d9-a62708f57815.png) 将参数列表中的参数通过集合形式以注解中的value作为key 而形参作为value 存储到param的集合中以Object类型返回到前一次操作。 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658219219930-e8b13a01-e600-483c-b760-a0df4a20ecff.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658219328984-4a83ddf2-30dc-403f-8bcf-4e1d64f6e9ea.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658219520919-f61cbb44-f7da-4e6d-8b8e-7b6ee99814d0.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658219636870-633aee5b-32fd-43d4-b324-9745a4d703cb.png) #### 2、Statement设置参数 为Statement设置参数 ```java /** * 设置参数 * @param ps * @throws SQLException */ @Override public void setParameters(PreparedStatement ps) throws SQLException { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); // 获取要设置的参数映射信息 List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { // 对每条语句进行传参 JDBC 一样的使用 通过序号设置参数 for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); // 只处理入参 pstmt.setString(1, "姓名" + i);类型 if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; // 获取属性名称 String propertyName = parameterMapping.getProperty(); // 判断是否存在该传入参数 if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } // 获取每个参数的类型处理器,去设置入参和获取返回值 TypeHandler typeHandler = parameterMapping.getTypeHandler(); // 获取每个参数的JdbcType JdbcType jdbcType = parameterMapping.getJdbcType(); // 如果都为空则获取为空的值 if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { // 给PreparedStatement设置参数 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException e) { throw new TypeException( "Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } catch (SQLException e) { throw new TypeException( "Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } } ``` **说明**: - 先获取boundSql语句中的参数列表 - 通过遍历对语句中的?占位符进行替换 - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658219937210-c172baef-9a4c-4926-97f4-297e1f076453.png) - 通过判断parameterMapping.getMode() != ParameterMode.*OUT 参数类型* - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658220156493-0e228eac-5d54-4d88-8929-826136110786.png) - ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658220279218-160b1143-eeda-4cb3-8a70-11ed33042c7c.png) 1. ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658220445658-7e67618d-b452-485c-85c8-5988e8cad3e4.png) #### 3、选择操作解析 SELECT操作解析 ```java 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 { 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; ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658220713697-4232c2d7-8c4b-4f07-8635-094753c4dc6f.png) **说明**: 1. 对返回值为空同时存在返回集处理器 上面的this.method.returnsMap()是指@MapKey控制返回 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658220862729-640c3311-afbc-4777-ab5a-b729f65ee649.png) 这里涉及到了一个 onvertArgsToSqlCommandParam解析入参 ```java public Object convertArgsToSqlCommandParam(Object[] args) { return this.paramNameResolver.getNamedParams(args); } ``` 解析@Param注解 names是**SortedMap**,在构造函数中赋值的,判断入参有没有@Param注解。 **key**是入参的顺序,从0开始;**value**是@Param中的值 如果没有@Param注解,则**value**值为arg0、arg1… 将入参名和值匹配 ```java public Object getNamedParams(Object[] args) { int paramCount = this.names.size(); if (args != null && paramCount != 0) { // 只有一个入参时,返回入参值 if (!this.hasParamAnnotation && paramCount == 1) { return args[((Integer)this.names.firstKey()).intValue()]; } else { // 多个入参时,返回一个Map Map param = new ParamMap(); int i = 0; for(Iterator i$ = this.names.entrySet().iterator(); i$.hasNext(); ++i) { Entry entry = (Entry)i$.next(); param.put(entry.getValue(), args[((Integer)entry.getKey()).intValue()]); String genericParamName = "param" + String.valueOf(i + 1); if (!this.names.containsValue(genericParamName)) { param.put(genericParamName, args[((Integer)entry.getKey()).intValue()]); } } return param; } } else { return null; } } ``` **示例1:** 多个入参,没有加@Param注解 ```java @Select("SELECT count(0) from es_inter_invokfaillog where rownum = #{num} and invok_type=#{type}") public Integer selectAny(int num,String type); ``` 样执行sql会报错,找不到num,可以改为#{arg1}或#{param1} **示例2**:多个入参,加@Param注解 ```java @Select("SELECT count(0) from es_inter_invokfaillog where rownum = #{num} and invok_type=#{type}") public Integer selectAny(@Param("num")int num,@Param("type")String type); ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658221327359-17d4ebb5-1b05-4ff8-a649-d90e0f453ec1.png) ```java result = sqlSession.selectOne(this.command.getName(), param); ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658221372473-3b192005-9464-475d-a8de-752c7d6dfe39.png) ```java public T selectOne(String statement, Object parameter) { return this.sqlSessionProxy.selectOne(statement, parameter); } ``` sqlSessionProxy是动态代理生成的,每一次执行方法时都会重新去 new 一个**DefaultSqlSession**,可以看下invoke方法部分代码 ```java // 获取session,这里有个事物的判断 SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); Object unwrapped; try { // 真正执行sql语句的地方 Object result = method.invoke(sqlSession, args); if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { sqlSession.commit(true); } unwrapped = result; } ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658221481734-c3ca0a67-3484-47fd-86ae-b5a544c14eeb.png) 查询是调用select的查询方法 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658222119704-3fea1a39-5080-40c6-86ff-c5ee3909a7a0.png) ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658222215593-a63295ec-eee0-4df1-b490-75de44295809.png) - 正在进行query的方法是在StatementHandler中的query方法中 ```java @Override public List query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 执行PreparedStatement,也就是执行SQL语句 ps.execute(); // 处理结果集 return resultSetHandler.handleResultSets(ps); } ``` 先获取PreparedStatement对象在对当前的sql语句解析execute 执行。 同时将ps传入返回几个集处理方法中进行JDBC和Java的类型转换 ![img](https://cdn.nlark.com/yuque/0/2022/png/25484710/1658222510538-bc348e85-aec2-4ad6-b854-94f5c198f76f.png) #### 4、返回结果集解析 转换 ```java /** * 返回集参数设置 处理 {@code stmt} 中的所有结果集,返回结果对象集合 * * @param stmt * @return * @throws SQLException */ @Override public List handleResultSets(Statement stmt) throws SQLException { // 设置日志的上下文 ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); //