簡介
Mybatis 是一個持久層框架,它對 JDBC 進行了高級封裝,使我們的代碼中不會出現任何的 JDBC 代碼,另外,它還通過 xml 或注解的方式將 sql 從 DAO/Repository 層中解耦出來,除了這些基本功能外,它還提供了動態 sql、延遲加載、緩存等功能。 相比 Hibernate,Mybatis 更面向數據庫,可以靈活地對 sql 語句進行優化。
前面已經說完 mybatis 的使用( Mybatis詳解系列(一)--持久層框架解決了什么及如何使用Mybatis ),現在開始分析源碼,和使用例子一樣,我用的 mybatis 是 3.5.4 版本的。考慮連貫性,我會按下面的順序來展開分析,計划兩篇博客寫完,本文只涉及第一點內容:
- 加載配置、初始化
SqlSessionFactory
; - 獲取
SqlSession
和Mapper
; - 執行
Mapper
方法。
這個過程基本符合下面的代碼的工作過程。
// 加載配置,初始化SqlSessionFactory對象
String resource = "Mybatis-config.xml";
InputStream in = Resources.getResourceAsStream(resource));
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
// 獲取 SqlSession 和 Mapper
SqlSession sqlSession = sqlSessionFactory.openSession();
EmployeeMapper baseMapper = sqlSession.getMapper(EmployeeMapper.class);
// 執行Mapper方法
Employee employee = baseMapper.selectByPrimaryKey(id);
// do something
注意,考慮可讀性,文中部分源碼經過刪減。
初始化的過程
這里簡單概括下初始化的整個流程,如下圖。

- 構建 xml 的“節點樹”。
XPathParser
使用的是 JDK 自帶的 JAXP API來解析並構建Document
對象,並且支持 XPath 功能。 - 初始化
Configuration
對象的成員屬性。XMLConfigBuilder
利用“節點樹”來構建Configuration
對象(也會去解析注解的配置),Configuration
對象包含了 configuration 文件和 mapper 文件的所有配置信息。這部分內容比較難,尤其是初始化 mapper 相關的配置。 - 創建
SqlSessionFactory
。SqlSessionFactoryBuilder
利用構建好的Configuration
對象來創建SqlSessionFactory
。
上面的過程只要進入到SqlSessionFactoryBuilder.build(InputStream)
方法就可以直觀的看到。
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
// 入參里我們可以指定使用哪個環境,還可以傳入properties來“覆蓋”xml中<properties>變量
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 1. 構建XMLConfigBuilder對象,這個過程會構建Document對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 2. 構建Configuration對象后,然后調用build(Configuration)
return build(parser.parse());
} catch(Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch(IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
// 3. 直接使用構造方法構建DefaultSqlSessionFactory對象
return new DefaultSqlSessionFactory(config);
}
接下來會具體分析第1和2點的代碼,第3點比較簡單,就不展開了。
構建xml節點樹
XMLConfigBuilder
使用XPathParser
來解析 xml 獲得“節點樹”,它本身會通過“節點樹”的配置信息來進行初始化操作。現在我們進入到XMLConfigBuilder
的構造方法:
private final XPathParser parser;
private String environment;
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
// 構建XPathParser對象,構建時去解析xml
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
// 這里只是初始化XMLConfigBuilder的幾個成員屬性
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// ······
}
XPathParser
的構造方法里將對 xml 進行解析,如下。點進 XPathParser.createDocument(InputSource)
方法就會發現 mybatis 使用的是 JAXP 的 API,這部分的內容就不在本文的討論范圍,感興趣可參考我的另一篇博客: 源碼詳解系列(三) ------ dom4j的使用和分析(重點對比和DOM、SAX的區別) 。
private final Document document;
private Properties variables;
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
// 初始化一列成員屬性,沒必要看
commonConstructor(validation, variables, entityResolver);
// 構建Document對象,使用的是JAXP的API
this.document = createDocument(new InputSource(reader));
}
這里補充說明下XMLMapperEntityResolver
這個類。它是EntityResolver
子類,xml 的解析會基於事件觸發對應的 Resolver 或 Handler,當解析到 dtd 等外部資源時會觸發EntityResolver
的resolveEntity
方法。在XMLMapperEntityResolver.resolveEntity
中,當解析到 mybatis-3-config.dtd、mybatis-3-mapper.dtd 等資源時,會直接從 classpath 下的 org/apache/ibatis/builder/xml/ 路徑獲取資源,而不需要通過 url 獲取。
注意,上面對構建的Document
對象,只是 configuration 文件的,並不包含 mapper 文件。
先認識下Configuration這個類
我們已經拿到了配置信息,接下來就是構建Configuration
對象了。
在此之前,我們先認識下Configurantion
這個類,如下圖。可以看到,這些成員屬性對應了 xml 文件中各個配置項,接下來講的就是如何初始化這些屬性。

進入到XMLConfigBuilder.parse()
方法,可以看到所有配置項的初始化順序。這里的XNode
類是 mybatis 對org.w3c.dom.Node
的包裝,為后續操作 xml 節點提供了更加簡便的接口。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 標記已經解析過
parsed = true;
// 通過Document對象構建configuration節點的XNode對象,並構建Configurantion對象
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// 以下初始化不同的配置項
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
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);
}
}
接下來會挑其中幾個配置項展開分析,而不會每個都講到,重點關注 typeHandlers 和 mapper 節點的配置。
properties
properties 是 xml 中使用的全局參數,可以在 xml 中顯式配置或引入外部 properties 文件,也可以在構建SqlSessionFactory
對象時通過方法入參傳入(比較少用),通過下面的代碼可以知道:
- properties節點的屬性 resource 和 url 只能配置一個,兩個都配置會報錯;
- 不同方式配置會覆蓋,優先級如下:方法入參方式 > xml 中引入外部 properties 文件方式 > xml 中顯示配置方式,優先級低的會被優先級高的覆蓋。
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 獲取xml里顯式配置的所有property
Properties defaults = context.getChildrenAsProperties();
// 獲取resource和url屬性值
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
// resource和url只能有一個
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
// 添加resource或url指定資源的properties,如果相同,就覆蓋
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 添加方法入參的properties,如果相同,就覆蓋
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 重新設置XPathParser對象和Configuration對象里的成員屬性,以備后面配置項使用
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
settings
setting 的初始化過程比較簡單,這里我們重點關注下MetaClass
這個類。
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 獲取settings子節點的配置信息
Properties props = context.getChildrenAsProperties();
// 判斷該配置項是否存在,不合法會拋錯
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
// 這里就是直接初始化屬性了
private void settingsElement(Properties props) {
// ······
}
通常情況下,如果要判斷一個配置參數是否存在,可能會在代碼中將參數集給寫死,但是 mybatis 沒有這么做,它提供了一個非常好用的工具類--MetaClass
。MetaClass
可以用來初始化某個類的參數集,例如Configuration
,並且提供了這些參數的Invoker
對象,通過它可以進行值的設置和獲取。這個類將在后續源碼分析中多次出現。
typeAliases
TypeAliasRegistry
,即別名注冊器,存放着 alias = Class 的鍵值對,這些別名僅限於在加載配置的時候使用。
我們可以通過兩種方式配置:package 和 typeAlias 的方式,而且這兩種方式可以共存。
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 遍歷typeAliases下的typeAlias或package節點
for (XNode child : parent.getChildren()) {
// 配置包的情況
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
// 使用包名注冊
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 配置具體類的情況
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// 加載指定類
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
// 如果沒有通過xml顯式設置別名,將讀取該類的Alias注解里的value值
// 如果沒有通過xml或注解顯式設置別名,將使用該Class對象的simpleName小寫作為別名
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
這里只看使用 package 注冊別名的情況,進入到TypeAliasRegistry.registerAliases(String)
方法。通過以下代碼可知,注冊別名時無法注冊接口或內部類。這里 mybatis 又提供了一個好用的工具類--ResolverUtil
,通過ResolverUtil
我們可以獲取到指定包路徑下的接口、注解或指定類的子類。
public void registerAliases(String packageName) {
// 查找指定包名下Object的子類,並注冊別名
registerAliases(packageName, Object.class);
}
public void registerAliases(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
// 查找指定包名下superType的子類
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for (Class<?> type : typeSet) {
// 跳過內部類和接口
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// 注冊指定類的別名
registerAlias(type);
}
}
}
接着進入TypeAliasRegistry.registerAlias(Class<?>)
。因為按 package 注冊別名的方式沒有在 xml 中指定別名,所以,這里會試圖從類的Alias
注解里獲取,如果沒有,默認使用該類的 simpleName。
public void registerAlias(Class<?> type) {
// 獲取指定類的simpleName
String alias = type.getSimpleName();
// 獲取指定類的Alias注解
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
// 如果不為空,設置別名為注解里的value
alias = aliasAnnotation.value();
}
// 注冊指定類的別名
registerAlias(alias, type);
}
最后進入TypeAliasRegistry.registerAlias(String, Class<?>)
方法,通過以下代碼可知,別名都會被轉化為小寫,而且,如果同一個別名注冊多個不同的類,會報錯。最終會以 alias=Class 的鍵值對存入TypeAliasRegistry
維護的 map中,供其他配置項使用。
// 存放着 alias=Class 的鍵值對
private final Map<String, Class<?>> typeAliases = new HashMap<>();
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// 取別名的小寫
String key = alias.toLowerCase(Locale.ENGLISH);
// 如果相同的別名或類已經注冊過,會拋錯
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
// 存入鍵值對
typeAliases.put(key, value);
}
plugins
插件/攔截器的初始化比較簡單,就簡單過一下吧。通過代碼可知,我們可以在 plugin 節點下增加 property節點。
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 獲取interceptor名
String interceptor = child.getStringAttribute("interceptor");
// 獲取interceptor的參數
Properties properties = child.getChildrenAsProperties();
// 實例化。注意,這里解析Class時會先從別名注冊器查,沒有才會用Class.forName的方式實例化
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 設置參數
interceptorInstance.setProperties(properties);
// 添加到configuration的interceptorChain
configuration.addInterceptor(interceptorInstance);
}
}
}
environments
這里的Environment
對象包含了兩個部分:事務工廠和數據源,並且使用 id 作為唯一標識。在下面的代碼中,事務工廠和數據源的實例化過程有點類似於插件的過程,這里就不展開了。
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
// 如果沒有指定環境,會使用default
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
// 判斷是否指定環境
if (isSpecifiedEnvironment(id)) {
// 根據配置的transactionManager創建TransactionFactory對象
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 根據配置的dataSource創建DataSourceFactory對象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
// 獲取數據源
DataSource dataSource = dsFactory.getDataSource();
// 根據id(環境名)、數據源和事務工廠構建並設置Environment對象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
typeHandlers*
配置TypeHandler的規則
TypeHandler
用於處理參數映射和結果集映射,一個TypeHandler
一般需要包含 javaType 和 jdbcType 兩個屬性來標識,如果某個 javaType 和數據庫的 jdbcType 關系為的 一對一或一對多,則可以不用設置 jdbcType。例如BooleanTypeHandler
、ByteTypeHandler
。
在分析源碼前,我們先來看看聲明 javaType 和 jdbcType 的幾種方式:
- xml 中聲明,如下
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler" javaType="String" jdbcType="VARCHAR"/>
</typeHandlers>
- 在注解中聲明,如下:
@MappedTypes(value = String.class)
@MappedJdbcTypes(value = JdbcType.VARCHAR)
public class ExampleTypeHandler implements TypeHandler<String> {
}
- 在泛型中聲明,如下。這種只能用來配置 javaType,而且,必須繼承
BaseTypeHandler
或TypeReference
才行。
public class BigDecimalTypeHandler extends BaseTypeHandler<BigDecimal> {
}
兼容的配置方式越多,代碼邏輯也會更復雜,如果 xml 中沒有顯式地配置 javaType 或 jdbcType,mybatis 會嘗試去推斷出來,只要明白這個邏輯,接下來的代碼就簡單很多了。
源碼分析
現在開始分析源碼吧。我們可以使用 package 和 typeHandler 的兩種配置方式,且它們可以共存。
private void typeHandlerElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 使用包名注冊的情況
if ("package".equals(child.getName())) {
String typeHandlerPackage = child.getStringAttribute("name");
typeHandlerRegistry.register(typeHandlerPackage);
} else {
//使用具體類名注冊的情況
String javaTypeName = child.getStringAttribute("javaType");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
// javaType不為空,jdbcType為空的情況
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
// javaType不為空,jdbcType不為空的情況
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
// javaType為空,jdbcType為空的情況
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
按 package 注冊類型處理器的方式有點像前面提到的按 package 注冊別名,都會先加載指定包里的類,這里就不展開了,直接看按類名注冊的情況(不指定 javaType 和 jdbcType),進入到TypeHandlerRegistry.register(Class<?>)
方法。這種情況下,mybatis 會先去推斷出該類型處理器對應的 javaType,方法如下:
- 通過 MappedTypes 注解的 value 來判斷;
- 通過泛型判斷,這種類型處理器需要繼承BaseTypeHandler,而不僅僅只是實現TypeHandler。(3.1.0之后才支持)
public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
// 獲取指定類型處理器的MappedTypes注解,里面的value就是該類型處理器處理的javaType
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
// 獲取MappedTypes注解的value,並遍歷
for (Class<?> javaTypeClass : mappedTypes.value()) {
// 根據javaType注冊類型處理器
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
// 如果沒有MappedTypes注解,mybatis 3.1.0之后會通過泛型推斷出javaType,但這種類型處理器需要繼承BaseTypeHandler,而不僅僅只是實現TypeHandler
if (!mappedTypeFound) {
register(getInstance(null, typeHandlerClass));
}
}
接下來就是推斷 jdbcType 了,這里會通過 MappedJdbcTypes 注解來確定(可配置多個 jdbcType),如果設置了includeNullJdbcType=true
,則會將 jdbcTyp 為 null 情況也注冊上去。如果沒有MappedJdbcTypes 注解,會直接將 jdbcTyp 為 null 情況也注冊上去。
public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
// 實例化類型處理器,並根據javaType注冊
register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
// 強轉javaType為Type類型
register((Type) javaType, typeHandler);
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 獲取類型處理器的MappedJdbcTypes注解,里面的value就是該類型處理器處理的jdbcType
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
// 獲取MappedJdbcTypes注解的value,並遍歷
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
// 根據javaType和jdbcType注冊類型處理器
register(javaType, handledJdbcType, typeHandler);
}
// 讀取MappedJdbcTypes注解的includeNullJdbcType,如果為true,則根據javaType注冊類型處理器
// 當includeNullJdbcType為true時,即使不指定jdbcType,該類型處理器也能被使用。從 Mybatis 3.4.0 開始,如果某個 Java 類型只有一個注冊的類型處理器,即使沒有設置 includeNullJdbcType=true,那么這個類型處理器也會是 ResultMap 使用 Java 類型時的默認處理器。
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
// 根據javaType注冊類型處理
register(javaType, null, typeHandler);
}
}
最后就是具體的注冊過程了。mybatis 進行參數或結果集映射時一般用到的是 typeHandlerMap,其他的成員屬性一般用於判斷是否有某種類型處理器。
// javaType=(jdbcType=typeHandler)
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
// class=typeHandler,這個沒什么用
private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>();
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
// 只有javaType非空時才會放入typeHandlerMap
if (javaType != null) {
// 從typeHandlerMap里獲取當前javaType的jdbcType=TypeHandler
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
// 如果這張表為空,則重置
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
// 放入當前需要注冊的jdbcType=TypeHandler,注意,相同的會被覆蓋掉
map.put(jdbcType, handler);
// 放入javaType=map
typeHandlerMap.put(javaType, map);
}
// allTypeHandlersMap放入了所有的handler,包括javaType為空的。
allTypeHandlersMap.put(handler.getClass(), handler);
}
mappers*
mapper 的節點對象
接下來就是初始化中最難的部分了。因為 mybatis 的 mapper 支持了非常多個語法,甚至還允許使用注解配置,所以,在對 mapper 的解析方面需要非常復雜的邏輯。我們先來看看 mapper 中的配置項,如下。
ResultMap的組成
接下來我只會寫 resultMap 節點的 xml 配置,其他的就不寫了。為了更好地理清代碼邏輯,我們先看看 resultMap 的幾種配置方式。
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int" />
</constructor>
<result property="title" column="blog_title" />
<association property="author" javaType="Author">
<id property="id" column="author_id" />
<result property="username" column="author_username" />
<result property="password" column="author_password" />
<result property="email" column="author_email" />
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id" />
<result property="subject" column="post_subject" />
<association property="author" javaType="Author" />
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id" />
</collection>
<collection property="tags" ofType="Tag">
<id property="id" column="tag_id" />
</collection>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultMap="resultMap01"/>
<case value="2" resultMap="resultMap02"/>
<case value="3" resultMap="resultMap03"/>
<case value="4" resultMap="resultMap04"/>
</discriminator>
</resultMap>
針對上面的配置,需要重點理解:
- 整個 resultMap 將作為
ResultMap
對象存在,並使用 id 作為唯一標識。除了 id="detailedBlogResultMap" 的ResultMap
對象,association 、collection 和 case 節點也會生成新的ResultMap
對象(如果不是配置 resultMap 和 select 屬性的話)。 - idArg、result、association 和 collection 節點都會被轉換為
ResultMapping
對象被ResultMap
對象持有,區別在於 association 和 collection 的ResultMapping
對象會持有 nestedResultMapId 來指向另外一個ResultMap
對象,持有 nestedQueryId 來指向另外一個MappedStatement
對象。 - discriminator 節點,將轉換為
Discriminator
對象被ResultMap
對象持有。
源碼分析
那么,開始看源碼吧。mapper 的配置支持下面兩種配置,兩者可以共存:
- mapper 節點配置。支持 resource、url 和 class 屬性,但這三個屬性只能配置一個,不然會報錯。
- package 節點配置。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 使用包配置的情況
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 使用mapper配置的情況
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// resource屬性不為空
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// url屬性不為空
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// class屬性不為空
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
// resource、url和class只能存在一個
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
使用 package 配置 mapper 的情況,會有加載包內類的過程,和前面的 typeAliases 差不多,所以這里選擇使用 mapper 配置(屬性為class)的情況,進入到Configuration.addMapper(Class<T>)
。在注冊 mapper 時,其實有兩個內容:
- 注冊 mapper 接口,初始化 mapperRegistry 里的 type=mapperProxyFactory 的map。
MapperProxyFactory
用於生成Mapper
的代理類,后面會講到。 - 解析 mapper 的 xml 文件和注解,初始化 mappedStatements、caches、resultMaps、parameterMaps 等屬性。
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> void addMapper(Class<T> type) {
// 只有是接口才行
if (type.isInterface()) {
// 該mapper是不是已經注冊
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 注冊該mapper接口
knownMappers.put(type, new MapperProxyFactory<>(type));
// 接下來解析mapper的xml和注解,不要被MapperAnnotationBuilder這個類名誤導,接下來不止會解析注解,也會解析xml
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
進入到MapperAnnotationBuilder.parse()
方法,這里先解析 xml 文件,再解析注解。接下來我們只看 xml 的,注解的就不看了。
// 存放已加載的資源
protected final Set<String> loadedResources = new HashSet<>();
public void parse() {
String resource = type.toString();
// 該資源未被加載才進入
if (!configuration.isResourceLoaded(resource)) {
// 加載xml
loadXmlResource();
// 標記已加載
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 接下來是解析注解
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 未解析完成,會放入對應的集合中,等待最后再解析
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 因為存在嵌套引用的問題,有些內容還沒解析完,這里會做最后的解析
parsePendingMethods();
}
進入到MapperAnnotationBuilder.loadXmlResource()
方法。這里的XMLMapperBuilder
用於解析 mapper 文件的配置,前面說到的XMLConfigBuilder
則是解析 configurantion 文件的配置,它們都是BaseBuilder
的子類。
private void loadXmlResource() {
// 該命名空間未被加載,才會進入
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 根據mapper獲取xml
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
// 和XMLConfigBuilder一樣,這里會解析xml並構建document
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
// 進入解析
xmlParser.parse();
}
}
}
進入到XMLMapperBuilder.parse()
。我們會發現,如果使用 resource 或 url 的方式來配置 mapper,那么 Mapper 接口的注冊會在這個方法里。
public void parse() {
// 該資源未加載才會進入
if (!configuration.isResourceLoaded(resource)) {
// 構建mapper節點的XNode對象,並解析
configurationElement(parser.evalNode("/mapper"));
// 標記已解析
configuration.addLoadedResource(resource);
// 注冊Mapper接口,其實這個注冊過了的
bindMapperForNamespace();
}
// 因為存在嵌套引用的問題,有的節點還沒初始化完成,這里繼續初始化
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// mapper文件的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"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
前面已經說過,我們只看 resultMap 的構建,進入到XMLMapperBuilder.resultMapElements(List<XNode>)
。
private void resultMapElements(List<XNode> list) {
// 我們可以配置多個resultMap,這里一個個遍歷
for (XNode resultMapNode : list) {
try {
// 解析resultMap節點
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
private ResultMap resultMapElement(XNode resultMapNode) {
return resultMapElement(resultMapNode, Collections.emptyList(), null);
}
// 注意,這個類傳入的resultMapNode不僅是resultMap節點,也可以是association、collection或case節點
// 如果是association、collection或case節點,enclosingType為當前resultMap節點的type,additionalResultMappings為所屬resultMap的ResultMappings
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 獲取當前的類名
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 獲取該類的Class對象。如果為空,針對association和case的情況會通過enclosingType來推斷
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
// 如果為constructor節點
if ("constructor".equals(resultChild.getName())) {
// 這里會將每個idArg或arg轉換為ResultMapping對象,並放入resultMappings
processConstructorElement(resultChild, typeClass, resultMappings);
// 如果為discriminator節點
} else if ("discriminator".equals(resultChild.getName())) {
// discriminator將轉換為Discriminator對象
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
// 這種就是的result、collection或association節點了
} else {
List<ResultFlag> flags = new ArrayList<>();
// 標記id
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// 將result、collection或association節點轉換為ResultMapping對象,並放入resultMappings,如果是collection或association節點,會指向生成的新的ResultMap對象或已有的ResultMap對象
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 獲取resultMap的id、extends和autoMapping屬性
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 創建ResultMapResolver對象
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
// 解析resultMap,這里所謂的解析,其實就是將extends的東西放入resultMappings
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
// 如果沒有解析完成,放入集合incompleteResultMaps,等待后面再解析
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
以上,mybatis 初始化的源碼基本已分析完,不足的地方歡迎指正。
相關源碼請移步:mybatis-demo
本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/12704076.html