一、什么是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源码
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(类型处理器信息)等。
public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}
· 解析XML文件
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
//解析XML文件
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//...
}
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的核心代码(理解即可,我没细看):
public boolean scanDocument(boolean complete)throws IOException, XNIException { //... int event = next(); do {switch (event) {case XMLStreamConstants.START_DOCUMENT :
//遇到了开始的document元素 break; case XMLStreamConstants.START_ELEMENT :
//遇到了开始的element元素
break;
case XMLStreamConstants.CHARACTERS :
//字符集处理fEntityScanner.checkNodeCount(fEntityScanner.fCurrentEntity); fDocumentHandler.characters(getCharacterData(),null); break; case XMLStreamConstants.SPACE: //空格处理 break;
//...
case XMLStreamConstants.COMMENT :
//注释处理
fEntityScanner.checkNodeCount(fEntityScanner.fCurrentEntity); fDocumentHandler.comment(getCharacterData(),null); break; case XMLStreamConstants.DTD : //DTD约束 break;
//...
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);
//...
}
· 解析document树
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
//解析XML文件生成document树
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//解析document树,生成Configuration对象
return build(parser.parse());
//...
}
public Configuration parse() {
//...
//1、parser.evalNode,将document树解析成XNode结点对象
//2、解析XNode为Configuration对象
parseConfiguration(parser.evalNode("/configuration")); return configuration;}
四、parseConfiguration
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
private void parseConfiguration(XNode root) {
//... environmentsElement(root.evalNode("environments"));
//...
}
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
private void parseConfiguration(XNode root) {
//... mapperElement(root.evalNode("mappers"));
//...
}
public <T> void addMapper(Class<T> type) {mapperRegistry.addMapper(type);}
public <T> void addMapper(Class<T> type) {
//mapper必须为接口
if (type.isInterface()) {
//...knownMappers.put(type, new MapperProxyFactory<T>(type));
//...
}
}
从addMapper中可以看出,所有的mapper都会被放到mapperRegistry中key-mapper接口,value-动态代理工厂,并且mapper必须为接口类型。
因此在Mybatis官方文档示例中的session.getMapper就相当于从mapperRegistry这个map中取出来对应的mapper接口。
但是此时只解析的mapper接口,而XML中的增删改查的方法还没有被解析出来。
1、根据mapper接口文件获取文件路径名。
public <T> void addMapper(Class<T> type) {
//... MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse();
//...
}
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
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();}
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();
} }}
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的核心方法:
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方法进行加载。
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对象进行赋值。完成最后使命。
public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}
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的作用域和线程相关。