一、初探Configuration类
我们先来看一下MyBatis的XML配置文件的结构,(摘自mybatis.org)
下面这个是Configuration类的部分变量
一点不一样是不是???
其实Configuration类是由XMLConfigBuilder(继承自BaseBuilder类)解析而来的,由如下方法(parseConfiguration)解析
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectionFactoryElement(root.evalNode("reflectionFactory"));
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);
}
}
该方法的调用者是同一个类的parse()方法,在第一篇笔记里面就提到过,SqlSessionFactoryBuilder类负责构建SqlSessionFactory,这其中重要的一步就是解析配置文件,调用的正是这个parse()方法,调用链如下:
回到parseConfiguration方法,该方法负责将XML文件中的信息解析到Configuration类的变量中,使其一一对应起来,下面是最后一个方法mapperElement(root.evalNode("mappers"))的实现,负责读取<mappers>节点,其他的方法也是同样的作用
Configuration类就像是MyBatis的总管,里面包含了所有的信息,有一些属性不设置也不会影响configuration的构建,因为MyBatis会给这些属性赋上默认值,以保证MyBatis能够正常运行。
二、各个配置解析
1. properties全局参数
官方样例
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties> properties参数由propertiesElement()方法进行解析,方法实现如下:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
//注意,resource属性和url属性不能同时存在,否则将抛出无法解析的异常
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
//后面读取的属性会覆盖原来已有的,因为properties继承自HashTable,还是键值对,后put的值会覆盖之前put进来的值
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
//如果Configuration对象中variables属性不为空,则将其添加到properties对象中
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
//最后将这些参数保存至Configuration对象中
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
根据以上信息我们可以得出:
通过代码设置的configuration参数的优先级最高,因为他是在方法的最后面将那些值put进去的,然后就是properties子节点的优先级最低,最先被记载,也最容易被后面的参数覆盖,所以,如果采取读取外部文件的方式(resource或者url方式),子节点最好不要和其他参数重复。
2.settings
<settings>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true" />
<!--当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载,反之,每种属性都会按需加载-->
<setting name="aggressiveLazyLoading" value="true" />
</settings>
settings先是由settingsAsPropertiess()方法解析成properties对象,然后再由loadCustomVfs()方法和settingsElement()方法分别解析,实现如下:
//这个方法用来将settings节点解析成properties对象
private Properties settingsAsPropertiess(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
} //VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
//Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序。
//VFS用的是单例模式,有兴趣的可以去了解些
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
String value = props.getProperty("vfsImpl");
if (value != null) {
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
configuration.setVfsImpl(Resources.classForName(clazz));
}
}
}
} //处理每一个设置子节点,如果没有设置则赋上默认值
private void settingsElement(Properties props) throws Exception {
configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
3.typeAliases别名
类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余。
它还有一个 <package name="com.ys.po" /> 标签,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名
配置了类型别名之后我们使用resulttype="Alias"就不用些全限定名了,非常的方便。
我们可以自己定义别名,这个大部分是自己的实体类,MyBatis也为我们默认设置了一下别名(大部分在TypeAliasRegistry.class中),包括常见的int,byte等类型,还有settings里面的一些value值,比如LRU等,详细的别名设置可以参考官方,官网给出的非常详细了,再次不再赘述,下面贴一下Configuration构造方法设置的别名。
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
总结:
不管是通过 package 标签配置,还是通过 typeAlias 标签配置的别名,在mapper.xml文件中使用的时候,转换成小写是相等的,那么就可以使用。
如果不手动设置别名,默认是类名的小写。
如果配置了注解别名,注解别名会覆盖上面的所有配置。
4.typeHandler类型处理器
由于数据库可能来自不同的厂商,不同厂商设置的参数可能有所不同,同时数据库也可以自定义类型,typeHandler允许根据项目需要自定义设置java传递到数据库的参数中,或者从数据库中读取数据,我们也需要进行特殊处理这些都可以在typeHandler中实现,尤其是枚举。
说白了,typehandler就是完成数据库类型和java类型的相互转换,与typeAliases一样,typehandler也分为系统定义和用户定义两种,一般来说,系统定义的类型处理器已经可以完成大部分功能。
以下均为系统定义的处理器:
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");
String jdbcTypeName = child.getStringAttribute("jdbcType");
String handlerTypeName = child.getStringAttribute("handler");
Class<?> javaTypeClass = resolveClass(javaTypeName);
JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
Class<?> typeHandlerClass = resolveClass(handlerTypeName);
if (javaTypeClass != null) {
if (jdbcType == null) {
typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
} else {
typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
}
} else {
typeHandlerRegistry.register(typeHandlerClass);
}
}
}
}
}
TypeHandlerRegistry.calss类中注册的处理器,都是常见的类型:
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler()); register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler()); register(Short.class, new ShortTypeHandler());
register(short.class, new ShortTypeHandler());
register(JdbcType.SMALLINT, new ShortTypeHandler()); register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler()); register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler()); register(Float.class, new FloatTypeHandler());
register(float.class, new FloatTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler()); register(Double.class, new DoubleTypeHandler());
register(double.class, new DoubleTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler()); register(Reader.class, new ClobReaderTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(JdbcType.NVARCHAR, new NStringTypeHandler());
register(JdbcType.NCHAR, new NStringTypeHandler());
register(JdbcType.NCLOB, new NClobTypeHandler()); register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
register(JdbcType.ARRAY, new ArrayTypeHandler()); register(BigInteger.class, new BigIntegerTypeHandler());
register(JdbcType.BIGINT, new LongTypeHandler()); register(BigDecimal.class, new BigDecimalTypeHandler());
register(JdbcType.REAL, new BigDecimalTypeHandler());
register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
register(JdbcType.NUMERIC, new BigDecimalTypeHandler()); register(InputStream.class, new BlobInputStreamTypeHandler());
register(Byte[].class, new ByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
register(byte[].class, new ByteArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.BLOB, new BlobTypeHandler()); register(Object.class, UNKNOWN_TYPE_HANDLER);
register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER); register(Date.class, new DateTypeHandler());
register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
register(JdbcType.DATE, new DateOnlyTypeHandler());
register(JdbcType.TIME, new TimeOnlyTypeHandler()); register(java.sql.Date.class, new SqlDateTypeHandler());
register(java.sql.Time.class, new SqlTimeTypeHandler());
register(java.sql.Timestamp.class, new SqlTimestampTypeHandler()); // mybatis-typehandlers-jsr310
try {
register("java.time.Instant", "org.apache.ibatis.type.InstantTypeHandler");
register("java.time.LocalDateTime", "org.apache.ibatis.type.LocalDateTimeTypeHandler");
register("java.time.LocalDate", "org.apache.ibatis.type.LocalDateTypeHandler");
register("java.time.LocalTime", "org.apache.ibatis.type.LocalTimeTypeHandler");
register("java.time.OffsetDateTime", "org.apache.ibatis.type.OffsetDateTimeTypeHandler");
register("java.time.OffsetTime", "org.apache.ibatis.type.OffsetTimeTypeHandler");
register("java.time.ZonedDateTime", "org.apache.ibatis.type.ZonedDateTimeTypeHandler");
} catch (ClassNotFoundException e) {
// no JSR-310 handlers
} // issue #273
register(Character.class, new CharacterTypeHandler());
register(char.class, new CharacterTypeHandler());
}
5.mappers映射器
映射器是MyBatis最复杂、最核心的组件。
映射器提供DAO层接口到mapper.xml文件的映射,我们只需要调用DAO层的方法,就可以以执行对应的SQL语句,这里用到的是java的动态代理特性,我们先来看下config配置文件是中的mapper设置:
<!-- Using classpath relative resources -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
接下来看一下源码:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//首先判断pckage子元素是否存在,存在则解析之,一般package用的不多
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//重点是这个else里面的解析,分别解析resource,url,class
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//从下面这个if...else...可以看出,resource,url,class三个属性只能存在一个,否则将会抛出异常
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
//我们最常用的就是resource属性,下面我们追溯一下这个parse()方法
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
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里面放的是mapper.xml文件,MyBatis会根据这个xml文件去解析里面的DAO层接口,以及里面的SQL语句和相应的设置,下面是parse()源码:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//这个解析mapper里面的各个元素,比如cache-ref、parameterMap、resultMap、sql、select|insert|update|delete等
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//这个方法是根据namespace将这个mapper和DAO层接口绑定起来
bindMapperForNamespace();
}
//这里面会分别解析...,见名知意了
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
这个parse()还是比较复杂的,特别是对resultMap标签的解析,以及对于 select|insert|update|delete 标签的解析,后续可能还会单独着重介绍下的。
6.其他的几个配置
objectFactory
当MyBatis在构建一个结果返回的时候,都会使用objectFactory去构建POJO,当然,我们也可以自己去定制自己的对象工厂,不过一般来说,使用默认的就够了。
plugins
插件,例如分页插件等,这个还是比较复杂的,使用的时候要特别小心,使用插件将覆盖一些MyBatis内部核心对象的行为,最后能深入理解MyBatis内部运行原理之后在使用之。
environment
数据源配置,可配置多个数据源,还有事务配置,后续会详细讲解之
databasedIdProvider
数据库厂商标识一般用的较少,因为我们的系统同时运行在两个数据库厂商的可能性比较小。