mybatis的源碼有人已經做過一個中文的注釋,代碼github上有mybatis中文注釋源碼
mybatis架構有兩個非常重要的xml檔案,一個是mybatis的config檔案,一個就是mapper檔案,mybatis會根據config的xml檔案去生成一個Configuration類,在這個過程中也會根據配置的mapper檔案生成MappedStatement,這篇部落格探究的就是這樣一個過程,往下看
如果單單使用mybatis,我們的做法是導包,配置,然後如下
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}
是以從SqlSessionFactoryBuilder().build說起,點選進入build方法,建立了一個XMLConfigBuilder,然後build(parser.parse()),
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
先看parser.parse()方法,這方法中将之前的mybatis的xml檔案進行解析,生成了Configration類傳回,
//解析配置
private void parseConfiguration(XNode root) {
try {
//分步驟解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.類型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.對象工廠
objectFactoryElement(root.evalNode("objectFactory"));
//5.對象包裝工廠
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.設定
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.環境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.類型處理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
仔細分析這幾行代碼,首先看第一個properties解析
//1.properties
//<properties resource="org/mybatis/example/config.properties">
// <property name="username" value="dev_user"/>
// <property name="password" value="F2Fa3!33TYyg"/>
//</properties>
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
//如果在這些地方,屬性多于一個的話,MyBatis 按照如下的順序加載它們:
//1.在 properties 元素體内指定的屬性首先被讀取。
//2.從類路徑下資源或 properties 元素的 url 屬性中加載的屬性第二被讀取,它會覆寫已經存在的完全一樣的屬性。
//3.作為方法參數傳遞的屬性最後被讀取, 它也會覆寫任一已經存在的完全一樣的屬性,這些屬性可能是從 properties 元素體内和資源/url 屬性中加載的。
//傳入方式是調用構造函數時傳入,public XMLConfigBuilder(Reader reader, String environment, Properties props)
//1.XNode.getChildrenAsProperties函數友善得到孩子所有Properties
Properties defaults = context.getChildrenAsProperties();
//2.然後查找resource或者url,加入前面的Properties
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("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.");
}
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//3.Variables也全部加入Properties
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
具體的xml解析過程就沒必要詳細看了,最後可以看到所有的properties都被存入了Configuration的variables變量中,
然後往下看類型别名的解析,關于别名,首先Configuration類中定義了一個TypeAliasRegistry
//類型别名注冊機
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
這個TypeAliasRegistry中有一個Map存放了别名和别名的類
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
是以typeAliasesElement(root.evalNode("typeAliases"))這個方法中的操作就是解析出别名放入這個map中,定義别名的兩種方式具體可以看官網。
再往下看,插件的解析
//3.插件
//MyBatis 允許你在某一點攔截已映射語句執行的調用。預設情況下,MyBatis 允許使用插件來攔截方法調用
//<plugins>
// <plugin interceptor="org.mybatis.example.ExamplePlugin">
// <property name="someProperty" value="100"/>
// </plugin>
//</plugins>
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
//調用InterceptorChain.addInterceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
插件雖然比較複雜,但是解析的部分卻很簡單,主要是resolveClass方法
//根據别名解析Class,其實是去檢視 類型别名注冊/事務管理器别名
protected Class<?> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
return resolveAlias(alias);
} catch (Exception e) {
throw new BuilderException("Error resolving class. Cause: " + e, e);
}
}
這個别名的解析過程其實就是去之前說的那個别名的map中查詢,有的話就傳回,沒的話就直接轉成Class,是以mybatis裡面很多配置屬性type="xxx"的,例如datasource的type="POOLED",這個POOLED其實就是類型的别名。最後擷取到Class之後newInstance建立一個對象,放入Interceptor攔截器鍊中,這個攔截器鍊和SpringMvc類似,其實就是一個攔截器鍊對象InterceptorChain裡面放了一個List集合,調用的時候for循環依次調用,去看看代碼
protected final InterceptorChain interceptorChain = new InterceptorChain();
Configuration類中定義了這樣一個過濾器鍊,後面某個地方肯定會執行pluginAll方法
public Object pluginAll(Object target) {
//循環調用每個Interceptor.plugin方法
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
這地方用過插件就很熟悉了,plugin方法中我們基本都這樣寫,而這個方法就是建立了一個代理對象
return Plugin.wrap(target, this);
public static Object wrap(Object target, Interceptor interceptor) {
//取得簽名Map
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//取得要改變行為的類(ParameterHandler|ResultSetHandler|StatementHandler|Executor)
Class<?> type = target.getClass();
//取得接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//産生代理
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
先看擷取簽名getSignatureMap這個方法
//取得簽名Map
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//取Intercepts注解,例子可參見ExamplePlugin.java
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
//必須得有Intercepts注解,沒有報錯
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//value是數組型,Signature的數組
Signature[] sigs = interceptsAnnotation.value();
//每個class裡有多個Method需要被攔截,是以這麼定義
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
這裡從我們注釋在攔截器插件的類注解Intercepts 上擷取Signature數組,循環數組,解析結果放入signatureMap中,signatureMap是一個Class為鍵,Method的Set清單為Value的Map,說白了這個解析結果就是一個對象中需要攔截的哪幾個方法。
再回頭往下看,
很熟悉的動态代理方法,因為傳入的InvocationHandler也是Plugin這個類,是以invoke方法也在這個類中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//看看如何攔截
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//看哪些方法需要攔截
if (methods != null && methods.contains(method)) {
//調用Interceptor.intercept,也即插入了我們自己的邏輯
return interceptor.intercept(new Invocation(target, method, args));
}
//最後還是執行原來邏輯
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
分析一下這段代碼,這就是從剛才解析的需要攔截的方法的Map中取出該類的攔截清單方法,看看是不是包括目前的方法,是的話就執行intercept也就是我們寫的那些攔截方法。再最後執行方法本身的邏輯。标準老套娃!
再回到XMLConfigBuilder中,接着往下
//4.對象工廠
objectFactoryElement(root.evalNode("objectFactory"));
這個就是解析出一個類方法放到Configuration的objectFactory中,覆寫它預設的對象工廠
然後是解析對象包裝工廠,反射器工廠,settings,environments等等原理和之前都差不多,是以跳過,
看重點最後一個mapperElement方法
//10.映射器
// 10.1使用類路徑
// <mappers>
// <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
// <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
// <mapper resource="org/mybatis/builder/PostMapper.xml"/>
// </mappers>
//
// 10.2使用絕對url路徑
// <mappers>
// <mapper url="file:///var/mappers/AuthorMapper.xml"/>
// <mapper url="file:///var/mappers/BlogMapper.xml"/>
// <mapper url="file:///var/mappers/PostMapper.xml"/>
// </mappers>
//
// 10.3使用java類名
// <mappers>
// <mapper class="org.mybatis.builder.AuthorMapper"/>
// <mapper class="org.mybatis.builder.BlogMapper"/>
// <mapper class="org.mybatis.builder.PostMapper"/>
// </mappers>
//
// 10.4自動掃描包下所有映射器
// <mappers>
// <package name="org.mybatis.builder"/>
// </mappers>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自動掃描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//10.1使用類路徑
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
//映射器比較複雜,調用XMLMapperBuilder
//注意在for循環裡每個mapper都重新new一個XMLMapperBuilder,來解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
//10.2使用絕對url路徑
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
//映射器比較複雜,調用XMLMapperBuilder
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
//10.3使用java類名
Class<?> mapperInterface = Resources.classForName(mapperClass);
//直接把這個映射加入配置
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
直接看package的解析,其實 <mapper xxx/>這種形式解析過程也類似,關鍵都是調用了configuration.addMapper這個方法,是以直接看這個方法,這個方法在Configuration類的mapperRegistry中
//看一下如何添加一個映射
public <T> void addMapper(Class<T> type) {
//mapper必須是接口!才會添加
if (type.isInterface()) {
if (hasMapper(type)) {
//如果重複添加了,報錯
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
//如果加載過程中出現異常需要再将這個mapper從mybatis中删除,這種方式比較醜陋吧,難道是不得已而為之?
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
重點就是new MapperProxyFactory<T>(type),這裡将存入一個Mapper的代理工廠類。
再往下看,建立了一個MapperAnnotationBuilder,然後再看parse方法。
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
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();
}
首先configuration.isResourceLoaded會判斷是否加載了mapper的xml,很顯然,如果用package方式的,走到這一步,就隻是找到了接口,将代理工廠存入map中,并沒有去加載xml,是以會loadXmlResource()
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
這裡将接口全面的.替換成了/,是以假如接口是a.test,那xml就一定得是a/test.xml,然後會建立一個XMLMapperBuilder,這裡可以回去mapperElement方法中看<mapper resource="xxx"/>的解析,也是通過XMLMapperBuilder,是以這些解析方式其實大同小異,然後再看XMLMapperBuilder的parse方法
//解析
public void parse() {
//如果沒有加載過再加載,防止重複加載
if (!configuration.isResourceLoaded(resource)) {
//配置mapper
configurationElement(parser.evalNode("/mapper"));
//标記一下,已經加載過了
configuration.addLoadedResource(resource);
//綁定映射器到namespace
bindMapperForNamespace();
}
//還有沒解析完的東東這裡接着解析?
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
先看configurationElement方法
//配置mapper元素
// <mapper namespace="org.mybatis.example.BlogMapper">
// <select id="selectBlog" parameterType="int" resultType="Blog">
// select * from Blog where id = #{id}
// </select>
// </mapper>
private void configurationElement(XNode context) {
try {
//1.配置namespace
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//2.配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
//3.配置cache
cacheElement(context.evalNode("cache"));
//4.配置parameterMap(已經廢棄,老式風格的參數映射)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//5.配置resultMap(進階功能)
resultMapElements(context.evalNodes("/mapper/resultMap"));
//6.配置sql(定義可重用的 SQL 代碼段)
sqlElement(context.evalNodes("/mapper/sql"));
//7.配置select|insert|update|delete TODO
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
首先看cache-ref的解析
//2.配置cache-ref,在這樣的 情況下你可以使用 cache-ref 元素來引用另外一個緩存。
//<cache-ref namespace="com.someone.application.data.SomeMapper"/>
private void cacheRefElement(XNode context) {
if (context != null) {
//增加cache-ref
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
先往configuration中存放cache-ref的map中添加目前解析的cache-ref的namespace,然後建立一個cache-ref解析器解析,
public Cache resolveCacheRef() {
//反調MapperBuilderAssistant解析
return assistant.useCacheRef(cacheRefNamespace);
}
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
這裡調用的是MapperBuilderAssistant這個助手的方法,而在這個助手類中,邏輯是這樣的,去configuration的cache的map中擷取cache,如果cache已經建立了,就傳回。如果還沒有建立,那麼就抛出一個IncompleteElementException異常,異常被外部捕獲,将目前cache-ref的解析器放入一個用來存放未完成cache-ref解析的清單中。
然後接下來解析cache,
//3.配置cache
cacheElement(context.evalNode("cache"));
方法中依舊是調用助手類的方法
//調用builderAssistant.useNewCache
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
接下來的幾個resultmap,sql等解析的過程基本類似。
目前解析完成之後,再往下看,會去解析之前未完全解析的各類對象,進入第一個方法
private void parsePendingResultMaps() {
Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
synchronized (incompleteResultMaps) {
Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
while (iter.hasNext()) {
try {
iter.next().resolve();
iter.remove();
} catch (IncompleteElementException e) {
// ResultMap is still missing a resource...
}
}
}
}
之前存入map中的未完全解析的解析器取出循環調用之前同樣的方法,而在此刻,之前需要等待建立的對象現在都已經建立完成,是以可以完成建立(我想了一下,這裡面好像沒有a需要b,b需要c的這種,被依賴的好像都是沒有需要依賴的)。
再回到MapperAnnotationBuilder中,接下去是方法的注解解析,和之前xml的差別就是解析的方法,跳過。
最終SqlSessionFactoryBuilder會執行到這行代碼,生成一個DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
到此解析結束。