手写Mybatis框架
一、综述
(一)Mybatis执行流程
Mybatis源码主流程如下图所示:
1、配置文件加载
全局配置文件加载:加载数据库信息和Mapper.xml文件
2、配置文件加载后返回一个SqlSessionFactory对象:对象中包含Configuration对象,该对象中包含所有的配置信息
3、对外提供SqlSession接口,封装相关增删改查方法,供SqlSessionFactory调用
4、对外提供Executor接口,真正的处理数据库操作
5、从Configuration中获取对应的MappedStatement对象和入参,对sql进行解析,并执行
6、将JDBC原生执行结果转换为xml文件中指定的返回结果
(二)MyBatis对象介绍
类名 | 作用 | 包含内容 | 子类/实现类 | 作用 | 包含内容 | |
Configuration | MyBatis全量配置信息对象 | 数据库信息DataBase和MappedStatement对象的map集合 | ||||
MappenStatement | 一个Mapper中方法的解析结果对象 | statementId、入参对象、出参对象、执行方式、SqlSource | ||||
SqlSource接口 | 提供获取BoundSql的方法 | RowSqlSource | 在构造时就使用入参SqlNode的apply获取对应sql信息,然后将#{}替换成 ? | 包含一个SqlSource,该类型为StaticSqlSource | ||
DynamicSqlSource |
1、在调用getBoundSql方法时,才使用属性SqlNode的apply方法获取sql 2、使用其属性SqlNode的apply方法拼装sql语句 3、将#{}替换成 ? 后封装成一个StaticSqlSource 3、使用StaticSqlSource中的getBoundSql方法获取BoundSql对象 |
SqlNode | ||||
StaticSqlSource | 上面的两种SqlSource最终解析后都会以StaticSqlSource形式存在,只不过RowSqlSource是在初始化时解析,而DynamicSqlSouce是在调用getBoundSql方法时解析 | sql和入参名称集合 | ||||
SqlNode | 提供apply方法拼装sql信息 | MixedSqlNode |
1、混合的SqlNode,非叶子节点 2、apply方法:循环SqlNode调用apply方法 |
SqlNode集合 | ||
TextSqlNode |
1、文本SqlNode,可能包含#{} 2、apply方法:将#{}替换成? |
sql | ||||
StaticTextSqlNode |
1、静态文本SqlNode 2、apply方法:拼装sql |
sql | ||||
IfSqlNode等 |
1、if条件的SqlNode 2、使用OGNL表达式判断,满足条件后调用对应的apply方法 |
test、SqlNode | ||||
BoundSql | 存储解析后的sql语句和入参信息 | sql语句和ParamterMapping集合 | ||||
ParamterMapping | 存储入参名称,后续使用反射从入参对象的指定名称中获取值 | name |
二、全局配置文件加载
(一)配置文件主信息加载
1、所有的配置信息都要包含在Configuration对象中,并最终以SqlSessionFactory对象的形式返回
那么,首先创建一个SqlSessionFactory接口,里面包含openSession方法,同时创建一个默认的实现类DefaultSqlSessionFactory,由于SqlSessionFactory重要包含Configuration对象,因此在其默认实现类中,需要有一个Configuration对象的属性,并需要有构造函数来设置。
SqlSessionFactory接口:
package com.lcl.galaxy.mybatis.frame.sqlsession; public interface MySqlSessionFactory { MySqlSession openSession(); }
SqlSessionFactory默认实现类DefaultSqlSessionFactory:
package com.lcl.galaxy.mybatis.frame.sqlsession; import com.lcl.galaxy.mybatis.frame.config.MyConfiguration; public class MyDefaultSqlSessionFactory implements MySqlSessionFactory { private MyConfiguration myConfiguration; public MyDefaultSqlSessionFactory(MyConfiguration myConfiguration){ this.myConfiguration = myConfiguration; } @Override public MySqlSession openSession() { return new MyDefualtSqlSession(myConfiguration); } }
2、加载主配置文件
这里使用构建者模式,最终构建出SqlSessionFactory,其解析方法为:根据主配置文件路径获取InputStream ---> 创建Document ---> 按照MyBatis的语义去解析Document对象 ---> 将所有对象封装成一个Configuration对象
package com.lcl.galaxy.mybatis.frame.sqlsession; import com.lcl.galaxy.mybatis.frame.config.MyConfiguration; import com.lcl.galaxy.mybatis.frame.config.MyResources; import com.lcl.galaxy.mybatis.frame.config.MyXmlConfigParser; import com.lcl.galaxy.mybatis.frame.util.DocumentUtils; import org.dom4j.Document; import java.io.InputStream; import java.net.URL; public class MysessionFactorBuilder { public static MySqlSessionFactory build(String resource) { InputStream inputStream = MyResources.class.getClassLoader().getResourceAsStream(resource); return build(inputStream); } public static MySqlSessionFactory build(InputStream inputStream) { MyXmlConfigParser myXmlConfigParser = new MyXmlConfigParser(); Document document = DocumentUtils.readInputStream(inputStream); MyConfiguration myConfiguration = myXmlConfigParser.parse(document.getRootElement()); return new MyDefaultSqlSessionFactory(myConfiguration); } }
(二)数据库信息加载
从上面的代码可以看到,最终是调用XmlConfigParser的parse方法对得到的Element进行解析的,那么接下来就是对RootElement的解析,我们看mybatis的主配置文件时可以发现,其主要的配置内容可以分为两类,一个是以environments标签设置的数据库信息和以mappers标签设置的mapper集合,那么在XmlConfigParser的parse方法中,可以分别对两种不同的标签分别做解析
public MyConfiguration parse(Element rootElement) { parseEnvironments(rootElement.element("environments")); parseMappers(rootElement.element("mappers")); return myConfiguration; }
可以看到,分别封装了两个方法对不同的标签做解析,这里先说对environments标签的解析,可以对照一下下面的主配置文件,主要就是解析数据库驱动、数据库地址、用户名和密码。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <typeAliases> <typeAlias type="com.lcl.galaxy.mybatis.simple.common.domain.UserDo" alias="user"/> </typeAliases> <!--配置环境--> <environments default="mysql"> <!-- 配置mysql的环境--> <environment id="mysql"> <!-- 配置事务 --> <transactionManager type="JDBC"></transactionManager> <!--配置连接池--> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://********"></property> <property name="username" value="********"></property> <property name="password" value="5H5eLQsp6yO4"></property> </dataSource> </environment> </environments> <!-- 配置映射文件的位置 --> <mappers> <!-- 使用resource加载xml文件 --> <mapper resource="mapper/frame/UserMapper.xml"></mapper> <!-- 使用package加载package下所有的注解Mapper --> --> </mappers> </configuration>
解析时,需要解析environments中defualt标签的值(默认使用数据库信息),然后在environments标签中可能有多个environment标签,这里要使用标签的id属性值和defualt属性值一致的数据库信息。
private void parseEnvironments(Element environments) { Properties properties = null; String aDefault = environments.attributeValue("default"); List<Element> elements = environments.elements("environment"); for (Element element: elements) { String id = element.attributeValue("id"); if(id.equals(aDefault)){ parseEnvironment(element); break; } } } private void parseEnvironment(Element element) { Properties properties = null; BasicDataSource dataSource = new BasicDataSource(); Element dataSourceElement = element.element("dataSource"); String type = dataSourceElement.attributeValue("type"); type = type == null || type.equals("") ? "POOLED":type; if (type.equals("POOLED")){ properties = parseProperties(dataSourceElement); dataSource.setDriverClassName(properties.getProperty("driver")); dataSource.setUrl(properties.getProperty("url")); dataSource.setUsername(properties.getProperty("username")); dataSource.setPassword(properties.getProperty("password")); } myConfiguration.setDataSource(dataSource); } private Properties parseProperties(Element dataSourceElement) { Properties properties = new Properties(); List<Element> elements = dataSourceElement.elements("property"); for (Element property: elements) { properties.put(property.attributeValue("name"),property.attributeValue("value")); } return properties; }
这里解析出来的是一个数据库的相关对象,同时将DataSource对象设置到Configuration对象中。
package com.lcl.galaxy.mybatis.frame.config; import lombok.Data; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Data public class MyConfiguration { private DataSource dataSource; private Map<String, MyMappedStatement> mappedStatementMap = new HashMap<>(); public MyMappedStatement getMyMappedStatement(String statementId){ return mappedStatementMap.get(statementId); } public void SetMyMappedStatement(String statementId, MyMappedStatement mappedStatement){ mappedStatementMap.put(statementId, mappedStatement); } }
(三)Mapper集合加载
可以看到Configuration中还保存了Map<String, MyMappedStatement> mappedStatementMap属性,该属性是解析Mapper.xml文件得到的。
由于mappers标签中会配置多个mapper标签,因此需要先加载到mappers集合后,再对集合中的每一个mapper标签进行解析。
private void parseMappers(Element mappers) { List<Element> elementList = mappers.elements("mapper"); for (Element mapperElement: elementList) { String resource = mapperElement.attributeValue("resource"); InputStream inputStream = MyResources.getResourceAsStream(resource); Document mapperDocument = DocumentUtils.readInputStream(inputStream); MyXmlMapperParse myXmlMapperParse = new MyXmlMapperParse(myConfiguration); myXmlMapperParse.parse(mapperDocument); } }
首先是解析mapper.xml文件的namespace,然后在去解析sql,例如select、update、delete、insert等标签,为了简单,这里手写源码就只写一个select标签。在一个mapper文件中可能会有多个select标签,因此在获取到所有的select标签集合后,循环解析。
package com.lcl.galaxy.mybatis.frame.config; import org.dom4j.Document; import org.dom4j.Element; import java.util.List; public class MyXmlMapperParse { private MyConfiguration myConfiguration; public MyXmlMapperParse(MyConfiguration myConfiguration){ this.myConfiguration = myConfiguration; } public void parse(Document mapperDocument) { Element rootElement = mapperDocument.getRootElement(); String namespace = rootElement.attributeValue("namespace"); List<Element> elements = rootElement.elements("select"); for (Element element: elements) { MyXmlStatementParser myXmlStatementParser = new MyXmlStatementParser(myConfiguration); myXmlStatementParser.parse(element, namespace); } } }
三、mapper.xml文件加载
(一)创建MyMappedStatement
在解析select模块时,可以解析到statementId(namespace+selectId)、入参类型、出参类型和执行方式,其中出入参类型是通过反射获取,同时会获取一个SqlSource对象。同时将以上内容封装到MappedStatement中;前面说的Configuration中还保存了Map<String, MyMappedStatement> mappedStatementMap,就是保存的这里封装的MappedStatement对象。
因此mappedStatementMap中存储的就是一个个sql信息,key是namespace+id,value就是MappedStatement对象。
package com.lcl.galaxy.mybatis.frame.config;
import com.lcl.galaxy.mybatis.frame.sqlsource.MySqlSource;
import org.dom4j.Element;
public class MyXmlStatementParser {
private static final String CONNECTOR = ".";
private MyConfiguration myConfiguration;
public MyXmlStatementParser(MyConfiguration myConfiguration) {
this.myConfiguration = myConfiguration;
}
public void parse(Element element, String namespace) {
String id = element.attributeValue("id");
String statementId = namespace + CONNECTOR + id;
String parameterType = element.attributeValue("parameterType");
String resultType = element.attributeValue("resultType");
Class<?> parameterTypeClass = resolveClass(parameterType);
Class<?> resultTypeClass = resolveClass(resultType);
String statementType = element.attributeValue("statementType");
statementType = statementType == null || statementType.equals("")? "PREPARED":statementType;
MySqlSource mySqlSource = createMySqlSource(element);
MyMappedStatement mappedStatement = new MyMappedStatement(statementId,parameterTypeClass,resultTypeClass,statementType,mySqlSource);
myConfiguration.getMappedStatementMap().put(statementId,mappedStatement);
}
private MySqlSource createMySqlSource(Element element) {
MyXmlScriptParser myXmlScriptParser = new MyXmlScriptParser(myConfiguration);
return myXmlScriptParser.parseScriptNode(element);
}
private Class<?> resolveClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
package com.lcl.galaxy.mybatis.frame.config;
import com.lcl.galaxy.mybatis.frame.sqlsource.MySqlSource;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class MyMappedStatement {
private String statementId;
private Class<?> paramterType;
private Class<?> resultType;
private String statementType;
private MySqlSource mySqlSource;
}
(二)SqlSource创建
在上面已经提到,MappedStatement中封装了SqlSource、入参对象、出参对象、执行类型、statementId这些信息,除了SqlSouce,其余的都已经在上面的代码中获取,接下来就是获取SqlSource(跟着上面的代码中createMySqlSource方法继续往下写)。
首先创建一个SqlSource接口,并提供getBoundSql方法,该方法返回一个BoundSql对象,该对象中存在一个sql语句和对应的入参对象。
package com.lcl.galaxy.mybatis.frame.sqlsource;
public interface MySqlSource {
MyBoundSql getBoundSql(Object param);
}
package com.lcl.galaxy.mybatis.frame.sqlsource; import lombok.Data; import java.util.List; @Data public class MyBoundSql { private String sql; private List<MyParameterMapping> myParameterMappings; public MyBoundSql(String sql, List<MyParameterMapping> myParameterMappingList) { this.sql = sql; this.myParameterMappings = myParameterMappingList; } }
SqlSouce接口有三个实现类,分别是DynamicSqlSource、RowSqlSource、StaticSqlSource
(1)RowSqlSource:文本SqlSource,对应的是StaticSqlNode,所以在构建RowSqlNode对象时,就已经将其进行解析完成,并封装成为StaticSqlSource;所以,如果是最顶层的RowSqlSource,会在Mapper文件加载时,就已经解析完成sql语句和对应入参对象;而如果非最顶层的RowSqlSource,则会在执行被调用执行sql时进行解析。因此在getBound方法中,只需要返回已经处理过的sql和入参对象即可,不需要再次解析。
(2)DynamicSqlSource:动态SqlSource,由于是动态sql,因此getBoundSql方法需要在调用时才可以确定执行语句,因此该实现方法在执行时在写
(3)StaticSqlSource:上面两种SqlSouce解析后的结果,都封装到该SqlSource中
可以总体说明一下其三者的区别:
去别点 | StaticSqlSource | RowSqlSource | DynamicSqlSource |
语句内容 | 封装后面两种SqlSource解析后的结果 | 纯文本sql,可能包含#{},但不包含${}和动态sql的sql信息 | 动态sql,可能包含${}或其他动态sql的sql信息 |
加载顺序 |
在该对象构造时就完成sql解析 解析方式:由于#{}已被替换为 ?,入参集合为#{}内的属性 |
在sql调用时加载时已组装sql和入参对象 原因:由于存在${}或其他动态内容,因此需要在执行时根据入参内容组装 |
|
入参对象赋值 | 入参对象为#{}内的内容 | 入参对象需要在执行时具体确定 | |
对应SqlNode | TextSqlNode | DynamicSqlNode、IfSqlNode、WhereSqlNode、ForeachSlqNode..... | |
处理方式 | 类似于JDBC中的PreparedStatement的处理,是预处理 | 类似于JDBC的Statement,是字符串的拼接 |
然后根据Element创建SqlNode,并判断该SqlNode是动态的还是非动态的,如果是动态的,则将创建一个DynamicSqlSource封装到MappedStatement中,如果是非动态的,则创建一个RawSqlSource并封装到MappedStatement中。
public MySqlSource parseScriptNode(Element element) {
MyMixedSqlNode rootSqlNode = parseDynamicTags(element);
MySqlSource mySqlSource = null;
if(isDynamic){
mySqlSource = new MyDynamicSqlSource(rootSqlNode);
}else {
mySqlSource = new MyRawSqlSource(rootSqlNode);
}
return mySqlSource;
}
上面已经说过,DynamicSqlSource是需要在被执行时才会对sql进行解析,因此其构造方法单单是对SqlNode的赋值,而RowSqlSource是只需要在mapper文件加载时加载一次即可,因此在其构造方法中,需要对sql进行解析,这里解析是调用其对应的SqlNode的apply方法进行解析(apply方法主要是解析将sql解析为JDBC可执行的sql)。
package com.lcl.galaxy.mybatis.frame.sqlsource; import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext; import com.lcl.galaxy.mybatis.frame.sqlnode.MySqlNode; public class MyRawSqlSource implements MySqlSource { private MySqlSource mySqlSource; public MyRawSqlSource(MySqlNode mySqlNode) { MyDynamicContext context = new MyDynamicContext(null); mySqlNode.apply(context); MySqlSourceParser mySqlSourceParser = new MySqlSourceParser(); mySqlSource = mySqlSourceParser.parse(context.getSql()); } }
解析完毕后,对获取到的sql语句进行处理,判断其是否包含#{},如果包含,则将其替换为 ? ,并将#{}中的属性设置为入参属性,然后将解析后的sql和入参集合封装到StaticSqlSource并返回。
package com.lcl.galaxy.mybatis.frame.sqlsource; import com.lcl.galaxy.mybatis.frame.util.GenericTokenParser; import com.lcl.galaxy.mybatis.frame.util.ParameterMappingTokenHandler; import com.lcl.galaxy.mybatis.frame.util.TokenHandler; public class MySqlSourceParser { public MySqlSource parse(String sqlText) { ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser tokenParser = new GenericTokenParser("#{","}" ,tokenHandler); String sql = tokenParser.parse(sqlText); return new MyStaticSqlSource(sql, tokenHandler.getParameterMappings()); } }
至于替换工具及OGNL表达式工具,在最后附的有代码
(三)SqlNode创建
在创建SqlNode之前,先创建一个动态sql上下文对象DynamicContext,它用来存储sql语句和绑定的值。
在该类中,定义一个StringBuilder对象,用来拼装sql语句,然后提供一个Map<String, Object>对象用来存储对应的绑定值;同时提供构造方法,入参为绑定值,并将该绑定值放入map集合中。
package com.lcl.galaxy.mybatis.frame.config;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class MyDynamicContext {
private StringBuilder sb = new StringBuilder();
private Map<String, Object> bingds = new HashMap<>();
public MyDynamicContext(Object param){
bingds.put("_param", param);
}
public Map<String, Object> getBingds(){
return bingds;
}
public void appendSql(String sql){
sb.append(sql);
}
public String getSql(){
return sb.toString();
}
}
然后是创建一个SqlNode接口,提供了一个入参为动态sql上下文DynamicContext对象的apply方法,用来做sql解析
package com.lcl.galaxy.mybatis.frame.sqlnode;
import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
public interface MySqlNode {
void apply(MyDynamicContext context);
}
该接口实现类有MixSqlNode、TextSqlNode、StaticTextSqlNode、IfSqlNode、WhereSqlNode、ForeachSqlNode等,其作用如下:
实现类 | 描述 | 属性 | 对应SqlSource | SQL解析时间 |
MixSqlNode | 这是一个混合的SqlNode | SqlNode集合 | ||
StaticTextSqlNode |
静态的SqlNode 静态文本SqlNode,里面不包含${}(一定带有${},可能会带有#{}) |
sql | RowSqlSource | Mapper文件加载 |
TextSqlNode |
文本SqlNode 文本SqlNode,里面包含了带有${}的纯文本(一定带有${},可能会带有#{}) |
sql | DynamicSqlSource | 在调用执行时,才会解析 |
IfSqlNode |
if条件的SqlNode 里面包含了OGNL表达式的test语句和SqlNode,具体包含了SqlNode是那一类,需要具体解析 |
test、SqlNode | 在调用执行时,会使用OGNL表达式判断test条件是否成功,如果成功则根据属性值SqlNode具体类型进行加载 |
这里说一下SqlSource和SqlNode的关系,首先,一个SqlSource中只有一个SqlNode,该SqlNode即为MixedSqlNode,在MixedSqlNode中又包含一个SqlNode集合,集合中可能存在各种SqlNode对象,因此SqlNode是一个树形结构,在对顶端,只会是TextSqlNode、StaticTextSqlNode、IfSqlNode、WhereSqlNode和ForeachSqlNode这些SqlNode。
MixedSqlNode:由于MixedSqlNode不会是最顶端的SqlNode,因此需要循环其中的SqlNode集合并调用apply方法进行sql解析
package com.lcl.galaxy.mybatis.frame.sqlnode;
import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import java.util.List;
public class MyMixedSqlNode implements MySqlNode {
private List<MySqlNode> mySqlNodeList;
public MyMixedSqlNode(List<MySqlNode> mySqlNodeList) {
this.mySqlNodeList = mySqlNodeList;
}
@Override
public void apply(MyDynamicContext context) {
for (MySqlNode mySqlNode : mySqlNodeList){
mySqlNode.apply(context);
}
}
}
TextSqlNode:该SqlNode因为是一个纯文本,因此需要判断该文本sql是否为动态的(是否包含${},如果包含,在执行sql时要将对应的值替换到该该值中),同时提供一个String sql变量来接收该文本值;
这里我暂时没有写apply方法,因为带有${}的sql需要根据具体的入参来进行设置,所以到后面sql执行时在写负责sql解析的apply方法。
package com.lcl.galaxy.mybatis.frame.sqlnode;
import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.util.GenericTokenParser;
import com.lcl.galaxy.mybatis.frame.util.OgnlUtils;
import com.lcl.galaxy.mybatis.frame.util.SimpleTypeRegistry;
import com.lcl.galaxy.mybatis.frame.util.TokenHandler;
public class MyTextSqlNode implements MySqlNode{
private String sql;
public MyTextSqlNode(String sql) {
this.sql = sql;
}
public boolean isDynamic() {
if(this.sql.indexOf("${") > -1){
return true;
}
return false;
}
StaticTextSqlNode:该SqlNode是静态纯文本对象,里面一定不包含${},但是有可能包含#{},里面也提供了一个sql属性用来赋值,直接追加到DynamicContext对象的sql中。
package com.lcl.galaxy.mybatis.frame.sqlnode;
import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.util.GenericTokenParser;
import com.lcl.galaxy.mybatis.frame.util.ParameterMappingTokenHandler;
public class MyStaticTextSqlNode implements MySqlNode {
private String sql;
public MyStaticTextSqlNode(String sql) {
this.sql = sql;
}
@Override
public void apply(MyDynamicContext context) {
context.appendSql(" " + sql);
}
}
在第二点中创建SqlSource时,首先是创建了一个混合的SqlNode(MixedSqlNode),其创建方法是,先拿到该Element下的所有node节点,然后判断其类型,
如果是文本类型,则使用sql构建一个TextSqlNode对象,然后调用该对象的isDynamic方法,判断是否存在动态的语句(是否包含${}),如果是动态的则将该TextSqlNode放入MixedSqlNode的SqlNode集合中,并sql语句的动态标志为true;如果不是动态的,则使用sql封装成一个StaticTextSqlNode,并放入MixedSqlNode的SqlNode集合中。
如果非文本类型,则首先需要根据额node的名称获取对应的Handler,这里需要在进行配置文件解析时就对不同的node名称设置Handler,然后在handler中对后续的内容进行解析
private MyMixedSqlNode parseDynamicTags(Element element) {
List<MySqlNode> mySqlNodeList = new ArrayList<>();
for (int i=0; i< element.nodeCount(); i++){
Node myNode = element.node(i);
if(myNode instanceof Text){
String sql = myNode.getText().trim();
if(sql == null || "".equals(sql)){
continue;
}
MyTextSqlNode myTextSqlNode = new MyTextSqlNode(sql);
if (myTextSqlNode.isDynamic()){
mySqlNodeList.add(myTextSqlNode);
isDynamic = true;
}else {
mySqlNodeList.add(new MyStaticTextSqlNode(sql));
}
}else if(myNode instanceof Element){
Element node2Element = (Element) myNode;
String nodeName = node2Element.getName().toLowerCase();
MyNodeHandler myNodeHandler = nodeHandlerMap.get(nodeName);
myNodeHandler.handleNode(node2Element, mySqlNodeList);
parseScriptNode(node2Element);
isDynamic = true;
}
}
return new MyMixedSqlNode(mySqlNodeList);
}
在这里,为了演示,只写了一个IfSqlNodeHandler,在该handler中,首先解析其test属性中的内容进行封装,后续执行时,需要使用OGNL表达式判断是否满足;其次需要解析 if 标签内的sql语句,这里同样可能存在多种SqlNode,因此也需要一个MixedSqlNode进行接收,所以,这里就重新递归调用上面的parseDynamicTags方法,直到解析出所有的根节点为可执行的TextSqlNode或StaticSqlNode为止。
public MyXmlScriptParser(MyConfiguration myConfiguration){
this.myConfiguration = myConfiguration;
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
nodeHandlerMap.put("if", new MyIfNodeHandler());
nodeHandlerMap.put("where", new MyWhereNodeHandler());
nodeHandlerMap.put("foreach", new MyForeachNodeHandler());
}
package com.lcl.galaxy.mybatis.frame.sqlnode.handler;
import com.lcl.galaxy.mybatis.frame.sqlnode.MySqlNode;
import org.dom4j.Element;
import java.util.List;
public interface MyNodeHandler {
void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList);
}
private class MyIfNodeHandler implements MyNodeHandler {
@Override
public void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList) {
MyMixedSqlNode myMixedSqlNode = parseDynamicTags(node2Element);
String test = node2Element.attributeValue("test");
MyIfSqlNode myIfSqlNode = new MyIfSqlNode(test, myMixedSqlNode);
mySqlNodeList.add(myIfSqlNode);
}
}
private class MyWhereNodeHandler implements MyNodeHandler {
@Override
public void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList) {
}
}
private class MyForeachNodeHandler implements MyNodeHandler {
@Override
public void handleNode(Element node2Element, List<MySqlNode> mySqlNodeList) {
}
}
当解析到MixedSqlNode后,将test和MixedSqlNode封装到IfSqlNode中。
这里IfSqlNode同样还没有写负责sql解析的apply方法,因为该种sql解析需要在sql调用时根据入参判断是否需要拼装。
package com.lcl.galaxy.mybatis.frame.sqlnode;
import com.lcl.galaxy.mybatis.frame.config.MyDynamicContext;
import com.lcl.galaxy.mybatis.frame.util.OgnlUtils;
public class MyIfSqlNode implements MySqlNode{
private String test;
private MySqlNode mySqlNode;
public MyIfSqlNode(String test, MyMixedSqlNode myMixedSqlNode) {
this.test = test;
this.mySqlNode = myMixedSqlNode;
}
}
四、提供SqlSession接口
(一)在调用时动态解析sql
创建SqlSession接口,提供一个查询方法,该放入入参为statementId和入参对象param,然后写一个SqlSession的默认实现类DefualtSqlSession,提供对该方法的实现,在实现方法中,首先根据入参的statementId从Configuration对象种获取到MappedStatement,然后获取MappedStatement中的内容进行处理。
package com.lcl.galaxy.mybatis.frame.sqlsession; import java.util.List; public interface MySqlSession { <T> T selectOne(String statementId, Object param); <T> List<T> selctList(String statementId, Object param); }
package com.lcl.galaxy.mybatis.frame.sqlsession; import com.lcl.galaxy.mybatis.frame.config.MyConfiguration; import com.lcl.galaxy.mybatis.frame.config.MyMappedStatement; import com.lcl.galaxy.mybatis.frame.executor.MyCachingExecutor; import com.lcl.galaxy.mybatis.frame.executor.MyExecutor; import com.lcl.galaxy.mybatis.frame.executor.MySimpleExecutor; import java.util.List; public class MyDefualtSqlSession implements MySqlSession { private MyConfiguration myConfiguration; public MyDefualtSqlSession(MyConfiguration myConfiguration) { this.myConfiguration = myConfiguration; } @Override public <T> T selectOne(String statementId, Object param) { List<Object> objects = this.selctList(statementId, param); if(objects == null || objects.size() == 0){ return null; }else if(objects.size() != 1){ throw new RuntimeException("查询出多条数据"); } return (T) objects.get(0); } @Override public <T> List<T> selctList(String statementId, Object param) { MyMappedStatement myMappedStatement = myConfiguration.getMyMappedStatement(statementId); MyExecutor myExecutor = new MyCachingExecutor(new MySimpleExecutor()); return myExecutor.query(myMappedStatement, myConfiguration, param); } }
可以发现,上面的代码中有新创建的Executor接口,该接口中提供sql执行的方法query,其中入参为statementId,param和主配置信息Configuration
其实按照MyBatis的设计,是有一级缓存和二级缓存的,因此,需要提供一个CachingExecutor实现类来对二级缓存做处理,由于现在Mybatis的二级缓存基本上已不再使用,这里为了演示核心处理逻辑,在该实现类中不再处理,直接调用新建的Executor具体处理类BaseExecutor来处理
package com.lcl.galaxy.mybatis.frame.executor; import com.lcl.galaxy.mybatis.frame.config.MyConfiguration; import com.lcl.galaxy.mybatis.frame.config.MyMappedStatement; import java.util.List; public class MyCachingExecutor implements MyExecutor { private MyExecutor myExecutor; public MyCachingExecutor(MyExecutor myExecutor) { this.myExecutor = myExecutor; } @Override public <T> List<T> query(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param) { return myExecutor.query(myMappedStatement, myConfiguration, param); } }
package com.lcl.galaxy.mybatis.frame.executor; import com.lcl.galaxy.mybatis.frame.config.MyConfiguration; import com.lcl.galaxy.mybatis.frame.config.MyMappedStatement; import java.util.HashMap; import java.util.List; import java.util.Map; public abstract class MyBaseExecutor implements MyExecutor { private Map<String, Object> oneLevelMap = new HashMap(); @Override public <T> List<T> query(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param) { String sql = myMappedStatement.getMySqlSource().getBoundSql(param).getSql(); Object object = oneLevelMap.get(sql); if(object != null){ return (List<T>) object; } object = queryFromDataBase(myMappedStatement, myConfiguration, param); return (List<T>) object; } protected abstract List<Object> queryFromDataBase(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param); }
由于MaBatis中提供一级缓存,且一级缓存必须使用,因此在BaseExecutor中,提供了一个一级缓存oneLevelMap,首先调用MappedStatement中获取SqlSource,然后调用SqlSource中的getBoundSql方法来获取到要执行的sql,然后使用sql语句从一级缓存中获取对应的结果,如果没有结果,则调用queryFromDataBase方法进行查询。这里比较重要的一点就是如何从SqlSource的getBound方法中获取对应的sql。
这里就要重新回来写SqlSource接口的三个实现类的getBoundSql方法,StaticSqlSource、RowSqlSource、DynamicSqlSource,由于RowSqlSource在mapper文件加载时就会解析sql并将其封装成StaticSqlSource,因此在执行时,只有StaticSqlSource和DynamicSqlSource会被调用,其中StaticSqlSource已经封装好了BoundSql,因此直接就可以获取,而DynamicSqlSource是需要根据入参对象来进行解析的,因此需要在getBoundSql方法中调用apply方法进行追加sql,然后对其sql进行解析,对有#{}的替换成 ? ,且将SQL语句和替换后的入参对象封装成StaticSqlSource并返回。
(二)执行sql
在上一步拿到BoundSql后,即可真正的进行数据查询,这里可以写一个MyBaseExecutor的子类,并重写queryFromDataBase方法,在该方法中,根据封装到MappedStatement中的StaticSqlSource和传入的入参对象param封装成一个BoundSql对象,然后就是创建数据库连接,预执行,赋值,执行,获取结果和结果转换。
@Override protected List<Object> queryFromDataBase(MyMappedStatement myMappedStatement, MyConfiguration myConfiguration, Object param) { List<Object> resultList = new ArrayList<>(); try { Connection connection = getConnection(myConfiguration); MyBoundSql myBoundSql = getBoundSql(myMappedStatement.getMySqlSource(), param); if ("PREPARED".equals(myMappedStatement.getStatementType().toUpperCase())){ PreparedStatement preparedStatement = createStatement(connection, myBoundSql); handlerParamter(preparedStatement, myBoundSql, param); log.info("sql语句:{}", myBoundSql.getSql()); ResultSet resultSet = preparedStatement.executeQuery(); handleResult(resultList, myMappedStatement, resultSet); } }catch (Exception e){ e.printStackTrace(); } return resultList; }
1、获取数据库连接
private Connection getConnection(MyConfiguration myConfiguration) throws SQLException { DataSource dataSource = myConfiguration.getDataSource(); Connection connection = dataSource.getConnection(); return connection; }
2、获取BoundSql
private MyBoundSql getBoundSql(MySqlSource mySqlSource, Object param) { MyBoundSql boundSql = mySqlSource.getBoundSql(param); return boundSql; }
3、预执行
private PreparedStatement createStatement(Connection connection, MyBoundSql myBoundSql) throws Exception { PreparedStatement preparedStatement = connection.prepareStatement(myBoundSql.getSql()); return preparedStatement; }
4、设置参数
这里写的比较简答,应该是判断类型为简单类型时,则直接设置第一个参数即可(这里只写了int和String两个类型);如果非简单类型,则需要获取BoundSql中的入参对象集合,循环该集合获取每一个入参的名称,通过反射获取到入参param对应的类对象,然后根据名称从该对象中获取字段Field,然后通过反射获取到param对象中相应属性的值,并替换sql中的占位符。
private void handlerParamter(PreparedStatement preparedStatement, MyBoundSql myBoundSql, Object param) throws Exception { if(param instanceof Integer){ preparedStatement.setInt(1, (Integer) param); }else if(param instanceof String){ preparedStatement.setString(1, String.valueOf(param)); }else { List<MyParameterMapping> myParameterMappings = myBoundSql.getMyParameterMappings(); for (int i=0; i< myParameterMappings.size(); i++){ MyParameterMapping myParameterMapping = myParameterMappings.get(i); String name = myParameterMapping.getName(); Class<?> clazz = param.getClass(); Field field = clazz.getDeclaredField(name); field.setAccessible(true); Object object = field.get(param); preparedStatement.setObject(i+1, object); } } }
5、执行sql
ResultSet resultSet = preparedStatement.executeQuery();
6、结果映射
首先需要通过反射获取到出参对象的类,然后循环查询结果,在每一个循环中,通过出参对象类的newInstance()方法创建一个出参类型的对象,然后获取查询结果列数并循环,获取每一列的名字,通过反射获取该名字对应的字段Field,最终再通过反射设置该字段的值。
private void handleResult(List<Object> resultList, MyMappedStatement myMappedStatement, ResultSet rs) throws Exception { Class<?> resultType = myMappedStatement.getResultType(); while (rs.next()){ Object object = resultType.newInstance(); ResultSetMetaData rsMetaData = rs.getMetaData(); int columnCount = rsMetaData.getColumnCount(); for (int i = 0; i< columnCount; i++){ String columName = rsMetaData.getColumnName(i+1); Field declaredField = resultType.getDeclaredField(columName); declaredField.setAccessible(true); declaredField.set(object, rs.getObject(i + 1)); } resultList.add(object); } }
五、工具类
(一)Document工具类
package com.lcl.galaxy.mybatis.frame.util; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.io.SAXReader; import java.io.InputStream; public class DocumentUtils { public static Document readInputStream(InputStream inputStream) { SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(inputStream); return document; } catch (DocumentException e) { e.printStackTrace(); } return null; } }
(二)简单类工具
package com.lcl.galaxy.mybatis.frame.util; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Date; import java.util.HashSet; import java.util.Set; public class SimpleTypeRegistry { private static final Set<Class<?>> SIMPLE_TYPE_SET = new HashSet<>(); static { SIMPLE_TYPE_SET.add(String.class); SIMPLE_TYPE_SET.add(Byte.class); SIMPLE_TYPE_SET.add(Short.class); SIMPLE_TYPE_SET.add(Character.class); SIMPLE_TYPE_SET.add(Integer.class); SIMPLE_TYPE_SET.add(Long.class); SIMPLE_TYPE_SET.add(Float.class); SIMPLE_TYPE_SET.add(Double.class); SIMPLE_TYPE_SET.add(Boolean.class); SIMPLE_TYPE_SET.add(Date.class); SIMPLE_TYPE_SET.add(Class.class); SIMPLE_TYPE_SET.add(BigInteger.class); SIMPLE_TYPE_SET.add(BigDecimal.class); } public static boolean isSimpleType(Class<?> clazz) { return SIMPLE_TYPE_SET.contains(clazz); } }
(三)字符转处理类
package com.lcl.galaxy.mybatis.frame.util; public interface TokenHandler { String handleToken(String content); }
package com.lcl.galaxy.mybatis.frame.util; import com.lcl.galaxy.mybatis.frame.sqlsource.MyParameterMapping; import org.apache.ibatis.mapping.ParameterMapping; import java.util.ArrayList; import java.util.List; public class ParameterMappingTokenHandler implements TokenHandler { private List<MyParameterMapping> parameterMappings = new ArrayList<>(); // context是参数名称 @Override public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; } private MyParameterMapping buildParameterMapping(String content) { MyParameterMapping parameterMapping = new MyParameterMapping(content); return parameterMapping; } public List<MyParameterMapping> getParameterMappings() { return parameterMappings; } public void setParameterMappings(List<MyParameterMapping> parameterMappings) { this.parameterMappings = parameterMappings; } }
package com.lcl.galaxy.mybatis.frame.util; public class GenericTokenParser { private final String openToken; private final String closeToken; private final TokenHandler 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) { 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(); } }
(四)OGNL表达式处理类
package com.lcl.galaxy.mybatis.frame.util; import ognl.Ognl; import ognl.OgnlContext; import java.math.BigDecimal; public class OgnlUtils { /** * 根据Ongl表达式,获取指定对象的参数值 * @param expression * @param paramObject * @return */ public static Object getValue(String expression, Object paramObject) { try { OgnlContext context = new OgnlContext(); context.setRoot(paramObject); //mybatis中的动态标签使用的是ognl表达式 //mybatis中的${}使用的是ognl表达式 Object ognlExpression = Ognl.parseExpression(expression);// 构建Ognl表达式 Object value = Ognl.getValue(ognlExpression, context, context.getRoot());// 解析表达式 return value; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 通过Ognl表达式,去计算boolean类型的结果 * @param expression * @param parameterObject * @return */ public static boolean evaluateBoolean(String expression, Object parameterObject) { Object value = OgnlUtils.getValue(expression, parameterObject); if (value instanceof Boolean) { return (Boolean) value; } if (value instanceof Number) { return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; } return value != null; } }
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~