【Mybatis】【配置文件解析】【一】Mybatis源码解析-properties和settings的解析
1 前言
好了,这章开始我们进入Mybatis的知识的源码分析,首当其冲的就是配置文件的解析,我们知道在实用Mybatis的时候,我们的自定义sql都是在XML文件中进行编写的,甚至以前我们的一些数据源信息也是在里边的,那么我们就看看Mybatis是如何解析的吧。
首先我们来看下XML可以配置哪些东西,知道有哪些东西我们才知道解析哪些东西对不对。
中文官网哈:https://mybatis.org/mybatis-3/zh/configuration.html
可以看到哈,大概有9个部分,那么下面我们就来看看解析的源码。
2 源码分析
2.1 入口来源
那么你知道入口在哪里么,比如哪个类是发起点呢?其实就是我们的SqlSessionFactoryBuilder,看一段下边的代码熟悉么,通过SqlSessionFactoryBuilder构建我们的SqlSessionFactory。
@Test public void test() throws IOException { // 配置文件 String resource = "Config.xml"; // 加载成流 InputStream resourceAsStream = Resources.getResourceAsStream(resource); // 通过SqlSessionFactoryBuilder构建SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); System.out.println(sqlSessionFactory); }
那么进入到SqlSessionFactoryBuilder里边看一下:
// 构建SqlSessionFactory public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { // 创建XML解析器 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); // parser.parse()就是解析我们的XML,生成Configuration也就是我们的配置对象,然后构建SqlSessionFactory return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
所以我们最后负责解析我们的XML的类就是XMLConfigBuilder,采用的是XPath解析。引申一点:解析XML的方式除了XPath还有Dom4j等。
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // 从根节点configuration开始解析 parseConfiguration(parser.evalNode("/configuration")); return configuration; } // 这就是解析的方法 每一部分又都交给每个小弟去解析了 private void parseConfiguration(XNode root) { try { // 解析 properties 配置 propertiesElement(root.evalNode("properties")); // 解析 settings 配置,并将其转换为 Properties 对象 Properties settings = settingsAsProperties(root.evalNode("settings")); // 加载 vfs loadCustomVfs(settings); loadCustomLogImpl(settings); // 解析 typeAliases 配置 typeAliasesElement(root.evalNode("typeAliases")); // 解析 plugins 配置 pluginElement(root.evalNode("plugins")); // 解析 objectFactory 配置 objectFactoryElement(root.evalNode("objectFactory")); // 解析 objectWrapperFactory 配置 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 解析 reflectorFactory 配置 reflectorFactoryElement(root.evalNode("reflectorFactory")); // settings 中的信息设置到 Configuration 对象中 settingsElement(settings); // 解析 environments 配置 environmentsElement(root.evalNode("environments")); // 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 解析 typeHandlers 配置 typeHandlerElement(root.evalNode("typeHandlers")); // 解析 mappers 配置 啧啧啧重点啊 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
好了,那我们接下里可能会分两三节来逐步解析我们的XML,因为都塞一个文章里,怕你们看的累哈。
2.2 解析 properties
我们先来看下properties的dtd约束:
<!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
>
<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
>
可以看到properties上边可以有url和resource两个属性值,下边可以有多个property子节点,property子节点又有name和value两个属性值。
那么我们来看下properties的解析:
/** * * @param context * @throws Exception
* * blog-derby.properties: * driver=org.apache.derby.jdbc.EmbeddedDriver * url=jdbc:derby:ibderby;create=true * username=root123 * password=root123 * * <properties resource="blog-derby.properties"> * <property name="jdbc.username" value="root"/> * <property name="hello" value="root"/> * </properties> * * Properties类的数据类型:!!!可以看到他继承的Hashtable * Properties extends Hashtable<Object,Object> */ private void propertiesElement(XNode context) throws Exception { if (context != null) { // 这个就是加载 properties 下的每个 property 属性 Properties defaults = context.getChildrenAsProperties(); // 这是获取 properties 上的 resource 和 url 的两个属性值 String resource = context.getStringAttribute("resource"); String url = context.getStringAttribute("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."); } /** * 分别根据 resource 或者 url 去加载指定的配置 * 这里就涉及到覆盖 也就是说 resource 或者 url 里的配置会覆盖掉 Properties下的 property 的属性值 * 简而言之 比如 blog-derby.properties 的 jdbc.username值 会覆盖掉 property 中的jdbc.username值 */ if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); } // 当前的配置值和 configuration 上的键值对进行合并 Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); // 合并完设置回去 configuration.setVariables(defaults); } }
看上去解析的内容还是比较简单的,大概的整体执行过程是:
- 获取properties下的每个property的属性值;
- 再获取properties的属性resource或者url值,注意两者不能同时存在;
- 根据不同的属性值去加载对应的配置值,如果都没有配置那就不用去加载了,有的话这里会覆盖掉第一步里边的属性值;
- 合并configuration上的属性值,并设置回去
整体上就是这样,那么我们再来看下分别对url或者resource的解析。
2.2.1 resource属性值的解析
public static Properties getResourceAsProperties(String resource) throws IOException { Properties props = new Properties(); // 还是通过加载流的方式进行读取解析成Properties try (InputStream in = getResourceAsStream(resource)) { props.load(in); } return props; }
2.2.2 url属性值的解析
public static Properties getUrlAsProperties(String urlString) throws IOException { Properties props = new Properties(); try (InputStream in = getUrlAsStream(urlString)) { props.load(in); } return props; } public static InputStream getUrlAsStream(String urlString) throws IOException { URL url = new URL(urlString); // 通过请求的方式获取到流 URLConnection conn = url.openConnection(); return conn.getInputStream(); }
2.3 解析 settings
settings的dtd约束如下:
<!ELEMENT settings (setting+)>
<!ELEMENT setting EMPTY>
<!ATTLIST setting
name CDATA #REQUIRED
value CDATA #REQUIRED
>
可以看到settings下可以一个或者多个setting,那么我们来看下settings的解析:
private Properties settingsAsProperties(XNode context) { if (context == null) { return new Properties(); } // 获取 settings 下的每个 setting 的属性值 Properties props = context.getChildrenAsProperties(); // 创建 Configuration 类的元信息对象 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); // 每个 setting 的 name 属性都要有对应的 setter 方法 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; }
这里我们再来看下configuration的元信息对象是干什么的,
// 私有构造器 private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) { this.reflectorFactory = reflectorFactory; // this.reflector = reflectorFactory.findForClass(type); } public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) { // 调用私有构造器 return new MetaClass(type, reflectorFactory); }
会发现两个新的类Reflector和ReflectorFactory从名字上猜大概就是用于解析类信息的,ReflectorFactory的默认实现又是DefaultReflectorFactory,关系是什么样的呢:
- ReflectorFactory -> Reflector 的工厂类,兼有缓存 Reflector 对象的功能 DefaultReflectorFactory是它的默认实现
- Reflector -> 反射器,用于解析和存储目标类中的元信息
那么我们看下DefaultReflectorFactory怎么得到Reflector的:
private boolean classCacheEnabled = true; // 缓存 private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<>(); @Override public Reflector findForClass(Class<?> type) { // 是否开启缓存默认开启 if (classCacheEnabled) { // 有就从缓存中拿, 没有就创建 return reflectorMap.computeIfAbsent(type, Reflector::new); } else { return new Reflector(type); } }
那么我们再来看下Reflector的实例化:
public class Reflector { private final Class<?> type; private final String[] readablePropertyNames; private final String[] writablePropertyNames; private final Map<String, Invoker> setMethods = new HashMap<>(); private final Map<String, Invoker> getMethods = new HashMap<>(); private final Map<String, Class<?>> setTypes = new HashMap<>(); private final Map<String, Class<?>> getTypes = new HashMap<>(); private Constructor<?> defaultConstructor; private Map<String, String> caseInsensitivePropertyMap = new HashMap<>(); public Reflector(Class<?> clazz) { type = clazz; // 默认的构造器 addDefaultConstructor(clazz); // get方法 addGetMethods(clazz); // set方法 addSetMethods(clazz); // 属性字段 addFields(clazz); // 从 getMethods 映射中获取可读属性名数组 readablePropertyNames = getMethods.keySet().toArray(new String[0]); // 从 setMethods 映射中获取可写属性名数组 writablePropertyNames = setMethods.keySet().toArray(new String[0]); // 将所有属性名的大写形式作为键,属性名作为值,存入到 caseInsensitivePropertyMap 中 for (String propName : readablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } for (String propName : writablePropertyNames) { caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); } } }
可以看到一个Reflector实例就是某一个类的所有信息汇总丫,是不是,针对Reflector是怎么解析汇总类的信息的,我会单独拿出来一节讲哈。
那么让我们再看下metaConfig.hasSetter(String.valueOf(key))是怎么判断的:
public boolean hasSetter(String name) { // 属性分词器 PropertyTokenizer prop = new PropertyTokenizer(name); // 属性是否含有嵌套比如:jdbc.name.xxx 这样的就是属性嵌套了 if (prop.hasNext()) { // 先判断 当前的 name是否有 setter 方法 if (reflector.hasSetter(prop.getName())) { // 有的话获取到 当前属性的一个 MetaClass 然后进行 递归的查询 MetaClass metaProp = metaClassForProperty(prop.getName()); return metaProp.hasSetter(prop.getChildren()); } else { // 当前name没有 setter方法会直接返回 return false; } } else { // 没有属性嵌套的话 会直接判断当时的属性是否存在 setter 方法 return reflector.hasSetter(prop.getName()); } }
Reflector判断某个属性是否含有setter方法就比较简单了,因为它在初始化的时候,就把类的各种信息都解析汇总好了:
public boolean hasSetter(String propertyName) { return setMethods.containsKey(propertyName); }
好那么关于settings的解析就结束了,setting解析完要放进configuration的,我们来看下。
2.4 设置 settings 配置到 Configuration 中
这个步骤比较简单,就是设置,那么我们来看下都有哪些属性的配置:
private void settingsElement(Properties props) { 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"), false)); 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.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType"))); 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.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler"))); configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false)); configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true)); configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false)); configuration.setLogPrefix(props.getProperty("logPrefix")); configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory"))); configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false)); configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType"))); }
你可能会问了,这些属性都是干什么的,表达什么意思呢,官网其实就有我们看下:
设置名 | 描述 | 有效值 | 默认值 |
---|---|---|---|
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 |
lazyLoadTriggerMethods | 指定对象的哪些方法触发一次延迟加载。 | 用逗号分隔的方法列表。 | equals,clone,hashCode,toString |
defaultScriptingLanguage | 指定动态 SQL 生成使用的默认脚本语言。 | 一个类型别名或全限定类名。 | org.apache.ibatis.scripting.xmltags.XMLLanguageDriver |
defaultEnumTypeHandler | 指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5) |
一个类型别名或全限定类名。 | org.apache.ibatis.type.EnumTypeHandler |
callSettersOnNulls | 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 | true | false | false |
returnInstanceForEmptyRow | 当返回行的所有列都是空时,MyBatis默认返回 null 。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2) |
true | false | false |
logPrefix | 指定 MyBatis 增加到日志名称的前缀。 | 任何字符串 | 未设置 |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J(3.5.9 起废弃) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | 未设置 |
proxyFactory | 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 | CGLIB (3.5.10 起废弃) | JAVASSIST | JAVASSIST (MyBatis 3.3 以上) |
vfsImpl | 指定 VFS 的实现 | 自定义 VFS 的实现的类全限定名,以逗号分隔。 | 未设置 |
useActualParamName | 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) |
true | false | true |
configurationFactory | 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) |
一个类型别名或完全限定类名。 | 未设置 |
shrinkWhitespacesInSql | 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5) | true | false | false |
defaultSqlProviderType | 指定一个拥有 provider 方法的 sql provider 类 (新增于 3.5.6). 这个类适用于指定 sql provider 注解上的type (或 value ) 属性(当这些属性在注解中被忽略时)。 (e.g. @SelectProvider ) |
类型别名或者全限定名 | 未设置 |
nullableOnForEach | 为 'foreach' 标签的 'nullable' 属性指定默认值。(新增于 3.5.9) | true | false | false |
argNameBasedConstructorAutoMapping | 当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。(新增于 3.5.10) | true | false | false |
3 小结
好我们本节,就暂时介绍两个的配置解析一个是properties、一个是settings,下节我们继续,有理解不会的地方欢迎指正哈。