1. 程式人生 > >mybatis原始碼學習(一) 原生mybatis原始碼學習

mybatis原始碼學習(一) 原生mybatis原始碼學習

最近這一週,主要在學習mybatis相關的原始碼,所以記錄一下吧,算是一點學習心得

個人覺得,mybatis的原始碼,大致可以分為兩部分,一是原生的mybatis,二是和spring整合之後的mybatis原始碼學習(也就是mybatis-spring這個jar包的相關原始碼),這邊筆記,主要來學習原生mybatis;

還是先用描述一下,原生mybatis從解析xml到執行SQL的一個流程:

  1.第一步:首先會通過sqlSessionFactoryBuilder來build一個SQLSessionFactory,在build的過程中,會對mybatis的配置檔案、mapper.xml檔案進行解析,

     1.1將mapper.xml中對應的select/update/delete/insert節點,包裝成mappedStatement物件,然後把這個類物件,存方到了一個map中(mappedStatements),key值是namespace的value+select節點的id,value就是mappedStatement;

     1.2然後通過反射,根據當前namespace,獲取到一個class,將class存到了另外一個map中,knownMappers中,key值是通過反射生成的class物件,value是根據class,生成的MapperProxyFactory物件,這兩個map,在執行查詢的時候,有用到

 

 2.根據sqlSessionFactoryBuilder生成SqlSessionFactory,再根據sqlSesionFactory,建立sqlSession,這時候,就可以呼叫slqSession的selectOne();selectList()來執行查詢,或者通過sqlSession.getMapper()來獲取到介面的代理物件,在執行增刪改查sql

 

下面,我們根據上述的步驟來解析mybatis原始碼,主要來說如何解析mybatis配置檔案

 1 String resource = "mybatis-config.xml";
 2 InputStream inputStream = Resources.getResourceAsStream(resource);
 3 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 4 SqlSession sqlSession = sqlSessionFactory.openSession();
 5 //        List<OperChannel> list = sqlSession.selectList("com.springsource.study.mybatis.OperChannelMapper.selectAll");
 6 //        OperChannel operChannel = sqlSession.selectOne("com.springsource.study.mybatis.OperChannelMapper.selectAll",1);
 7 //        System.out.println(operChannel.toString());
 8 
 9 System.out.println("+++++++++++++++++");
10 OperChannelMapper operChannelMapper = sqlSession.getMapper(OperChannelMapper.class);
11 System.out.println(operChannelMapper.selectAll(1));

這是我自己寫的一個測試類,首先我們從sqlSessionFactoryBuilder().buidl(inputStream)說起:

 1.首先會獲取到一個XMLConfigBuilder(),然後呼叫XMLConfigBuilder的parse()方法來解析mybatis的配置檔案;需要知道的是,對mybatis配置檔案的解析,最後會存放到一個configuration.class中,可以簡單理解為和配置檔案對應的一個類,在後面,生成的environment、mappedStatements都是configuration的一個屬性

 

 1 public Configuration parse() {
 2   //在new XMLConfigBuilder的時候,預設置為了false,表示當前xml只能被解析一次
 3 if (parsed) {
 4   throw new BuilderException("Each XMLConfigBuilder can only be used once.");
 5 }
 6   parsed = true;
 7   //將配置檔案解析成了configuration物件,在該方法中完成
 8   parseConfiguration(parser.evalNode("/configuration"));
 9   return configuration;
10 }
11 
12 /**
13 * @param root
14 * 原生mybatis在執行的時候,解析mybatis配置檔案
15 *  這裡解析的節點,都是配置檔案中配置的xml資訊
16 */
17 private void parseConfiguration(XNode root) {
18 try {
19   //issue #117 read properties first
20   propertiesElement(root.evalNode("properties"));
21   Properties settings = settingsAsProperties(root.evalNode("settings"));
22   loadCustomVfs(settings);
23   loadCustomLogImpl(settings);
24   typeAliasesElement(root.evalNode("typeAliases"));
25   pluginElement(root.evalNode("plugins"));
26   objectFactoryElement(root.evalNode("objectFactory"));
27   objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
28   reflectorFactoryElement(root.evalNode("reflectorFactory"));
29   settingsElement(settings);
30   // read it after objectFactory and objectWrapperFactory issue #631
31   //在這裡解析environment資訊,獲取到資料來源
32   environmentsElement(root.evalNode("environments"));
33   databaseIdProviderElement(root.evalNode("databaseIdProvider"));
34   typeHandlerElement(root.evalNode("typeHandlers"));
35   //解析mapper配置資訊,將SQL封裝成mapperstatement
36   mapperElement(root.evalNode("mappers"));
37 } catch (Exception e) {
38   throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
39   }
40 }

在parseConfiguration裡面,主要是對配置檔案中各個節點屬性來進行解析;我們著重來說mapper節點的解析,其中對environment的解析也提一下吧,在獲取資料來源的時候,會先獲取到<DataSource>節點中type,根據type建立一個DataSourceFactory,然後把DataSource節點中配置的property屬性的value和name,存到properties中,然後從dataSourceFactory中獲取到一個數據源;

我們來說對mappers的解析:

 在mybatis配置檔案中,對mapper.xml的配置方式有四種,分別是package、resource、URL、mapperClass,這四種優先順序就是寫的前後順序,因為在原始碼中,是按照這四個順序來進行解析的

 解析的mappers節點之後,會對節點中的每個節點進行遍歷,我們本次,只以resource為例:

  

 1 private void mapperElement(XNode parent) throws Exception {
 2 if (parent != null) {
 3   for (XNode child : parent.getChildren()) {
 4     //在配置檔案中,配置mapper.xml檔案有四種方式,這裡按照優先順序記進行解析
 5     if ("package".equals(child.getName())) {
 6       String mapperPackage = child.getStringAttribute("name");
 7       configuration.addMappers(mapperPackage);
 8     } else {
 9       String resource = child.getStringAttribute("resource");
10       String url = child.getStringAttribute("url");
11       String mapperClass = child.getStringAttribute("class");
12       if (resource != null && url == null && mapperClass == null) {
13         ErrorContext.instance().resource(resource);
14         InputStream inputStream = Resources.getResourceAsStream(resource);
15         //例項化一個mapper解析器
16         XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
17         //解析SQL語句
18         mapperParser.parse();
19       } else if (resource == null && url != null && mapperClass == null) {
20         ErrorContext.instance().resource(url);
21         InputStream inputStream = Resources.getUrlAsStream(url);
22         XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
23         mapperParser.parse();
24       } else if (resource == null && url == null && mapperClass != null) {
25         Class<?> mapperInterface = Resources.classForName(mapperClass);
26         configuration.addMapper(mapperInterface);
27       } else {
28         //配置檔案的配置只能是這四種,否則會報錯
29         throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
30       }
31     }
32   }
33 }
34 }

 

 在mapperParse.parse()方法中,會對<mapper>節點進行解析,然後獲取到mapper節點對應xml檔案中的所有屬性

 1 private void configurationElement(XNode context) {
 2     try {
 3       String namespace = context.getStringAttribute("namespace");
 4       if (namespace == null || namespace.equals("")) {
 5         throw new BuilderException("Mapper's namespace cannot be empty");
 6       }
 7       builderAssistant.setCurrentNamespace(namespace);
 8       cacheRefElement(context.evalNode("cache-ref"));
 9       cacheElement(context.evalNode("cache"));
10       parameterMapElement(context.evalNodes("/mapper/parameterMap"));
11       resultMapElements(context.evalNodes("/mapper/resultMap"));
12       sqlElement(context.evalNodes("/mapper/sql"));
13       //這裡是解析增刪改查語句的
14       buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
15     } catch (Exception e) {
16       throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
17     }
18   }

這個方法就是對mapper.xml檔案中各個節點的解析,其中,比較重要的是對增刪改查節點的解析:

 會獲取到當前mapper中所有的增刪改查節點,然後再遍歷,遍歷的時候,會獲取到每個節點的所有引數資訊,由於這個解析具體引數的篇幅比較長,就不貼上了,org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode 在這個方法中;

 其中需要提的是,以select為例,解析到select節點的所有引數和引數值之後,會把所以引數build成一個mappedStatement物件,然後存放到mappedStatements這個map中,key值就是namespace+id(這裡的ID就是select/update...配置的id屬性,一般配置成方法名);

 

把所有的增刪改查存到mappedStatements之後,會進行另外一個操作,就是根據namespace,通過反射,建立一個Class,物件,然後把class物件存到另外一個map中,knownMappers

 1 private void bindMapperForNamespace() {
 2     String namespace = builderAssistant.getCurrentNamespace();
 3     if (namespace != null) {
 4       Class<?> boundType = null;
 5       try {
 6         boundType = Resources.classForName(namespace);
 7       } catch (ClassNotFoundException e) {
 8         //ignore, bound type is not required
 9       }
10       if (boundType != null) {
11         if (!configuration.hasMapper(boundType)) {
12           // Spring may not know the real resource name so we set a flag
13           // to prevent loading again this resource from the mapper interface
14           // look at MapperAnnotationBuilder#loadXmlResource
15           configuration.addLoadedResource("namespace:" + namespace);
16           configuration.addMapper(boundType);
17         }
18       }
19     }
20   }

put方法就在最後一行程式碼,configuration.addMapper(boundType);

這裡也比較簡單,就是new MapperProxyFactory()作為value,class作為key,然後存到map中,後面會用到


 

截止到這裡,對配置檔案,SQL的解析就完成了,下面我們來說如何執行SQL

  1.List<OperChannel> list = sqlSession.selectList("com.springsource.study.mybatis.OperChannelMapper.selectAll");

  我們先說這種方法來請求SQL,這裡就是呼叫DefaultSqlSession 的selectList()方法,

   首先會根據如此那終端額全類名+方法名,從mappedStatements這個map中獲取到mappedStatement物件,這也就是為什麼namespace一定要和介面的包名+類名一直的原因

   獲取到mappedStatement物件之後,就是先查詢快取,快取中沒有就從資料庫查詢,資料庫查到之後,存到一級快取中,這裡的大致的原理是這樣的,後面,會單獨寫一遍筆記。來記錄,如何具體執行SQL的

 

 2.OperChannelMapper operChannelMapper = sqlSession.getMapper(OperChannelMapper.class);

    System.out.println(operChannelMapper.selectAll(1));

    這是第二種方式,和第一種方式稍微有些不同

    這裡的入參是mapperInterface.class,會根據入參,從上面提到的knownMappers中獲取到mapperProxyFactory物件,然後再呼叫mapperProxyFactory.newInstance()方法來生成代理物件

1 protected T newInstance(MapperProxy<T> mapperProxy) {
2         return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
3     }
4 
5     public T newInstance(SqlSession sqlSession) {
6         MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
7         return this.newInstance(mapperProxy);
8     }

在使用jdk代理生成代理物件時,需要傳三個入參,這裡重要是mapperproxy,就是實現了invocationHandler介面的實現類,生成了介面的代理物件之後,

在執行selectAll()的時候,由於這時候,是代理物件,所以會執行mapperproxy的invoke方法,

 

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}

if (this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}

MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}

在這裡,mapperMethod.execute()方法中,會判斷當前SQL是select?update?delete?等,然後呼叫DefaultSqlSession對應的方法;
也就是說,selSession.getMapper()這種方式其實是通過代理物件來執行SQL的;

截止到這裡,基本上就是原生mybatis解析、執行的邏輯,後面有新的認識,會繼續更