本篇博客將主要對 mybatis 總體介紹,包括 mybatis 的項目結構,執行的主要流程,初始化流程,API 等各模塊進行簡單的串聯,讓你可以對 mybatis 有一個總體的把握。另外在 mybatis 源碼的閱讀過程當中,若是不想寫 demo 能夠直接使用項目中的單元測試;html
mybatis的主要功能和使用 demo,在網上已經有不少了我就再也不囉嗦了,同時 官方文檔 也很是的詳細;另外 mybatis 中使用了多種設計模式,包括建造者、動態代理、策略、裝飾器模式等,在查看源碼的時候,最好先對這些設計模式有必定的瞭解;java
其中 mybatis 的模塊結構以下:mysql
<img src="https://img2018.cnblogs.com/blog/1119937/201908/1119937-20190815185530368-1985790502.png" width = "800" alt="" align=center />sql
mybatis 的執行流程以下:數據庫
具體過程如圖所示:apache
<img src="https://img2018.cnblogs.com/blog/1119937/201908/1119937-20190815185556997-769547107.png" width = "650" alt="" align=center />設計模式
mybatis 中包含了不少的配置項,具體每一項的講解 官網 也很詳細,其結構大體以下:(另外正如上面說的 mybatis 的配置項最後都由 Configuration 類維護,這其實就是外觀模式)緩存
configuration(配置) properties(屬性) settings(設置) typeAliases(類型別名) typeHandlers(類型處理器) objectFactory(對象工廠) plugins(插件) environments(環境配置) environment(環境變量) transactionManager(事務管理器) dataSource(數據源) mappers(映射器)
Java API 初始化的方式雖然不經常使用,可是相較於 XML 的方式能夠更清楚的看到 Configuration 的構成,其示例以下:安全
PooledDataSource dataSource = new PooledDataSource(); dataSource.setDriver("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT"); dataSource.setUsername("root"); dataSource.setPassword("root"); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(UserMapper.class); sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
相交於 Java API 的方式,XML 配置初始化,必然會多出 XML 的解析部分;代碼以下:session
String resource = "org/apache/ibatis/builder/MapperConfig.xml"; Reader reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); SqlSession sqlSession = sqlSessionFactory.openSession();
下面是一個相對完整的配置示例:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="org/apache/ibatis/databases/blog/blog-derby.properties"/> <settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="false"/> ... </settings> <typeAliases> <typeAlias alias="Author" type="org.apache.ibatis.domain.blog.Author"/> <typeAlias alias="Blog" type="org.apache.ibatis.domain.blog.Blog"/> ... </typeAliases> <typeHandlers> <typeHandler javaType="String" jdbcType="VARCHAR" handler="org.apache.ibatis.builder.CustomStringTypeHandler"/> </typeHandlers> <objectFactory type="org.apache.ibatis.builder.ExampleObjectFactory"> <property name="objectFactoryProperty" value="100"/> </objectFactory> <plugins> <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin"> <property name="pluginProperty" value="100"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"> <property name="" value=""/> </transactionManager> <!--<dataSource type="UNPOOLED">--> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="org/apache/ibatis/builder/AuthorMapper.xml"/> <mapper resource="org/apache/ibatis/builder/BlogMapper.xml"/> ... </mappers> </configuration>
其解析的流程以下:
<img src="https://img2018.cnblogs.com/blog/1119937/201908/1119937-20190815185630032-229242454.png" width = "800" alt="" align=center />
主要代碼以下:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
從上面的代碼和流程圖中能夠看到,XML 初始化的主要流程被封裝到了 XMLConfigBuilder 當中;主要的代碼邏輯以下:
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } 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); } }
try (SqlSession session = sqlMapper.openSession()) { Author author = session.selectOne("org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor", new Author(101)); }
這種方式經過 namespace + sqlId 的方式直接指定 MappedStatement;這種方式由於直接編寫字符串和強類型轉換,既不安全也稍顯麻煩,因此如今已經不推薦使用了;
@Override public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds); registerCursor(cursor); return cursor; } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
try (SqlSession session = sqlMapper.openSession()) { AuthorMapper mapper = session.getMapper(AuthorMapper.class); Author author = mapper.selectAuthor(500); }
這種方式不經避免了以上的問題,同時也可以使用註解的方式編寫 sql,並且可使用 IDE 提示;如今通常都推薦使用這種方式;可是其最終也是調用了上面的接口;
首先在初始化的時候經過 bindMapperForNamespace,註冊對應的 Mapper(要求namespace和Mapper的全限定名保持一致);
// XMLMapperBuilder 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)) { configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } } } // MapperRegistry 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)); // 添加代理工廠 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
使用的時候,經過 class 類名獲取 MapperProxyFactory 代理工廠,製造一個新的 Mapper 代理(注意這裏時每次都要生成一個代理類,由於其中包含了 SqlSession,而 SqlSession 是線程不安全的因此不能緩存,可是我以爲這裏任然是能夠優化的,有興趣你能夠本身嘗試一下);
try (SqlSession session = sqlMapper.openSession()) { AuthorMapper mapper = session.getMapper(AuthorMapper.class); // 代理類 } // MapperRegistry public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); // 建立代理對象 } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } // MapperProxyFactory public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } // MapperProxy public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { // 從Object中繼承的方法 return method.invoke(this, args); } else if (method.isDefault()) { // 有默認實現的接口方法 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); // 而後由 MapperMethod 執行,這裏使用策略模式,後面還會詳細講解 }
SqlSession 是線程不安全的,因此在示例代碼中每次使用都會將其關閉?
在 mybatis 中還有一個類 SqlSessionManager 裏面有一個 ThreadLocal 用來管理 SqlSession,在 Spring 中也一樣是用 SqlSessionHolder 來管理的,因此並不會每次都建立一個新的 SqlSession;
以上內容只是大體將了 mybatis 的主要結構,後面的章節還會分模塊進行講解;
另外本文主要參考了《MyBatis技術內幕》,有興趣的能夠自行查看;
原文出處:https://www.cnblogs.com/sanzao/p/11359871.html