30分钟教你写一个mybatis框架
目标:将解析mybatis配置文件和mapper文件,封装jdbc实现mybatis4大组件,创建sqlSession。以下代码gitee地址为 https://gitee.com/zumengjie/shouxie-mybatis
第一部分解析mybatis配置文件,解析mapper文件。
XMLConfigBuilder类解析mybatis配置文件,创建一个Configuration对象,该对象是mybatis的核心配置类。对配置文件中的<environments>标签解析,<environments>包含多个<environment>每个包含<dataSource>根据<environments>标签的default属性选择一个environment,读取对应的<dataSource>配置信息。根据<dataSource>的type属性,确定要使用的连接池。使用<dataSource>中配置的数据库信息进而创建数据源DataSource,将DataSource设置到Configuration中。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<configuration> <!-- mybatis 数据源环境配置 --> <environments default="dev"> <environment id="dev"> <!-- 配置数据源信息 --> <dataSource type="DBCP"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/petstore?serverTimezone=GMT%2B8&characterEncoding=utf8&useUnicode=true&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <!-- 映射文件加载 --> <mappers> <!-- resource指定映射文件的类路径 --> <mapper resource="mapper/manageUser.xml"></mapper> <!-- <mapper resource="mapper/UserMapper.xml"></mapper> --> <!-- <mapper resource="mapper/UserMapper.xml"></mapper> --> <!-- <mapper resource="mapper/UserMapper.xml"></mapper> --> <!-- <mapper resource="mapper/UserMapper.xml"></mapper> --> </mappers> </configuration>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package builder; import mapping.Configuration; import org.apache.commons.dbcp.BasicDataSource; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; import java.util.List; import java.util.Properties; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-08 10:42 * @notify 解析mybatisConfig的类,当前解析 environments mappers * 创建Configuration 设置DataSource * mappers 则获取输入流,交给XMLMapperBuilder解析每一个mapper文件 * @version 1.0 */ public class XMLConfigBuilder { private Configuration configuration = new Configuration(); public Configuration parse(Element rootElement) throws Exception { //解析<environments> Element environments = rootElement.element("environments"); parseEnvironments(environments); //解析<mappers> Element mappers = rootElement.element("mappers"); parseMappers(mappers); return configuration; } //解析<environments> private void parseEnvironments(Element environments) { //查询environments default="dev" String aDefault = environments.attributeValue("default"); //获取全部的 environment List<Element> environment = environments.elements("environment"); //循环所有的 environment for (Element env : environment) { //如果当前 environment 的id和默认的id相同则继续向下解析 if (env.attributeValue("id").equals(aDefault)) { parseEnvironment(env); } } } //解析environment private void parseEnvironment(Element environment) { //解析<dataSource type="DBCP"> Element dataSource = environment.element("dataSource"); parseDataSource(dataSource); } //解析dataSource private void parseDataSource(Element dataSource) { //获取连接池类型 String type = dataSource.attributeValue("type"); //设置 <dataSource type="DBCP"> 连接池 if (type.equals("DBCP")) { //创建 DBCP连接池 BasicDataSource dataSource1 = new BasicDataSource(); //创建配置类 Properties properties = new Properties(); //获取全部的property List<Element> propertys = dataSource.elements("property"); //循环拿到<property name="driver" value="com.mysql.jdbc.Driver"/> for (Element prop : propertys) { //获取标签name属性值 String name = prop.attributeValue("name"); //获取标签value属性值 String value = prop.attributeValue("value"); //设置到配置类 properties.put(name, value); } //设置连接池属性 dataSource1.setDriverClassName(properties.get("driver").toString()); dataSource1.setUrl(properties.get("url").toString()); dataSource1.setUsername(properties.get("username").toString()); dataSource1.setPassword(properties.get("password").toString()); //给Configuration设置数据源信息 configuration.setDataSource(dataSource1); } } //解析<mappers> private void parseMappers(Element mappers) throws Exception { //拿到所有的<mapper resource="mapper/UserMapper.xml"></mapper> List<Element> mapperElements = mappers.elements("mapper"); //遍历解析每一个 mapper.xml for (Element mapperElement : mapperElements) { parseMapper(mapperElement); } } //解析每一个mapper标签 private void parseMapper(Element mapperElement) throws Exception { //TODO 此处还有url等方式 String resource = mapperElement.attributeValue("resource"); //根据文件名获取输入流 InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resource); //dom4j解析 SAXReader saxReader = new SAXReader(); Document document = saxReader.read(inputStream); //获取跟标签 Element rootElement = document.getRootElement(); XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(configuration); mapperBuilder.parse(rootElement); } }
循环<mappers>标签获取多个<mapper>循环解析mapper配置文件,使用XMLMapperBuilder类获取每个mapper配置文件的namespace,在解析不同的sql标签(<insert><select>)。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<mapper namespace="manageUser"> <!-- select标签,封装了SQL语句信息、入参类型、结果映射类型 --> <select id="getManageUserById" parameterType="pojo.ManageUser" resultType="pojo.ManageUser" statementType="prepared"> SELECT * FROM manage_user WHERE id = #{id} </select> <insert id="insertManage" parameterType="pojo.ManageUser" statementType="prepared"> insert into manage_user values(#{id},#{username},#{password},#{create_date}); </insert> <select id="getManageUserByUserName" parameterType="pojo.ManageUser" resultType="pojo.ManageUser" statementType="prepared"> SELECT * FROM manage_user WHERE id = #{id} <if test="username!=null and username!=''"> and username = ${username} </if> </select> </mapper>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package builder; import mapping.Configuration; import org.dom4j.Element; import java.util.List; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-08 11:13 * @notify 解析mapper文件获取namespace,读取到<insert>和<select>标签,交给XMLStatementBuilder * @version 1.0 */ public class XMLMapperBuilder { private String namespace = ""; private Configuration configuration; public XMLMapperBuilder(Configuration configuration) { this.configuration = configuration; } //解析每一个mapper文件 public void parse(Element rootElement) throws Exception { //查询namespace namespace = rootElement.attributeValue("namespace"); //获取select标签 List<Element> selectElements = rootElement.elements("select"); parse(selectElements, "select"); List<Element> insertElements = rootElement.elements("insert"); parse(insertElements, "insert"); } public void parse(List<Element> selectElements, String sqlType) throws Exception { for (Element selectElement : selectElements) { XMLStatementBuilder xmlStatementBuilder = new XMLStatementBuilder(configuration); xmlStatementBuilder.parseStatementElement(selectElement, namespace, sqlType); } } }
XMLStatementBuilder类解析具体的sql标签。每一个sql标签都是一个MappedStatement对象,而每一个mapper文件中有N个sql标签,一个项目又有M个mapper文件。所以一个Configuration中有一个map,key是statementid,(由mapper文件的namespace和sql标签的id组成)value是MappedStatement。一个MappedStatement对象由statementid,入参类型,返回值类型,sqlType属于<select>还是<insert>等等 statementType的值分别对应:statement不进行预编译,prepared预编译,callable执行存储过程。最后还有一个专门用来存储sql语句的对象SqlSource,SqlSource是一个接口。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package builder; import mapping.Configuration; import mapping.MappedStatement; import org.dom4j.Element; import sqlnode.impl.MixedSqlNode; import sqlsource.SqlSource; import sqlsource.impl.DynamicSqlSource; import sqlsource.impl.RawSqlSource; import utils.ResolveType; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-08 11:24 * @notify 解析<insert><select>标签,读取入参,返回值,id等信息,标签内容交给XMLScriptBuilder * @version 1.0 */ public class XMLStatementBuilder { private Configuration configuration; public XMLStatementBuilder(Configuration configuration) { this.configuration = configuration; } //解析标签 public void parseStatementElement(Element selectElement, String namespace, String sqlType) throws Exception { //读取id String statementId = selectElement.attributeValue("id"); //如果id不存在则返回 if (statementId == null || selectElement.equals("")) { return; } //拼接namespace statementId = namespace + "." + statementId; //查询 parameterType 属性 String parameterType = selectElement.attributeValue("parameterType"); //通过类名获取Class Class<?> parameterClass = ResolveType.resolveType(parameterType); //查询 resultType 属性 String resultType = selectElement.attributeValue("resultType"); Class<?> resultClass = null; if (resultType != null && !resultType.equals("")) { //通过类名获取Class resultClass = ResolveType.resolveType(resultType); } //获取statementType属性 String statementType = selectElement.attributeValue("statementType"); //设置默认的statementType属性 statementType = statementType == null || statementType == "" ? "prepared" : statementType; // 解析SQL信息 SqlSource sqlSource = createSqlSource(selectElement); // TODO 建议使用构建者模式去优化 MappedStatement mappedStatement = new MappedStatement(statementId, parameterClass, resultClass, statementType, sqlSource, sqlType); //设置Configuration参数 configuration.addMappedStatement(statementId, mappedStatement); } //获取sqlSource private SqlSource createSqlSource(Element selectElement) throws Exception { XMLScriptBuilder xmlScriptBuilder = new XMLScriptBuilder(); SqlSource sqlSource = xmlScriptBuilder.parseScriptNode(selectElement); return sqlSource; } }
XMLScriptBuilder解析sql脚本,这一部分较为复杂。首先我们先明确,我们需要解析的内容是sql标签中的标签体,也就是sql脚本,需要将sql脚本组成一个SqlSource。
<select>
select * from manage_user whereid = #{id}
<if test="username!=null and username!=''">
and username = ${username}
</if>
</select>
这样的sql脚本包含两个节点,一个是只包含普通的文本的sql节点,另一个则是if标签的sql节点。当然,真实的mybatis还包含<where>等节点。封装sql节点引入一个接口,SqlNode。每种节点最终都需要放到SqlSource中,我们可以在SqlSource中使用一个集合来存储,但是我们还有一个更好的选择,使用MixedSqlNode。现在MixedSqlNode中存储多个SqlNode根据不同的节点不同,我们将是文本节点和包含${}的节点封装成TextSqlNode,将只包含文本且只包含#{}的节点封装成StaticTextSqlNode。较为麻烦的是if标签,因为一个if标签里可能会包含带有#{}的文本内容,或者带有${}的文本内容,或许,if标签里还有if标签。这里我们必须要用到递归解析了。我们假设if标签中包含TextSqlNode和另一个if标签,此时我们就需要把两个标签放到if标签中,辛好我们有MixedSqlNode,于是if标签中的test表达式和MixedSqlNode组成了IfSqlNode,现在我们解析了全部的sql脚本将所有的SqlNode封装到MixedSqlNode,然后组装一个SqlSource。前边我们SqlSource是一个接口,现在我们将sql脚本中只包含StaticTextSqlNode节点的SqlSource封装成RawSqlSource,而包含TextSqlNode和IfSqlNode节点SqlSource封装成DynamicSqlSource。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package builder;/* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-08 11:30 * @notify * @version 1.0 */ import org.dom4j.Element; import org.dom4j.Node; import org.dom4j.Text; import sqlnode.SqlNode; import sqlnode.impl.IfSqlNode; import sqlnode.impl.MixedSqlNode; import sqlnode.impl.StaticTextSqlNode; import sqlnode.impl.TextSqlNode; import sqlsource.SqlSource; import sqlsource.impl.DynamicSqlSource; import sqlsource.impl.RawSqlSource; import java.util.ArrayList; import java.util.List; public class XMLScriptBuilder { private boolean isDynamic = false; //解析标签体 public SqlSource parseScriptNode(Element selectElement) throws Exception { MixedSqlNode mixedSqlNode = parseDynamicTags(selectElement); SqlSource sqlSource; if (isDynamic) {//如果包含${}或者其他的子标签则为动态的 sqlSource = new DynamicSqlSource(mixedSqlNode); } else {//全部的sqlNode都是文本,并且只包含#{} sqlSource = new RawSqlSource(mixedSqlNode); } return sqlSource; } private MixedSqlNode parseDynamicTags(Element selectElement) { //存储一个 <select> 中的所有sqlNode List<SqlNode> sqlNodes = new ArrayList<>(); //查询总结点数量 int nodeCount = selectElement.nodeCount(); //遍历全部的sql节点 for (int i = 0; i < nodeCount; i++) { //获取当前节点 Node node = selectElement.node(i); //如果是纯文本的 if (node instanceof Text) { //拿到文本节点 String sqlText = node.getText().trim(); if (!sqlText.equals("")) { //如果包含 ${} 则创建 TextSqlNode 该节点只包含文本和${} if (sqlText.indexOf("${") > -1) { //如果包含${}或者其他的子标签则为动态的 isDynamic = true; //将TextSqlNode添加到节点集合中 sqlNodes.add(new TextSqlNode(sqlText)); } else { //将StaticTextSqlNode添加到节点集合中 sqlNodes.add(new StaticTextSqlNode(sqlText)); } } } else if (node instanceof Element) { //如果包含${}或者其他的子标签则为动态的 isDynamic = true; //拿到节点名称 String nodeName = node.getName(); //如果是 if则表示是if标签 if (nodeName.equals("if")) { //将node转换成element Element element = (Element) node; //拿到if的条件 String test = element.attributeValue("test"); /* 此处递归调用,因为if标签中还有子节点 设sql为 select * from user <if test='name!=null and name!='''> and name = #{name} </if> 此时 select * from user 已经转换成了sqlNode 接下来的if标签,里边也包含子节点,所以递归, 第二次进入 (parseDynamicTags) 会创建一个StaticTextSqlNode,将该SqlNode添加到 List<SqlNode> sqlNodes = new ArrayList<>(); 而这个集合最终会被封装成一个MixedSqlNode返回到第一次 调用(parseDynamicTags),所以此处使用 MixedSqlNode接收,并将该MixedSqlNode传递给IfSqlNode,然后 添加到sqlNodes中 */ MixedSqlNode mixedSqlNode = parseDynamicTags(element); sqlNodes.add(new IfSqlNode(test, mixedSqlNode)); } } } //返回节点集合包装类 return new MixedSqlNode(sqlNodes); } }
以上过程只是对sql解析的大概描述。现在我们缕清思路,Configuration包含的是mybatis的全局配置,其中包含DataSource和所有的mapper配置文件信息。形成了一个MappedStatement,MappedStatement包含的是sql标签的属性和内容,属性包含id,入参,出参,statement类型等,内容则是多个SqlNode形成的SqlSource。我们没有使用集合来存储SqlNode,而是在SqlSource内部持有一个MixedSqlNode此类是SqlNode接口的实现类,MixedSqlNode里有一个list集合存放各种类型的SqlNode,例如TextSqlNode,IfSqlNode等。
SqlSource接口,该接口中只有一个方法。getBoundSql(Object param);返回一个BoundSql对象。我们先不管BoundSql是干嘛的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlsource; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 14:41 * @notify 获取sql语句 * @version 1.0 */ import mapping.BoundSql; public interface SqlSource { /* 传入参数,这个param就是sql的入参 boundSQL 信息返回拼接的sql可能是${} 或者 #{} 如果是${}则可以直接获取sql 如果是 #{} 还需要有方法获取 ?代表的属性。 */ BoundSql getBoundSql(Object param)throws Exception; }
接着看SqlSource的实现类。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlsource.impl; import mapping.BoundSql; import mapping.DynamicContext; import sqlnode.SqlNode; import sqlsource.SqlSource; import sqlsource.SqlSourceParser; import tokenparser.GenericTokenParser; import tokenparser.ParameterMappingTokenHandler; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 14:52 * @notify #{} 中的内容进行处理 RawSqlSource只包含 StaticTextSqlNode * @version 1.0 */ public class RawSqlSource implements SqlSource { private SqlNode mixedSqlNode; public RawSqlSource(SqlNode mixedSqlNode) throws Exception { this.mixedSqlNode = mixedSqlNode; } @Override public BoundSql getBoundSql(Object param) throws Exception { //执行该sqlSource中的所有sqlNode DynamicContext dynamicContext = new DynamicContext(null); mixedSqlNode.apply(dynamicContext); //将 #{ } 替换成 ? 然后将内容存储到 ParameterMapping 中 SqlSourceParser parser = new SqlSourceParser(dynamicContext); SqlSource sqlSource = parser.parse(); return sqlSource.getBoundSql(param); } }
调用RawSqlSource的getBoundSql(Object param);方法,创建了一个DynamicContext对象。这个对象有一个StringBuilder对象,和Object类型参数。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package mapping; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 18:02 * @notify 每个SqlSource有多个SqlNode,而DynamicContext负责将多个sqlNode解析出来的sql信息拼接到一块 * @version 1.0 */ public class DynamicContext { //多个sqlNode调用自己的apply()都把解析好的sql放到这里。 private StringBuilder sb = new StringBuilder(); //sql入参 private Object param; public String getSql() { return sb.toString(); } public DynamicContext(Object param) { this.param = param; } public void appendSql(String sqlText) { sb.append(sqlText); sb.append(" "); } public Object getParam() { return param; } }
接着我们调用SqlNode的apply(DynamicContext context)。其实当前的SqlNode其实就是MixedSqlNode
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlnode; import mapping.DynamicContext; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 18:00 * @notify * @version 1.0 */ public interface SqlNode { void apply(DynamicContext context)throws Exception; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlnode.impl; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 18:10 * @notify 因为一个sqlSource有多个sqlNode 所以将所有的sqlNode封装成一个对象。 * 集中的管理所有的sqlNode * @version 1.0 */ import mapping.DynamicContext; import sqlnode.SqlNode; import java.util.List; public class MixedSqlNode implements SqlNode { //封装SqlNode集合信息 private List<SqlNode> sqlNodes; public MixedSqlNode(List<SqlNode> sqlNodes) { this.sqlNodes = sqlNodes; } /* * 对外提供对数据封装的操作 * */ @Override public void apply(DynamicContext context) throws Exception { //执行sqlSource中所有sqlNode的apply()不同的sqlNode有不同的解析方式。最终都会将自己解析的sql放到DynamicContext中 for (SqlNode sqlNode : sqlNodes) { sqlNode.apply(context); } } }
但我们调用MixedSqlNode的apply(DynamicContext context);其实就是循环调用每一个SqlNode自己的apply(DynamicContext context);前边我们说过,
只包含StaticTextSqlNode节点的才能被封装成RawSqlSource。所以RawSqlSource中存储的只是多个StaticTextSqlNode。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlnode.impl; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 18:10 * @notify 纯文本的sql节点,包含#{} 但是不包含 ${} 的 * @version 1.0 */ import mapping.DynamicContext; import sqlnode.SqlNode; public class StaticTextSqlNode implements SqlNode { private String sqlText; public StaticTextSqlNode(String sqlText){ this.sqlText = sqlText; } /* * 每一个StaticTextSqlNode 仅仅包含纯文本的sql语句和#{}所以不需要额外的处理,他本身的apply,只是 * 作为拼接。设想select * from 是一个sqlNode where id = #{id} 是第二个sqlNode只要拼接就可以了。不管作为 * 第几个sqlNode,StaticTextSqlNode不需要做其他的操作${}需要将其中的文本替换,而IfSqlNode则需要判断test等。 * */ @Override public void apply(DynamicContext context)throws Exception { //添加到 DynamicContext context.appendSql(sqlText); } }
StaticTextSqlNode的apply(DynamicContext context);只是将sql文本连接到DynamicContext中的StringBuilder中。当MixedSqlNode的apply(DynamicContext context);执行接收后,DynamicContext中的字符串其实是
select * from table where id = #{id} 此时我们需要将#{id}替换成 ?这里使用到了SqlSourceParser。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlsource;/* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-08 14:31 * @notify 解决重用性 RawSqlSource 和 DynamicSqlSource 都需要对 #{} 进行替换 ? * @version 1.0 */ import mapping.DynamicContext; import sqlsource.impl.StaticSqlSource; import tokenparser.GenericTokenParser; import tokenparser.ParameterMappingTokenHandler; public class SqlSourceParser { private DynamicContext dynamicContext = null; public SqlSourceParser(DynamicContext context) { this.dynamicContext = context; } public SqlSource parse() throws Exception { //将 #{ } 替换成 ? 然后将内容存储到 ParameterMapping 中 ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler); String sql = tokenParser.parse(dynamicContext.getSql()); return new StaticSqlSource(sql, parameterMappingTokenHandler.getParameterMappings()); } }
SqlSourceParser.parser()方法使用GenericTokenParser工具类的parse(String sql)方法,对字符串中指定开头和结尾中间部分做操作。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package tokenparser; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 14:52 * @notify 工具类,对sql字符串处理 ${} 和#{} * @version 1.0 */ public class GenericTokenParser { private final String openToken; private final String closeToken; private final TokenHandler handler; /* * openToken:字符串开始 #{ 或者 ${ * closeToken 字符串结束 } * handler 大括号中间需要处理的方法。 * */ public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; } /** * 解析${}和#{} * * @param text * @return */ public String parse(String text) throws Exception{ if (text == null || text.isEmpty()) { return ""; } // search open token int start = text.indexOf(openToken, 0); if (start == -1) { return text; } char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; while (start > -1) { if (start > 0 && src[start - 1] == '\\') { // this open token is escaped. remove the backslash and continue. builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { // found open token. let's search close token. if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) { if (end > offset && src[end - 1] == '\\') { // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else { expression.append(src, offset, end - offset); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } }
此类接收三个参数,前两个就是#{ 和 } 最后一个参数,对匹配到该规则需要做的操作。是一个接口TokenHandler我们使用在这里我们使用它的实现类ParameterMappingTokenHandler
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package tokenparser; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 14:52 * @notify 对${} 和#{} 中的内容进行处理 * @version 1.0 */ public interface TokenHandler { String handleToken(String content)throws Exception; }
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package tokenparser.impl; import mapping.ParameterMapping; import tokenparser.TokenHandler; import java.util.ArrayList; import java.util.List; public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<>(); // content是参数名称 // content 就是#{}中的内容 public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } //TODO 将参数名封装成 ParameterMapping 此处还要想办法将参数类型设置到 ParameterMapping private ParameterMapping buildParameterMapping(String content) { ParameterMapping parameterMapping = new ParameterMapping(); parameterMapping.setName(content); return parameterMapping; } //获取所有 ? 的对象 public List<ParameterMapping> getParameterMappings() { return parameterMappings; } }
当匹配到#{id}会将字符串替换成 ?并且id保存到ParameterMapping对象中,因为一个sql可能包含多个#{}所以ParameterMappingTokenHandler内部持有一个list,每一个ParameterMapping则是一个?的真实属性。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package mapping; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 14:54 * @notify 如果SQL是 ? 占位符的形式,就需要这个对象,这个对象封装的则是 ? 代表的参数。 * @version 1.0 */ public class ParameterMapping { //参数名 private String name; //参数类型 private Class<?> type; public String getName() { return name; } public void setName(String name) { this.name = name; } public Class<?> getType() { return type; } public void setType(Class<?> type) { this.type = type; } }
到此时我们已经完全对sql脚本进行了解析,使其成为一个可被执行的sql语句select * from table where id = ? and name = ? 而且我们还获取两个?代表的真实属性名。
将sql脚本字符串和?代表的属性包装类封装成一个StaticSqlSource。这个类也是SqlSource接口的实现类,但是它仅仅用于返回最终的结果,不包含实际业务逻辑。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package sqlsource.impl; import mapping.BoundSql; import mapping.ParameterMapping; import sqlsource.SqlSource; import java.util.ArrayList; import java.util.List; /* * @auther 顶风少年 * @mail dfsn19970313@foxmail.com * @date 2020-01-04 14:52 * @notify RawSqlSource 和 DynamicSqlSource处理后的结构。 * @version 1.0 */ public class StaticSqlSource implements SqlSource { private String sql; private List<ParameterMapping> parameterMappings = new ArrayList<>(); public StaticSqlSource(String sql, List<ParameterMapping> parameterMappings) { this.sql = sql; this.parameterMappings = parameterMappings; } @Override public BoundSql getBoundSql(Object param)throws Exception { return new BoundSql(sql,parameterMappings); } }
在这里我们也可以清晰的看到,BoundSql其实就是可执行的sql和需要入参的?名称。最终RawSqlSource的getBoundSql(Object param);使用的则是StaticSqlSource的getBoundSql(Object param);
下面我们在看DynamicSqlSource这个SqlSource中包含的SqlNode可能是任意类型的SqlNode,所以getBoundSql(Object param)方法中调用MixedSqlNode可能调用的是TextSqlNode,IfSqlNode,