看过此篇,你就赚到了。别人写的源码系列,虽然有源码,但是没有关键的图示和debug过程,只是讲了有啥用,不适用。在我写的MyBatis源码系列中,都会结合debug过程+图示来阐述,我们从SqlSessionFactoryBuilder说起。SqlSessionFactoryBuilder,见名知意,是SqlSessionFactory的建造者(Builder)。那么我们猜想,既然是建造SqlSessionFactory,如果让我去写,那么总需要提供一个全参数的建造方法和一些特定参数的建造方法。我们的猜想对与否?验证一下。
一、SqlSessionFactoryBuilder源码解析
在SqlSessionFactoryBuilder类中,我们可以看到如下的构造SqlSessionFactory的方法build(我们这里只说包含Reader的方法,InputStream分析方法类似):
public SqlSessionFactory build(Reader reader) { return this.build((Reader)reader, (String)null, (Properties)null); } public SqlSessionFactory build(Reader reader, String environment) { return this.build((Reader)reader, environment, (Properties)null); } public SqlSessionFactory build(Reader reader, Properties properties) { return this.build((Reader)reader, (String)null, properties); } public SqlSessionFactory build(Reader reader, String environment, Properties properties) { SqlSessionFactory var5; try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); var5 = this.build(parser.parse()); } catch (Exception var14) { throw ExceptionFactory.wrapException("Error building SqlSession.", var14); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException var13) { ; } } return var5; }
果不其然,跟我们的猜想一样,SqlSessionFactoryBuilder提供了构造SqlSessionFactory全量参数方法public SqlSessionFactory build(Reader reader, String environment, Properties properties),也提供了根据不同特殊要求提供的构造SqlSessionFactory的方法。其实,这一点都不需要惊讶。如果你们研究过其他框架的源码,就知道,这是一种很有效的方式,在jdk的源码中,很多的设计思路跟它一模一样,此处大家可以借鉴。
那我们按照总分的路线去深入研究build(建造)SqlSessionFactory 的方法,那就从全量参数方法开始整。
在全量参数方法build中,我们需要关注的只有两行:
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); var5 = this.build(parser.parse());
①XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
reader是一个文件流读取器,这是将文件转换为流后进行数据配置节点读取的。我们在解析xml中常用到。
environment是一个环境参数,MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中,这是用来实现多数据源配置使用的,默认情况下,使用默认default。在解析
properties作为可选参数,是用来进行属性覆盖的。如果我们想覆盖xml中
②var5 = this.build(parser.parse());
这行是根据xml配置文件的流读取器、使用的环境、设置的属性来得到XMLConfigBuilder,它的作用是什么?就是用来执行解析任务--解析我们篇一中db-core.xml中的内容。
二、XMLConfigBuilder(reader, environment, properties)解密
XMLConfigBuilder的构造函数如下(我们这里只分析使用Reader的构造函数,使用InputStream的同理):
public XMLConfigBuilder(Reader reader) { this((Reader)reader, (String)null, (Properties)null); } public XMLConfigBuilder(Reader reader, String environment) { this((Reader)reader, environment, (Properties)null); } public XMLConfigBuilder(Reader reader, String environment, Properties props) { this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); }
在XMLConfigBuilder中,我们也直接看最长的全参数的构造函数:
public XMLConfigBuilder(Reader reader, String environment, Properties props) { this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props); }
在这里,又构造了XPathParser,我们看下它的构造函数:
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) { this.commonConstructor(validation, variables, entityResolver); this.document = this.createDocument(new InputSource(reader)); }
这里四个参数分别用在了两个方法中(这两方法都在XPathParser类中),我们分别看一下:
①commonConstructor方法:
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { this.validation = validation; this.entityResolver = entityResolver; this.variables = variables; XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); }
在commonConstructor方法中,将构造出的实体查找器entityResolver(XMLMapperEntityResolver)设置给了XPathParser的entityResolver属性,构造了XPathFactory,以及使用XPathFactory构造了节点解析的xpath属性,所以它起了一个名字叫commonConstructor--公共的构造器方法。我们看图:
②createDocument方法:
private Document createDocument(InputSource inputSource) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(this.validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(this.entityResolver); builder.setErrorHandler(new ErrorHandler() { public void error(SAXParseException exception) throws SAXException { throw exception; } public void fatalError(SAXParseException exception) throws SAXException { throw exception; } public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(inputSource); } catch (Exception var4) { throw new BuilderException("Error creating document instance. Cause: " + var4, var4); } }
一看设置的这一堆估计一圈人都懵了,其实它就是指定工厂类按照何种规则去解析xml文档,没啥特殊的,大家可以自己写个示例用它去解析跟踪文档格式,这里就不细说。方法最后一句,builder.parse(inputSource),将我们的Reader包装成inputSource后,解析成文档树(文档工厂创建使用的设计模式就是典型的抽象工厂模式,大家可以学学)。我们看设置完属性的图:
我们可以看到,一行代码涉及到的后续过程挺多,但是,没什么可怕的。你深入研究后还觉得难吗?
这一行的作用就是完成了两个关键步骤:准备解析器,准备好解析的文档树。
其实,所有的配置文档解析都是一个套路,Spring中的源码解析也一样。
得到XPathParser后,我们XMLConfigBuilder构造函数的内容就全了,看代码:
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; }
这此构造函数中, 首先调用了XMLConfigBuilder父类的构造函数,传入了Configuration对象,我们看看这个对象的构造函数:
public Configuration() { this.safeRowBoundsEnabled = false; this.safeResultHandlerEnabled = true; this.mapUnderscoreToCamelCase = false; this.aggressiveLazyLoading = true; this.multipleResultSetsEnabled = true; this.useGeneratedKeys = false; this.useColumnLabel = true; this.cacheEnabled = true; this.callSettersOnNulls = false; this.localCacheScope = LocalCacheScope.SESSION; this.jdbcTypeForNull = JdbcType.OTHER; this.lazyLoadTriggerMethods = new HashSet(Arrays.asList("equals", "clone", "hashCode", "toString")); this.defaultExecutorType = ExecutorType.SIMPLE; this.autoMappingBehavior = AutoMappingBehavior.PARTIAL; this.variables = new Properties(); this.objectFactory = new DefaultObjectFactory(); this.objectWrapperFactory = new DefaultObjectWrapperFactory(); this.mapperRegistry = new MapperRegistry(this); this.lazyLoadingEnabled = false; this.interceptorChain = new InterceptorChain(); this.typeHandlerRegistry = new TypeHandlerRegistry(); this.typeAliasRegistry = new TypeAliasRegistry(); this.languageRegistry = new LanguageDriverRegistry(); this.mappedStatements = new Configuration.StrictMap("Mapped Statements collection"); this.caches = new Configuration.StrictMap("Caches collection"); this.resultMaps = new Configuration.StrictMap("Result Maps collection"); this.parameterMaps = new Configuration.StrictMap("Parameter Maps collection"); this.keyGenerators = new Configuration.StrictMap("Key Generators collection"); this.loadedResources = new HashSet(); this.sqlFragments = new Configuration.StrictMap("XML fragments parsed from previous mappers"); this.incompleteStatements = new LinkedList(); this.incompleteCacheRefs = new LinkedList(); this.incompleteResultMaps = new LinkedList(); this.incompleteMethods = new LinkedList(); this.cacheRefMap = new HashMap(); this.typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); this.typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); this.typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); this.typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); this.typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); this.typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); this.typeAliasRegistry.registerAlias("FIFO", FifoCache.class); this.typeAliasRegistry.registerAlias("LRU", LruCache.class); this.typeAliasRegistry.registerAlias("SOFT", SoftCache.class); this.typeAliasRegistry.registerAlias("WEAK", WeakCache.class); this.typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); this.typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); this.typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); this.typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); this.typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); this.typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); this.typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); this.typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); this.typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); this.typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); this.typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); this.typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); this.languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); this.languageRegistry.register(RawLanguageDriver.class); }
不要被这么长一堆吓住,其实很简单,就是对Configuration类对象属性进行初始化, this.typeAliasRegistry.registerAlias("xxx", xxx.class);只是给对应的注册类起了个别名。上面的内容我们用到再说。
我们回到XMLConfigBuilder构造函数:
构造函数第一句,super(new Configuration());我们看下XMLConfigBuilder父类BaseBuilder的构造函数:
public BaseBuilder(Configuration configuration) { this.configuration = configuration; this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); }
这里初始化了三个参数:configuration,typeAliasRegistry, typeHandlerRegistry。
其中typeAliasRegistry, typeHandlerRegistry就是使用了Configuration类构造函数初始化的对象:
this.typeHandlerRegistry = new TypeHandlerRegistry(); this.typeAliasRegistry = new TypeAliasRegistry();
一个是类型处理器注册类,一个是类型别名注册类,我们可以看下他们各自的构造函数。
TypeAliasRegistry别名注册类:它只是对我们常见的基本类型,数组等做了一个,保存在一个Map中(Map
public TypeAliasRegistry() { this.registerAlias("string", String.class); this.registerAlias("byte", Byte.class); this.registerAlias("long", Long.class); this.registerAlias("short", Short.class); this.registerAlias("int", Integer.class); this.registerAlias("integer", Integer.class); this.registerAlias("double", Double.class); this.registerAlias("float", Float.class); this.registerAlias("boolean", Boolean.class); this.registerAlias("byte[]", Byte[].class); this.registerAlias("long[]", Long[].class); this.registerAlias("short[]", Short[].class); this.registerAlias("int[]", Integer[].class); this.registerAlias("integer[]", Integer[].class); this.registerAlias("double[]", Double[].class); this.registerAlias("float[]", Float[].class); this.registerAlias("boolean[]", Boolean[].class); this.registerAlias("_byte", Byte.TYPE); this.registerAlias("_long", Long.TYPE); this.registerAlias("_short", Short.TYPE); this.registerAlias("_int", Integer.TYPE); this.registerAlias("_integer", Integer.TYPE); this.registerAlias("_double", Double.TYPE); this.registerAlias("_float", Float.TYPE); this.registerAlias("_boolean", Boolean.TYPE); this.registerAlias("_byte[]", byte[].class); this.registerAlias("_long[]", long[].class); this.registerAlias("_short[]", short[].class); this.registerAlias("_int[]", int[].class); this.registerAlias("_integer[]", int[].class); this.registerAlias("_double[]", double[].class); this.registerAlias("_float[]", float[].class); this.registerAlias("_boolean[]", boolean[].class); this.registerAlias("date", Date.class); this.registerAlias("decimal", BigDecimal.class); this.registerAlias("bigdecimal", BigDecimal.class); this.registerAlias("biginteger", BigInteger.class); this.registerAlias("object", Object.class); this.registerAlias("date[]", Date[].class); this.registerAlias("decimal[]", BigDecimal[].class); this.registerAlias("bigdecimal[]", BigDecimal[].class); this.registerAlias("biginteger[]", BigInteger[].class); this.registerAlias("object[]", Object[].class); this.registerAlias("map", Map.class); this.registerAlias("hashmap", HashMap.class); this.registerAlias("list", List.class); this.registerAlias("arraylist", ArrayList.class); this.registerAlias("collection", Collection.class); this.registerAlias("iterator", Iterator.class); this.registerAlias("ResultSet", ResultSet.class); }
如图:
我们再看看TypeHandlerRegistry类型处理器注册类,它是将java类型和JdbcType都标记出对应的类型处理器,并且把对应的类型处理器都注册在各自的Map中:
public TypeHandlerRegistry() { this.register((Class)Boolean.class, (TypeHandler)(new BooleanTypeHandler())); this.register((Class)Boolean.TYPE, (TypeHandler)(new BooleanTypeHandler())); this.register((JdbcType)JdbcType.BOOLEAN, (TypeHandler)(new BooleanTypeHandler())); this.register((JdbcType)JdbcType.BIT, (TypeHandler)(new BooleanTypeHandler())); this.register((Class)Byte.class, (TypeHandler)(new ByteTypeHandler())); this.register((Class)Byte.TYPE, (TypeHandler)(new ByteTypeHandler())); this.register((JdbcType)JdbcType.TINYINT, (TypeHandler)(new ByteTypeHandler())); this.register((Class)Short.class, (TypeHandler)(new ShortTypeHandler())); this.register((Class)Short.TYPE, (TypeHandler)(new ShortTypeHandler())); this.register((JdbcType)JdbcType.SMALLINT, (TypeHandler)(new ShortTypeHandler())); this.register((Class)Integer.class, (TypeHandler)(new IntegerTypeHandler())); this.register((Class)Integer.TYPE, (TypeHandler)(new IntegerTypeHandler())); this.register((JdbcType)JdbcType.INTEGER, (TypeHandler)(new IntegerTypeHandler())); this.register((Class)Long.class, (TypeHandler)(new LongTypeHandler())); this.register((Class)Long.TYPE, (TypeHandler)(new LongTypeHandler())); this.register((Class)Float.class, (TypeHandler)(new FloatTypeHandler())); this.register((Class)Float.TYPE, (TypeHandler)(new FloatTypeHandler())); this.register((JdbcType)JdbcType.FLOAT, (TypeHandler)(new FloatTypeHandler())); this.register((Class)Double.class, (TypeHandler)(new DoubleTypeHandler())); this.register((Class)Double.TYPE, (TypeHandler)(new DoubleTypeHandler())); this.register((JdbcType)JdbcType.DOUBLE, (TypeHandler)(new DoubleTypeHandler())); this.register((Class)String.class, (TypeHandler)(new StringTypeHandler())); this.register((Class)String.class, JdbcType.CHAR, (TypeHandler)(new StringTypeHandler())); this.register((Class)String.class, JdbcType.CLOB, (TypeHandler)(new ClobTypeHandler())); this.register((Class)String.class, JdbcType.VARCHAR, (TypeHandler)(new StringTypeHandler())); this.register((Class)String.class, JdbcType.LONGVARCHAR, (TypeHandler)(new ClobTypeHandler())); this.register((Class)String.class, JdbcType.NVARCHAR, (TypeHandler)(new NStringTypeHandler())); this.register((Class)String.class, JdbcType.NCHAR, (TypeHandler)(new NStringTypeHandler())); this.register((Class)String.class, JdbcType.NCLOB, (TypeHandler)(new NClobTypeHandler())); this.register((JdbcType)JdbcType.CHAR, (TypeHandler)(new StringTypeHandler())); this.register((JdbcType)JdbcType.VARCHAR, (TypeHandler)(new StringTypeHandler())); this.register((JdbcType)JdbcType.CLOB, (TypeHandler)(new ClobTypeHandler())); this.register((JdbcType)JdbcType.LONGVARCHAR, (TypeHandler)(new ClobTypeHandler())); this.register((JdbcType)JdbcType.NVARCHAR, (TypeHandler)(new NStringTypeHandler())); this.register((JdbcType)JdbcType.NCHAR, (TypeHandler)(new NStringTypeHandler())); this.register((JdbcType)JdbcType.NCLOB, (TypeHandler)(new NClobTypeHandler())); this.register((Class)Object.class, JdbcType.ARRAY, (TypeHandler)(new ArrayTypeHandler())); this.register((JdbcType)JdbcType.ARRAY, (TypeHandler)(new ArrayTypeHandler())); this.register((Class)BigInteger.class, (TypeHandler)(new BigIntegerTypeHandler())); this.register((JdbcType)JdbcType.BIGINT, (TypeHandler)(new LongTypeHandler())); this.register((Class)BigDecimal.class, (TypeHandler)(new BigDecimalTypeHandler())); this.register((JdbcType)JdbcType.REAL, (TypeHandler)(new BigDecimalTypeHandler())); this.register((JdbcType)JdbcType.DECIMAL, (TypeHandler)(new BigDecimalTypeHandler())); this.register((JdbcType)JdbcType.NUMERIC, (TypeHandler)(new BigDecimalTypeHandler())); this.register((Class)Byte[].class, (TypeHandler)(new ByteObjectArrayTypeHandler())); this.register((Class)Byte[].class, JdbcType.BLOB, (TypeHandler)(new BlobByteObjectArrayTypeHandler())); this.register((Class)Byte[].class, JdbcType.LONGVARBINARY, (TypeHandler)(new BlobByteObjectArrayTypeHandler())); this.register((Class)byte[].class, (TypeHandler)(new ByteArrayTypeHandler())); this.register((Class)byte[].class, JdbcType.BLOB, (TypeHandler)(new BlobTypeHandler())); this.register((Class)byte[].class, JdbcType.LONGVARBINARY, (TypeHandler)(new BlobTypeHandler())); this.register((JdbcType)JdbcType.LONGVARBINARY, (TypeHandler)(new BlobTypeHandler())); this.register((JdbcType)JdbcType.BLOB, (TypeHandler)(new BlobTypeHandler())); this.register(Object.class, this.UNKNOWN_TYPE_HANDLER); this.register(Object.class, JdbcType.OTHER, this.UNKNOWN_TYPE_HANDLER); this.register(JdbcType.OTHER, this.UNKNOWN_TYPE_HANDLER); this.register((Class)Date.class, (TypeHandler)(new DateTypeHandler())); this.register((Class)Date.class, JdbcType.DATE, (TypeHandler)(new DateOnlyTypeHandler())); this.register((Class)Date.class, JdbcType.TIME, (TypeHandler)(new TimeOnlyTypeHandler())); this.register((JdbcType)JdbcType.TIMESTAMP, (TypeHandler)(new DateTypeHandler())); this.register((JdbcType)JdbcType.DATE, (TypeHandler)(new DateOnlyTypeHandler())); this.register((JdbcType)JdbcType.TIME, (TypeHandler)(new TimeOnlyTypeHandler())); this.register((Class)java.sql.Date.class, (TypeHandler)(new SqlDateTypeHandler())); this.register((Class)Time.class, (TypeHandler)(new SqlTimeTypeHandler())); this.register((Class)Timestamp.class, (TypeHandler)(new SqlTimestampTypeHandler())); this.register((Class)Character.class, (TypeHandler)(new CharacterTypeHandler())); this.register((Class)Character.TYPE, (TypeHandler)(new CharacterTypeHandler())); }
如图:
最终设置完的XMLConfigBuilder属性如图,其中最关键的就是configuration,这是所有解析配置的核心的核心。
这里props我们没有传值为null,到了解析properties标签属性时,大家就可以看到这个值做什么用了。
三、【核心方法】this.build(parser.parse())解密
我们先看parser.parse()。parser指的是我们第一步得到的XMLConfigBuilder,在它的parse方法中:
public Configuration parse() { if (this.parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } else { this.parsed = true; this.parseConfiguration(this.parser.evalNode("/configuration")); return this.configuration; } }
里面的关键方法this.parseConfiguration(this.parser.evalNode("/configuration")):
this.parser.evalNode("/configuration")标记出根节点,然后从根节点开始解析各属性。
这里就是MyBatis的核心配置文件中可以配置的所有节点啦!!
比如,我们配置的properties节点,就是使用 this.propertiesElement(root.evalNode("properties"));来解析,这里面就要用到我们核心的核心---configuration对象,一个对象包含了很多信息。这一个解析方法包含了我们db-core.xml文件的所有解析内容。
private void parseConfiguration(XNode root) { try { this.propertiesElement(root.evalNode("properties")); this.typeAliasesElement(root.evalNode("typeAliases")); this.pluginElement(root.evalNode("plugins")); this.objectFactoryElement(root.evalNode("objectFactory")); this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); this.settingsElement(root.evalNode("settings")); this.environmentsElement(root.evalNode("environments")); this.databaseIdProviderElement(root.evalNode("databaseIdProvider")); this.typeHandlerElement(root.evalNode("typeHandlers")); this.mapperElement(root.evalNode("mappers")); } catch (Exception var3) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3); } }
这里的每一个解析方法都可以作为作为一节来讲,我们后续帖逐渐来阐述。
解析完核心配置文件的所有标签后,我们的configuration对象信息就都完善了,此时SqlSessionFactoryBuilder的build开始执行,去得到SqlSessionFactory,这里使用了DefaultSqlSessionFactory。
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
在这里,只是把Configuration对象传到了DefaultSqlSessionFactory的构造方法中,这样SqlSessionFactory接口终于有了自己的实现类DefaultSqlSessionFactory。
以上就是SqlSessionFactory获取的所有步骤,虽然过程繁杂,其实很简单。
四、properties标签解析和elements标签解析
先来说两个我们用到的,properties标签解析和elements标签解析,解答我们开头说的问题。
1、properties标签的解析
properties标签的解析使用的方法是this.propertiesElement(root.evalNode("properties"))。
首先使用root.evalNode("properties")得到我们properties标签的内容,因此context值为:
其中,Properties defaults = context.getChildrenAsProperties()是用来获取properties标签下的子元素内容的,比如:
通过这一行代码,就可以获取到property元素内容,存放在Properties(它是一个hashTable)中。
这里我们没有子元素,因此,defaults中没有内容。
我们继续往下,在properties标签中,我们使用了resource属性,未使用url属性。
String resource = context.getStringAttribute("resource"); String url = context.getStringAttribute("url");
因此,上面得到的resource值为db-info.properties,url为null。下面的判断也告诉我们,在properties标签中,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."); }
这里我们resource不为空,使用Resources.getResourceAsProperties(resource)获取到属性的key-value值,全部放在defaults中。在db-info.properties中,我们放了数据源的四个属性,因此,这里最后的大小为4。
if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); }
下面,从configuration对象中,获取到Variables内容,这个内容是啥?