又是搞砸Mybatis源碼的一天
1.上期回顧
前面初識(shí) mybatis 章節(jié),阿粉首先搭建了一個(gè)簡單的項(xiàng)目,只用了 mybatis 的 jar 包。然后通過一個(gè)測試代碼,講解了幾個(gè)重要的類和步驟。
先看下這個(gè)測試類:
- public class MybatisTest {
- @Test
- public void testSelect() throws IOException {
- String resource = "mybatis-config.xml";
- InputStream inputStream = Resources.getResourceAsStream(resource);
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- SqlSession session = sqlSessionFactory.openSession();
- try {
- FruitMapper mapper = session.getMapper(FruitMapper.class);
- Fruit fruit = mapper.findById(1L);
- System.out.println(fruit);
- } finally {
- session.close();
- }
- }
- }
這章的話,阿粉會(huì)帶著大家理解一下源碼,基于上面測試的代碼。阿粉先申明一下,源碼一章肯定是講不完的,所以阿粉會(huì)分成幾個(gè)章節(jié),并且講源碼的話,代碼肯定比較多,比較干,所以請大家事先準(zhǔn)備好開水哦。
2.源碼分析
這里阿粉會(huì)根據(jù) SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream) 這段代碼來分析 mybatis 做了哪些事情。
2.1設(shè)計(jì)模式
通過這段代碼,我們首先分析一下用到了哪些設(shè)計(jì)模式呢?
首先 SqlSessionFactory 這個(gè)類用到了工廠模式,并且上一章阿粉也說到了,這個(gè)類是全局唯一的,所以它還使用了單列模式。
然后是 SqlSessionFactoryBuilder 這個(gè)類,一看就知道用到了建造者模式。
所以,只看這段代碼就用到了工廠模式,單列模式和建造者模式。
2.2mybatis做了什么事情
這里就開始源碼之旅了,首先 ctrl + 左鍵點(diǎn)擊 build 方法,我們會(huì)看到
- public SqlSessionFactory build(InputStream inputStream) {
- return build(inputStream, null, null);
- }
再點(diǎn)擊 build ,然后就是
- public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
- try {
- XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
- return build(parser.parse());
- //后面代碼省略
- ...
- }
- }
首先 XMLConfigBuilder 這個(gè)類就是用來解析我們的配置文件 mybatis-config.xml ,調(diào)用這個(gè)類的構(gòu)造函數(shù),主要是創(chuàng)建一個(gè) Configuration 對象,這個(gè)對象的屬性就對應(yīng)mybatis-config.xml里面的一級標(biāo)簽。這里不貼代碼,有興趣的自己點(diǎn)開看下。
然后就是 build(parser.parse()) 這段代碼,先執(zhí)行 parse()方法,再執(zhí)行 build() 方法。不過這里,阿粉先說下 build() 這個(gè)方法,首先parse() 方法返回的就是 Configuration 對象,然后我們點(diǎn)擊 build
- public SqlSessionFactory build(Configuration config) {
- return new DefaultSqlSessionFactory(config);
- }
這里就是返回的默認(rèn)的SqlSessionFactory 。
然后我們再說 parse() 方法,因?yàn)檫@個(gè)是核心方法。我們點(diǎn)進(jìn)去看下。
- public Configuration parse() {
- if (parsed) {
- throw new BuilderException("Each XMLConfigBuilder can only be used once.");
- }
- parsed = true;
- parseConfiguration(parser.evalNode("/configuration"));
- return configuration;
- }
首先是 if 判斷,就是防止解析多次。看 parseConfiguration方法。
- private void parseConfiguration(XNode root) {
- try {
- //issue #117 read properties first
- 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);
- }
- }
這里就開始了解析mybatis-config.xml里面的一級標(biāo)簽了。阿粉不全部講,只說幾個(gè)主要的。
- settings:全局配置,比如我們的二級緩存,延遲加載,日志打印等
- mappers:解析dao層的接口和mapper.xml文件
- properties:這個(gè)一般是配置數(shù)據(jù)庫的信息,如:數(shù)據(jù)庫連接,用戶名和密碼等,不講
- typeAliases :參數(shù)和返回結(jié)果的實(shí)體類的別名,不講
- plugins:插件,如:分頁插件,不講
- objectFactory,objectWrapperFactory:實(shí)例化對象用的,比如返回結(jié)果是一個(gè)對象,就是通過這個(gè)工廠反射獲取的,不講
- environments:事物和數(shù)據(jù)源的配置,不講
- databaseIdProvider:這個(gè)是用來支持不同廠商的數(shù)據(jù)庫
- typeHandlers:這個(gè)是java類型和數(shù)據(jù)庫的類型做映射,比如數(shù)據(jù)庫的 varchar類型對應(yīng) java 的String 類型,不講
2.3解析settings
我們點(diǎn)擊 settingsElement(settings) 這個(gè)方法
- private void settingsElement(Properties props) {
- configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
- configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
- configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
- configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
- configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
- configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
- configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
- configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
- configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
- configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
- configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
- configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
- configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
- configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
- configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
- configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
- configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
- configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
- configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
- configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
- configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
- configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
- configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
- configuration.setLogPrefix(props.getProperty("logPrefix"));
- configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
- }
這個(gè)就是設(shè)置我們的全局配置信息, setXXX 方法的第一個(gè)參數(shù)是 mybatis-config.xml 里面標(biāo)簽里面配置的。有的同學(xué)就會(huì)問了,阿粉阿粉,我們里面沒有配置那么多啊,這里怎么設(shè)置那么多屬性,值是什么呢?那我們看下方法的第二個(gè)參數(shù),這個(gè)就是默認(rèn)值。比如 cacheEnabled 這個(gè)屬性,緩存開關(guān),沒有配置的話,默認(rèn)是開啟的 true。所有以后看全局配置的默認(rèn)值,不用去官網(wǎng)看了,直接在這個(gè)方法里面看。
2.4解析mappers
點(diǎn)擊 mapperElement 方法
- 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 {
- String resource = child.getStringAttribute("resource");
- String url = child.getStringAttribute("url");
- String mapperClass = child.getStringAttribute("class");
- if (resource != null && url == null && mapperClass == null) {
- 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) {
- 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<?> 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.");
- }
- }
- }
- }
- }
這里有4個(gè)判斷 page (包),resource(相對路徑,阿粉配的就是這個(gè),所有按照這個(gè)講解源碼),url(絕對路徑),class(單個(gè)接口)
XMLMapperBuilder 這個(gè)類是用來解析mapper.xml文件的。然后我們點(diǎn)擊 parse 方法。
- public void parse() {
- if (!configuration.isResourceLoaded(resource)) {
- configurationElement(parser.evalNode("/mapper"));
- configuration.addLoadedResource(resource);
- bindMapperForNamespace();
- }
- parsePendingResultMaps();
- parsePendingCacheRefs();
- parsePendingStatements();
- }
if 判斷是判斷 mapper 是不是已經(jīng)注冊了,單個(gè)Mapper重復(fù)注冊會(huì)拋出異常。
configurationElement:解析 mapper 里面所有子標(biāo)簽。
- private void configurationElement(XNode context) {
- try {
- 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);
- }
- }
cacheRefElement:緩存
cacheElement:是否開啟二級緩存
parameterMapElement:配置的參數(shù)映射
resultMapElements:配置的結(jié)果映射
sqlElement:公用sql配置
buildStatementFromContext:解析 select|insert|update|delete 標(biāo)簽,獲得 MappedStatement 對象,一個(gè)標(biāo)簽一個(gè)對象
bindMapperForNamespace:把namespace對應(yīng)的接口的類型和代理工廠類綁定起來。
- private void bindMapperForNamespace() {
- String namespace = builderAssistant.getCurrentNamespace();
- if (namespace != null) {
- Class<?> boundType = null;
- try {
- boundType = Resources.classForName(namespace);
- } catch (ClassNotFoundException e) {
- //ignore, bound type is not required
- }
- if (boundType != null) {
- if (!configuration.hasMapper(boundType)) {
- // Spring may not know the real resource name so we set a flag
- // to prevent loading again this resource from the mapper interface
- // look at MapperAnnotationBuilder#loadXmlResource
- configuration.addLoadedResource("namespace:" + namespace);
- configuration.addMapper(boundType);
- }
- }
- }
- }
看最后 addMapper 方法
- public <T> void addMapper(Class<T> type) {
- 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<>(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 {
- if (!loadCompleted) {
- knownMappers.remove(type);
- }
- }
- }
- }
判斷 namespace對應(yīng)的類是否是接口,然后判斷是否已經(jīng)加載了這個(gè)接口,最后將namespace對應(yīng)的接口和 MapperProxyFactory 放到 map 容器中。MapperProxyFactory 工廠模式,創(chuàng)建 MapperProxy 對象,這個(gè)一看就是代理對象。這個(gè)就是根據(jù)接口里面的方法獲取 mapper.xml里面對應(yīng)的sql語句的關(guān)鍵所在。具體的等下次講解getMapper()源碼的時(shí)候在深入的講解。
3.時(shí)序圖
4.總結(jié)
在這一步,我們主要完成了 config 配置文件、Mapper 文件、Mapper 接口解析。我們得到了一個(gè)最重要的對象Configuration,這里面存放了全部的配置信息,它在屬性里面還有各種各樣的容器。最后,返回了一個(gè)DefaultSqlSessionFactory,里面持有了 Configuration 的實(shí)例。