30分钟教你写一个mybatis框架
Posted 顶风少年的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了30分钟教你写一个mybatis框架相关的知识,希望对你有一定的参考价值。
XMLConfigBuilder类解析mybatis配置文件,创建一个Configuration对象,该对象是mybatis的核心配置类。对配置文件中的<environments>标签解析,<environments>包含多个<environment>每个包含<dataSource>根据<environments>标签的default属性选择一个environment,读取对应的<dataSource>配置信息。根据<dataSource>的type属性,确定要使用的连接池。使用<dataSource>中配置的数据库信息进而创建数据源DataSource,将DataSource设置到Configuration中。
<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>
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>)。
<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>
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是一个接口。
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。
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是干嘛的。
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的实现类。
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类型参数。
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
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; }
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而StaticTextSqlNode节点只包含文本内容且文本内容中只有#{}。所以apply方法
以上是关于30分钟教你写一个mybatis框架的主要内容,如果未能解决你的问题,请参考以下文章