Mybatis源码01-SpringBoot启动时mybatis加载过程
使用了mybatis这么久还没有具体探究了SpringBoot启动时候对于mybatis是怎么加载的。
1、首先项目构建时我们会引入相关的依赖:
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.1</version></dependency><!--MyBatis依赖--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.1</version></dependency><!--mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.9</version></dependency>
mybatis-spring-boot-starter这个包声明了自动构建MyBatis的一些配置,以及创建SqlSessionFactory的类
1 MybatisAutoConfiguration
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean factory = new SqlSessionFactoryBean();// 设置数据源factory.setDataSource(dataSource);factory.setVfs(SpringBootVFS.class);// 加载 MyBatis 全局配置文件 例:如果配置了 mybatis.config-location(如 classpath:mybatis-config.xml),则加载该全局配置文件。if (StringUtils.hasText(this.properties.getConfigLocation())) {factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));}// 将 application.yml 中的 MyBatis 配置(以 mybatis.configuration.* 开头的属性)应用到 SqlSessionFactoryapplyConfiguration(factory);if (this.properties.getConfigurationProperties() != null) {factory.setConfigurationProperties(this.properties.getConfigurationProperties());}// 注册 MyBatis 插件 例如分页插件if (!ObjectUtils.isEmpty(this.interceptors)) {factory.setPlugins(this.interceptors);}// 多数据库支持if (this.databaseIdProvider != null) {factory.setDatabaseIdProvider(this.databaseIdProvider);}// 设置类型别名扫描包if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());}// 设置类型别名的父类if (this.properties.getTypeAliasesSuperType() != null) {factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());}// 设置类型处理器扫描包if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());}// 注册自定义类型处理器if (!ObjectUtils.isEmpty(this.typeHandlers)) {factory.setTypeHandlers(this.typeHandlers);}// 加载sql mapper.xml文件路径 路径通过 mybatis.mapper-locations 配置。if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {factory.setMapperLocations(this.properties.resolveMapperLocations());}// 动态 SQL 语言驱动Set<String> factoryPropertyNames = Stream.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName).collect(Collectors.toSet());Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {// Need to mybatis-spring 2.0.2+factory.setScriptingLanguageDrivers(this.languageDrivers);if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {defaultLanguageDriver = this.languageDrivers[0].getClass();}}if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {// Need to mybatis-spring 2.0.2+factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);}// 调用 SqlSessionFactoryBean 的 getObject() 方法,生成最终的 SqlSessionFactory 实例。return factory.getObject();}
作用
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">sqlSessionFactory</font>**
方法的主要目的是 自动构建并配置 MyBatis 的**** **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">SqlSessionFactory</font>**
****实例,使得开发者无需手动编写大量 XML 或 Java 配置即可集成 MyBatis。该方法通过以下步骤实现:
- 依赖注入关键组件
利用 Spring Boot 的自动装配机制,注入必要的依赖(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">DataSource</font>**
、MyBatis 配置属性等)。 - 应用 MyBatis 全局配置
加载**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">mybatis-config.xml</font>**
(如果存在)或通过属性配置 MyBatis 的行为(如缓存、插件等)。 - 配置 Mapper 文件位置
扫描并注册 Mapper 接口或 XML 文件,使其能被 MyBatis 识别。 - 整合 Spring 事务管理
确保**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">SqlSessionFactory</font>**
与 Spring 的事务管理器兼容。
二、 SqlSessionFactoryBean
factory.getObject() 创建**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">sqlSessionFactory</font>**
最终在buildSqlSessionFactory这个方法完成创建。
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {final Configuration targetConfiguration;XMLConfigBuilder xmlConfigBuilder = null;if (this.configuration != null) {// 使用已经有的Configuration 对象targetConfiguration = this.configuration;// 合并全局变量(configurationProperties)if (targetConfiguration.getVariables() == null) {targetConfiguration.setVariables(this.configurationProperties);} else if (this.configurationProperties != null) {targetConfiguration.getVariables().putAll(this.configurationProperties);}} else if (this.configLocation != null) {// 通过 XML 配置文件初始化 ConfigurationxmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);targetConfiguration = xmlConfigBuilder.getConfiguration();} else {LOGGER.debug(() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");// 创建默认 Configuration 并设置全局变量targetConfiguration = new Configuration();Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);}/*** 设置核心扩展组件* ObjectFactory:控制对象(如 POJO)的实例化方式。* ObjectWrapperFactory:用于包装返回对象(如集合)。* VFS:虚拟文件系统实现(如处理 Spring Boot 内嵌 JAR 中的资源)*/Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);// 扫描包下的类注册别名if (hasLength(this.typeAliasesPackage)) {scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream().filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface()).filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);}// 显式注册类型别名if (!isEmpty(this.typeAliases)) {Stream.of(this.typeAliases).forEach(typeAlias -> {targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");});}// 注册插件if (!isEmpty(this.plugins)) {Stream.of(this.plugins).forEach(plugin -> {targetConfiguration.addInterceptor(plugin);LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");});}// 处理类型处理器if (hasLength(this.typeHandlersPackage)) {scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())).forEach(targetConfiguration.getTypeHandlerRegistry()::register);}if (!isEmpty(this.typeHandlers)) {Stream.of(this.typeHandlers).forEach(typeHandler -> {targetConfiguration.getTypeHandlerRegistry().register(typeHandler);LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");});}if (!isEmpty(this.scriptingLanguageDrivers)) {Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {targetConfiguration.getLanguageRegistry().register(languageDriver);LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'");});}Optional.ofNullable(this.defaultScriptingLanguageDriver).ifPresent(targetConfiguration::setDefaultScriptingLanguage);// 多数据库支持if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmlstry {targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));} catch (SQLException e) {throw new NestedIOException("Failed getting a databaseId", e);}}Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);// 解析 XML 配置文件if (xmlConfigBuilder != null) {try {xmlConfigBuilder.parse();LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");} catch (Exception ex) {throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);} finally {ErrorContext.instance().reset();}}// SpringManagedTransactionFactory 确保 MyBatis 事务由 Spring 管理。targetConfiguration.setEnvironment(new Environment(this.environment,this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory,this.dataSource));// 加载和解析 Mapper XML文件if (this.mapperLocations != null) {if (this.mapperLocations.length == 0) {LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");} else {for (Resource mapperLocation : this.mapperLocations) {if (mapperLocation == null) {continue;}try {XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());xmlMapperBuilder.parse();} catch (Exception e) {throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);} finally {ErrorContext.instance().reset();}LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");}}} else {LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");}// 构建 SqlSessionFactoryreturn this.sqlSessionFactoryBuilder.build(targetConfiguration);}
图例解析:
三、 XMLMapperBuilder
XMLMapperBuilder继承于BaseBuilder。他们对于XML文件本身技术上的加载和解析都委托给了XPathParser,最终用的是jdk自带的xml解析器而非第三方比如dom4j,底层使用了xpath方式进行节点解析。new XPathParser(reader, true, props, new XMLMapperEntityResolver())的参数含义分别是Reader,是否进行DTD 校验,属性配置,XML实体节点解析器。
entityResolver比较好理解,跟Spring的XML标签解析器一样,有默认的解析器,也有自定义的比如tx,dubbo等,主要使用了策略模式,在这里mybatis硬编码为了XMLMapperEntityResolver。
XMLMapperEntityResolver的定义如下
public class XMLMapperEntityResolver implements EntityResolver {private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";/** Converts a public DTD into a local one * 将公共的DTD转换为本地模式* * @param publicId The public id that is what comes after "PUBLIC"* @param systemId The system id that is what comes after the public id.* @return The InputSource for the DTD* * @throws org.xml.sax.SAXException If anything goes wrong*/@Overridepublic InputSource resolveEntity(String publicId, String systemId) throws SAXException {try {if (systemId != null) {String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);}}return null;} catch (Exception e) {throw new SAXException(e.toString());}}private InputSource getInputSource(String path, String publicId, String systemId) {InputSource source = null;if (path != null) {try {InputStream in = Resources.getResourceAsStream(path);source = new InputSource(in);source.setPublicId(publicId);source.setSystemId(systemId); } catch (IOException e) {// ignore, null is ok}}return source;}}
该类的核心作用是通过本地加载 DTD/XSD 验证文件,避免 XML 解析时从网络下载外部资源
继续往下看
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {commonConstructor(validation, variables, entityResolver);this.document = createDocument(new InputSource(reader));}private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {this.validation = validation;this.entityResolver = entityResolver;this.variables = variables;XPathFactory factory = XPathFactory.newInstance();this.xpath = factory.newXPath();}
主要看 createDocument
private Document createDocument(InputSource inputSource) {// important: this must only be called AFTER common constructortry {DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();factory.setValidating(validation);//设置由本工厂创建的解析器是否支持XML命名空间 TODO 什么是XML命名空间factory.setNamespaceAware(false);factory.setIgnoringComments(true);factory.setIgnoringElementContentWhitespace(false);//设置是否将CDATA节点转换为Text节点factory.setCoalescing(false);//设置是否展开实体引用节点,这里应该是sql片段引用的关键factory.setExpandEntityReferences(true);DocumentBuilder builder = factory.newDocumentBuilder();//设置解析mybatis xml文档节点的解析器,也就是上面的XMLMapperEntityResolverbuilder.setEntityResolver(entityResolver);builder.setErrorHandler(new ErrorHandler() {@Overridepublic void error(SAXParseException exception) throws SAXException {throw exception;}@Overridepublic void fatalError(SAXParseException exception) throws SAXException {throw exception;}@Overridepublic void warning(SAXParseException exception) throws SAXException {}});return builder.parse(inputSource);} catch (Exception e) {throw new BuilderException("Error creating document instance. Cause: " + e, e);}}
主要是根据mybatis自身需要创建一个文档解析器,然后调用parse将输入input source解析为DOM XML文档并返回。
得到XPathParser实例之后,就调用另一个使用XPathParser作为配置来源的重载构造函数了,如下:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {super(new Configuration());ErrorContext.instance().resource("SQL Mapper Configuration");this.configuration.setVariables(props);this.parsed = false;this.environment = environment;this.parser = parser;}
其中调用了父类BaseBuilder的构造器(主要是设置类型别名注册器,以及类型处理器注册器):
XMLConfigBuilder创建完成之后,SqlSessionFactoryBean调用xmlMapperBuilder.parse();创建Configuration。所有,真正Configuration构建逻辑就在xmlMapperBuilder.parse();里面,如下所示:
public void parse() {if (!configuration.isResourceLoaded(resource)) {configurationElement(parser.evalNode("/mapper"));configuration.addLoadedResource(resource);bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();}
1. 检查资源是否已加载 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">configuration.isResourceLoaded(resource)</font>**
)
- 作用:
确保同一 Mapper XML 文件不会被重复解析(如多线程环境或错误配置时)。 - 实现原理:
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">configuration</font>**
对象维护了一个**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">Set<String> loadedResources</font>**
,存储所有已加载的 XML 文件路径(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">com/example/UserMapper.xml</font>**
)。
2. 解析**** **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><mapper></font>**
****根标签 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">configurationElement(...)</font>**
)
- 作用:
解析 XML 文件中**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><mapper></font>**
标签下的所有配置元素,包括:**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><cache></font>**
:二级缓存配置**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><resultMap></font>**
:结果集映射**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><sql></font>**
:可重用的 SQL 片段**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><select|insert|update|delete></font>**
:SQL 语句定义
实现逻辑:
- java复制下载
private void configurationElement(XNode context) {// 获取 namespace 属性(必须对应 Mapper 接口的全限定名)String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.isEmpty()) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 解析缓存配置cacheRefElement(context.evalNode("cache-ref"));cacheElement(context.evalNode("cache"));// 解析所有 <resultMap>resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析所有 <sql> 片段sqlElement(context.evalNodes("/mapper/sql"));// 解析所有 SQL 语句(select|insert|update|delete)buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
3. 标记资源为已加载 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">addLoadedResource</font>**
)
- 目的:
将当前 XML 文件路径(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">com/example/UserMapper.xml</font>**
)添加到**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">loadedResources</font>**
集合,后续再次解析同一文件时会直接跳过。
4. 绑定 Mapper 接口 (**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">bindMapperForNamespace</font>**
)
- 作用:
将 XML 中**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">namespace</font>**
属性指定的接口(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">com.example.UserMapper</font>**
)注册到 MyBatis,使得接口方法与 XML 中的 SQL 语句关联。
实现逻辑:
- java复制下载
private void bindMapperForNamespace() {// 获取 namespace 对应的接口类Class<?> boundType = Resources.classForName(namespace);// 将接口添加到 MapperRegistryif (boundType != null && !configuration.hasMapper(boundType)) {configuration.addMapper(boundType);}
}
- 关键点:
MyBatis 通过动态代理为接口生成实现类,将方法调用映射到 XML 中的 SQL 语句。
5. 处理延迟解析的依赖项
- 背景:
某些配置项(如**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><resultMap></font>**
)可能依赖其他尚未解析的资源(如其他 XML 文件中的定义),需要延迟解析。 - 具体方法:
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">parsePendingResultMaps()</font>**
解析之前未能完全初始化的**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ResultMap</font>**
(例如,引用了其他**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">ResultMap</font>**
的情况)。**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">parsePendingCacheRefs()</font>**
处理**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);"><cache-ref></font>**
标签引用的其他命名空间的缓存配置。**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">parsePendingStatements()</font>**
完成所有 SQL 语句的最终解析(如确认引用的**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">resultMap</font>**
或**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">parameterType</font>**
已存在)。