本篇博客将主要对 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