一、什么是Mybatis
引用Mybatis文档中的介绍:Mybatis是一款优秀的持久层框架,他支持自定义Sql、存储过程以及高级映射。Mybatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。Mybatis可以通过简单的XML或注解来配置和映射原始类型、接口和Java的POJO为数据库中的记录。
而对于我自己想学习Mybatis的源码的原因,一是想了解Mybatis的源码,二是想学习下Mybatis中的设计模式,比如责任链、代理、装饰者模式等。
二、传统的JDBC操作
1、加载数据库驱动。
2、通过DriverManager注册驱动。
3、通过DriverManager创建数据库连接Connection。
4、通过Connection创建Statement/PreparedStatement。
5、通过Statement进行数据库操作。
6、处理结果集。
7、关闭资源。
由于传统的JDBC中,如果每一个线程对数据库进行操作时,都需要进行上述的七步操作,从而导致开发中不停的造轮子,因此我们可以引入Mybatis框架,交给Mybatis来帮我们做这些事,我们只需要关心对应的配置信息和sql的编写即可。
三、SqlSessionFactoryBuilder
引用Mybatis官方文档,构建SqlSessionFactory时会使用到SqlSessionFactoryBuilder。
因此SqlSessionFactoryBuilder的作用为构建SqlSessionFactory。
· SqlSessionFactoryBuilder源码
代码语言:javascript复制public class SqlSessionFactoryBuilder {
//...
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
build中的源码过程即对parser进行解析,解析完后,生成SqlSessionFactroy对象。
观察DefaultSqlSessionFactory(config)构造函数,可以发现,SqlSessionFactory类中维护了一个configuration对象,用于存储environment(数据库相关环境信息)、mapperRegistry(mapper或dao层接口映射器信息)、typeHandlerRegistr(类型处理器信息)等。
代码语言:javascript复制public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
· 解析XML文件
代码语言:javascript复制public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
//解析XML文件
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
代码语言:javascript复制 //...
代码语言:javascript复制}
代码语言:javascript复制public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver); //调用jdk方法,生成document树 this.document = createDocument(new InputSource(inputStream));
}
引用Mybatis官方文档配置的xml信息为:
========>
转换为document树后的结点信息为:
解析document的核心代码(理解即可,我没细看):
代码语言:javascript复制public boolean scanDocument(boolean complete)
throws IOException, XNIException {
//...
int event = next();
do {
switch (event) {
case XMLStreamConstants.START_DOCUMENT :
//遇到了开始的document元素
break;
case XMLStreamConstants.START_ELEMENT :
代码语言:javascript复制 //遇到了开始的element元素
代码语言:javascript复制 break;
case XMLStreamConstants.CHARACTERS : //字符集处理
fEntityScanner.checkNodeCount(fEntityScanner.fCurrentEntity);
fDocumentHandler.characters(getCharacterData(),null);
break;
case XMLStreamConstants.SPACE:
//空格处理
break;
代码语言:javascript复制 //...
代码语言:javascript复制 case XMLStreamConstants.COMMENT : //注释处理 fEntityScanner.checkNodeCount(fEntityScanner.fCurrentEntity);
fDocumentHandler.comment(getCharacterData(),null);
break;
case XMLStreamConstants.DTD :
//DTD约束
break;
代码语言:javascript复制 //...
代码语言:javascript复制 case XMLStreamConstants.NAMESPACE : //nameSpace处理 break;
case XMLStreamConstants.ATTRIBUTE : //attribute处理 break;
case XMLStreamConstants.END_ELEMENT :
//element元素结束符号
break;
default :
throw new InternalError("processing event: " event);
}
event = next();
} while (event!=XMLStreamConstants.END_DOCUMENT && complete);
代码语言:javascript复制 //...
代码语言:javascript复制}
· 解析document树
代码语言:javascript复制public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
//解析XML文件生成document树
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //解析document树,生成Configuration对象 return build(parser.parse());
代码语言:javascript复制 //...
代码语言:javascript复制}
代码语言:javascript复制public Configuration parse() {
//...
//1、parser.evalNode,将document树解析成XNode结点对象
代码语言:javascript复制 //2、解析XNode为Configuration对象
代码语言:javascript复制 parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
四、parseConfiguration
代码语言:javascript复制private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " e, e);
}
}
选择几个我认为是重要的看下是如何解析的。
· environment
代码语言:javascript复制private void parseConfiguration(XNode root) {
//...
environmentsElement(root.evalNode("environments"));
代码语言:javascript复制 //...
}
代码语言:javascript复制private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) { //从上下文中设置默认的environment对象 environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
//获取mybatis事物管理器对象
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")) //获取数据库连接dataSource资源对象 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource(); //构造器模式创建environment对象 Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
environment比较简单,即加载了配置的事物管理器、数据库资源对象,并通过构造器模式封装成environment对象被加载到configuration中。
· mapper
代码语言:javascript复制private void parseConfiguration(XNode root) {
//...
mapperElement(root.evalNode("mappers"));
代码语言:javascript复制 //...
代码语言:javascript复制}
代码语言:javascript复制public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
代码语言:javascript复制public <T> void addMapper(Class<T> type) { //mapper必须为接口 if (type.isInterface()) {
//...
knownMappers.put(type, new MapperProxyFactory<T>(type));
代码语言:javascript复制 //...
代码语言:javascript复制 }
}
从addMapper中可以看出,所有的mapper都会被放到mapperRegistry中key-mapper接口,value-动态代理工厂,并且mapper必须为接口类型。
因此在Mybatis官方文档示例中的session.getMapper就相当于从mapperRegistry这个map中取出来对应的mapper接口。
但是此时只解析的mapper接口,而XML中的增删改查的方法还没有被解析出来。
1、根据mapper接口文件获取文件路径名。
代码语言:javascript复制public <T> void addMapper(Class<T> type) {
//...
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
代码语言:javascript复制 //...
代码语言:javascript复制}
代码语言:javascript复制public MapperAnnotationBuilder(Configuration configuration, Class<?> type) { //获取文件的路径名 String resource = type.getName().replace('.', '/') ".java (best guess)";
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type; //...}
2、开始加载mapper.xml
代码语言:javascript复制public void parse() { //获取接口路径名 String resource = type.toString();
//防止重复加载 if (!configuration.isResourceLoaded(resource)) { //加载mapper.xml loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
//加载缓存 parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
if (!method.isBridge()) { //加载mapper中的sql注解信息 parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
} //移除掉未完成信息 parsePendingMethods();
}
代码语言:javascript复制private void loadXmlResource() {
//防止重复加载
if (!configuration.isResourceLoaded("namespace:" type.getName())) { //xml标识,路径名 String xmlResource = type.getName().replace('.', '/') ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); //加载xml xmlParser.parse();
}
}
}
代码语言:javascript复制public void parse() {
if (!configuration.isResourceLoaded(resource)) { //加载xml中的namespace、resultMap、paramterMap、sql、增删改查标签 configurationElement(parser.evalNode("/mapper"));
//将mapper资源,放入到已加载列表中 configuration.addLoadedResource(resource);
//绑定mapper.xml文件和nameSpace: 路径 到队列中 bindMapperForNamespace();
}
//加载完成后移除队列操作处理
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
加载mapper.xml的核心方法:
代码语言:javascript复制private void configurationElement(XNode context) {
try {//namespace作为标识符,不能为空String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//parameterMap的初始化 parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//resultMap的初始化 resultMapElements(context.evalNodes("/mapper/resultMap"));
//sql标签的初始化 sqlElement(context.evalNodes("/mapper/sql"));
//增删改查标签的初始化 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " e, e);
}
}
到此刻为止,mapper.xml中的所有信息已经被加载到mapperRegistry中了。
3、开始加载mapper接口中带注解的方法
通过parseStatement方法进行加载。
代码语言:javascript复制public void parse() { //...parseStatement(method); //...
}
mapper元素的加载,其实在于mapperRegistry注册中心。加载mapper元素时会将xml中所有的mapper进行循环遍历,全部放到mapperRegistry中(key-mapper.xml根据路径转换后的class接口,value-该class接口的工厂类,内部其实为一个JDK动态代理类)。
然后开始先对mapper.xml文件中的元素进行加载。根据顺序加载mybatis缓存、parameterMap、resultMap、sql标签信息、增删改查标签信息进行初始化封装。最后将namespace: mapper.java文件路径名的信息作为资源名放入到队列中,作为是否重复加载的依据。
接着再通过parseStatement方法对mapper.java中的增删改查sql注解进行加载。全部加载完毕后,将他们从未加载的队列中移除。
注意点:阅读源码时发现,至少有两处地方都对同一个class文件进行了对mapperRegistry的注册。
原因:spring可能无法知道真正的mapper资源是否被加载,因此这里设置了一个特殊的标识符即namespace: mapper.java文件的路径名放入到队列中作为是否重复加载的依据,所以在最后还需要调用下mapperRegistry.put。
五、SqlSessionFactory
此时的configuration中已经封装了绝大部分mybatis的信息,最后,SqlSessionFactoryBuilder的build方法执行完毕后,会调用SqlSessionFactory的初始化方法,加载出SqlSessionFactory,并将configuration对象进行赋值。完成最后使命。
代码语言:javascript复制public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
代码语言:javascript复制public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
} //...
六、总结
SqlSessionFactoryBuilder类的作用其实就是解析配置文件、创建SqlSessionFactory。
问题一:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession的作用域如何?
SqlSessionFactoryBuilder被创建出来就是为了解析Mybatis的配置文件、创建SqlSessionFactory,一旦SqlSessionFactory被创建出来,该类就不再被需要了。
SqlSessionFactory一旦被创建就与服务器应用共存亡,用来创建SqlSession。相当于数据库连接池。
SqlSession相当于一个数据库中的事务,由每个线程来创建SqlSession,进行数据库操作。因此SqlSession的作用域和线程相关。