【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); } }

看上去解析的内容还是比较简单的,大概的整体执行过程是:

  1. 获取properties下的每个property的属性值;
  2. 再获取properties的属性resource或者url值,注意两者不能同时存在;
  3. 根据不同的属性值去加载对应的配置值,如果都没有配置那就不用去加载了,有的话这里会覆盖掉第一步里边的属性值;
  4. 合并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,关系是什么样的呢:

  1. ReflectorFactory -> Reflector 的工厂类,兼有缓存 Reflector 对象的功能 DefaultReflectorFactory是它的默认实现
  2. 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: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN
  • FAILING: 映射失败 (抛出 SqlSessionException)
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,下节我们继续,有理解不会的地方欢迎指正哈。

 

posted @ 2023-02-25 19:32  酷酷-  阅读(134)  评论(0编辑  收藏  举报