注:本文的内容大部分转载自 Mybatis详解 - Java全栈知识体系
这篇文章记录我在学习 Mybaits
源码的一些记录,包含两部分,这是第一部分,主要是学习一下 Mybatis
的总体设计。
源码下载和构建 先把源码下载下来,可以参考这篇文章:MyBatis源码阅读准备 。
在测试过程中使用的 Demo
来自 mybatis-cache-demo ,原文链接:聊聊MyBatis缓存机制 - 美团技术团队 。
源码和 Demo
准备好之后,将源码导入到 Demo
的项目中,修改 Demo
依赖中的 mybatis
,将其修改为我们下载下来的源码,如下:
1 2 3 4 5 <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.3 .0 -SNAPSHOT</version> </dependency>
最终项目的结构如下:
总体设计 接口层 传统的 Mybatis
工作模式是使用 SqlSession
对象完成和数据库的交互:创建一个 SqlSession
,用来和数据库进行交互,然后根据 Statement Id
和参数来操作数据库。
上面的方式比较简单:new
一个对象,然后调用这个对象的各个方法。但是它不符合面向对象语言的特性,为了适应这一特性,Mybaits
增加了一种支持接口的调用方式,也就是我们现在常用的方式。
如下图,MyBatis
将配置文件中的每一个 <mapper>
节点抽象为一个 Mapper
接口,而这个接口中声明的方法和跟 <mapper>
节点中的 <select|update|delete|insert>
节点相对应,即 <select|update|delete|insert>
节点的 id
值为 Mapper
接口中的方法名称,parameterType
值表示 Mapper
对应方法的入参类型,而 resultMap
值则对应了 Mapper
接口表示的返回值类型或者返回结果集的元素类型。
根据 MyBatis
的配置规范配置好后,通过 SqlSession.getMapper(XXXMapper.class)
方法,MyBatis
会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper
实例。
我们使用 Mapper
接口的某一个方法时,MyBatis
会根据这个方法的方法名和参数类型,确定 Statement Id
,底层还是通过 SqlSession.select("statementId",parameterObject)
或者 SqlSession.update("statementId",parameterObject)
等等来实现对数据库的操作。
MyBatis
引用 Mapper
接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置 SQL
语句,这样就可以脱离 XML
配置文件,实现“0配置”)。
数据处理层 主要包含三个功能:
参数映射。 这个是指对于 java
数据类型和 jdbc
数据类型之间的转换,包括两个过程:
查询阶段:我们要将java类型的数据,转换成jdbc类型的数据,通过 preparedStatement.setXXX()
来设值;
结果映射:对 resultset
查询结果集的 jdbcType
数据转换成 java
数据类型。
通过传入参数构建动态 SQL
语句。 MyBatis
通过传入的参数值,使用 Ognl
来动态地构造 SQL
语句。
SQL
语句的执行以及封装查询结果集成 List<E>
框架支持层
事务管理机制 事务管理机制对于 ORM
框架而言是不可缺少的一部分,事务管理机制的质量也是考量一个 ORM
框架是否优秀的一个标准。
连接池管理机制 由于创建一个数据库连接所占用的资源比较大, 对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要。
缓存机制 为了提高数据利用率和减小服务器和数据库的压力,MyBatis
会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到 SqlSession
中,在允许的时间间隔内,对于完全相同的查询,MyBatis
会直接将缓存结果返回给用户,而不用再到数据库中查找。
SQL
语句的配置方式 传统的 MyBatis
配置 SQL
语句方式就是使用 XML
文件进行配置的,但是这种方式不能很好地支持面向接口编程的理念。为了支持面向接口的编程,MyBatis
引入了 Mapper
接口的概念。面向接口的引入,对使用注解来配置 SQL
语句成为可能,用户只需要在接口上添加必要的注解即可,不用再去配置 XML
文件了。但是,目前的 MyBatis
只是对注解配置 SQL
语句提供了有限的支持,某些高级功能还是要依赖 XML
配置文件配置 SQL
语句。
引导层 引导层是配置和启动MyBatis配置信息的方式。MyBatis
提供两种方式来引导 MyBatis
:基于 XML
配置文件的方式、基于 Java API
的方式。
Mybatis
初始化首先按照传统方式来对 Mybatis
进行初始化:
1 2 3 4 5 6 7 8 9 10 String resource = "mybatis-config.xml" ; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo" );
根据下面的图看一下 Mybatis
的初始化流程:
接着分析源码,看看初始化过程经历了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public SqlSessionFactory build (InputStream inputStream) { return build(inputStream, null , null ); } public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); Configuration config = parser.parse(); return build(config); } 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); }
初始化过程中涉及到以下几个对象:
SqlSessionFactoryBuilder
: SqlSessionFactory
的构造器,用于创建 SqlSessionFactory
,采用了 Builder
设计模式
Configuration
:该对象包含了 mybatis-config.xml
文件中所有 mybatis
配置信息
SqlSessionFactory
:SqlSession
工厂类,以工厂形式创建 SqlSession
对象,采用了 Factory
工厂设计模式
XmlConfigParser
:负责将 mybatis-config.xml
配置文件解析成 Configuration
对象,供 SqlSessonFactoryBuilder
使用,创建 SqlSessionFactory
重点看下 mybatis
是如何将 xml
配置文件转换成 Configuration
对象的,先看流程图:
XMLConfigBuilder
会将 XML
配置文件的信息转换为 Document
对象 而 XML
配置定义文件 DTD
转换成 XMLMapperEntityResolver
对象,然后将二者封装到 XpathParser
对象中,XpathParser
的作用是提供根据 Xpath
表达式获取基本的 DOM
节点 Node
信息的操作。看下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public Configuration parse () { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once." ); } parsed = true ; XNode configurationNode = parser.evalNode("/configuration" ); parseConfiguration(configurationNode); return configuration; } private void parseConfiguration (XNode root) { try { propertiesElement(root.evalNode("properties" )); typeAliasesElement(root.evalNode("typeAliases" )); pluginElement(root.evalNode("plugins" )); objectFactoryElement(root.evalNode("objectFactory" )); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" )); settingsElement(root.evalNode("settings" )); 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); } }
在上述代码中,有一个非常重要的地方,就是解析 XML
配置文件子节点 <mappers>
的方法 mapperElements(root.evalNode("mappers"))
, 它将解析我们配置的 Mapper.xml
配置文件,Mapper
配置文件可以说是 MyBatis
的核心,MyBatis
的特性和理念都体现在此 Mapper
的配置和设计上。
从上述代码可知,节点解析有10步,我们重点看两个:对 environments
的解析、对 mappers
的解析。
先看对 environments
的解析,看下 environments
的配置,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <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 >
然后结合上面的配置看下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 private void environmentsElement (XNode context) throws Exception { if (context != null ) { if (environment == null ) { environment = context.getStringAttribute("default" ); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id" ); if (isSpecifiedEnvironment(id)) { TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager" )); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource" )); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } } private boolean isSpecifiedEnvironment (String id) { if (environment == null ) { throw new BuilderException("No environment specified." ); } else if (id == null ) { throw new BuilderException("Environment requires an id attribute." ); } else if (environment.equals(id)) { return true ; } return false ; } private DataSourceFactory dataSourceElement (XNode context) throws Exception { if (context != null ) { String type = context.getStringAttribute("type" ); Properties props = context.getChildrenAsProperties(); DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory." ); }
在配置 dataSource
时使用了 $driver
这种表达式,它是通过 PropertyParser
来解析的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class PropertyParser { public static String parse (String string, Properties variables) { VariableTokenHandler handler = new VariableTokenHandler(variables); GenericTokenParser parser = new GenericTokenParser("${" , "}" , handler); return parser.parse(string); } private static class VariableTokenHandler implements TokenHandler { private Properties variables; public VariableTokenHandler (Properties variables) { this .variables = variables; } public String handleToken (String content) { if (variables != null && variables.containsKey(content)) { return variables.getProperty(content); } return "${" + content + "}" ; } } }
再看下对 mapper
的解析:
1 2 3 4 5 <mappers > <mapper resource ="mapper/studentMapper.xml" /> <mapper resource ="mapper/classMapper.xml" /> </mappers >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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); try (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); try (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." ); } } } } }
通过 resource
和 url
配置的 mapper
需要进行一步解析操作:mapperParser.parse()
,相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public void parse () { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper" )); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } private void configurationElement (XNode context) { try { String namespace = context.getStringAttribute("namespace" ); if (namespace == null || namespace.isEmpty()) { 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); } } private void bindMapperForNamespace () { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null ) { Class<?> boundType = null ; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { } if (boundType != null && !configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } }
最后,将上述 Mybatis
的初始化过程用序列图细化:
配置解析过程详解 上面说了构建 configuration
对象的过程,但是其中详细的配置解析过程并没有深入,这里就来看一下。先重新看下解析 Configuration
节点相关的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public Configuration parse () { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once." ); } parsed = true ; XNode configurationNode = parser.evalNode("/configuration" ); parseConfiguration(configurationNode); return configuration; } private void parseConfiguration (XNode root) { try { propertiesElement(root.evalNode("properties" )); typeAliasesElement(root.evalNode("typeAliases" )); pluginElement(root.evalNode("plugins" )); objectFactoryElement(root.evalNode("objectFactory" )); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" )); settingsElement(root.evalNode("settings" )); 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); } }
configuration
中包含10个子节点,分别是:properties
、typeAliases
、plugins
、objectFactory
、objectWrapperFactory
、settings
、environments
、databaseIdProvider
、typeHandlers
、mappers
。
properties
1 2 3 4 5 6 7 8 9 10 <properties > <property name ="driver" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/test1" /> <property name ="username" value ="root" /> <property name ="password" value ="root" /> </properties >
properties
节点可以进行两种配置,如上所述。
如果两种方法同时配置了,那么 首先会加载文件中的xml配置,其次是加载外部指定的properties,最后加载Java Configuration的配置 。
因为配置存放在 Properties
,它继承自 HashTable
类,当依次将上述几种配置源 put
进去时,后加载的配置会覆盖先加载的配置。所以,最终应用配置时 Configuration
配置优先级最高,其次是外部的 properties
配置文件,最后是当前 xml
中的配置 。
TypeHandler
可以利用这个实现一个自定义的 [java
类型 <-> jdbc
类型] 转换器 ,如下示例,只需要实现 BaseTypeHandler.class
即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(String.class) public class MySimpleTypeHandler extends BaseTypeHandler <String > { public void setNonNullParameter (PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter + "@自定义Handler存储" ); } public String getNullableResult (ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName).split("@" )[0 ] + "@自定义Handler返回" ; } public String getNullableResult (ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex).split("@" )[0 ] + "@自定义Handler返回" ; } public String getNullableResult (CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex).split("@" )[0 ] + "@自定义Handler返回" ; } }
在 mybatis
中的配置如下,这里的配置是全局生效的:
1 2 3 <typeHandlers > <typeHandler handler ="typehandler.MySimpleTypeHandler" /> </typeHandlers >
也可以在 Mapper
文件中加上对 TypeHandler
的配置,实现局部配置:
1 2 3 4 5 6 7 8 9 10 <resultMap id ="BaseResultMap" type ="Student" > <result column ="name" property ="name" jdbcType ="VARCHAR" typeHandler ="typehandler.MySimpleTypeHandler" /> <result column ="id" property ="id" jdbcType ="INTEGER" /> <result column ="age" property ="age" jdbcType ="TINYINT" /> </resultMap > <select id ="getStudentById" parameterType ="int" resultMap ="BaseResultMap" useCache ="true" > SELECT id,name,age FROM student WHERE id = #{id} </select >
详细介绍见 Mybatis(四): 类型转换器模块详解 和 TypeHandler - Java全栈知识体系
关于 typeHandler
实现类有几个注意点还需要提一下:
通过类型处理器的泛型,MyBatis
可以得知该类型处理器处理的 Java
类型 ,不过这种行为可以通过两种方法改变:
在类型处理器的配置元素(typeHandler
元素)上增加一个 javaType
属性(比如:javaType="String"
)
在类型处理器的类上增加一个 @MappedTypes
注解指定与其关联的 Java
类型列表。 如果在 javaType
属性中也同时指定,则注解上的配置将被忽略。
可以通过两种方式来指定关联的 JDBC
类型:
在类型处理器的配置元素上增加一个 jdbcType
属性(比如:jdbcType="VARCHAR"
)
在类型处理器的类上增加一个 @MappedJdbcTypes
注解指定与其关联的 JDBC
类型列表。 如果在 jdbcType
属性中也同时指定,则注解上的配置将被忽略。
如果配置 jdbcType
属性或者 @MappedJdbcTypes
注解之后,insert/update
语句没有走 typeHandler
。那么就检查一下 insert/update
语句中变更的属性值是否加上了 jdbcType
的配置,如果没有配置就不会生效,如下:
1 2 3 <insert id ="addStudent" parameterType ="entity.StudentEntity" useGeneratedKeys ="true" keyProperty ="id" > INSERT INTO student(name,age) VALUES(#{name, jdbcType=VARCHAR}, #{age}) </insert >
除了最基础的 BaseTypeHandler
外,mybatis
还默认实现了许多特定类型的 TypeHandler
,如 EnumTypeHandler
,这里有一个案例介绍如何实现一个自己的 EnumTypeHandler
,优雅的实现枚举类型与 sql
的交互:如何在MyBatis中优雅的使用枚举 。
参考文章 Mybatis详解 - Java全栈知识体系
Mybatis(四): 类型转换器模块详解
如何在MyBatis中优雅的使用枚举