1、MyBatis整体架构
本次解析使用的MyBatis版本为3.4.6。
MyBatis最上层是接口层,接口层用来给开发人员在Mapper或者Dao接口中定义进行哪种操作,查询、新增、更新或删除。
中间层是数据处理层,主要是配置Mapper接口 -> XML文件之间的参数映射、SQL解析、SQL执行、结果映射等过程。
最下层是基础支持层,用来提供功能支撑,包括连接管理、事务管理、配置加载、缓存处理等。
1.1、接口层
单独使用 MyBatis 执行数据库的操作时,常见代码如下:
InputStream is = Resources.getResourceAsStream("spring/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session = factory.openSession();
User user = session.selectOne("org.xxx.dao.IUserDao.queryUserInfoById", 1L);
SqlSessionFactoryBuilder、SqlSessionFactory 和 SqlSession 是 MyBatis 主要的接口类,下面章节进行详细介绍。
1.2、数据处理层
配置解析
在MyBatis初始化过程中,会加载 mybatis-config.xml 配置文件、映射配置文件以及 Mapper接口中的注解信息,解析后的配置信息生成相应的对象,保存在 Configuration 对象中,之后根据该对象创建 SqlSessionFactory 对象。待 MyBatis 初始化完成后,可以通过 SqlSessionFactory 对象进行数据库操作。
SQL解析
MyBatis 中的 scripting 模块会根据用户传入的参数,解析映射文件中定义的动态 SQL节点,生成数据库可执行的 SQL语句。
SQL执行
MyBatis层级结构如上图所示,先简单介绍一下各个组件:
- SqlSession:它是 MyBatis 的核心API,主要用来执行命令,获取映射,管理事务。接收开发人员提供的StatementId和参数,并返回操作结果。
- Executor:执行器,是 MyBatis 调度的核心,负责SQL语句的生成以及查询缓存的维护。
- StatementHandler:用于封装JDBC中Statement的操作。即声明SQL、设置参数、执行SQL等。通常一次SQL执行完之后就要关闭它,最多它的生命周期也不会超过一次事务的范围。
- ParameterHandler:负责对用户传递过来的参数转换成JDBC Statement所需要的参数。
- ResultSetHandler:负责将 JDBC 返回的ResultSet结果集对象转换成List类型的集合。
- TypeHandler:用于 Java类型和 JDBC类型之间的转换。
- BoundSql:表示动态生成的 SQL语句以及相应的参数信息。
- MappedStatement:动态SQL 的封装,存储了一个 SQL所有的信息。
- SqlSource:表示从 XML文件或注解方法读取的映射语句的内容,它创建将从用户接收的输入参数传递给数据库的SQL。
- Configuration:MyBatis 所有的配置信息都维持在 Configuration 对象中。
1.3、基础支持层
类型转换模块
主要有两个功能,一是 MyBatis 的别名机制,能够简化配置文件。二是实现 JDBC类型与Java类型的转换,在 SQL语句绑定参数时,会将数据由 Java类型转换为 JDBC类型,在映射结果时,会将数据由 JDBC类型转换为 Java类型。
解析器模块
主要有两个功能,一是封装了XPath,为 MyBatis 初始化时解析 mybatis-config.xml 配置文件以及 xxxMapper.xml 映射配置文件提供支持。二是为处理动态 SQL语句中的占位符提供支持。
事务管理模块
一般地,Mybatis 与 Spring 框架集成,由 Spring 框架管理事务。
缓存模块
MyBatis中有一级缓存和二级缓存,这两级缓存与 MyBatis 以及整个应用是运行在同一个JVM中的,共享同一块内存,如果这两级缓存的数据量较大,则会影响系统的其他功能。缓存大量数据时,优先考虑Redis等缓存。
Binding模块
在调用 SqlSession 相应方法执行数据库操作时,需要指定映射文件中定义的 SQL节点,如果 SQL 中出现了指定失败的情况,那只能在运行时发现。为了可以尽早发现这种错误,MyBatis 通过 Binding模块将用户自定义的 Mapper接口与映射文件关联起来,系统可以通过调用 Mapper接口中的方法执行相应的 SQL语句完成数据库操作。在开发中,我们只是创建了 Mapper接口,而没有其实现类,这是因为 MyBatis 自动为 Mapper接口创建了动态代理对象。
2、解析配置文件
2.1、概述
常见的 mybatis-config.xml 配置文件如下所示:
<?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>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="xxx"/>
<property name="username" value="xxx"/>
<property name="password" value="xxx"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.xxx.mapper"/>
</mappers>
</configuration>
配置文件中配置了数据库连接信息、处理器(声明处理器、参数处理器、结果映射器等)、mapper配置文件的包路径等信息,从第一部分 MyBatis 执行数据库操作的代码中可以看出,MyBatis 使用 SqlSessionFactoryBuilder 中的 build 方法去解析配置文件,那么接下来从源码的角度进行分析 MyBatis 是怎么解析的。
2.2、SqlSessionFactoryBuilder源码解析
从文件名可以看出,SqlSessionFactoryBuilder 使用的是建造者模式,方便根据传入不同的属性去生成 SqlSessionFactory,优点为各个建造者方法之间相互独立,利于系统扩展。
public class SqlSessionFactoryBuilder {
// 通过文件输入流生成SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
// 具体生成SqlSessionFactory的方法,其他方法最终都调用该方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 新建Configuration对象的建造者类
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 先使用XMLConfigBuilder的parse方法构建Configuration对象,然后调用下边的build方法
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
// 使用Configuration对象生成SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
// 返回新建的DefaultSqlSessionFactory
return new DefaultSqlSessionFactory(config);
}
...
}
从源码中可以看出,使用 XMLConfigBuilder 的 parse 去解析配置文件,那我们先从 XMLConfigBuilder 源码里去分析。
2.2.1、XMLConfigBuilder源码解析
public class XMLConfigBuilder extends BaseBuilder {
public Configuration parse() {
// 如果已经解析过了,抛出异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 设置解析标识为ture
parsed = true;
// 解析配置文件中configuration节点的内容,即从根节点解析
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 解析属性,properties
propertiesElement(root.evalNode("properties"));
// 解析设置,settings
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载自定义VFS
loadCustomVfs(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"));
// (核心)解析mapper文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
}
从源码可以看出,依次解析配置文件中的各个标签节点,如 properties、settings、plugins 等,其中最重要的就是解析标签,加载的所有配置都会保存在 Configuration 对象中,所以,我们先看下 Configuration 对象。
2.2.1.1、Configuration对象
public class Configuration {
protected Environment environment;
// 允许在嵌套语句中使用分页(RowBounds),允许则设置为false,默认为false
protected boolean safeRowBoundsEnabled;
// 允许在嵌套语句中使用分页(ResultHandler),允许则设置为false
protected boolean safeResultHandlerEnabled = true;
// 是否开启自动驼峰命名规则映射,即从数据库列名到Java属性名的映射
protected boolean mapUnderscoreToCamelCase;
// 当开启时,任何方法的调用都会加载该对象的所有属性,否则每个属性按需加载,默认为false
protected boolean aggressiveLazyLoading;
// 是否允许单一语句返回多结果集(需要兼容驱动)
protected boolean multipleResultSetsEnabled = true;
// 允许JDBC支持自动生成主键,需要兼容驱动,默认值为true
protected boolean useGeneratedKeys;
// 使用列标签替代列名
protected boolean useColumnLabel = true;
// 是否启用缓存,默认是开启的
protected boolean cacheEnabled = true;
// 指定当结果集中值为null时,是否调用映射对象的setter方法
protected boolean callSettersOnNulls;
// 允许使用方法签名中的名称作为语句参数名称,使用该特性,必须采用Java8编译,且加上-parameters选项
protected boolean useActualParamName = true;
// 当返回行的所有列都是空的,MyBatis默认返回null。当开启这个设置时,MyBatis会返回一个空实例
protected boolean returnInstanceForEmptyRow;
// 指定MyBatis增加到日志名称的前缀
protected String logPrefix;
// 指定MyBatis所有日志的具体实现,未指定时将自动查找。一般使用log4j
protected Class <? extends Log> logImpl;
// 指定VFS的实现,VFS是MyBatis提供的用于访问AS内资源的一个简便接口
protected Class <? extends VFS> vfsImpl;
// MyBatis利用本地缓存机制(Local Cache)防止循环引用或加速重复嵌套查询。
// 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
// 当没有为参数提供特定的JDBC类型时,为空值指定JDBC类型
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
// 指定对象的哪个方法触发一次延迟加载
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
// 设置超时时间,决定了驱动等待数据库响应的秒数,默认不超时
protected Integer defaultStatementTimeout;
// 为驱动的结果集设置默认获取数量
protected Integer defaultFetchSize;
// 设置默认的执行器。
// SIMPLE:普通的执行器
// REUSE:执行器会重用预处理语句(prepared statements)
// BATCH:执行器将重用语句并执行批量更新
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// 指定MyBatis应该如何自动映射列到字段或属性。
// NONE:取消自动映射
// PARTIAL:只会自动映射没有定义嵌套结果集映射的结果集
// PARTIAL:会自动映射任意复杂的结果集,无论是否嵌套
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
// 指定发现自动映射目标未知列(或者未知属性类型)的行为
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
// setttings下的properties属性
protected Properties variables = new Properties();
// 反射器工厂,用于操作属性、构造器
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
// 对象工厂,所有的resultMap类都需要依赖对象工厂来实例化
protected ObjectFactory objectFactory = new DefaultObjectFactory();
// 对象包装工厂,主要用来创建非原生对象,比如增加了某些监控或者特殊属性的代理类
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
// 延迟加载的全局开关,当开启时,所有关联对象都会延迟加载
protected boolean lazyLoadingEnabled = false;
// 指定MyBatis创建具有延迟加载能力的对象所用到的代理工具,MyBatis3.3+使用JAVASSIST
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
// MyBatis可以根据不同的数据库厂商执行不同的语句,多厂商支持基于映射语句中的databaseId属性
protected String databaseId;
protected Class<?> configurationFactory;
// 存储动态SQL
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
// 存储缓存
protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
// 存储ResultMap
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
// 存储ParameterMap
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
// 存储KeyGenerator,密钥生成器
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
...
// 内部类,集成HashMap,用来存储SQL语句、缓存、resultmap等
protected static class StrictMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -4950446264854982944L;
private final String name;
public StrictMap(String name, int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
this.name = name;
}
...
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key);
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
public V get(Object key) {
V value = super.get(key);
if (value == null) {
throw new IllegalArgumentException(name + " does not contain value for " + key);
}
if (value instanceof Ambiguity) {
throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
+ " (try using the full name including the namespace, or rename one of the entries)");
}
return value;
}
...
}
}
2.2.1.2、解析mappers标签——mapperElement
常见的mappers标签包含以下四种方式,分别为映射文件相对路径、绝对路径、Mapper接口全路径、映射文件包名路径。
<mappers>
<mapper resource="xxx/xxx/xxxMapper.xml"/>
<mapper url="file:./xxx/xxx/xxxMapper.xml"/>
<mapper class="xxx.xxx.xxxMapper"/>
<package name="xxx.xxx.mapper"/>
</mappers>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历mappers子节点标签
for (XNode child : parent.getChildren()) {
// 如果为package标签
if ("package".equals(child.getName())) {
// 获取name属性的值,即映射文件包名路径
String mapperPackage = child.getStringAttribute("name");
// 添加到configuration对象的mappers
configuration.addMappers(mapperPackage);
} else {
// 类路径
String resource = child.getStringAttribute("resource");
// url绝对路径
String url = child.getStringAttribute("url");
// java类名
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
// 解析类路径
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 每次循环都新建XMLMapperBuilder进行解析,并且对应的配置对象也封装在configuration中
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 解析url绝对路径
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
// 和上述一样,使用XMLMapperBuilder进行解析
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 解析java类名
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.");
}
}
}
}
}
通过源码可以了解到,会对 mappers 标签里的子标签进行遍历,对于资源标识为 package 和 class 的,使用 configuration 的 addMappers 和 addMapper 方法解析;资源标识为 resource 和 url 的,每个 mapper 子标签都会新建一个 XMLMapperBuilder 去解析对应的 mapper 映射文件,下面分别看一下 Configuration的addMappers 和 XMLMapperBuilder 的 parse 方法。
2.2.1.3、mapper注册器——MapperRegistry
mapper 注册器用于将所有的 mapper接口添加到内存中,Mapper 注册器自身维护着两个属性,config 和 knownMappers。knownMappers 表示某个类路径对应的 Mapper 代理工厂,MapperProxyFactory 是通过代理模式创建处一个 MapperProxy,MapperProxy 实现了 InvocationHandler 接口,这表示 MapperProxy 会通过 invoke() 方法实现 Mapper 接口指定方法的调用。
public class Configuration {
...
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
...
// 根据mapper映射文件包名路径,进行解析,调用的是MapperRegistry的addMappers方法
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
}
public class MapperRegistry {
private final Configuration config;
// 维护了接口类与代理工厂的映射关系
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public <T> void addMapper(Class<T> type) {
// 对应MyBatis mapper接口文件,必须是interface,不能是class
if (type.isInterface()) {
// 判断是否加载过
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 为mapper接口创建一个MapperProxyFactory代理
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 使用MapperAnnotationBuilder进行解析
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public void addMappers(String packageName, Class<?> superType) {
// MyBatis框架提供的搜索classpath下指定package以及子package中符合条件(注解或继承某个类/接口)的类
// 默认使用Thread.currentThread().getContextClassLoader()返回的加载器
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
// 无条件的加载所有类,因为调用方传递了Object.class作为父类,这也给以后的指定mapper接口预留了余地
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 所有匹配的class都被存储在ResolverUtil.matchaes字段中
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// 调用addMapper方法进行具体的mapper类/接口解析
addMapper(mapperClass);
}
}
// Configuration对象调用该方法,传入mapper映射文件包名路径
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
}
通过源码可以了解到,对于资源标识为 package 和 class 的,使用 configuration 的 addMapper 方法解析,最后会调用 mapper 注册器(MapperRegistry)的 addMapper 方法,会去解析指定的 mapper 接口。接下来进一步分析解析指定 mapper 接口的源码,mapper 注解构造器(MapperAnnotationBuilder)。
2.2.1.4、mapper注解构造器——MapperAnnotationBuilder
MapperAnnotationBuilder 就是解析指定的 mapper 接口对应的 Class 对象,包含 MyBatis 框架定义的注解,并生成 Cache、ResultMap、MappedStatement 三种类型对象,不过会优先解析 mapper 映射文件,mapper 映射文件必须与 Class 对象所在的包路径一致,且文件名要与类名一致。
public class MapperAnnotationBuilder {
// 只有4个元素:@Select、@Insert、@Update、@Delete,sql语句保存在注解中
private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<Class<? extends Annotation>>();
// 只有4个元素:@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider,sql语句保存在注解指定的类的指定方法中
private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<Class<? extends Annotation>>();
// 核心配置对象
private final Configuration configuration;
// Mapper构建助手,用于组装解析出来的配置,生成Cache、ResultMap、MappedStatement等对象
private final MapperBuilderAssistant assistant;
// 要解析的mapper接口Class对象
private final Class<?> type;
// 解析的mapper接口Class对象
public void parse() {
// Class对象的唯一标识
String resource = type.toString();
// 如果当前对象已经解析过,则不再解析
if (!configuration.isResourceLoaded(resource)) {
// 加载并解析mapper接口对应的mapper映射文件,即xxMapper.xml
loadXmlResource();
// 把Class对应的标识添加到已加载的资源列表中
configuration.addLoadedResource(resource);
// 设置当前namespace为接口Class的全限定名
assistant.setCurrentNamespace(type.getName());
// 解析缓存对象
parseCache();
// 解析缓存引用,会覆盖以前解析的缓存对象
parseCacheRef();
// 获取所有方法,解析方法上的注解,生成MappedStatement和ResultMap
Method[] methods = type.getMethods();
// 遍历获取到的所有方法
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 解析一个方法生成对应的MappedStatement对象
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析挂起的方法
parsePendingMethods();
}
/**
* 解析指定的mapper映射文件
* MapperAnnotationBuilder每解析一个mapper映射文件,都会以文件所在路径为mapper配置文件的唯一标识,并把标识添加到已加载的资源文件列表中
*/
private void loadXmlResource() {
// 若是已加载资源列表中指定key已存在,则不再解析xml文件,资源名称为namespace:全限定名
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 根据Class对象生成mapper配置文件路径
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
// 获取文件字节流
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
// 使用XMLMapperBuilder进行解析
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
...
// 解析注解生成新的ResultMap对象
private String parseResultMap(Method method) {
// 获取目标bean的类型
Class<?> returnType = getReturnType(method);
// 获取方法上的@ConstructorArgs注解
ConstructorArgs args = method.getAnnotation(ConstructorArgs.class);
// 获取方法上的@Results
Results results = method.getAnnotation(Results.class);
// 获取方法上的@TypeDiscriminator注解
TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
// 根据方法生成resultMap的惟一标识,格式为:类全限定名.方法名-参数类型简单名称
String resultMapId = generateResultMapName(method);
// resultMapId和返回值类型已经解析完毕,再解析剩下的构造方法映射、属性映射和鉴别器,以后添加结果映射到配置对象中
applyResultMap(resultMapId, returnType, argsIf(args), resultsIf(results), typeDiscriminator);
return resultMapId;
}
// 解析方法上的注解,MyBatis可以以注解方式编写SQL
void parseStatement(Method method) {
// 获取输入参数的类型,这个参数没用,由于如今都是以输入参数对象为根,经过ognl表达式寻值的
Class<?> parameterTypeClass = getParameterType(method);
// 经过方法上的@Lang注解获取语言驱动
LanguageDriver languageDriver = getLanguageDriver(method);
// 经过方法上的@Select等注解获取SqlSource
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
// 若是成功建立了SqlSource,则继续
if (sqlSource != null) {
// 获取方法上的@Options注解
Options options = method.getAnnotation(Options.class);
// 映射语句id为类的全限定名.方法名
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
// 经过注解获取Sql命令类型
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
// 若是是insert或update命令
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// 首先检查@SelectKey注解 ,它会覆盖任何其余的配置
// 获取方法上的SelectKey注解
SelectKey selectKey = method.getAnnotation(SelectKey.class);
// 若是存在@SelectKey注解
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
// 其余sql命令均没有键生成器
keyGenerator = NoKeyGenerator.INSTANCE;
}
if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
}
// 处理方法上的@ResultMap注解
String resultMapId = null;
// 获取注解,@ResultMap注解表明引用已经存在的resultMap对象的id
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
// 若是方法上存在@ResultMap注解,则生成引用id便可
if (resultMapAnnotation != null) {
// 获取指定的多个resultMapId
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
// 遍历String数组,拼接为一个String,逗号分隔
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
// 不存在@ResultMap注解,且语句为select类型,则经过解析@Args、@Results等注解生成新的ResultMap对象
} else if (isSelect) {
// 生成新的ResultMap对象,加入Configuration配置对象,同时返回resultMapId
resultMapId = parseResultMap(method);
}
// 构建MappedStatement并添加到配置对象中
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
...
}
总结一下解析流程:
1、判断当前 class 对象是否解析过,如果没有解析过,才进行解析。
2、先解析 class 对象对应的 mapper 映射文件,先 key 放置到已加载资源列表中,key = namespace:全限定名,然后使用 XMLMapperBuilder 进行解析(看到这里大家是否还有印象,在 mappers 标签里的子标签进行遍历时,对于资源标识为 resource 和 url 的,也是通过 XMLMapperBuilder 去解析,殊途共归了)
3、解析方法上的注解,因为 MyBatis 可以以注解方式编写 SQL,所以这里获取这些 SQL 生成 MappedStatement 对象
下面开始分析解析 mapper 映射文件的源码 XMLMapperBuilder。
2.2.1.5、mapper映射文件构造器——XMLMapperBuilder
public void parse() {
// 如果没有加载过,才会去加载
if (!configuration.isResourceLoaded(resource)) {
// 配置mapper
configurationElement(parser.evalNode("/mapper"));
// 标记已经加载过了
configuration.addLoadedResource(resource);
// 绑定映射器到namespace
bindMapperForNamespace();
}
// 可以忽略
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// 获取mapper文件中的namespace属性,配置namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
// 配置cache
cacheElement(context.evalNode("cache"));
// 配置parameterMap。已经废弃
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 配置resultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 配置sql(定义可重用的SQL代码段)
sqlElement(context.evalNodes("/mapper/sql"));
// 配置select|insert|update|delete,根据前面的sql片段创建在Mapper中真正的配置对象MappedStatement
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);
}
}
// 传入 select|insert|update|delete 标签的节点列表进行构建 MappedStatement
private void buildStatementFromContext(List<XNode> list) {
// 判断databaseId
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 遍历所有 select|insert|update|delete 标签节点列表
for (XNode context : list) {
// 使用XMLStatementBuilder进行解析
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// sql语句有问题的,放置在configuration中
configuration.addIncompleteStatement(statementParser);
}
}
}
从源码中看出,将 mapper 映射文件中的 select、insert、update、delete 标签解析,使用的是 XMLStatementBuilder 中的 parseStatementNode 方法,下面来看这个方法:
2.2.1.6、mapper映射文件声明构造器——XMLStatementBuilder
public class XMLStatementBuilder extends BaseBuilder {
private final MapperBuilderAssistant builderAssistant;
private final XNode context;
private final String requiredDatabaseId;
...
// 这里解析select、insert、update、delete 标签
public void parseStatementNode() {
// 获取id属性
String id = context.getStringAttribute("id");
// 获取databaseId属性
String databaseId = context.getStringAttribute("databaseId");
// 如果databaseId不匹配,返回
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 驱动程序每次批量返回的结果行数
Integer fetchSize = context.getIntAttribute("fetchSize");
// 超时时间
Integer timeout = context.getIntAttribute("timeout");
// parameterMap,已废弃
String parameterMap = context.getStringAttribute("parameterMap");
// 参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 结果集
String resultMap = context.getStringAttribute("resultMap");
// 结果类型
String resultType = context.getStringAttribute("resultType");
// 脚本语言,mybatis3.2的新功能 不重要
String lang = context.getStringAttribute("lang");
// 得到语言驱动 不重要
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
// 结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute("resultSetType");
// 语句类型, STATEMENT|PREPARED|CALLABLE 的一种,获取 Statement类型 这个是需要和 JDBC做映射的
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
// 获取命令类型(select|insert|update|delete)
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 是否要缓存select结果
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 解析之前先解析<include>SQL片段 这个在前面 XMLMapperBuilder 中已经构建完成,这里需要调用并解析
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析之前先解析<selectKey> selectKey主要涉及需要某些特殊关系来设置主键的值
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析成SqlSource,一般是DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 调用助手类去真正创建MappedStatement然后加入配置Configuration中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
}
// MappedStatement创建助手
public class MapperBuilderAssistant extends BaseBuilder {
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout,
String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache,
boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
LanguageDriver lang, String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
// 为id加上namespace前缀,由于要放置到configuration中,为了防重复,在mapper映射文件的CRUD标签加上namespace就构建了独一无二的id
id = applyCurrentNamespace(id, false);
// 是否是select语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 使用传入的参数生成MappedStatement构造器,这里CRUD相关SQL的所有信息都包含在MappedStatement中了
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
// 参数映射,由于parameterMap被弃用,所以这里一般为空。真正传入的其实应该是parameterType这个Class
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 构建MappedStatement
MappedStatement statement = statementBuilder.build();
// 将MappedStatement添加到configuration的mappedStatements属性中
configuration.addMappedStatement(statement);
return statement;
}
}
mapper 映射文件声明构造器(XMLStatementBuilder)去解析 mapper 映射文件的每个 CRUD(即select、insert、update、delete标签),会去解析标签的所有元素,常用的就是 id、parameterType、resultMap、resultType,将每个标签解析成 SqlSource(SQL的封装)。最后将解析出来的所有属性,使用 MappedStatement 创建助手(MapperBuilderAssistant)去构建 MappedStatement 对象,放置在 configuration 的 mappedStatements 属性中,key为 namespace + id。
2.3、构建流程总结
先简单画一下构建流程图:
- 先通过 Resource 读取 MyBatis 配置文件(mybatis-config.xml),将其转换成输入流。
- 将输入流传入 SqlSession工厂构建器(SqlSessionFactoryBuilder),调用 build方法传入输入流生成 XML配置文件构造器(XMLConfigBuilder)。
- XMLConfigBuilder 会解析 mybatis-config.xml 文件中的所有标签,其中最重要的就是解析标签,加载的所有配置都会保存在 Configuration 对象中。
- 遍历 mapper 标签,根据资源标识类型进行不同的操作,对于资源标识为 package 和 class 的,使用configuration 的 addMappers 和 addMapper 方法解析;资源标识为 resource 和 url 的,新建一个 mapper映射文件构造器(XMLMapperBuilder)去解析对应的 mapper 映射文件。
- 先看 Configuration 对象的 addMappers,调用 mapper 注册器(MapperRegistry)的 addMappers 方法。mapper 注册器解析流程为:资源标识为 package,获取配置包下的所有 mapper 映射文件,再获取文件中指向的 mapper 接口 Class 对象集合,遍历解析 Class 对象。资源标识为 class,则直接解析 Class 对象。调用 mapper 注解构造器(MapperAnnotationBuilder)的 parse 方法解析 Class 对象。
- mapper注解构造器主要分为两个步骤,一是调用 XMLMapperBuilder 解析 mapper接口对应的 mapper映射文件,这里和解析资源标识为 resource 和 url 的 mapper 标签,殊途同归了;二是解析 mapper 接口方法上的SQL语句,生成对应的 MappedStatement 对象。
- mapper映射文件构造器先解析 mapper 映射文件中的 resultMap、sql 等标签,然后调用 mapper 映射文件声明构造器(XMLStatementBuilder)解析CRUD标签(select、insert、update、delete),遍历这些标签,然后封装为SqlSource,最后调用 MappedStatement 创建助手(MapperBuilderAssistant)封装成MappedStatement 对象,一个 CRUD 标签生成一个 MappedStatement 对象。
- 最后将 MappedStatement 添加到 Configuration 对象的 mappedStatements 属性中。