MiniSpring框架学习-JDBC 访问框架 MiniBatis如何将 SQL 语句配置化15. MiniBatis如何将 SQL 语句配置化一、先用 MyBatis 建立直觉二、先看最终使用方式三、SQL 放到 Mapper XML四、MapperNodeXML 中一条 SQL 的内存模型五、SqlSessionFactory启动时解析 XML1. 配置入口2. 初始化并扫描目录3. 解析 XML 并放入 Map六、SqlSession按 sqlId 执行 SQL七、完整调用链八、当前实现边界教程https://github.com/YaleGuo/minis极客时间手把手带你写一个 MiniSpring前言这节源教程写的不错清晰易懂点赞15. MiniBatis如何将 SQL 语句配置化前面几章里JdbcTemplate已经帮我们收起了 JDBC 的固定流程获取连接、创建PreparedStatement、绑定参数、关闭资源、转换异常。但业务代码里还有一块东西很显眼finalStringsqlselect id, name, birthday from users where id ?;SQL 直接写在 Java 代码里可以工作但当 SQL 变长、变复杂或者需要多人协作维护时Java 类会越来越乱。所以这一节模仿 MyBatis 的一个核心思路把 SQL 放到 XML 里Java 代码只通过一个 SQL id 去执行它。一、先用 MyBatis 建立直觉在真实 MyBatis 里最基础的调用可以写成这样try(SqlSessionsessionsqlSessionFactory.openSession()){Blogblogsession.selectOne(org.mybatis.example.BlogMapper.selectBlog,101);}这段代码表面上只有两步打开一次 SqlSession ↓ 根据 statement id 执行一条 SQL但它背后已经提前做过一件重要的事MyBatis 会把配置文件和 Mapper XML 解析成内存里的元数据。比如下面这个 idorg.mybatis.example.BlogMapper.selectBlog通常可以拆成namespace org.mybatis.example.BlogMapper id selectBlog然后 MyBatis 就可以用namespace id找到 XML 中配置好的 SQL、参数信息和结果映射规则。粗略地说MyBatis 的设计逻辑是启动阶段 读取配置和 Mapper XML 把每条 SQL 解析成内存对象 用 namespace.id 建立索引 运行阶段 业务代码传入 statement id 和参数 框架找到对应 SQL 绑定参数 执行 JDBC 把 ResultSet 映射成对象这里有个小细节openSession()不是每次都重新加载 XML。XML 通常在SqlSessionFactory构建或初始化时已经解析好了openSession()更像是打开一次执行会话让后续的selectOne(...)有地方执行 SQL、管理执行过程。这么做的好处很直接Java 代码更干净业务方法不用塞一大段 SQLSQL 有独立位置长 SQL、复杂 SQL 更容易阅读和维护SQL 可以用namespace.id统一管理不容易散落在各个 Service 里Java 层表达“我要做什么”XML 层表达“具体 SQL 怎么写”。我们这一章不做完整 MyBatis只先实现它里面最容易看清的一小段SQL 配置化。这一节实现的是教学版 MiniBatis不是完整 MyBatis。它只完成这条主线Mapper XML ↓ 启动时解析 MapperNode ↓ 放入 Mapkey namespace.id SqlSession.selectOne(sqlId, args, callback) ↓ 找到 SQL JdbcTemplate.query(...)二、先看最终使用方式现在UserService.getUserInfo(...)不再直接写查询 SQL而是写一个 SQL idpackagecom.chenhai.jdbc.example;importcom.chenhai.beans.factory.annotation.Autowired;importcom.chenhai.batis.SqlSession;importcom.chenhai.batis.SqlSessionFactory;importcom.chenhai.jdbc.core.JdbcTemplate;importjava.sql.ResultSet;importjava.sql.Date;importjava.util.List;/** * 演示业务层怎样使用 JdbcTemplate 和教学版 SqlSession。 * * 本节新增的变化是查询 SQL 可以放到 Mapper XML 中 * 业务代码只通过 SQL id 发起调用。 */publicclassUserService{AutowiredprivateJdbcTemplatejdbcTemplate;/** * SQL 配置化入口。字段名必须和 XML 中的 bean idsqlSessionFactory 一致。 */AutowiredprivateSqlSessionFactorysqlSessionFactory;publicUsergetUserInfo(intuserId){StringsqlIdcom.chenhai.jdbc.example.User.getUserInfo;SqlSessionsqlSessionthis.sqlSessionFactory.openSession();returnsqlSession.selectOne(sqlId,newObject[]{userId},statement-{try(ResultSetresultSetstatement.executeQuery()){returnresultSet.next()?mapUser(resultSet):null;}});}publicListUsergetUsers(intminUserId){finalStringsqlselect id, name, birthday from users where id ?;returnjdbcTemplate.query(sql,newObject[]{minUserId},(resultSet,rowNum)-mapUser(resultSet));}publicintupdateUserName(intuserId,Stringname){finalStringsqlupdate users set name ? where id ?;returnjdbcTemplate.update(sql,newObject[]{name,userId});}privateUsermapUser(ResultSetresultSet)throwsjava.sql.SQLException{UserusernewUser();user.setId(resultSet.getInt(id));user.setName(resultSet.getString(name));DatebirthdayresultSet.getDate(birthday);if(birthday!null){user.setBirthday(newjava.util.Date(birthday.getTime()));}returnuser;}}对比一下变化上一章 Java 代码直接写 SQL 这一章 Java 代码写 sqlId SQL 放在 mapper/UserMapper.xml注意当前教学版还没有做到“根据 Mapper 接口自动生成代理对象”。所以业务代码仍然手动调用sqlSession.selectOne(sqlId,args,callback)结果映射也仍然由调用者传入的PreparedStatementCallback完成。三、SQL 放到 Mapper XML项目中的 XML 文件是src/main/resources/mapper/UserMapper.xml内容如下?xml version1.0 encodingUTF-8?mappernamespacecom.chenhai.jdbc.example.User!-- namespace id 组成 SQL 唯一标识 com.chenhai.jdbc.example.User.getUserInfo parameterType 和 resultType 当前主要用于教学说明真正的参数绑定和结果映射 仍由 SqlSession 传入的 PreparedStatementCallback 完成。 --selectidgetUserInfoparameterTypejava.lang.IntegerresultTypecom.chenhai.jdbc.example.Userselect id, name, birthday from users where id ?/select/mapper这里最关键的是两个属性属性作用namespace一组 SQL 的命名空间id当前 SQL 在这个命名空间下的名字两者拼起来就是 SQL idcom.chenhai.jdbc.example.User.getUserInfo这个 id 会作为 Map 的 key。运行时业务代码传入这个 key就能找到对应 SQL。parameterType和resultType在当前版本里只是保存到MapperNode中方便理解 MyBatis 的配置结构真正的参数绑定还是JdbcTemplate做结果映射还是回调做。四、MapperNodeXML 中一条 SQL 的内存模型XML 是文本不适合每次查询时反复解析。更合理的方式是容器启动时解析一次把每条 SQL 变成 Java 对象放到内存里。这个 Java 对象就是MapperNodepackagecom.chenhai.batis;/** * Mapper XML 中一条 SQL 语句的内存模型。 * * MyBatis 不会在业务调用时才去读 XML而是在启动阶段把 XML 解析成 * 类似 MapperNode 的对象并放入 Map。 */publicclassMapperNode{privateStringnamespace;privateStringid;privateStringparameterType;privateStringresultType;privateStringsql;privateStringparameter;publicStringgetStatementId(){returnthis.namespace.this.id;}OverridepublicStringtoString(){returngetStatementId() : this.sql;}// 省略普通 getter/setter}它对应 XML 里的这几块mapper namespace... select id... parameterType... resultType... SQL 文本 /select /mapper可以理解成Mapper XML MapperNode ---------- ---------- namespace --- namespace select.id --- id parameterType --- parameterType resultType --- resultType SQL 文本 --- sql五、SqlSessionFactory启动时解析 XMLSqlSessionFactory做两件事packagecom.chenhai.batis;/** * Factory 负责保存 Mapper 元数据并创建 SqlSession。 */publicinterfaceSqlSessionFactory{SqlSessionopenSession();MapperNodegetMapperNode(Stringname);}默认实现是DefaultSqlSessionFactory。它在容器启动时扫描mapperLocations指定的目录解析里面的 XML。1. 配置入口applicationContext.xml中这样配置!-- MiniBatis 教程配置。 1. mapperLocations 指向 classpath 下的 mapper 目录。 2. sqlSessionFactory 启动时解析 Mapper XML把 namespace.id 映射到 SQL。 3. 业务代码只传 SQL id真正执行仍然委托给 jdbcTemplate。 --beanidsqlSessionFactoryclasscom.chenhai.batis.DefaultSqlSessionFactoryinit-methodinitpropertytypeStringnamemapperLocationsvaluemapper//beaninit-methodinit表示 MiniSpring 创建完这个 Bean、注入完属性后会调用init()。2. 初始化并扫描目录publicvoidinit(){if(this.mapperLocationsnull||this.mapperLocations.trim().isEmpty()){thrownewIllegalStateException(mapperLocations must be configured);}scanLocation(this.mapperLocations);}privatevoidscanLocation(Stringlocation){URLlocationUrlgetClass().getClassLoader().getResource(location);if(locationUrlnull){thrownewIllegalStateException(Mapper location not found: location);}FiledirnewFile(decodePath(locationUrl));File[]filesdir.listFiles();if(filesnull){return;}for(Filefile:files){StringchildLocationlocation/file.getName();if(file.isDirectory()){scanLocation(childLocation);}elseif(file.getName().endsWith(.xml)){buildMapperNodes(childLocation);}}}这段逻辑和前面 IoC 扫描配置文件有点像先找到 classpath 下的mapper目录再递归处理里面的 XML 文件。3. 解析 XML 并放入 MapprivateMapString,MapperNodebuildMapperNodes(StringfilePath){SAXReadersaxReadernewSAXReader();URLxmlPathgetClass().getClassLoader().getResource(filePath);if(xmlPathnull){thrownewIllegalStateException(Mapper XML not found: filePath);}try{DocumentdocumentsaxReader.read(xmlPath);ElementrootElementdocument.getRootElement();StringnamespacerootElement.attributeValue(namespace);if(namespacenull||namespace.trim().isEmpty()){thrownewIllegalStateException(Mapper namespace must not be empty: filePath);}Iterator?nodesrootElement.elementIterator();while(nodes.hasNext()){Elementnode(Element)nodes.next();MapperNodemapperNodebuildMapperNode(namespace,node);this.mapperNodeMap.put(mapperNode.getStatementId(),mapperNode);}returnthis.mapperNodeMap;}catch(Exceptione){thrownewIllegalStateException(Parse mapper XML failed: filePath,e);}}privateMapperNodebuildMapperNode(Stringnamespace,Elementnode){Stringidnode.attributeValue(id);if(idnull||id.trim().isEmpty()){thrownewIllegalStateException(Mapper statement id must not be empty: namespace);}MapperNodemapperNodenewMapperNode();mapperNode.setNamespace(namespace);mapperNode.setId(id);mapperNode.setParameterType(node.attributeValue(parameterType));mapperNode.setResultType(node.attributeValue(resultType));mapperNode.setSql(node.getTextTrim());mapperNode.setParameter();returnmapperNode;}解析完成后内存里大概是这样mapperNodeMap | -- key: | com.chenhai.jdbc.example.User.getUserInfo | -- value: MapperNode { namespace com.chenhai.jdbc.example.User id getUserInfo parameterType java.lang.Integer resultType com.chenhai.jdbc.example.User sql select id, name, birthday from users where id ? }业务调用时不用再读 XML直接从这个 Map 里拿就行。六、SqlSession按 sqlId 执行 SQLSqlSessionFactory负责保存 SQL 配置SqlSession负责面向业务代码执行 SQL。接口很小packagecom.chenhai.batis;importcom.chenhai.jdbc.core.JdbcTemplate;importcom.chenhai.jdbc.core.PreparedStatementCallback;/** * 当前教学版只实现 selectOne并保留 PreparedStatementCallback 作为结果处理回调。 */publicinterfaceSqlSession{voidsetJdbcTemplate(JdbcTemplatejdbcTemplate);voidsetSqlSessionFactory(SqlSessionFactorysqlSessionFactory);TTselectOne(StringsqlId,Object[]args,PreparedStatementCallbackTcallback);}创建SqlSession时把两个依赖塞进去OverridepublicSqlSessionopenSession(){DefaultSqlSessionsqlSessionnewDefaultSqlSession();sqlSession.setJdbcTemplate(this.jdbcTemplate);sqlSession.setSqlSessionFactory(this);returnsqlSession;}这两个依赖分别负责依赖作用SqlSessionFactory根据sqlId找到MapperNodeJdbcTemplate执行 SQL、绑定参数、关闭资源DefaultSqlSession.selectOne(...)的核心代码是OverridepublicTTselectOne(StringsqlId,Object[]args,PreparedStatementCallbackTcallback){if(this.jdbcTemplatenull){thrownewIllegalStateException(JdbcTemplate must be configured);}if(this.sqlSessionFactorynull){thrownewIllegalStateException(SqlSessionFactory must be configured);}MapperNodemapperNodethis.sqlSessionFactory.getMapperNode(sqlId);if(mapperNodenull){thrownewIllegalArgumentException(No mapped SQL statement found: sqlId);}returnthis.jdbcTemplate.query(mapperNode.getSql(),args,callback);}所以selectOne(...)并没有重新实现一套 JDBC。它只是多做了一步sqlId ↓ MapperNode ↓ SQL 字符串 ↓ JdbcTemplate.query(...)七、完整调用链再回到业务代码publicUsergetUserInfo(intuserId){StringsqlIdcom.chenhai.jdbc.example.User.getUserInfo;SqlSessionsqlSessionthis.sqlSessionFactory.openSession();returnsqlSession.selectOne(sqlId,newObject[]{userId},statement-{try(ResultSetresultSetstatement.executeQuery()){returnresultSet.next()?mapUser(resultSet):null;}});}实际执行过程是UserService.getUserInfo(1) ↓ sqlSessionFactory.openSession() ↓ 创建 DefaultSqlSession ↓ 注入 JdbcTemplate 和 SqlSessionFactory ↓ sqlSession.selectOne(sqlId, args, callback) ↓ sqlSessionFactory.getMapperNode(sqlId) ↓ 取出 MapperNode.sql ↓ jdbcTemplate.query(sql, args, callback) ↓ ArgumentPreparedStatementSetter 绑定参数 ↓ PreparedStatement.executeQuery() ↓ callback 把 ResultSet 转成 User换成组件关系图就是UserService | | sqlId args callback v SqlSession | | 根据 sqlId 找 SQL v SqlSessionFactory | | mapperNodeMap v MapperNode | | sql v JdbcTemplate | | JDBC 固定流程 v DataSource / Database八、当前实现边界这一章只实现了 SQL 配置化的第一步重点是把“SQL 文本从 Java 代码移到 XML”讲清楚。当前还没有实现Mapper 接口代理#{id}这种命名参数解析自动根据resultType反射封装对象insert、update、delete的独立语义动态 SQL例如if、where、foreach一级缓存、二级缓存事务和真实 MyBatis 的SqlSession生命周期。所以当前版本里parameterType / resultType 只是被解析并保存还没有真正驱动参数绑定和结果映射。 PreparedStatementCallback 仍然由业务代码提供用来执行 SQL 和处理 ResultSet。 JdbcTemplate 仍然是最终执行 JDBC 的核心。对应测试主要验证两件事DefaultSqlSessionFactory能把mapper/UserMapper.xml解析成MapperNodeSqlSession.selectOne(...)能通过 SQL id 找到 SQL并委托JdbcTemplate执行。这一节可以总结成一句话MiniBatis 先不急着做完整 ORM而是先把 SQL 配置化启动时解析 XML运行时按namespace.id找 SQL最后仍然交给JdbcTemplate执行。
MiniSpring框架学习笔记-JDBC 访问框架: MiniBatis如何将 SQL 语句配置化?
MiniSpring框架学习-JDBC 访问框架 MiniBatis如何将 SQL 语句配置化15. MiniBatis如何将 SQL 语句配置化一、先用 MyBatis 建立直觉二、先看最终使用方式三、SQL 放到 Mapper XML四、MapperNodeXML 中一条 SQL 的内存模型五、SqlSessionFactory启动时解析 XML1. 配置入口2. 初始化并扫描目录3. 解析 XML 并放入 Map六、SqlSession按 sqlId 执行 SQL七、完整调用链八、当前实现边界教程https://github.com/YaleGuo/minis极客时间手把手带你写一个 MiniSpring前言这节源教程写的不错清晰易懂点赞15. MiniBatis如何将 SQL 语句配置化前面几章里JdbcTemplate已经帮我们收起了 JDBC 的固定流程获取连接、创建PreparedStatement、绑定参数、关闭资源、转换异常。但业务代码里还有一块东西很显眼finalStringsqlselect id, name, birthday from users where id ?;SQL 直接写在 Java 代码里可以工作但当 SQL 变长、变复杂或者需要多人协作维护时Java 类会越来越乱。所以这一节模仿 MyBatis 的一个核心思路把 SQL 放到 XML 里Java 代码只通过一个 SQL id 去执行它。一、先用 MyBatis 建立直觉在真实 MyBatis 里最基础的调用可以写成这样try(SqlSessionsessionsqlSessionFactory.openSession()){Blogblogsession.selectOne(org.mybatis.example.BlogMapper.selectBlog,101);}这段代码表面上只有两步打开一次 SqlSession ↓ 根据 statement id 执行一条 SQL但它背后已经提前做过一件重要的事MyBatis 会把配置文件和 Mapper XML 解析成内存里的元数据。比如下面这个 idorg.mybatis.example.BlogMapper.selectBlog通常可以拆成namespace org.mybatis.example.BlogMapper id selectBlog然后 MyBatis 就可以用namespace id找到 XML 中配置好的 SQL、参数信息和结果映射规则。粗略地说MyBatis 的设计逻辑是启动阶段 读取配置和 Mapper XML 把每条 SQL 解析成内存对象 用 namespace.id 建立索引 运行阶段 业务代码传入 statement id 和参数 框架找到对应 SQL 绑定参数 执行 JDBC 把 ResultSet 映射成对象这里有个小细节openSession()不是每次都重新加载 XML。XML 通常在SqlSessionFactory构建或初始化时已经解析好了openSession()更像是打开一次执行会话让后续的selectOne(...)有地方执行 SQL、管理执行过程。这么做的好处很直接Java 代码更干净业务方法不用塞一大段 SQLSQL 有独立位置长 SQL、复杂 SQL 更容易阅读和维护SQL 可以用namespace.id统一管理不容易散落在各个 Service 里Java 层表达“我要做什么”XML 层表达“具体 SQL 怎么写”。我们这一章不做完整 MyBatis只先实现它里面最容易看清的一小段SQL 配置化。这一节实现的是教学版 MiniBatis不是完整 MyBatis。它只完成这条主线Mapper XML ↓ 启动时解析 MapperNode ↓ 放入 Mapkey namespace.id SqlSession.selectOne(sqlId, args, callback) ↓ 找到 SQL JdbcTemplate.query(...)二、先看最终使用方式现在UserService.getUserInfo(...)不再直接写查询 SQL而是写一个 SQL idpackagecom.chenhai.jdbc.example;importcom.chenhai.beans.factory.annotation.Autowired;importcom.chenhai.batis.SqlSession;importcom.chenhai.batis.SqlSessionFactory;importcom.chenhai.jdbc.core.JdbcTemplate;importjava.sql.ResultSet;importjava.sql.Date;importjava.util.List;/** * 演示业务层怎样使用 JdbcTemplate 和教学版 SqlSession。 * * 本节新增的变化是查询 SQL 可以放到 Mapper XML 中 * 业务代码只通过 SQL id 发起调用。 */publicclassUserService{AutowiredprivateJdbcTemplatejdbcTemplate;/** * SQL 配置化入口。字段名必须和 XML 中的 bean idsqlSessionFactory 一致。 */AutowiredprivateSqlSessionFactorysqlSessionFactory;publicUsergetUserInfo(intuserId){StringsqlIdcom.chenhai.jdbc.example.User.getUserInfo;SqlSessionsqlSessionthis.sqlSessionFactory.openSession();returnsqlSession.selectOne(sqlId,newObject[]{userId},statement-{try(ResultSetresultSetstatement.executeQuery()){returnresultSet.next()?mapUser(resultSet):null;}});}publicListUsergetUsers(intminUserId){finalStringsqlselect id, name, birthday from users where id ?;returnjdbcTemplate.query(sql,newObject[]{minUserId},(resultSet,rowNum)-mapUser(resultSet));}publicintupdateUserName(intuserId,Stringname){finalStringsqlupdate users set name ? where id ?;returnjdbcTemplate.update(sql,newObject[]{name,userId});}privateUsermapUser(ResultSetresultSet)throwsjava.sql.SQLException{UserusernewUser();user.setId(resultSet.getInt(id));user.setName(resultSet.getString(name));DatebirthdayresultSet.getDate(birthday);if(birthday!null){user.setBirthday(newjava.util.Date(birthday.getTime()));}returnuser;}}对比一下变化上一章 Java 代码直接写 SQL 这一章 Java 代码写 sqlId SQL 放在 mapper/UserMapper.xml注意当前教学版还没有做到“根据 Mapper 接口自动生成代理对象”。所以业务代码仍然手动调用sqlSession.selectOne(sqlId,args,callback)结果映射也仍然由调用者传入的PreparedStatementCallback完成。三、SQL 放到 Mapper XML项目中的 XML 文件是src/main/resources/mapper/UserMapper.xml内容如下?xml version1.0 encodingUTF-8?mappernamespacecom.chenhai.jdbc.example.User!-- namespace id 组成 SQL 唯一标识 com.chenhai.jdbc.example.User.getUserInfo parameterType 和 resultType 当前主要用于教学说明真正的参数绑定和结果映射 仍由 SqlSession 传入的 PreparedStatementCallback 完成。 --selectidgetUserInfoparameterTypejava.lang.IntegerresultTypecom.chenhai.jdbc.example.Userselect id, name, birthday from users where id ?/select/mapper这里最关键的是两个属性属性作用namespace一组 SQL 的命名空间id当前 SQL 在这个命名空间下的名字两者拼起来就是 SQL idcom.chenhai.jdbc.example.User.getUserInfo这个 id 会作为 Map 的 key。运行时业务代码传入这个 key就能找到对应 SQL。parameterType和resultType在当前版本里只是保存到MapperNode中方便理解 MyBatis 的配置结构真正的参数绑定还是JdbcTemplate做结果映射还是回调做。四、MapperNodeXML 中一条 SQL 的内存模型XML 是文本不适合每次查询时反复解析。更合理的方式是容器启动时解析一次把每条 SQL 变成 Java 对象放到内存里。这个 Java 对象就是MapperNodepackagecom.chenhai.batis;/** * Mapper XML 中一条 SQL 语句的内存模型。 * * MyBatis 不会在业务调用时才去读 XML而是在启动阶段把 XML 解析成 * 类似 MapperNode 的对象并放入 Map。 */publicclassMapperNode{privateStringnamespace;privateStringid;privateStringparameterType;privateStringresultType;privateStringsql;privateStringparameter;publicStringgetStatementId(){returnthis.namespace.this.id;}OverridepublicStringtoString(){returngetStatementId() : this.sql;}// 省略普通 getter/setter}它对应 XML 里的这几块mapper namespace... select id... parameterType... resultType... SQL 文本 /select /mapper可以理解成Mapper XML MapperNode ---------- ---------- namespace --- namespace select.id --- id parameterType --- parameterType resultType --- resultType SQL 文本 --- sql五、SqlSessionFactory启动时解析 XMLSqlSessionFactory做两件事packagecom.chenhai.batis;/** * Factory 负责保存 Mapper 元数据并创建 SqlSession。 */publicinterfaceSqlSessionFactory{SqlSessionopenSession();MapperNodegetMapperNode(Stringname);}默认实现是DefaultSqlSessionFactory。它在容器启动时扫描mapperLocations指定的目录解析里面的 XML。1. 配置入口applicationContext.xml中这样配置!-- MiniBatis 教程配置。 1. mapperLocations 指向 classpath 下的 mapper 目录。 2. sqlSessionFactory 启动时解析 Mapper XML把 namespace.id 映射到 SQL。 3. 业务代码只传 SQL id真正执行仍然委托给 jdbcTemplate。 --beanidsqlSessionFactoryclasscom.chenhai.batis.DefaultSqlSessionFactoryinit-methodinitpropertytypeStringnamemapperLocationsvaluemapper//beaninit-methodinit表示 MiniSpring 创建完这个 Bean、注入完属性后会调用init()。2. 初始化并扫描目录publicvoidinit(){if(this.mapperLocationsnull||this.mapperLocations.trim().isEmpty()){thrownewIllegalStateException(mapperLocations must be configured);}scanLocation(this.mapperLocations);}privatevoidscanLocation(Stringlocation){URLlocationUrlgetClass().getClassLoader().getResource(location);if(locationUrlnull){thrownewIllegalStateException(Mapper location not found: location);}FiledirnewFile(decodePath(locationUrl));File[]filesdir.listFiles();if(filesnull){return;}for(Filefile:files){StringchildLocationlocation/file.getName();if(file.isDirectory()){scanLocation(childLocation);}elseif(file.getName().endsWith(.xml)){buildMapperNodes(childLocation);}}}这段逻辑和前面 IoC 扫描配置文件有点像先找到 classpath 下的mapper目录再递归处理里面的 XML 文件。3. 解析 XML 并放入 MapprivateMapString,MapperNodebuildMapperNodes(StringfilePath){SAXReadersaxReadernewSAXReader();URLxmlPathgetClass().getClassLoader().getResource(filePath);if(xmlPathnull){thrownewIllegalStateException(Mapper XML not found: filePath);}try{DocumentdocumentsaxReader.read(xmlPath);ElementrootElementdocument.getRootElement();StringnamespacerootElement.attributeValue(namespace);if(namespacenull||namespace.trim().isEmpty()){thrownewIllegalStateException(Mapper namespace must not be empty: filePath);}Iterator?nodesrootElement.elementIterator();while(nodes.hasNext()){Elementnode(Element)nodes.next();MapperNodemapperNodebuildMapperNode(namespace,node);this.mapperNodeMap.put(mapperNode.getStatementId(),mapperNode);}returnthis.mapperNodeMap;}catch(Exceptione){thrownewIllegalStateException(Parse mapper XML failed: filePath,e);}}privateMapperNodebuildMapperNode(Stringnamespace,Elementnode){Stringidnode.attributeValue(id);if(idnull||id.trim().isEmpty()){thrownewIllegalStateException(Mapper statement id must not be empty: namespace);}MapperNodemapperNodenewMapperNode();mapperNode.setNamespace(namespace);mapperNode.setId(id);mapperNode.setParameterType(node.attributeValue(parameterType));mapperNode.setResultType(node.attributeValue(resultType));mapperNode.setSql(node.getTextTrim());mapperNode.setParameter();returnmapperNode;}解析完成后内存里大概是这样mapperNodeMap | -- key: | com.chenhai.jdbc.example.User.getUserInfo | -- value: MapperNode { namespace com.chenhai.jdbc.example.User id getUserInfo parameterType java.lang.Integer resultType com.chenhai.jdbc.example.User sql select id, name, birthday from users where id ? }业务调用时不用再读 XML直接从这个 Map 里拿就行。六、SqlSession按 sqlId 执行 SQLSqlSessionFactory负责保存 SQL 配置SqlSession负责面向业务代码执行 SQL。接口很小packagecom.chenhai.batis;importcom.chenhai.jdbc.core.JdbcTemplate;importcom.chenhai.jdbc.core.PreparedStatementCallback;/** * 当前教学版只实现 selectOne并保留 PreparedStatementCallback 作为结果处理回调。 */publicinterfaceSqlSession{voidsetJdbcTemplate(JdbcTemplatejdbcTemplate);voidsetSqlSessionFactory(SqlSessionFactorysqlSessionFactory);TTselectOne(StringsqlId,Object[]args,PreparedStatementCallbackTcallback);}创建SqlSession时把两个依赖塞进去OverridepublicSqlSessionopenSession(){DefaultSqlSessionsqlSessionnewDefaultSqlSession();sqlSession.setJdbcTemplate(this.jdbcTemplate);sqlSession.setSqlSessionFactory(this);returnsqlSession;}这两个依赖分别负责依赖作用SqlSessionFactory根据sqlId找到MapperNodeJdbcTemplate执行 SQL、绑定参数、关闭资源DefaultSqlSession.selectOne(...)的核心代码是OverridepublicTTselectOne(StringsqlId,Object[]args,PreparedStatementCallbackTcallback){if(this.jdbcTemplatenull){thrownewIllegalStateException(JdbcTemplate must be configured);}if(this.sqlSessionFactorynull){thrownewIllegalStateException(SqlSessionFactory must be configured);}MapperNodemapperNodethis.sqlSessionFactory.getMapperNode(sqlId);if(mapperNodenull){thrownewIllegalArgumentException(No mapped SQL statement found: sqlId);}returnthis.jdbcTemplate.query(mapperNode.getSql(),args,callback);}所以selectOne(...)并没有重新实现一套 JDBC。它只是多做了一步sqlId ↓ MapperNode ↓ SQL 字符串 ↓ JdbcTemplate.query(...)七、完整调用链再回到业务代码publicUsergetUserInfo(intuserId){StringsqlIdcom.chenhai.jdbc.example.User.getUserInfo;SqlSessionsqlSessionthis.sqlSessionFactory.openSession();returnsqlSession.selectOne(sqlId,newObject[]{userId},statement-{try(ResultSetresultSetstatement.executeQuery()){returnresultSet.next()?mapUser(resultSet):null;}});}实际执行过程是UserService.getUserInfo(1) ↓ sqlSessionFactory.openSession() ↓ 创建 DefaultSqlSession ↓ 注入 JdbcTemplate 和 SqlSessionFactory ↓ sqlSession.selectOne(sqlId, args, callback) ↓ sqlSessionFactory.getMapperNode(sqlId) ↓ 取出 MapperNode.sql ↓ jdbcTemplate.query(sql, args, callback) ↓ ArgumentPreparedStatementSetter 绑定参数 ↓ PreparedStatement.executeQuery() ↓ callback 把 ResultSet 转成 User换成组件关系图就是UserService | | sqlId args callback v SqlSession | | 根据 sqlId 找 SQL v SqlSessionFactory | | mapperNodeMap v MapperNode | | sql v JdbcTemplate | | JDBC 固定流程 v DataSource / Database八、当前实现边界这一章只实现了 SQL 配置化的第一步重点是把“SQL 文本从 Java 代码移到 XML”讲清楚。当前还没有实现Mapper 接口代理#{id}这种命名参数解析自动根据resultType反射封装对象insert、update、delete的独立语义动态 SQL例如if、where、foreach一级缓存、二级缓存事务和真实 MyBatis 的SqlSession生命周期。所以当前版本里parameterType / resultType 只是被解析并保存还没有真正驱动参数绑定和结果映射。 PreparedStatementCallback 仍然由业务代码提供用来执行 SQL 和处理 ResultSet。 JdbcTemplate 仍然是最终执行 JDBC 的核心。对应测试主要验证两件事DefaultSqlSessionFactory能把mapper/UserMapper.xml解析成MapperNodeSqlSession.selectOne(...)能通过 SQL id 找到 SQL并委托JdbcTemplate执行。这一节可以总结成一句话MiniBatis 先不急着做完整 ORM而是先把 SQL 配置化启动时解析 XML运行时按namespace.id找 SQL最后仍然交给JdbcTemplate执行。