前言
本来打算此次写一篇关于SqlSession的解析,但发现SqlSession涉及的知识太多。所以先结合mybatis配置文件(我们项目中常写的如mybatisConfig.xml),来分析下mybatis初始化时做了些什么,进而分析语句的执行。此篇源码解读主要结合官网mybatis文档,来进行分析。
源码解析
mybatis初始化时,会构建一个全局的Configuration类,包含了mybatis的配置信息。查看mybatis配置文件顶层结构如下。接下来一个个配置项进行分析。
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
properties(属性)
属性既可以在外部文件中指定(resource / url),也可以在</propertie>标签中指定。例如:
<properties resource="com/xiaobing/resource/jdbcConfig.properties"> <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/auth" /> <property name="username" value="root" /> <property name="password" value="root" /> </properties>
其中jdbcConfig.properties文件中也可以指定属性,例如:
driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/auth
配置好了这些属性,可以在后面的配置文件中使用这些值,如${driver}:
<dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource>
那此时有一个问题,如果我在</propertie>标签中指定了属性值,又在外部文件中指定了相同的属性值,如上例子中的resource指向的文件。那木mybatis优先会使用哪一种。分析源码。关于配置文件解析,直接定位在XMLConfigBuilder中的parseConfiguration(XNode root)方法,为什么会到这个文件,后面分析SqlSession时会进行解释。
private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); //issue #117 read properties first // 读取properties配置 typeAliasesElement(root.evalNode("typeAliases")); // 读取别名设置 pluginElement(root.evalNode("plugins")); // 读取插件设置 objectFactoryElement(root.evalNode("objectFactory")); // 读取对象工厂设置 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 读取对象包装工厂设置 settingsElement(root.evalNode("settings")); // 读取setting设置 environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631 // 读取环境设置 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 读取数据库ID提供信息 typeHandlerElement(root.evalNode("typeHandlers")); // 读取类型转换处理器 mapperElement(root.evalNode("mappers")); // 读取Sql } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
此方法包含了所有配置文件的解析。分析propertiesElement()属性解析方法。
private void propertiesElement(XNode context) throws Exception { if (context != null) { Properties defaults = context.getChildrenAsProperties(); // 获取properties中的键值对 String resource = context.getStringAttribute("resource"); // 获取resource指定的文件地址 String url = context.getStringAttribute("url"); // 获取url指定的文件地址 if (resource != null && url != null) { // resource和url不能同时存在 throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); // 此处使用putAll.如果出现相同属性,则会覆盖 } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); // 同理 } Properties vars = configuration.getVariables(); // configuration中设置的properties if (vars != null) { defaults.putAll(vars); // 同理 } parser.setVariables(defaults); configuration.setVariables(defaults); // 将最终的properties放入configuration配置类中 } }
分析可得,先解析</propertie>标签的属性值,在解析属性文件中的属性值,在解析设置configuration时预设值的propertie值,逐层覆盖。所以可得出优先级,configuration时预设值的propertie值 > 属性文件中的属性值 > </propertie>标签的属性值;这也解释了网上关于properties属性优先级排序。
settings(设置)
此处关于settings的配置实在太多,还有些配置未列出来。详情信息如下。
设置名 |
描述 |
有效值 |
默认值 |
cacheEnabled |
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 |
true | false |
true |
lazyLoadingEnabled |
延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 |
true | false |
false |
aggressiveLazyLoading |
开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。 |
true | false |
false (在 3.4.1 及之前的版本中默认为 true) |
multipleResultSetsEnabled |
是否允许单个语句返回多结果集(需要数据库驱动支持)。 |
true | false |
true |
useColumnLabel |
使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 |
true | false |
true |
useGeneratedKeys |
允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 |
true | false |
False |
autoMappingBehavior |
指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 |
NONE, PARTIAL, FULL |
PARTIAL |
autoMappingUnknownColumnBehavior |
指定发现自动映射目标未知列(或未知属性类型)的行为。
|
NONE, WARNING, FAILING |
NONE |
defaultExecutorType |
配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 |
SIMPLE REUSE BATCH |
SIMPLE |
defaultStatementTimeout |
设置超时时间,它决定数据库驱动等待数据库响应的秒数。 |
任意正整数 |
未设置 (null) |
defaultFetchSize |
为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 |
任意正整数 |
未设置 (null) |
defaultResultSetType |
指定语句默认的滚动策略。(新增于 3.5.2) |
FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置) |
未设置 (null) |
safeRowBoundsEnabled |
是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 |
true | false |
False |
safeResultHandlerEnabled |
是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 |
true | false |
True |
mapUnderscoreToCamelCase |
是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 |
true | false |
False |
localCacheScope |
MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 |
SESSION | STATEMENT |
SESSION |
jdbcTypeForNull |
当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 |
JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 |
OTHER |
azyLoadTriggerMethods |
指定对象的哪些方法触发一次延迟加载。 |
用逗号分隔的方法列表。 |
equals,clone,hashCode,toString |
配置文件中一个完整<settings>元素实例如下:
<settings> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="multipleResultSetsEnabled" value="true"/> <setting name="useColumnLabel" value="true"/> <setting name="useGeneratedKeys" value="false"/> <setting name="autoMappingBehavior" value="PARTIAL"/> <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/> <setting name="defaultExecutorType" value="SIMPLE"/> <setting name="defaultStatementTimeout" value="25"/> <setting name="defaultFetchSize" value="100"/> <setting name="safeRowBoundsEnabled" value="false"/> <setting name="mapUnderscoreToCamelCase" value="false"/> <setting name="localCacheScope" value="SESSION"/> <setting name="jdbcTypeForNull" value="OTHER"/> <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> </settings>
关于源码解析,此处实际开发中这些属性用到较少,自己比较感兴趣的是关于mybatis二级缓存的配置,后面会出一篇博客介绍cacheEnabled设置。内容太多,就不一一分析了,感兴趣的可以自己查看。
typeAliases(类型别名)
typeAliases用于对一个类型名进行别名设置,意在降低冗余的全限定类名书写。例如:
<typeAliases> <typeAlias type="com.xiaobing.pojo.SysUser" alias="_user"></typeAlias> </typeAliases>
使用时用_user代替com.xiaobing.pojo.SysUser。除了支持单条语句的定义,还支持在包下扫描需要设置别名的类。例如:
<typeAliases>
<package name="com.xiaobing.pojo"/>
</typeAliases>
package com.xiaobing.pojo; import org.apache.ibatis.type.Alias; @Alias("sysDept") public class SysDept { }
使用注解@Alias就能将此类的全限定类名设置一个别名。分析解析typeAliases源码:
private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 是否是package String typeAliasPackage = child.getStringAttribute("name"); // 获得包名 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); // 去指定包下查看使用@Alias了注解的类,进行类型别名注册 } else { // 单个类型的注册 String alias = child.getStringAttribute("alias"); // 获得别名 String type = child.getStringAttribute("type"); // 获得类型 try { Class<?> clazz = Resources.classForName(type); // 转为class对象 if (alias == null) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); // 进行类型别名注册 } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } }
别名注册用到了TypeAliasRegistry类,在分析类型转换器时忘了介绍这个类,这个类其实存放了一个map,用于存放别名对应的真实class类。在初始化时放置了常用的别名与之对应的class类。
typeHandlers(类型处理器)
类型处理器专门写了一篇博客去介绍,此处不做详解,见上篇文章。配置方式如下:
<!-- mybatisConfig.xml --> <typeHandlers> <typeHandler handler="com.xiaobing.custom.config.CustomTypeHandle"/> </typeHandlers>
@MappedJdbcTypes(JdbcType.VARCHAR) @MappedTypes(String.class) public class CustomTypeHandle extends BaseTypeHandler<String>{ @Override public void setNonNullParameter(PreparedStatement ps, int i, java.lang.String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i,parameter); } @Override public java.lang.String getNullableResult(ResultSet rs, java.lang.String columnName) throws SQLException { return rs.getString(columnName); } @Override public java.lang.String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public java.lang.String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } }
用户自定义了一个类型处理器,Java类型是String,Jdbc类型是VARCHAR。上篇博客关于注册类型处理器的过程,为先从TYPE_HANDLER_MAP中拿出Java类型对应的key为Jdbc类型value为类型处理器的map,然后在该map中放置自定义的Jdbc类型,类型处理器。注意此时使用的是map.put(k,v)。也就是说,类型注册器中对于一个java类型和一个jdbc类型,只存在唯一的类型处理器。所以上文自定义的Java类型是String,Jdbc类型是VARCHAR的类型处理器会覆盖默认的Java类型是String,Jdbc类型是VARCHAR的类型处理器。此处也讲清楚了网上说的自定义类型处理器将会覆盖已有的类型处理器。若不清楚,建议回看上篇博文。
分析解析typeHandlers源码:
private void typeHandlerElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 整个包注册 String typeHandlerPackage = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else { // 单类注册 String javaTypeName = child.getStringAttribute("javaType"); // 获取类型转换需转换的java名称 String jdbcTypeName = child.getStringAttribute("jdbcType"); // 获取类型转换需转换的jdbc名称 String handlerTypeName = child.getStringAttribute("handler"); // 获取转化器名称 Class<?> javaTypeClass = resolveClass(javaTypeName); // 获取java类型的class,此处可以是设置的别名 JdbcType jdbcType = resolveJdbcType(jdbcTypeName); // 获取jdbc类型 Class<?> typeHandlerClass = resolveClass(handlerTypeName); // 获取类型处理器的class if (javaTypeClass != null) { // 三种注册方式,见上篇博客关于typeHandle分析 if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } }
如解析配置文件时可见,在<typeHandler/>标签中除了指定类型处理器所在包外,还可以设置jdbc类型和java类型,此处设置了就自动忽略类型处理器所在类上的@MappedJdbcTypes,@MappedTypes注解了。因为都一个意思。
objectFactory(对象工厂)
每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。比如:
<!-- mybatis-config.xml --> <objectFactory type="org.mybatis.example.ExampleObjectFactory"> <property name="someProperty" value="100"/> </objectFactory>
// ExampleObjectFactory.java public class ExampleObjectFactory extends DefaultObjectFactory { public Object create(Class type) { return super.create(type); } public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) { return super.create(type, constructorArgTypes, constructorArgs); } public void setProperties(Properties properties) { super.setProperties(properties); } public <T> boolean isCollection(Class<T> type) { return Collection.class.isAssignableFrom(type); } }
DefaultObjectFactory类也比较简单,分析下吧。
public class DefaultObjectFactory implements ObjectFactory, Serializable { private static final long serialVersionUID = -8855120656740914948L; public <T> T create(Class<T> type) { return create(type, null, null); } // 根据类名创建对象 constructorArgTypes构造函数参数类型列表 constructorArgs构造函数参数列表 public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { Class<?> classToCreate = resolveInterface(type); @SuppressWarnings("unchecked") // we know types are assignable T created = (T) instantiateClass(classToCreate, constructorArgTypes, constructorArgs); return created; } public void setProperties(Properties properties) { // no props for default } // 利用class 的反射去创建对象实例 private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) { try { Constructor<T> constructor; if (constructorArgTypes == null || constructorArgs == null) { constructor = type.getDeclaredConstructor(); // 获取无参构造方法 if (!constructor.isAccessible()) { constructor.setAccessible(true); // 设置可写 } return constructor.newInstance(); // 实例化 } constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[constructorArgTypes.size()])); if (!constructor.isAccessible()) { constructor.setAccessible(true); } return constructor.newInstance(constructorArgs.toArray(new Object[constructorArgs.size()])); } catch (Exception e) { // 若有异常,抛出 StringBuilder argTypes = new StringBuilder(); if (constructorArgTypes != null) { for (Class<?> argType : constructorArgTypes) { argTypes.append(argType.getSimpleName()); argTypes.append(","); } } StringBuilder argValues = new StringBuilder(); if (constructorArgs != null) { for (Object argValue : constructorArgs) { argValues.append(String.valueOf(argValue)); argValues.append(","); } } throw new ReflectionException("Error instantiating " + type + " with invalid types (" + argTypes + ") or values (" + argValues + "). Cause: " + e, e); } } ...... }
如前文所讲,主要通过类的反射到构造方法,再去创建实例。
plugins(插件)
插件使用较少,不太熟悉,按照官方文档的解释。有点类似于拦截器,在某个动作执行前后做属于自己的业务操作。MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
<!-- mybatis-config.xml --> <plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin> </plugins>
// ExamplePlugin.java @Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) public class ExamplePlugin implements Interceptor { private Properties properties = new Properties(); public Object intercept(Invocation invocation) throws Throwable { // implement pre processing if need Object returnObject = invocation.proceed(); // implement post processing if need return returnObject; } public void setProperties(Properties properties) { this.properties = properties; } }
查看解析plugins的源码如下:
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); // 获取插件拦截器 Properties properties = child.getChildrenAsProperties(); // 插件设置的properties值 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); // 实例化 interceptorInstance.setProperties(properties); // 放入插件拦截器 configuration.addInterceptor(interceptorInstance); // 添加到configuration中 } } }
environments(环境配置)
environments允许用户设置多套环境,如开发环境,生成环境。
<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>
<environment id="develop">
<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>
注意配置时需要配置事务类型管理器,数据源。
事务类型处理器mybatis支持JDBC和MANAGED两种类型,其中JDBC是直接使用数据库的事务管理机制。MANAGED为空的实现,主要依赖于容器的事务管理,后面会出一个分析Spring模块的事务配置。对于数据源详情,请看我前面关于数据源分析的博客。此处,不做解释。
分析解析environments源码:
private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { // 没有指定environment,则选择默认环境 environment = context.getStringAttribute("default"); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); if (isSpecifiedEnvironment(id)) { // 找到设置的environment 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()); // 放入全局配置类中 } } } }
databaseIdProvider(数据库厂商标识)
暂且略过,以后有时间在补充
mappers(映射器)
Sql映射文件详解,mybatis关键,后文会着重分析sql映射文件。(懒癌发作了--)