在单独使用 MyBatis 时,第一步要做的事情就是根据配置文件构建SqlSessionFactory
对象。
x1// 使用工具类 Resources 将配置文件转换为输入流
2InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
3
4// 使用构造器构造 SqlSessionFactory
5SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
显然,这里的 build 方法是我们分析核心配置文件解析过程的入口方法。
301// -☆- SqlSessionFactoryBuilder
2public SqlSessionFactory build(InputStream inputStream) {
3 // 调用重载方法
4 return build(inputStream, null, null);
5}
6
7public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
8 try {
9 // 创建XML配置文件解析器
10 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
11
12 // 调用 parse 方法解析配置文件,生成 Configuration 对象
13 return build(parser.parse());
14 } catch (Exception e) {
15 throw ExceptionFactory.wrapException("Error building SqlSession.", e);
16 } finally {
17 ErrorContext.instance().reset();
18 try {
19 inputStream.close();
20 } catch (IOException e) {
21 // Intentionally ignore. Prefer previous error.
22 }
23 }
24}
25
26public SqlSessionFactory build(Configuration config) {
27 // 创建 DefaultSqlSessionFactory
28 return new DefaultSqlSessionFactory(config);
29}
30
从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过XMLConfigBuilder
进行解析的。不过目前这里还没有非常明确的解析逻辑,所以我们继续往下看parse方法。
121// -☆- XMLConfigBuilder
2public Configuration parse() {
3 if (parsed) {
4 throw new BuilderException("Each XMLConfigBuilder can only be used once.");
5 }
6 parsed = true;
7
8 // 解析配置
9 parseConfiguration(parser.evalNode("/configuration"));
10 return configuration;
11}
12
到这里大家可以看到一些端倪了,注意一个 xpath 表达式/configuration
。这个表达式代表的是 MyBatis 的<configuration/>
标签,这里选中这个标签,并传递给parseConfiguration方法。我们继续跟下去。
461
2private void parseConfiguration(XNode root) {
3 try {
4 // 解析 properties 配置
5 propertiesElement(root.evalNode("properties"));
6
7 // 解析 settings 配置,并将其转换为 Properties 对象
8 Properties settings = settingsAsProperties(root.evalNode("settings"));
9
10 // 加载 vfs
11 loadCustomVfs(settings);
12
13 // 解析 typeAliases 配置
14 typeAliasesElement(root.evalNode("typeAliases"));
15
16 // 解析 plugins 配置
17 pluginElement(root.evalNode("plugins"));
18
19 // 解析 objectFactory 配置
20 objectFactoryElement(root.evalNode("objectFactory"));
21
22 // 解析 objectWrapperFactory 配置
23 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
24
25 // 解析 reflectorFactory 配置
26 reflectorFactoryElement(root.evalNode("reflectorFactory"));
27
28 // settings 中的信息设置到 Configuration 对象中
29 settingsElement(settings);
30
31 // 解析 environments 配置
32 environmentsElement(root.evalNode("environments"));
33
34 // 解析 databaseIdProvider,获取并设置 databaseId 到 Configuration 对象
35 databaseIdProviderElement(root.evalNode("databaseIdProvider"));
36
37 // 解析 typeHandlers 配置
38 typeHandlerElement(root.evalNode("typeHandlers"));
39
40 // 解析 mappers 配置
41 mapperElement(root.evalNode("mappers"));
42 } catch (Exception e) {
43 throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
44 }
45}
46
到此,一个 MyBatis 的解析过程就出来了,每个配置的解析逻辑都封装在了相应的方法中。在下面分析过程中,我不打算按照方法调用的顺序进行分析,我会适当进行一定的调整。同时,MyBatis 中配置较多,对于一些不常用的配置,这里会略过。
properties
节点的配置内容示例如下:
41<properties resource="jdbc.properties">
2 <property name="username" value="dev_user"/>
3 <property name="password" value="F2Fa3!33TYyg"/>
4</properties>
参照上面的配置,来分析一下 propertiesElement
的逻辑:
541// -☆- XMLConfigBuilder
2private void propertiesElement(XNode context) throws Exception {
3 if (context != null) {
4 // 解析子节点方式配置的属性和值 => defaults
5 Properties defaults = context.getChildrenAsProperties();
6
7 // 获取 resource 和 url 属性的值
8 String resource = context.getStringAttribute("resource");
9 String url = context.getStringAttribute("url");
10
11 // 不能同时配置 resource 和 url
12 if (resource != null && url != null) {
13 throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
14 }
15
16 if (resource != null) {
17 // 从文件系统中加载并解析属性文件 => defaults(可能会覆盖子节点中配置的属性)
18 defaults.putAll(Resources.getResourceAsProperties(resource));
19 } else if (url != null) {
20 // 通过 url 加载并解析属性文件 => defaults(可能会覆盖子节点中配置的属性)
21 defaults.putAll(Resources.getUrlAsProperties(url));
22 }
23
24 // 获取Configuration中的全局变量 => defaults
25 Properties vars = configuration.getVariables();
26 if (vars != null) {
27 defaults.putAll(vars);
28 }
29
30 // 把 defaults 设置到 parser 和 configuration 中
31 parser.setVariables(defaults);
32 configuration.setVariables(defaults);
33 }
34}
35
36// 扩展:解析子节点方式配置的属性和值
37public Properties getChildrenAsProperties() {
38 Properties properties = new Properties();
39
40 // 遍历子节点
41 for (XNode child : getChildren()) {
42 // 获取 property 节点的 name 和 value 属性
43 String name = child.getStringAttribute("name");
44 String value = child.getStringAttribute("value");
45
46 // 设置属性到Properties集合中
47 if (name != null && value != null) {
48 properties.setProperty(name, value);
49 }
50 }
51
52 return properties;
53}
54
上面是 properties 节点解析的主要过程,不是很复杂,但需要注意一点,通过 resource 和 url 引用外部Props文件中的属性会覆盖掉子节点配置的属性。
settings 相关配置是 MyBatis 中非常重要的配置,这些配置用于调整 MyBatis 运行时的行为。settings 配置繁多,在对这些配置不熟悉的情况下,保持默认配置即可,下面先来看一个比较简单的配置。
61<settings>
2 <setting name="cacheEnabled" value="true"/>
3 <setting name="lazyLoadingEnabled" value="true"/>
4 <setting name="autoMappingBehavior" value="PARTIAL"/>
5</settings>
6
接下来,对照上面的配置,来分析settingsAsProperties方法源码,并不复杂,只是将setting子节点配置的属性转换为Properties而已。
211// -☆- XMLConfigBuilder
2private Properties settingsAsProperties(XNode context) {
3 if (context == null) {
4 return new Properties();
5 }
6
7 // 解析子节点方式配置的属性和值 => props
8 Properties props = context.getChildrenAsProperties();
9
10 // 创建 Configuration 类的“元信息”对象
11 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
12
13 // 遍历配置的 settings
14 for (Object key : props.keySet()) {
15 // 校验 Configuration 中是否存在相关属性(setXxx)
16 if (!metaConfig.hasSetter(String.valueOf(key))) {
17 throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
18 }
19 }
20 return props;
21}
注意:
MetaClass
:用来解析目标类的一些元信息,比如类的成员变量,getter/setter 方法等,文末将会专题介绍。
转换出来的 Properties 要有一个存放的地方,以使其他代码可以找到这些配置。这个存放地方就是 Configuration
对象,下面就来看一下将 settings 配置设置到 Configuration 对象中的过程。
321 private void settingsElement(Properties props) {
2 // 设置 autoMappingBehavior 属性,默认值为 PARTIAL
3 configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
4 configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
5 // 设置 cacheEnabled 属性,默认值为 true
6 configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
7 configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
8 configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
9 configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
10 configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
11 configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
12 configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
13 configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
14 configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
15 configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
16 configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
17 configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
18 configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
19 configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
20 configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
21 configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
22 configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
23 configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
24 // 解析默认的枚举处理器
25 configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
26 configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
27 configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
28 configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
29 configuration.setLogPrefix(props.getProperty("logPrefix"));
30 configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
31 }
32
上面代码就是调用 Configuration 的 setter 方法,就没太多逻辑了。重点需要注意一个resolveClass
方法,它的源码如下:
221// -☆- BaseBuilder
2protected Class<?> resolveClass(String alias) {
3 if (alias == null) {
4 return null;
5 }
6
7 try {
8 // 别名解析
9 return resolveAlias(alias);
10 } catch (Exception e) {
11 throw new BuilderException("Error resolving class. Cause: " + e, e);
12 }
13}
14
15// 别名注册器
16protected final TypeAliasRegistry typeAliasRegistry;
17
18 // 通过typeAliasRegistry(别名注册器)解析别名为全类名
19protected Class<?> resolveAlias(String alias) {
20 return typeAliasRegistry.resolveAlias(alias);
21}
22
这里出现了一个新的类TypeAliasRegistry
,用途就是将别名和类型进行映射,这样就可以用别名表示某个类了,方便使用。既然聊到了别名,那下面我们不妨看看别名的配置的解析过程。
在MyBatis中,可以为类定义一个简短的别名,在书写配置的时候使用别名来配置,MyBatis在解析配置时会自动将别名替换为对应的全类名。有两种配置方式,第一种是按包进行配置,MyBatis会扫描包路径下的所有类(忽略匿名类/接口/内部类)自动生成别名(可以配合Alias注解自定义别名)。
41<typeAliases>
2 <package name="xyz.coolblog.model1"/>
3 <package name="xyz.coolblog.model2"/>
4</typeAliases>
另一种方式是通过手动的方式,明确为某个类配置别名。
41<typeAliases>
2 <typeAlias alias="article" type="xyz.coolblog.model.Article" />
3 <typeAlias type="xyz.coolblog.model.Author" />
4</typeAliases>
下面我们来看一下两种不同的别名配置是怎样解析的。
311// -☆- XMLConfigBuilder
2private void typeAliasesElement(XNode parent) {
3 if (parent != null) {
4 for (XNode child : parent.getChildren()) {
5 // ⭐️ 从指定的包中解析别名和类型的映射
6 if ("package".equals(child.getName())) {
7 String typeAliasPackage = child.getStringAttribute("name");
8 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
9
10 // ⭐️ 从 typeAlias 节点中解析别名和类型的映射
11 } else {
12 // 获取 alias 和 type 属性值,alias 不是必填项,可为空
13 String alias = child.getStringAttribute("alias");
14 String type = child.getStringAttribute("type");
15 try {
16 // 加载 type 对应的类型
17 Class<?> clazz = Resources.classForName(type);
18
19 // 注册别名到类型的映射
20 if (alias == null) {
21 typeAliasRegistry.registerAlias(clazz);
22 } else {
23 typeAliasRegistry.registerAlias(alias, clazz);
24 }
25 } catch (ClassNotFoundException e) {
26 throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
27 }
28 }
29 }
30 }
31}
上面的代码通过一个if-else
条件分支来处理两种不同的配置,这里我用⭐️标注了出来。下面我们来分别看一下这两种配置方式的解析过程,首先来看一下手动配置方式的解析过程。
在别名的配置中,type
属性是必须要配置的,而alias
属性则不是必须的。这个在配置文件的 DTD 中有规定。如果使用者未配置 alias 属性,则需要 MyBatis 自行为目标类型生成别名。对于别名为空的情况,注册别名的任务交由void registerAlias(Class<?>)
方法处理。若不为空,则由void registerAlias(String, Class<?>)
进行别名注册。这两个方法的分析如下:
311private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
2
3public void registerAlias(Class<?> type) {
4 // 获取全路径类名的简称
5 String alias = type.getSimpleName();
6 Alias aliasAnnotation = type.getAnnotation(Alias.class);
7 if (aliasAnnotation != null) {
8 // 从注解中取出别名
9 alias = aliasAnnotation.value();
10 }
11 // 调用重载方法注册别名和类型映射
12 registerAlias(alias, type);
13}
14
15public void registerAlias(String alias, Class<?> value) {
16 if (alias == null) {
17 throw new TypeException("The parameter alias cannot be null");
18 }
19 // 将别名转成小写
20 String key = alias.toLowerCase(Locale.ENGLISH);
21 /*
22 * 如果 TYPE_ALIASES 中存在了某个类型映射,这里判断当前类型与映射中的类型是否一致,
23 * 不一致则抛出异常,不允许一个别名对应两种类型
24 */
25 if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
26 throw new TypeException(
27 "The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
28 }
29 // 缓存别名到类型映射
30 TYPE_ALIASES.put(key, value);
31}
如上,若用户为明确配置 alias 属性,MyBatis 会使用类名的小写形式作为别名。比如,全限定类名xyz.coolblog.model.Author
的别名为author
。若类中有@Alias
注解,则从注解中取值作为别名。
从指定的包中解析并注册别名过程主要由别名的解析和注册两步组成。下面来看一下相关代码:
231public void registerAliases(String packageName) {
2 // 调用重载方法注册别名
3 registerAliases(packageName, Object.class);
4}
5
6public void registerAliases(String packageName, Class<?> superType) {
7 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
8 /*
9 * 查找某个包下的父类为 superType 的类。从调用栈来看,这里的
10 * superType = Object.class,所以 ResolverUtil 将查找所有的类。
11 * 查找完成后,查找结果将会被缓存到内部集合中。
12 */
13 resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
14 // 获取查找结果
15 Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
16 for (Class<?> type : typeSet) {
17 // 忽略匿名类,接口,内部类
18 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
19 // 为类型注册别名
20 registerAlias(type);
21 }
22 }
23}
上面的代码不多,相关流程也不复杂,可简单总结为下面两个步骤:
查找指定包下的所有类
遍历查找到的类型集合,为每个类型注册别名
在这两步流程中,第2步流程对应的代码上一节已经分析过了,这里不再赘述。第1步的功能理解起来不难,但是背后对应的代码有点多。限于篇幅原因,这里我不打算详细分析这一部分的代码,只做简单的流程总结。如下:
通过 VFS(虚拟文件系统)获取指定包下的所有文件的路径名,比如xyz/coolblog/model/Article.class
。
筛选以.class
结尾的文件名
将路径名转成全限定的类名,通过类加载器加载类名
对类型进行匹配,若符合匹配规则,则将其放入内部集合中
以上就是类型资源查找的过程,并不是很复杂,大家有兴趣自己看看吧。
最后,我们来看一下一些 MyBatis 内部类及一些常见类型的别名注册过程。如下:
611// -☆- TypeAliasRegistry
2public TypeAliasRegistry() {
3 // 注册 String 的别名
4 registerAlias("string", String.class);
5
6 // 注册基本类型包装类的别名
7 registerAlias("byte", Byte.class);
8 // 省略部分代码,下同
9
10 // 注册基本类型包装类数组的别名
11 registerAlias("byte[]", Byte[].class);
12
13 // 注册基本类型的别名
14 registerAlias("_byte", byte.class);
15
16 // 注册基本类型包装类的别名
17 registerAlias("_byte[]", byte[].class);
18
19 // 注册 Date, BigDecimal, Object 等类型的别名
20 registerAlias("date", Date.class);
21 registerAlias("decimal", BigDecimal.class);
22 registerAlias("object", Object.class);
23
24 // 注册 Date, BigDecimal, Object 等数组类型的别名
25 registerAlias("date[]", Date[].class);
26 registerAlias("decimal[]", BigDecimal[].class);
27 registerAlias("object[]", Object[].class);
28
29 // 注册集合类型的别名
30 registerAlias("map", Map.class);
31 registerAlias("hashmap", HashMap.class);
32 registerAlias("list", List.class);
33 registerAlias("arraylist", ArrayList.class);
34 registerAlias("collection", Collection.class);
35 registerAlias("iterator", Iterator.class);
36
37 // 注册 ResultSet 的别名
38 registerAlias("ResultSet", ResultSet.class);
39}
40
41// -☆- Configuration
42public Configuration() {
43 // 注册事务工厂的别名
44 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
45 // 省略部分代码,下同
46
47 // 注册数据源的别名
48 typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
49
50 // 注册缓存策略的别名
51 typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
52 typeAliasRegistry.registerAlias("LRU", LruCache.class);
53
54 // 注册日志类的别名
55 typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
56 typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
57
58 // 注册动态代理工厂的别名
59 typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
60 typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
61}
好了,以上就是别名解析的全部流程,大家看懂了吗?如果觉得没啥障碍的话,那继续往下看呗。
插件是 MyBatis 提供的一个拓展机制,通过插件机制我们可在 SQL 执行过程中的某些点上做一些自定义操作。实现一个插件需要比简单,首先需要让插件类实现Interceptor
接口。然后在插件类上添加@Intercepts
和@Signature
注解,用于指定想要拦截的目标方法。MyBatis 允许拦截下面接口中的一些方法:
可拦截的类 | 类中可拦截的方法 |
---|---|
Executor | update/query/flushStatements/commit/rollback/getTransaction/close/isClosed |
ParameterHandler | getParameterObject/setParameters |
ResultSetHandler | handleResultSets/handleOutputParameters |
StatementHandler | prepare/parameterize/batch/update/query |
比较常见的插件有分页插件、分表插件等,有兴趣的朋友可以去了解下。本节我们来分析一下插件的配置的解析过程,先来了解插件的配置。如下:
51<plugins>
2 <plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
3 <property name="key" value="value"/>
4 </plugin>
5</plugins>
解析过程分析如下:
251private void pluginElement(XNode parent) throws Exception {
2 if (parent != null) {
3 for (XNode child : parent.getChildren()) {
4 String interceptor = child.getStringAttribute("interceptor");
5 // 获取配置信息
6 Properties properties = child.getChildrenAsProperties();
7 // 解析拦截器的类型,并创建拦截器
8 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
9 // 设置属性
10 interceptorInstance.setProperties(properties);
11 // 添加拦截器到 Configuration 中
12 configuration.addInterceptor(interceptorInstance);
13 }
14 }
15}
16
17// Configuration使用InterceptorChain来保存所有的拦截器
18public void addInterceptor(Interceptor interceptor) {
19 interceptorChain.addInterceptor(interceptor);
20}
21
22// InterceptorChain内部是一个ArrayList
23public void addInterceptor(Interceptor interceptor) {
24 interceptors.add(interceptor);
25}
如上,插件解析的过程还是比较简单的。首先是获取配置,然后再解析拦截器类型,并实例化拦截器。最后向拦截器中设置属性,并将拦截器添加到 Configuration 中。好了,关于插件配置的分析就先到这,继续往下分析。
在 MyBatis 中,事务管理器和数据源是配置在 environments 中的。它们的配置大致如下:
111<environments default="development">
2 <environment id="development">
3 <transactionManager type="JDBC"/>
4 <dataSource type="POOLED">
5 <property name="driver" value="${jdbc.driver}"/>
6 <property name="url" value="${jdbc.url}"/>
7 <property name="username" value="${jdbc.username}"/>
8 <property name="password" value="${jdbc.password}"/>
9 </dataSource>
10 </environment>
11</environments>
接下来我们对照上面的配置进行分析,如下:
311private String environment;
2
3private void environmentsElement(XNode context) throws Exception {
4 if (context != null) {
5 if (environment == null) {
6 // 获取 default 属性
7 environment = context.getStringAttribute("default");
8 }
9 for (XNode child : context.getChildren()) {
10 // 获取 id 属性
11 String id = child.getStringAttribute("id");
12 /*
13 * 检测当前 environment 节点的 id 与其父节点 environments 的属性 default
14 * 内容是否一致,一致则返回 true,否则返回 false
15 */
16 if (isSpecifiedEnvironment(id)) {
17 // 解析 transactionManager 节点,逻辑和插件的解析逻辑很相似,不在赘述
18 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
19 // 解析 dataSource 节点,逻辑和插件的解析逻辑很相似,不在赘述
20 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
21 // 创建 DataSource 对象
22 DataSource dataSource = dsFactory.getDataSource();
23 Environment.Builder environmentBuilder = new Environment.Builder(id)
24 .transactionFactory(txFactory)
25 .dataSource(dataSource);
26 // 构建 Environment 对象,并设置到 configuration 中
27 configuration.setEnvironment(environmentBuilder.build());
28 }
29 }
30 }
31}
environments 配置的解析过程没什么特别之处,按部就班解析就行了,不多说了。
我们在向数据库存取数据时,需要将数据库字段类型
和 Java类型
进行相互转换,处理这个转换的模块就是类型处理器TypeHandler
。下面,我们来看一下类型处理器的配置方法:
111<!-- 自动扫描(javaType和jdbcTyp使用@MappedTypes和@MappedJdbcTypes注解配置) -->
2<typeHandlers>
3 <package name="xyz.coolblog.handlers"/>
4</typeHandlers>
5
6<!-- 手动配置 -->
7<typeHandlers>
8 <typeHandler jdbcType="TINYINT"
9 javaType="xyz.coolblog.constant.ArticleTypeEnum"
10 handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
11</typeHandlers>
下面开始分析代码。
381private void typeHandlerElement(XNode parent) throws Exception {
2 if (parent != null) {
3 for (XNode child : parent.getChildren()) {
4 // 从指定的包中注册 TypeHandler
5 if ("package".equals(child.getName())) {
6 String typeHandlerPackage = child.getStringAttribute("name");
7 // 注册方法 ①
8 typeHandlerRegistry.register(typeHandlerPackage);
9
10 // 从 typeHandler 节点中解析别名到类型的映射
11 } else {
12 // 获取 javaType,jdbcType 和 handler 等属性值
13 String javaTypeName = child.getStringAttribute("javaType");
14 String jdbcTypeName = child.getStringAttribute("jdbcType");
15 String handlerTypeName = child.getStringAttribute("handler");
16
17 // 解析上面获取到的属性值
18 Class<?> javaTypeClass = resolveClass(javaTypeName);
19 JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
20 Class<?> typeHandlerClass = resolveClass(handlerTypeName);
21
22 // 根据 javaTypeClass 和 jdbcType 值的情况进行不同的注册策略
23 if (javaTypeClass != null) {
24 if (jdbcType == null) {
25 // 注册方法 ②
26 typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
27 } else {
28 // 注册方法 ③
29 typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
30 }
31 } else {
32 // 注册方法 ④
33 typeHandlerRegistry.register(typeHandlerClass);
34 }
35 }
36 }
37 }
38}
上面的代码中调用了 4 个重载的处理器注册方法,这些注册方法的逻辑不难理解,但之间的调用关系复杂,下面是它们的调用关系图。其中蓝色背景框内的方法称为开始方法,红色背景框内的方法称为终点方法,白色背景框内的方法称为中间方法。
下面我会分析从每个开始方法向下分析,为了避免冗余分析,我会按照③ → ② → ④ → ①
的顺序进行分析。
当代码执行到此方法时,表示javaTypeClass != null && jdbcType != null
条件成立,即明确配置了javaType
和jdbcType
。
221public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
2 // 参数齐全,直接调用终点方法
3 // 其中getInstance用于创建typeHandlerClass实例,优先使用javaTypeClass作为入参调用有参构造
4 register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
5}
6
7/** 类型处理器注册过程的终点 */
8private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
9 if (javaType != null) {
10 // JdbcType 到 TypeHandler 的映射
11 Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
12 if (map == null || map == NULL_TYPE_HANDLER_MAP) {
13 map = new HashMap<JdbcType, TypeHandler<?>>();
14 // 存储 javaType 到 Map<JdbcType, TypeHandler> 的映射
15 TYPE_HANDLER_MAP.put(javaType, map);
16 }
17 map.put(jdbcType, handler);
18 }
19
20 // 存储所有的 TypeHandler
21 ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
22}
类型处理器的实际注册过程是在该终点方法完成的,就是把类型
和处理器
进行双层映射而已,外层映射是JavaType和多个JdbcType的映射,内层映射是JdbcType和TypeHandler的映射。
当代码执行到此方法时,表示javaTypeClass != null && jdbcType == null
条件成立,即仅设置了javaType
。
231public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
2 // 调用中间方法register(Type, TypeHandler),去获取jdbcType
3 register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
4}
5
6private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
7 // 获取 @MappedJdbcTypes 注解(用于解析jdbcType)
8 MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
9 if (mappedJdbcTypes != null) {
10 // 遍历 @MappedJdbcTypes 注解中配置的值(获取所有配置的JdbcType)
11 for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
12 // 参数解析齐全后,调用终点方法
13 register(javaType, handledJdbcType, typeHandler);
14 }
15 if (mappedJdbcTypes.includeNullJdbcType()) {
16 // 调用终点方法,jdbcType = null
17 register(javaType, null, typeHandler);
18 }
19 } else {
20 // 调用终点方法,jdbcType = null
21 register(javaType, null, typeHandler);
22 }
23}
上面代码主要做的事情是尝试从注解中获取JdbcType
的值,然后调用终点方法注册。(注意JdbcType可以配置为NULL。
当代码执行到此方法时,表示javaTypeClass == null
条件成立,即javaType
和jdbcType
都未配置。
491public void register(Class<?> typeHandlerClass) {
2 boolean mappedTypeFound = false;
3 // 获取 @MappedTypes 注解(用于解析javaType)
4 MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
5 if (mappedTypes != null) {
6 // 遍历 @MappedTypes 注解中配置的值
7 for (Class<?> javaTypeClass : mappedTypes.value()) {
8 // 调用注册方法 ②
9 register(javaTypeClass, typeHandlerClass);
10 mappedTypeFound = true;
11 }
12 }
13 if (!mappedTypeFound) {
14 // 调用中间方法 register(TypeHandler)
15 register(getInstance(null, typeHandlerClass));
16 }
17}
18
19public <T> void register(TypeHandler<T> typeHandler) {
20 boolean mappedTypeFound = false;
21 // 获取 @MappedTypes 注解
22 MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
23 if (mappedTypes != null) {
24 for (Class<?> handledType : mappedTypes.value()) {
25 // 调用中间方法 register(Type, TypeHandler)
26 register(handledType, typeHandler);
27 mappedTypeFound = true;
28 }
29 }
30 // 自动发现映射类型
31 if (!mappedTypeFound && typeHandler instanceof TypeReference) {
32 try {
33 TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
34 // 获取参数模板中的参数类型,并调用中间方法 register(Type, TypeHandler)
35 register(typeReference.getRawType(), typeHandler);
36 mappedTypeFound = true;
37 } catch (Throwable t) {
38 }
39 }
40 if (!mappedTypeFound) {
41 // 调用中间方法 register(Class, TypeHandler)
42 register((Class<T>) null, typeHandler);
43 }
44}
45
46public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
47 // 调用中间方法 register(Type, TypeHandler)
48 register((Type) javaType, typeHandler);
49}
上面的代码主要用于解析javaType
,优先通过@MappedTypes
注解来解析,其次使用反射来获取javaType。不管是通过哪种方式,解析完成后都会调用中间方法register(Type, TypeHandler)
,这个方法负责解析jdbcType
,在上一节已经分析过。一个负责解析 javaType,另一个负责解析 jdbcType,逻辑比较清晰了。
该方法主要是用于自动扫描类型处理器,并调用其他方法注册扫描结果,注册时忽略内部类,接口,抽象类等。
131public void register(String packageName) {
2 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
3 // 从指定包中查找 TypeHandler
4 resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
5 Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
6 for (Class<?> type : handlerSet) {
7 // 忽略内部类,接口,抽象类等
8 if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
9 // 调用注册方法 ④
10 register(type);
11 }
12 }
13}
mappers
标签主要用于指定映射信息的存放位置,这些映射信息可以是注解形式或XML配置形式。
451// -☆- XMLConfigBuilder
2private void mapperElement(XNode parent) throws Exception {
3 if (parent != null) {
4 for (XNode child : parent.getChildren()) {
5 if ("package".equals(child.getName())) {
6 // 获取 <package> 节点中的 name 属性
7 String mapperPackage = child.getStringAttribute("name");
8 // 从指定包中查找 mapper 接口,并根据 mapper 接口解析映射配置
9 configuration.addMappers(mapperPackage);
10 } else {
11 // 获取 resource/url/class 等属性
12 String resource = child.getStringAttribute("resource");
13 String url = child.getStringAttribute("url");
14 String mapperClass = child.getStringAttribute("class");
15
16 // resource 不为空,且其他两者为空,则从指定路径中加载配置
17 if (resource != null && url == null && mapperClass == null) {
18 ErrorContext.instance().resource(resource);
19 InputStream inputStream = Resources.getResourceAsStream(resource);
20 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
21 // 解析映射文件
22 mapperParser.parse();
23
24 // url 不为空,且其他两者为空,则通过 url 加载配置
25 } else if (resource == null && url != null && mapperClass == null) {
26 ErrorContext.instance().resource(url);
27 InputStream inputStream = Resources.getUrlAsStream(url);
28 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
29 // 解析映射文件
30 mapperParser.parse();
31
32 // mapperClass 不为空,且其他两者为空,则通过 mapperClass 解析映射配置
33 } else if (resource == null && url == null && mapperClass != null) {
34 Class<?> mapperInterface = Resources.classForName(mapperClass);
35 // 解析映射注解
36 configuration.addMapper(mapperInterface);
37
38 // 以上条件不满足,则抛出异常
39 } else {
40 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
41 }
42 }
43 }
44 }
45}
上面代码主要逻辑是遍历 mappers 的子节点,并根据节点属性值判断通过什么方式加载映射文件或映射信息。
在展开映射文件的解析之前,先来看一下映射文件的解析入口。如下:
171// -☆- XMLMapperBuilder
2public void parse() {
3 // 检测映射文件是否已经被解析过
4 if (!configuration.isResourceLoaded(resource)) {
5 // 解析 mapper 节点
6 configurationElement(parser.evalNode("/mapper"));
7 // 添加资源路径到“已解析资源集合”中
8 configuration.addLoadedResource(resource);
9 // 通过命名空间绑定 Mapper 接口
10 bindMapperForNamespace();
11 }
12
13 // 处理未完成解析的节点
14 parsePendingResultMaps();
15 parsePendingCacheRefs();
16 parsePendingStatements();
17}
如上,映射文件解析入口逻辑包含三个核心操作,分别如下:
解析 mapper 节点
通过命名空间绑定 Mapper 接口
处理未完成解析的节点
这三个操作对应的逻辑,我将会在随后的章节中依次进行分析。下面,先来分析第一个操作对应的逻辑,下面是一个映射文件配置示例。
251<mapper namespace="xyz.coolblog.dao.AuthorDao">
2
3 <cache/>
4
5 <resultMap id="authorResult" type="Author">
6 <id property="id" column="id"/>
7 <result property="name" column="name"/>
8 <!-- ... -->
9 </resultMap>
10
11 <sql id="table">
12 author
13 </sql>
14
15 <select id="findOne" resultMap="authorResult">
16 SELECT
17 id, name, age, sex, email
18 FROM
19 <include refid="table"/>
20 WHERE
21 id = #{id}
22 </select>
23
24 <!-- <insert|update|delete/> -->
25</mapper>
上面是一个比较简单的映射文件,还有一些的节点没有出现在上面。以上每种配置中的每种节点的解析逻辑都封装在了相应的方法中,这些方法由 XMLMapperBuilder
类的 configurationElement 方法统一调用。
321private void configurationElement(XNode context) {
2 try {
3 // 获取 mapper 命名空间
4 String namespace = context.getStringAttribute("namespace");
5 if (namespace == null || namespace.equals("")) {
6 throw new BuilderException("Mapper's namespace cannot be empty");
7 }
8
9 // 设置命名空间到 builderAssistant 中
10 builderAssistant.setCurrentNamespace(namespace);
11
12 // 解析 <cache-ref> 节点
13 cacheRefElement(context.evalNode("cache-ref"));
14
15 // 解析 <cache> 节点
16 cacheElement(context.evalNode("cache"));
17
18 // 已废弃配置,这里不做分析
19 parameterMapElement(context.evalNodes("/mapper/parameterMap"));
20
21 // 解析 <resultMap> 节点
22 resultMapElements(context.evalNodes("/mapper/resultMap"));
23
24 // 解析 <sql> 节点
25 sqlElement(context.evalNodes("/mapper/sql"));
26
27 // 解析 <select>、...、<delete> 等节点
28 buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
29 } catch (Exception e) {
30 throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
31 }
32}
下面将会先分析 <cache>
节点的解析过程,然后再分析 <cache-ref>
节点,之后会按照顺序分析其他节点的解析过程。
MyBatis 提供了一级/二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,需要显示配置开启。
71<!-- 开启二级缓存:
2 eviction:FIFO表示按“先进先出”的策略淘汰缓存项
3 flushInterval:缓存每隔60秒刷新一次
4 size:缓存的容量为512个对象引用
5 readOnly:为true表示缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象
6-->
7<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
除此之外,还可以给 MyBatis 配置第三方缓存或者自己实现的缓存等。比如,我们将 Ehcache 缓存整合到 MyBatis 中,可以这样配置。
71<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
2 <property name="timeToIdleSeconds" value="3600"/>
3 <property name="timeToLiveSeconds" value="3600"/>
4 <property name="maxEntriesLocalHeap" value="1000"/>
5 <property name="maxEntriesLocalDisk" value="10000000"/>
6 <property name="memoryStoreEvictionPolicy" value="LRU"/>
7</cache>
下面来分析一下缓存配置的解析逻辑,如下:
191private void cacheElement(XNode context) throws Exception {
2 if (context != null) {
3 // 获取各种属性
4 String type = context.getStringAttribute("type", "PERPETUAL");
5 Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
6 String eviction = context.getStringAttribute("eviction", "LRU");
7 Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
8 Long flushInterval = context.getLongAttribute("flushInterval");
9 Integer size = context.getIntAttribute("size");
10 boolean readWrite = !context.getBooleanAttribute("readOnly", false);
11 boolean blocking = context.getBooleanAttribute("blocking", false);
12
13 // 获取子节点配置
14 Properties props = context.getChildrenAsProperties();
15
16 // 构建缓存对象
17 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
18 }
19}
上面代码中,大段代码用来解析 <cache>
节点的属性和子节点,这些代码没什么好说的。缓存的构建逻辑封装在 BuilderAssistant
类的 useNewCache 方法中,下面我们来看一下该方法的逻辑。
231// -☆- MapperBuilderAssistant
2public Cache useNewCache(Class<? extends Cache> typeClass,
3 Class<? extends Cache> evictionClass,Long flushInterval,
4 Integer size,boolean readWrite,boolean blocking,Properties props) {
5
6 // 使用建造模式构建缓存实例
7 Cache cache = new CacheBuilder(currentNamespace)
8 .implementation(valueOrDefault(typeClass, PerpetualCache.class))
9 .addDecorator(valueOrDefault(evictionClass, LruCache.class))
10 .clearInterval(flushInterval)
11 .size(size)
12 .readWrite(readWrite)
13 .blocking(blocking)
14 .properties(props)
15 .build();
16
17 // 添加缓存到 Configuration 对象中
18 configuration.addCache(cache);
19
20 // 设置当前缓存为刚创建的缓存
21 currentCache = cache;
22 return cache;
23}
上面使用了建造模式构建 Cache 实例,Cache 实例的构建过程略为复杂,我们跟下去看看。
261// -☆- CacheBuilder
2public Cache build() {
3 // 设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
4 setDefaultImplementations();
5
6 // 通过反射创建缓存
7 Cache cache = newBaseCacheInstance(implementation, id);
8 // 设置子节点配置的属性
9 setCacheProperties(cache);
10 // 仅对内置缓存 PerpetualCache 应用装饰器
11 if (PerpetualCache.class.equals(cache.getClass())) {
12 // 遍历装饰器集合,应用装饰器
13 for (Class<? extends Cache> decorator : decorators) {
14 // 通过反射创建装饰器实例
15 cache = newCacheDecoratorInstance(decorator, cache);
16 // 再次设置子节点配置的属性(for装饰器)
17 setCacheProperties(cache);
18 }
19 // 应用标准的装饰器,比如 LoggingCache、SynchronizedCache
20 cache = setStandardDecorators(cache);
21 } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
22 // 应用具有日志功能的缓存装饰器
23 cache = new LoggingCache(cache);
24 }
25 return cache;
26}
上面的构建过程流程较为复杂,这里总结一下。如下:
设置默认的缓存类型及装饰器
应用装饰器到 PerpetualCache 对象上
遍历装饰器类型集合,并通过反射创建装饰器实例
将属性设置到实例中
应用一些标准的装饰器
对非 LoggingCache 类型的缓存应用 LoggingCache 装饰器
在以上4个步骤中,最后一步的逻辑很简单,无需多说。下面按顺序分析前3个步骤对应的逻辑,如下:
101private void setDefaultImplementations() {
2 if (implementation == null) {
3 // 设置默认的缓存实现类
4 implementation = PerpetualCache.class;
5 if (decorators.isEmpty()) {
6 // 添加 LruCache 装饰器
7 decorators.add(LruCache.class);
8 }
9 }
10}
以上逻辑比较简单,主要做的事情是在 implementation 为空的情况下,为它设置一个默认值。如果大家仔细看前面的方法,会发现 MyBatis 做了不少判空的操作。比如:
101// 判空操作1,若用户未设置 cache 节点的 type 和 eviction 属性,这里设置默认值 PERPETUAL
2String type = context.getStringAttribute("type", "PERPETUAL");
3String eviction = context.getStringAttribute("eviction", "LRU");
4
5// 判空操作2,若 typeClass 或 evictionClass 为空,valueOrDefault 方法会为它们设置默认值
6Cache cache = new CacheBuilder(currentNamespace)
7 .implementation(valueOrDefault(typeClass, PerpetualCache.class))
8 .addDecorator(valueOrDefault(evictionClass, LruCache.class))
9 // 省略部分代码
10 .build();
既然前面已经做了两次判空操作,implementation 不可能为空,那么 setDefaultImplementations 方法似乎没有存在的必要了。其实不然,如果有人不按套路写代码。比如:
31Cache cache = new CacheBuilder(currentNamespace)
2 // 忘记设置 implementation
3 .build();
这里忘记设置 implementation,或人为的将 implementation 设为空。如果不对 implementation 进行判空,会导致 build 方法在构建实例时触发空指针异常,对于框架来说,出现空指针异常是很尴尬的,这是一个低级错误。这里以及之前做了这么多判空,就是为了避免出现空指针的情况,以提高框架的健壮性。好了,关于 setDefaultImplementations 方法的分析先到这,继续往下分析。
我们在使用 MyBatis 内置缓存时,一般不用为它们配置自定义属性。但使用第三方缓存时,则应按需进行配置。比如前面演示 MyBatis 整合 Ehcache 时,就为 Ehcache 配置了一些必要的属性。下面我们来看一下这部分配置是如何设置到缓存实例中的。
531private void setCacheProperties(Cache cache) {
2 if (properties != null) {
3 /*
4 * 为缓存实例生成一个“元信息”实例,forObject 方法调用层次比较深,但最终调用了
5 * MetaClass 的 forClass 方法。关于 MetaClass 的源码,我在上一篇文章中已经
6 * 详细分析过了,这里不再赘述。
7 */
8 MetaObject metaCache = SystemMetaObject.forObject(cache);
9 // 遍历子节点属性
10 for (Map.Entry<Object, Object> entry : properties.entrySet()) {
11 String name = (String) entry.getKey();
12 String value = (String) entry.getValue();
13 if (metaCache.hasSetter(name)) {
14 // 获取 setter 方法的参数类型
15 Class<?> type = metaCache.getSetterType(name);
16 /*
17 * 根据参数类型对属性值进行转换,并将转换后的值
18 * 通过 setter 方法设置到 Cache 实例中
19 */
20 if (String.class == type) {
21 metaCache.setValue(name, value);
22 } else if (int.class == type || Integer.class == type) {
23 /*
24 * 此处及以下分支包含两个步骤:
25 * 1.类型转换 → Integer.valueOf(value)
26 * 2.将转换后的值设置到缓存实例中 → metaCache.setValue(name, value)
27 */
28 metaCache.setValue(name, Integer.valueOf(value));
29 } else if (long.class == type || Long.class == type) {
30 metaCache.setValue(name, Long.valueOf(value));
31 }
32 else if (short.class == type || Short.class == type) {...}
33 else if (byte.class == type || Byte.class == type) {...}
34 else if (float.class == type || Float.class == type) {...}
35 else if (boolean.class == type || Boolean.class == type) {...}
36 else if (double.class == type || Double.class == type) {...}
37 else {
38 throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
39 }
40 }
41 }
42 }
43
44 // 如果缓存类实现了 InitializingObject 接口,则调用 initialize 方法执行初始化逻辑
45 if (InitializingObject.class.isAssignableFrom(cache.getClass())) {
46 try {
47 ((InitializingObject) cache).initialize();
48 } catch (Exception e) {
49 throw new CacheException("Failed cache initialization for '" +
50 cache.getId() + "' on '" + cache.getClass().getName() + "'", e);
51 }
52 }
53}
上面的大段代码用于对属性值进行类型转换,和设置转换后的值到 Cache 实例中。关于上面代码中出现的 MetaObject,大家可以自己尝试分析一下。最后,我们来看一下设置标准装饰器的过程。如下:
321private Cache setStandardDecorators(Cache cache) {
2 try {
3 // 创建“元信息”对象
4 MetaObject metaCache = SystemMetaObject.forObject(cache);
5 if (size != null && metaCache.hasSetter("size")) {
6 // 设置 size 属性,
7 metaCache.setValue("size", size);
8 }
9 if (clearInterval != null) {
10 // clearInterval 不为空,应用 ScheduledCache 装饰器
11 cache = new ScheduledCache(cache);
12 ((ScheduledCache) cache).setClearInterval(clearInterval);
13 }
14 if (readWrite) {
15 // readWrite 为 true,应用 SerializedCache 装饰器
16 cache = new SerializedCache(cache);
17 }
18 /*
19 * 应用 LoggingCache,SynchronizedCache 装饰器,
20 * 使原缓存具备打印日志和线程同步的能力
21 */
22 cache = new LoggingCache(cache);
23 cache = new SynchronizedCache(cache);
24 if (blocking) {
25 // blocking 为 true,应用 BlockingCache 装饰器
26 cache = new BlockingCache(cache);
27 }
28 return cache;
29 } catch (Exception e) {
30 throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
31 }
32}
以上代码为缓存应用了一些基本的装饰器,但除了 LoggingCache
和 SynchronizedCache
这两个是必要的装饰器外,其他的装饰器应用与否,取决于用户的配置。
在 MyBatis 中,二级缓存是可以共用的。这需要使用 <cache-ref>
节点配置参照缓存,比如像下面这样。
101<!-- Mapper1.xml -->
2<mapper namespace="xyz.coolblog.dao.Mapper1">
3 <!-- Mapper1 与 Mapper2 共用一个二级缓存 -->
4 <cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
5</mapper>
6
7<!-- Mapper2.xml -->
8<mapper namespace="xyz.coolblog.dao.Mapper2">
9 <cache/>
10</mapper>
接下来,我们对照上面的配置分析 cache-ref 的解析过程。如下:
171private void cacheRefElement(XNode context) {
2 if (context != null) {
3 configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
4 // 创建 CacheRefResolver 实例
5 CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
6 try {
7 // 解析参照缓存
8 cacheRefResolver.resolveCacheRef();
9 } catch (IncompleteElementException e) {
10 /*
11 * 这里对 IncompleteElementException 异常进行捕捉,并将 cacheRefResolver
12 * 存入到 Configuration 的 incompleteCacheRefs 集合中
13 */
14 configuration.addIncompleteCacheRef(cacheRefResolver);
15 }
16 }
17}
如上所示,<cache-ref>
节点的解析逻辑封装在了 CacheRefResolver 的 resolveCacheRef 方法中。
341// -☆- CacheRefResolver
2public Cache resolveCacheRef() {
3 // 调用 builderAssistant 的 useCacheRef(cacheRefNamespace) 方法
4 return assistant.useCacheRef(cacheRefNamespace);
5}
6
7// -☆- MapperBuilderAssistant
8public Cache useCacheRef(String namespace) {
9 if (namespace == null) {
10 throw new BuilderException("cache-ref element requires a namespace attribute.");
11 }
12 try {
13 unresolvedCacheRef = true;
14 // 根据命名空间从全局配置对象(Configuration)中查找相应的缓存实例
15 Cache cache = configuration.getCache(namespace);
16
17 /*
18 * 若未查找到缓存实例,此处抛出异常。这里存在两种情况导致未查找到 cache 实例,
19 * 分别如下:
20 * 1.使用者在 <cache-ref> 中配置了一个不存在的命名空间,导致无法找到 cache 实例
21 * 2.使用者所引用的缓存实例还未创建
22 */
23 if (cache == null) {
24 throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
25 }
26
27 // 修改当前缓存为查询出来的缓存
28 currentCache = cache;
29 unresolvedCacheRef = false;
30 return cache;
31 } catch (IllegalArgumentException e) {
32 throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
33 }
34}
关于XML和注解中同时配置缓存的问题?
XML和注解不能同时开启缓存。会报 IllegalArgumentException: Caches collection already contains value for org.example.dao.XxxMapper错误。因为缓存对象创建后会被添加到Configuration中,而保存所有cache对象的是一个MyBatis自定义的
StrictMap
类型,该类型继承自HashMap,在put时会校验元素是否已存在。其中一方开启缓存,另一方不能直接使用。由于XML解析和注解解析映射配置时分别创建了两个不同的对象(XmlMapperBuilder和MapperAnnotationBuilder类型),所以它们的内部类MapperBuilderAssistant中保存的currentCache(在解析cache节点时将创建的cache对象设置到currentCache)是两个不同的引用,因此由不同对象构建的MapperStatement(不能在XML和注解中配置同一个MapperStatement),保存了各自的cache对象。从而在查找二级缓存时,只能查找配置了cache节点的那一方。
可以使用缓存引用来解决上述问题。因为在缓存引用解析的过程中,会查找对应的cache设置到currentCache,后续构建MapperStatement时会保存此引用。
resultMap 是 MyBatis 框架中最重要的特性,主要用于映射结果,下面开始分析 resultMap 配置的解析过程。
811// -☆- XMLMapperBuilder
2private void resultMapElements(List<XNode> list) throws Exception {
3 // 遍历 <resultMap> 节点列表
4 for (XNode resultMapNode : list) {
5 try {
6 // 解析 resultMap 节点
7 resultMapElement(resultMapNode);
8 } catch (IncompleteElementException e) {
9 // ignore, it will be retried
10 }
11 }
12}
13
14private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
15 // 调用重载方法(节点名称,已解析的ResultMapping)
16 return resultMapElement(resultMapNode, Collections.<ResultMapping>emptyList());
17}
18
19private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
20 ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
21
22 // 获取 id 属性(嵌套映射没有id属性,调用resultMapNode.getValueBasedIdentifier()生成,
23 // 如id = mapper_resultMap[articleResult]_association[article_author])
24 String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
25
26 // 获取 type 属性(获取顺序依次是type->ofType->resultType->javaType)
27 String type = resultMapNode.getStringAttribute("type",
28 resultMapNode.getStringAttribute("ofType",
29 resultMapNode.getStringAttribute("resultType",
30 resultMapNode.getStringAttribute("javaType"))));
31
32 // 获取 extends(继承) 和 autoMapping 属性
33 String extend = resultMapNode.getStringAttribute("extends");
34 Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
35
36 // 解析 type 属性对应的类型
37 Class<?> typeClass = resolveClass(type);
38 Discriminator discriminator = null;
39
40 // 存放解析出来的 ResultMapping
41 List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
42
43 // 将参数传入的 ResultMapping 添加进来(一般是嵌套映射的父映射)
44 resultMappings.addAll(additionalResultMappings);
45
46 // 获取并遍历 <resultMap> 的子节点列表
47 List<XNode> resultChildren = resultMapNode.getChildren();
48 for (XNode resultChild : resultChildren) {
49 if ("constructor".equals(resultChild.getName())) {
50 // 解析 constructor 标签,并生成相应的 ResultMapping
51 processConstructorElement(resultChild, typeClass, resultMappings);
52 } else if ("discriminator".equals(resultChild.getName())) {
53 // 解析 discriminator 标签
54 discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
55 } else {
56 // 解析其它标签(id/result/association/collection)
57 List<ResultFlag> flags = new ArrayList<ResultFlag>();
58 // 判断是否为 id 标签,如果是,则添加标记
59 if ("id".equals(resultChild.getName())) {
60 flags.add(ResultFlag.ID);
61 }
62 // 解析 id 和 result 节点,并生成相应的 ResultMapping
63 resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
64 }
65 }
66
67 // 创建 ResultMapResolver 实例
68 ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,
69 discriminator, resultMappings, autoMapping);
70 try {
71 // 根据前面获取到的信息构建 ResultMap 对象
72 return resultMapResolver.resolve();
73 } catch (IncompleteElementException e) {
74 /*
75 * 如果发生 IncompleteElementException 异常,
76 * 这里将 resultMapResolver 添加到 incompleteResultMaps 集合中
77 */
78 configuration.addIncompleteResultMap(resultMapResolver);
79 throw e;
80 }
81}
上面的代码比较多,看起来有点复杂,这里总结一下:
获取 <resultMap>
节点的各种属性
遍历 <resultMap>
的子节点,并根据子节点名称执行相应的解析逻辑
构建 ResultMap 对象
若构建过程中发生异常,则将 resultMapResolver 添加到 incompleteResultMaps 集合中
如上流程,第1步和最后一步都是一些常规操作,无需过多解释。第2步和第3步则是接下来需要重点分析的操作,这其中,鉴别器 discriminator 不是很常用的特性,我觉得大家知道它有什么用就行了,所以就不分析了。
在 <resultMap>
节点中,子节点 <id>
和 <result>
都是常规配置,比较常见。下面我们直接分析这两个节点的解析过程。
381private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
2
3 // 获取映射的 java 属性名或构造函数形参名(property 或 name 属性配置)
4 String property;
5 if (flags.contains(ResultFlag.CONSTRUCTOR)) {
6 property = context.getStringAttribute("name");
7 } else {
8 property = context.getStringAttribute("property");
9 }
10
11 // 获取其他各种属性
12 String column = context.getStringAttribute("column");
13 String javaType = context.getStringAttribute("javaType");
14 String jdbcType = context.getStringAttribute("jdbcType");
15 String nestedSelect = context.getStringAttribute("select");
16
17 /*
18 * 解析 resultMap 属性,该属性出现在 <association> 和 <collection> 节点中。
19 * 若这两个节点不包含 resultMap 属性,则调用 processNestedResultMappings 方法解析嵌套 resultMap。
20 */
21 String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.<ResultMapping>emptyList()));
22
23 String notNullColumn = context.getStringAttribute("notNullColumn");
24 String columnPrefix = context.getStringAttribute("columnPrefix");
25 String typeHandler = context.getStringAttribute("typeHandler");
26 String resultSet = context.getStringAttribute("resultSet");
27 String foreignColumn = context.getStringAttribute("foreignColumn");
28 boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
29
30 // 解析 javaType、typeHandler 的类型以及枚举类型 JdbcType
31 Class<?> javaTypeClass = resolveClass(javaType);
32 Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
33 JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
34
35 // 构建 ResultMapping 对象
36 return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect,
37 nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
38}
上面的方法主要用于获取 <id>
和 <result>
节点的属性,其中,resultMap 属性的解析过程要相对复杂一些。该属性存在于 <association>
和 <collection>
节点中。下面以 <association>
节点为例,演示该节点的两种配置方式,分别如下:
第一种配置方式是通过 resultMap 属性引用其他的 <resultMap>
节点,配置如下:
111<resultMap id="articleResult" type="Article">
2 <id property="id" column="id"/>
3 <result property="title" column="article_title"/>
4 <!-- 引用 authorResult -->
5 <association property="article_author" column="article_author_id" javaType="Author" resultMap="authorResult"/>
6</resultMap>
7
8<resultMap id="authorResult" type="Author">
9 <id property="id" column="author_id"/>
10 <result property="name" column="author_name"/>
11</resultMap>
第二种配置方式是采取 resultMap 嵌套的方式进行配置,如下:
91<resultMap id="articleResult" type="Article">
2 <id property="id" column="id"/>
3 <result property="title" column="article_title"/>
4 <!-- resultMap 嵌套 -->
5 <association property="article_author" javaType="Author">
6 <id property="id" column="author_id"/>
7 <result property="name" column="author_name"/>
8 </association>
9</resultMap>
如上配置所示,<association>
的子节点也是一些结果映射配置,这些结果配置最终也会被解析成 ResultMap。
141private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings) throws Exception {
2 // 判断节点名称
3 if ("association".equals(context.getName())
4 || "collection".equals(context.getName())
5 || "case".equals(context.getName())) {
6 if (context.getStringAttribute("select") == null) {
7 // resultMapElement 是解析 ResultMap 入口方法
8 ResultMap resultMap = resultMapElement(context, resultMappings);
9 // 返回 resultMap id
10 return resultMap.getId();
11 }
12 }
13 return null;
14}
如上,这些嵌套映射配置也是由 resultMapElement 方法解析的,并在最后返回 resultMap.id设置到主映射中。
关于嵌套 resultMap 的解析逻辑就先分析到这,下面分析 ResultMapping 的构建过程。
471public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,JdbcType jdbcType,
2 String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,Class<? extends TypeHandler<?>> typeHandler,
3 List<ResultFlag> flags, String resultSet, String foreignColumn, boolean lazy) {
4
5 /* 解析javaType(获取java属性名对应set方法的返回类型)
6 * 若 javaType 为空,这里根据 property 的属性进行解析。
7 * 关于下面方法中的参数,这里说明一下:
8 * - resultType:即 <resultMap type="xxx"/> 中的 type 属性,即映射的类名
9 * - property:即 <result property="xxx"/> 中的 property 属性,即映射的属性名
10 */
11 Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
12
13 // 解析 TypeHandler
14 TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
15
16 /*
17 * 解析 column = {property1=column1, property2=column2} 的情况,
18 * 这里会将 column 拆分成多个 ResultMapping
19 */
20 List<ResultMapping> composites = parseCompositeColumnName(column);
21
22 // 通过建造模式构建 ResultMapping
23 return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
24 .jdbcType(jdbcType)
25 .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
26 .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
27 .resultSet(resultSet)
28 .typeHandler(typeHandlerInstance)
29 .flags(flags == null ? new ArrayList<ResultFlag>() : flags)
30 .composites(composites)
31 .notNullColumns(parseMultipleColumnNames(notNullColumn))
32 .columnPrefix(columnPrefix)
33 .foreignColumn(foreignColumn)
34 .lazy(lazy)
35 .build();
36}
37
38// -☆- ResultMapping.Builder
39public ResultMapping build() {
40 // 将 flags 和 composites 两个集合变为不可修改集合
41 resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
42 resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
43 // 如果未配置 typeHandler 属性,则从 TypeHandlerRegistry 中获取相应 TypeHandler
44 resolveTypeHandler();
45 validate();
46 return resultMapping;
47}
ResultMapping 的构建过程不是很复杂,主要过程说明如下:
获取映射属性名的 java 类型。
根据配置的 typeHandler 属性创建类型处理器实例。
处理复合 column。
通过建造器构建 ResultMapping 实例。
关于上面方法中出现的一些方法调用,这里接不跟下去分析了,大家可以自己看看。
constructor节点用于自定义映射对象的构造过程,可以通过有参构造来初始化构造的对象。有如下Java类。
81public class ArticleDO {
2 public ArticleDO(Integer id, String title, String content) {
3 this.id = id;
4 this.title = title;
5 this.content = content;
6 }
7 // ...
8}
ArticleDO 的构造方法对应的配置如下:
51<constructor>
2 <idArg column="id" name="id"/>
3 <arg column="title" name="title"/>
4 <arg column="content" name="content"/>
5</constructor>
下面分析 constructor 节点的解析过程。
151private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
2 // 获取子节点列表
3 List<XNode> argChildren = resultChild.getChildren();
4 for (XNode argChild : argChildren) {
5 List<ResultFlag> flags = new ArrayList<ResultFlag>();
6 // 向 flags 中添加 CONSTRUCTOR 标志
7 flags.add(ResultFlag.CONSTRUCTOR);
8 if ("idArg".equals(argChild.getName())) {
9 // 向 flags 中添加 ID 标志
10 flags.add(ResultFlag.ID);
11 }
12 // 构建 ResultMapping,上一节已经分析过
13 resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
14 }
15}
首先是获取并遍历子节点列表,然后为每个子节点创建 flags 集合,并添加 CONSTRUCTOR 标志。对于 idArg 节点,额外添加 ID 标志。最后一步则是构建 ResultMapping,该步逻辑前面已经分析过,这里就不多说了。
分析完 <resultMap>
的子节点 <id>
,<result>
以及 <constructor>
的解析过程,下面来看看 ResultMap 实例的构建过程。下面是之前分析过的 ResultMap 构建的入口。
221private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
2
3 // 获取 resultMap 节点中的属性
4 // ...
5
6 // 解析 resultMap 对应的类型
7 // ...
8
9 // 遍历 resultMap 节点的子节点,构建 ResultMapping 对象
10 // ...
11
12 // 创建 ResultMap 解析器
13 ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,
14 discriminator, resultMappings, autoMapping);
15 try {
16 // 根据前面获取到的信息构建 ResultMap 对象
17 return resultMapResolver.resolve();
18 } catch (IncompleteElementException e) {
19 configuration.addIncompleteResultMap(resultMapResolver);
20 throw e;
21 }
22}
ResultMap 的构建逻辑封装在 ResultMapResolver
的 resolve 方法中,下面从该方法进行分析。
41// -☆- ResultMapResolver
2public ResultMap resolve() {
3 return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
4}
上面的方法将构建 ResultMap 实例的任务委托给了 MapperBuilderAssistant
的 addResultMap,我们跟进到这个方法中看看。
531// -☆- MapperBuilderAssistant
2public ResultMap addResultMap(
3 String id, Class<?> type, String extend, Discriminator discriminator,
4 List<ResultMapping> resultMappings, Boolean autoMapping) {
5
6 // 为 ResultMap 的 id 和 extend 属性值拼接命名空间
7 id = applyCurrentNamespace(id, false);
8 extend = applyCurrentNamespace(extend, true);
9
10 // 合并扩展 ResultMap
11 if (extend != null) {
12 // 如果 extend 的结果集还未解析,则抛出 IncompleteElementException 异常
13 if (!configuration.hasResultMap(extend)) {
14 throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
15 }
16
17 // 从 Configuration 中获取所有扩展 ResultMapping
18 ResultMap resultMap = configuration.getResultMap(extend);
19 List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(resultMap.getResultMappings());
20
21 // 如果主映射已存在该 resultMapping,则将扩展中的移除
22 extendedResultMappings.removeAll(resultMappings);
23
24 // 检测主映射是否有构造器 (即resultMappings 集合中是否包含 CONSTRUCTOR 标志的元素)
25 boolean declaresConstructor = false;
26 for (ResultMapping resultMapping : resultMappings) {
27 if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
28 declaresConstructor = true;
29 break;
30 }
31 }
32
33 // 如果主映射存在构造器,则移除扩展中的构造器
34 if (declaresConstructor) {
35 Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings.iterator();
36 while (extendedResultMappingsIter.hasNext()) {
37 if (extendedResultMappingsIter.next().getFlags().contains(ResultFlag.CONSTRUCTOR)) {
38 extendedResultMappingsIter.remove();
39 }
40 }
41 }
42
43 // 合并扩展映射到主映射中
44 resultMappings.addAll(extendedResultMappings);
45 }
46
47 // 构建 ResultMap
48 ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
49 .discriminator(discriminator)
50 .build();
51 configuration.addResultMap(resultMap);
52 return resultMap;
53}
上面的方法主要用于合并 extend 属性指定的扩展映射,并删除一些多余的映射列。随后,通过建造模式构建 ResultMap 实例。
991// -☆- ResultMap
2public ResultMap build() {
3 if (resultMap.id == null) {
4 throw new IllegalArgumentException("ResultMaps must have an id");
5 }
6
7 // 保存所有被映射的数据库列名(大写形式)
8 resultMap.mappedColumns = new HashSet<String>();
9 // 保存所有被映射的java属性名
10 resultMap.mappedProperties = new HashSet<String>();
11 // 保存所有的id标记映射列(如果没有一个列有id标记,则把所有列都当作id列)
12 resultMap.idResultMappings = new ArrayList<ResultMapping>();
13 // 保存所有的构造器映射列(并按照构造方法参数列表的顺序进行排序)
14 resultMap.constructorResultMappings = new ArrayList<ResultMapping>();
15 // 保存所有的非构造器映射列
16 resultMap.propertyResultMappings = new ArrayList<ResultMapping>();
17 // 保存所有的构造器参数名
18 final List<String> constructorArgNames = new ArrayList<String>();
19
20 // 遍历所有 ResultMapping
21 for (ResultMapping resultMapping : resultMap.resultMappings) {
22 // 检测是否存在嵌套查询
23 resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
24 // 检测是否存在嵌套映射
25 resultMap.hasNestedResultMaps =
26 resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
27
28 // 将 column 转换成大写,并添加到 mappedColumns 集合中
29 final String column = resultMapping.getColumn();
30 if (column != null) {
31 resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
32 } else if (resultMapping.isCompositeResult()) {
33 // 复合列的特殊处理
34 for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
35 final String compositeColumn = compositeResultMapping.getColumn();
36 if (compositeColumn != null) {
37 resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
38 }
39 }
40 }
41
42 // 添加属性 property 到 mappedProperties 集合中
43 final String property = resultMapping.getProperty();
44 if (property != null) {
45 resultMap.mappedProperties.add(property);
46 }
47
48 // 检测当前 resultMapping 是否包含 CONSTRUCTOR 标志
49 if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
50 // 添加 resultMapping 到 constructorResultMappings 中
51 resultMap.constructorResultMappings.add(resultMapping);
52 // 添加属性(constructor 节点的 name 属性)到 constructorArgNames 中
53 if (resultMapping.getProperty() != null) {
54 constructorArgNames.add(resultMapping.getProperty());
55 }
56 } else {
57 // 添加 resultMapping 到 propertyResultMappings 中
58 resultMap.propertyResultMappings.add(resultMapping);
59 }
60
61 if (resultMapping.getFlags().contains(ResultFlag.ID)) {
62 // 添加 resultMapping 到 idResultMappings 中
63 resultMap.idResultMappings.add(resultMapping);
64 }
65 }
66
67 // 如果没有一个列有id标记,则把所有列都当作id列
68 if (resultMap.idResultMappings.isEmpty()) {
69 resultMap.idResultMappings.addAll(resultMap.resultMappings);
70 }
71 if (!constructorArgNames.isEmpty()) {
72 // 获取实际的构造方法参数列表(解析@Param注解获取实际配置的形参名(通过参数数量和类型来进行匹配构造器))
73 final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
74 if (actualArgNames == null) {
75 throw new BuilderException("Error in result map '" + resultMap.id
76 + "'. Failed to find a constructor in '"
77 + resultMap.getType().getName() + "' by arg names " + constructorArgNames
78 + ". There might be more info in debug log.");
79 }
80
81 // 对 constructorResultMappings 按照构造方法参数列表的顺序进行排序
82 Collections.sort(resultMap.constructorResultMappings, new Comparator<ResultMapping>() {
83
84 public int compare(ResultMapping o1, ResultMapping o2) {
85 int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
86 int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
87 return paramIdx1 - paramIdx2;
88 }
89 });
90 }
91
92 // 将以下这些集合变为不可修改集合
93 resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
94 resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
95 resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
96 resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
97 resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
98 return resultMap;
99}
以上代码看起来很复杂,但实际上只是将 ResultMapping 实例及属性分别存储到不同的集合中而已。写点代码测试一下,并把这些集合的内容打印到控制台上,大家直观感受一下。先定义一个映射文件,如下:
121<mapper namespace="xyz.coolblog.dao.ArticleDao">
2 <resultMap id="articleResult" type="xyz.coolblog.model.Article">
3 <constructor>
4 <idArg column="id" name="id"/>
5 <arg column="title" name="title"/>
6 <arg column="content" name="content"/>
7 </constructor>
8 <id property="id" column="id"/>
9 <result property="author" column="author"/>
10 <result property="createTime" column="create_time"/>
11 </resultMap>
12</mapper>
测试代码如下:
391public class ResultMapTest {
2
3
4 public void printResultMapInfo() throws Exception {
5 Configuration configuration = new Configuration();
6 String resource = "mapper/ArticleMapper.xml";
7 InputStream inputStream = Resources.getResourceAsStream(resource);
8 XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
9 builder.parse();
10
11 ResultMap resultMap = configuration.getResultMap("articleResult");
12
13 System.out.println("\n-------------------+✨ mappedColumns ✨+--------------------");
14 System.out.println(resultMap.getMappedColumns());
15
16 System.out.println("\n------------------+✨ mappedProperties ✨+------------------");
17 System.out.println(resultMap.getMappedProperties());
18
19 System.out.println("\n------------------+✨ idResultMappings ✨+------------------");
20 resultMap.getIdResultMappings().forEach(rm -> System.out.println(simplify(rm)));
21
22 System.out.println("\n---------------+✨ propertyResultMappings ✨+---------------");
23 resultMap.getPropertyResultMappings().forEach(rm -> System.out.println(simplify(rm)));
24
25 System.out.println("\n-------------+✨ constructorResultMappings ✨+--------------");
26 resultMap.getConstructorResultMappings().forEach(rm -> System.out.println(simplify(rm)));
27
28 System.out.println("\n-------------------+✨ resultMappings ✨+-------------------");
29 resultMap.getResultMappings().forEach(rm -> System.out.println(simplify(rm)));
30
31 inputStream.close();
32 }
33
34 /** 简化 ResultMapping 输出结果 */
35 private String simplify(ResultMapping resultMapping) {
36 return String.format("ResultMapping{column='%s', property='%s', flags=%s, ...}",
37 resultMapping.getColumn(), resultMapping.getProperty(), resultMapping.getFlags());
38 }
39}
结果如下:
<sql>
节点用来定义一些可重复使用的 SQL 语句片段,如表名,或表的列名等。在映射文件中,可以通过 <include>
节点引用 <sql>
节点定义的内容。下面是 <sql>
节点的使用方式,如下:
171<!--定义一个sql节点,id为table,内容为“article”-->
2<sql id="table">
3 article
4</sql>
5
6<!--定义一个带占位符的sql节点,占位符可以从全局属性中解析,也可以从include标签的属性解析-->
7<sql id="table">
8 ${table_prefix}_article
9</sql>
10
11<!--通过include标签引用定义的sql节点-->
12<select id="findOne" resultType="Article">
13 SELECT id, title
14 FROM <include refid="table"/>
15 WHERE id = #{id}
16</select>
17
下面分析一下 sql 节点的解析过程,如下:
91private void sqlElement(List<XNode> list) throws Exception {
2 if (configuration.getDatabaseId() != null) {
3 // 调用 sqlElement 解析 <sql> 节点
4 sqlElement(list, configuration.getDatabaseId());
5 }
6
7 // 再次调用 sqlElement,不同的是,这次调用,该方法的第二个参数为 null
8 sqlElement(list, null);
9}
这里需注意下 databaseId 属性的特殊处理,后面会多次用到。MyBatis一般采用 两次调用的方式来处理databaseId 问题,第一次带上下文中的数据库厂商调用,第二次使用NULL调用。即优先解析匹配数据库厂商标识的标签,如果不存在匹配的,则解析不带数据库厂商标识的标签。继续往下分析。
161private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
2 for (XNode context : list) {
3 // 获取 id 和 databaseId 属性
4 String databaseId = context.getStringAttribute("databaseId");
5 String id = context.getStringAttribute("id");
6
7 // id = currentNamespace + "." + id
8 id = builderAssistant.applyCurrentNamespace(id, false);
9
10 // 检测当前 databaseId 和 requiredDatabaseId 是否一致
11 if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
12 // 将 <id, XNode> 键值对缓存到 sqlFragments 中
13 sqlFragments.put(id, context);
14 }
15 }
16}
首先是获取 <sql>
节点的 id 和 databaseId 属性,然后为 id 属性值拼接命名空间。最后,通过检测当前 databaseId 和 requiredDatabaseId 是否一致,来决定保存还是忽略当前的 <sql>
节点。
下面,我们来看一下 databaseId 的匹配逻辑是怎样的。
241private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
2 if (requiredDatabaseId != null) {
3 // 当前 databaseId 和目标 databaseId 不一致时,返回 false
4 if (!requiredDatabaseId.equals(databaseId)) {
5 return false;
6 }
7 } else {
8 // 如果目标 databaseId 为空,但当前 databaseId 不为空。两者不一致,返回 false
9 if (databaseId != null) {
10 return false;
11 }
12 /*
13 * 如果当前 <sql> 节点的 id 与之前的 <sql> 节点重复,且先前节点
14 * databaseId 不为空。则忽略当前节点,并返回 false
15 */
16 if (this.sqlFragments.containsKey(id)) {
17 XNode context = this.sqlFragments.get(id);
18 if (context.getStringAttribute("databaseId") != null) {
19 return false;
20 }
21 }
22 }
23 return true;
24}
下面总结一下 databaseId 的匹配规则。
databaseId 与 requiredDatabaseId 不一致,即失配,返回 false
当前节点与之前的节点出现 id 重复的情况,若之前的 <sql>
节点 databaseId 属性不为空,返回 false。
若以上两条规则均匹配失败,此时返回 true
在上面三条匹配规则中,第二条规则稍微难理解一点。这里简单分析一下,考虑下面这种配置。
91<!-- databaseId 不为空 -->
2<sql id="table" databaseId="mysql">
3 article
4</sql>
5
6<!-- databaseId 为空 -->
7<sql id="table">
8 article
9</sql>
在上面配置中,两个 <sql>
节点的 id 属性值相同,databaseId 属性不一致。假设 configuration.databaseId = mysql,第一次调用 sqlElement 方法,第一个 <sql>
节点对应的 XNode 会被放入到 sqlFragments 中。第二次调用 sqlElement 方法时,requiredDatabaseId 参数为空。由于 sqlFragments 中已包含了一个 id 节点,且该节点的 databaseId 不为空,此时匹配逻辑返回 false,第二个节点不会被保存到 sqlFragments。
Statement节点指 SQL 语句节点,包括用于查询的<select>
节点,以及执行更新和其它类型语句<update>
、<insert>
和<delete>
节点,四者配置方式非常相似,因此放在一起进行解析。
221private void buildStatementFromContext(List<XNode> list) {
2 if (configuration.getDatabaseId() != null) {
3 // 调用重载方法构建 Statement
4 buildStatementFromContext(list, configuration.getDatabaseId());
5 }
6 // 调用重载方法构建 Statement,requiredDatabaseId 参数为空
7 buildStatementFromContext(list, null);
8}
9
10private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
11 for (XNode context : list) {
12 // 创建 Statement 建造类
13 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
14 try {
15 // 解析 Statement 节点,并将解析结果存储到 configuration 的 mappedStatements 集合中
16 statementParser.parseStatementNode();
17 } catch (IncompleteElementException e) {
18 // 解析失败,将解析器放入 configuration 的 incompleteStatements 集合中
19 configuration.addIncompleteStatement(statementParser);
20 }
21 }
22}
上面的解析方法没有什么实质性的解析逻辑,我们继续往下分析。
741public void parseStatementNode() {
2 // 获取 id 和 databaseId 属性
3 String id = context.getStringAttribute("id");
4 String databaseId = context.getStringAttribute("databaseId");
5
6 // 根据 databaseId 进行检测,检测逻辑和上一节基本一致,这里不再赘述
7 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
8 return;
9 }
10
11 // 获取各种属性
12 Integer fetchSize = context.getIntAttribute("fetchSize");
13 Integer timeout = context.getIntAttribute("timeout");
14 String parameterMap = context.getStringAttribute("parameterMap");
15 String parameterType = context.getStringAttribute("parameterType");
16 Class<?> parameterTypeClass = resolveClass(parameterType);
17 String resultMap = context.getStringAttribute("resultMap");
18 String resultType = context.getStringAttribute("resultType");
19 String lang = context.getStringAttribute("lang");
20 LanguageDriver langDriver = getLanguageDriver(lang);
21
22 // 通过别名解析 resultType 对应的类型
23 Class<?> resultTypeClass = resolveClass(resultType);
24 String resultSetType = context.getStringAttribute("resultSetType");
25
26 // 解析 Statement 类型,默认为 PREPARED
27 StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
28
29 // 解析 ResultSetType
30 ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
31
32 // 获取节点的名称,比如 <select> 节点名称为 select
33 String nodeName = context.getNode().getNodeName();
34 // 根据节点名称解析 SqlCommandType
35 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
36 boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
37 boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
38 boolean useCache = context.getBooleanAttribute("useCache", isSelect);
39 boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
40
41 // 解析 <include> 节点
42 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
43 includeParser.applyIncludes(context.getNode());
44
45 // 解析 <selectKey> 节点
46 processSelectKeyNodes(id, parameterTypeClass, langDriver);
47
48 // 解析 SQL 语句
49 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
50 String resultSets = context.getStringAttribute("resultSets");
51 String keyProperty = context.getStringAttribute("keyProperty");
52 String keyColumn = context.getStringAttribute("keyColumn");
53
54 KeyGenerator keyGenerator;
55 String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
56 keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
57 if (configuration.hasKeyGenerator(keyStatementId)) {
58 // 获取 KeyGenerator 实例
59 keyGenerator = configuration.getKeyGenerator(keyStatementId);
60 } else {
61 // 创建 KeyGenerator 实例
62 keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
63 configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
64 }
65
66 /*
67 * 构建 MappedStatement 对象,并将该对象存储到
68 * Configuration 的 mappedStatements 集合中
69 */
70 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
71 fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
72 resultSetTypeEnum, flushCache, useCache, resultOrdered,
73 keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
74}
上面的代码中大都是用来获取节点属性,以及解析部分属性等,抛去这部分代码,以上代码做的事情如下。
解析 <include>
节点。
解析 <selectKey>
节点。
解析 SQL,获取 SqlSource。
构建 MappedStatement 实例。
以上流程对应的代码比较复杂,每个步骤都能分析出一些东西来,下面我会每个步骤都进行分析。
<include>
节点的解析逻辑封装在 applyIncludes 中,该方法的代码如下:
111public void applyIncludes(Node source) {
2 // 创建一个临时 Properties,保存 configuration 中的变量
3 Properties variablesContext = new Properties();
4 Properties configurationVariables = configuration.getVariables();
5 if (configurationVariables != null) {
6 variablesContext.putAll(configurationVariables);
7 }
8
9 // 调用重载方法处理 <include> 节点(注意使用了variablesContext,这样就不会污染configuration中的数据了)
10 applyIncludes(source, variablesContext, false);
11}
由于解析 include 节点时会向 Properties 中添加新的元素,为了防止全局属性被污染,因此先创建了一个临时的 Properties,传给重载 applyIncludes 方法的使用。
711private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
2
3 // ⭐️ 第一个条件分支
4 if (source.getNodeName().equals("include")) {
5
6 /*
7 * 获取 <sql> 节点。若 refid 中包含属性占位符 ${},
8 * 则需先将属性占位符替换为对应的属性值
9 */
10 Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
11
12 /*
13 * 解析 <include> 的子节点 <property>,并将解析结果与 variablesContext 融合,
14 * 然后返回融合后的 Properties。若 <property> 节点的 value 属性中存在占位符 ${},
15 * 则将占位符替换为对应的属性值
16 */
17 Properties toIncludeContext = getVariablesContext(source, variablesContext);
18
19 /*
20 * 这里是一个递归调用,用于将 <sql> 节点内容中出现的属性占位符 ${} 替换为对应的
21 * 属性值。这里要注意一下递归调用的参数:
22 *
23 * - toInclude:<sql> 节点对象
24 * - toIncludeContext:<include> 子节点 <property> 的解析结果与
25 * 全局变量融合后的结果
26 */
27 applyIncludes(toInclude, toIncludeContext, true);
28
29 /*
30 * 如果 <sql> 和 <include> 节点不在一个文档中,
31 * 则从其他文档中将 <sql> 节点引入到 <include> 所在文档中
32 */
33 if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
34 toInclude = source.getOwnerDocument().importNode(toInclude, true);
35 }
36 // 将 <include> 节点替换为 <sql> 节点
37 source.getParentNode().replaceChild(toInclude, source);
38 while (toInclude.hasChildNodes()) {
39 // 将 <sql> 中的内容插入到 <sql> 节点之前
40 toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
41 }
42
43 /*
44 * 前面已经将 <sql> 节点的内容插入到 dom 中了,
45 * 现在不需要 <sql> 节点了,这里将该节点从 dom 中移除
46 */
47 toInclude.getParentNode().removeChild(toInclude);
48
49 // ⭐️ 第二个条件分支
50 } else if (source.getNodeType() == Node.ELEMENT_NODE) {
51 if (included && !variablesContext.isEmpty()) {
52 NamedNodeMap attributes = source.getAttributes();
53 for (int i = 0; i < attributes.getLength(); i++) {
54 Node attr = attributes.item(i);
55 // 将 source 节点属性中的占位符 ${} 替换成具体的属性值
56 attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
57 }
58 }
59
60 NodeList children = source.getChildNodes();
61 for (int i = 0; i < children.getLength(); i++) {
62 // 递归调用
63 applyIncludes(children.item(i), variablesContext, included);
64 }
65
66 // ⭐️ 第三个条件分支
67 } else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
68 // 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值
69 source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
70 }
71}
上面的代码由三个分支语句,外加两个递归调用组成,理解起来有一定难度,下面将结合案例来进行讲解。
151<mapper namespace="xyz.coolblog.dao.ArticleDao">
2 <sql id="table">
3 ${table_name}
4 </sql>
5
6 <select id="findOne" resultType="xyz.coolblog.dao.ArticleDO">
7 SELECT
8 id, title
9 FROM
10 <include refid="table">
11 <property name="table_name" value="article"/>
12 </include>
13 WHERE id = #{id}
14 </select>
15</mapper>
我们先来看一下 applyIncludes 方法第一次被调用时的状态,如下:
101参数值:
2source = <select> 节点
3节点类型:ELEMENT_NODE
4variablesContext = [ ] // 无内容
5included = false
6
7执行流程:
81. 进入条件分支2
92. 获取 <select> 子节点列表
103. 遍历子节点列表,将子节点作为参数,进行递归调用
第一次调用 applyIncludes 方法,source = <select>
,代码进入条件分支2。在该分支中,首先要获取 <select>
节点的子节点列表。可获取到的子节点如下:
编号 | 子节点 | 类型 | 描述 |
---|---|---|---|
1 | SELECT id, title FROM | TEXT_NODE | 文本节点 |
2 | <include refid="table"/> | ELEMENT_NODE | 普通节点 |
3 | WHERE id = #{id} | TEXT_NODE | 文本节点 |
在获取到子节点类列表后,接下来要做的事情是遍历列表,然后将子节点作为参数进行递归调用。在上面三个子节点中,子节点1和子节点3都是文本节点,调用过程一致。下面先来看下子节点1的调用过程,如下:
然后我们在看一下子节点2的调用过程,如下:
<selectKey>
可以在主语句执行之前或之后执行额外的查询操作。一般用于在插入数据前查询主键值,这对一些不支持主键自增的数据库来说非常实用。
91<insert id="saveAuthor">
2 <selectKey keyProperty="id" resultType="int" order="BEFORE">
3 select author_seq.nextval from dual
4 </selectKey>
5 insert into Author
6 (id, name, password)
7 values
8 (#{id}, #{username}, #{password})
9</insert>
下面我们来看一下 <selectKey>
节点的解析过程。
121private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
2 List<XNode> selectKeyNodes = context.evalNodes("selectKey");
3
4 // 处理 databaseId 问题(逻辑与之前一致)
5 if (configuration.getDatabaseId() != null) {
6 parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
7 }
8 parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
9
10 // 将 <selectKey> 节点从 dom 树中移除
11 removeSelectKeyNodes(selectKeyNodes);
12}
selectkey节点解析完成后,会被从 dom 树中移除,这样后续可以更专注的解析 <insert>
或 <update>
节点中的 SQL,无需再额外处理 <selectKey>
节点。
571private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass,
2 LanguageDriver langDriver, String skRequiredDatabaseId) {
3 for (XNode nodeToHandle : list) {
4 // id = parentId + !selectKey,比如 saveUser!selectKey
5 String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
6 // 获取 <selectKey> 节点的 databaseId 属性
7 String databaseId = nodeToHandle.getStringAttribute("databaseId");
8 // 匹配 databaseId
9 if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
10 // 解析 <selectKey> 节点
11 parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
12 }
13 }
14}
15
16// 实际解析逻辑
17private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass,
18 LanguageDriver langDriver, String databaseId) {
19
20 // 获取各种属性
21 String resultType = nodeToHandle.getStringAttribute("resultType");
22 Class<?> resultTypeClass = resolveClass(resultType);
23 StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
24 String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
25 String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
26 boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
27
28 // 设置默认值
29 boolean useCache = false;
30 boolean resultOrdered = false;
31 KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
32 Integer fetchSize = null;
33 Integer timeout = null;
34 boolean flushCache = false;
35 String parameterMap = null;
36 String resultMap = null;
37 ResultSetType resultSetTypeEnum = null;
38
39 // 创建 SqlSource
40 SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
41
42 // <selectKey> 节点中只能配置 SELECT 查询语句,
43 SqlCommandType sqlCommandType = SqlCommandType.SELECT;
44
45 // 构建 MappedStatement,并将 MappedStatement ,添加到 Configuration 的 mappedStatements map 中
46 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
47 fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
48 resultSetTypeEnum, flushCache, useCache, resultOrdered,
49 keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
50
51 // 拼接Id,查询上一步添加的 MappedStatement
52 id = builderAssistant.applyCurrentNamespace(id, false);
53 MappedStatement keyStatement = configuration.getMappedStatement(id, false);
54
55 // 创建 SelectKeyGenerator,并添加到 keyGenerators map 中
56 configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
57}
以上代码比较重要的步骤如下:
创建 SqlSource 实例
构建并缓存 MappedStatement 实例
构建并缓存 SelectKeyGenerator 实例
第1步和第2步调用的是公共逻辑,其他地方也会调用,这两步对应的源码后续会分两节进行讲解。第3步则是创建一个 SelectKeyGenerator
实例,SelectKeyGenerator 创建的过程本身没什么好说的,所以就不多说了。
前面分析了 <include>
和 <selectKey>
节点的解析过程,这两个节点解析完成后,都会以不同的方式从 dom 树中消失。所以目前的 SQL 语句节点由一些文本节点和普通节点组成,比如 <if>
、<where>
等。那下面我们来看一下移除掉 <include>
和 <selectKey>
节点后的 SQL 语句节点是如何解析的。
191// -☆- XMLLanguageDriver
2public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
3 XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
4 return builder.parseScriptNode();
5}
6
7// -☆- XMLScriptBuilder
8public SqlSource parseScriptNode() {
9 // 解析 SQL 语句节点
10 MixedSqlNode rootSqlNode = parseDynamicTags(context);
11 SqlSource sqlSource = null;
12 // 根据 isDynamic 状态创建不同的 SqlSource
13 if (isDynamic) {
14 sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
15 } else {
16 sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
17 }
18 return sqlSource;
19}
SQL 语句的解析逻辑被封装在了 XMLScriptBuilder
类的 parseScriptNode 方法中。该方法首先会调用 parseDynamicTags 解析 SQL 语句节点,在解析过程中,会判断节点是是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。若包含动态标记,则会将 isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource。下面,我们来看一下 parseDynamicTags 方法的逻辑。
551/** 该方法用于初始化 nodeHandlerMap 集合,该集合后面会用到 */
2private void initNodeHandlerMap() {
3 nodeHandlerMap.put("trim", new TrimHandler());
4 nodeHandlerMap.put("where", new WhereHandler());
5 nodeHandlerMap.put("set", new SetHandler());
6 nodeHandlerMap.put("foreach", new ForEachHandler());
7 nodeHandlerMap.put("if", new IfHandler());
8 nodeHandlerMap.put("choose", new ChooseHandler());
9 nodeHandlerMap.put("when", new IfHandler());
10 nodeHandlerMap.put("otherwise", new OtherwiseHandler());
11 nodeHandlerMap.put("bind", new BindHandler());
12}
13
14protected MixedSqlNode parseDynamicTags(XNode node) {
15 List<SqlNode> contents = new ArrayList<SqlNode>();
16 NodeList children = node.getNode().getChildNodes();
17 // 遍历子节点
18 for (int i = 0; i < children.getLength(); i++) {
19 XNode child = node.newXNode(children.item(i));
20 if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
21 // 获取文本内容
22 String data = child.getStringBody("");
23 TextSqlNode textSqlNode = new TextSqlNode(data);
24 // 若文本中包含 ${} 占位符,也被认为是动态节点
25 if (textSqlNode.isDynamic()) {
26 contents.add(textSqlNode);
27 // 设置 isDynamic 为 true
28 isDynamic = true;
29 } else {
30 // 创建 StaticTextSqlNode
31 contents.add(new StaticTextSqlNode(data));
32 }
33
34 // child 节点是 ELEMENT_NODE 类型,比如 <if>、<where> 等
35 } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
36 // 获取节点名称,比如 if、where、trim 等
37 String nodeName = child.getNode().getNodeName();
38 // 根据节点名称获取 NodeHandler
39 NodeHandler handler = nodeHandlerMap.get(nodeName);
40 /*
41 * 如果 handler 为空,表明当前节点对与 MyBatis 来说,是未知节点。
42 * MyBatis 无法处理这种节点,故抛出异常
43 */
44 if (handler == null) {
45 throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
46 }
47 // 处理 child 节点,生成相应的 SqlNode
48 handler.handleNode(child, contents);
49
50 // 设置 isDynamic 为 true
51 isDynamic = true;
52 }
53 }
54 return new MixedSqlNode(contents);
55}
上面的代码主要是用来判断节点是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。这里,不管是动态 SQL 节点还是静态 SQL 节点,我们都可以把它们看成是 SQL 片段,一个 SQL 语句由多个 SQL 片段组成。在解析过程中,这些 SQL 片段被存储在 contents 集合中。最后,该集合会被传给 MixedSqlNode 构造方法,用于创建 MixedSqlNode 实例。从 MixedSqlNode 类名上可知,它会存储多种类型的 SqlNode。除了上面代码中已出现的几种 SqlNode 实现类,还有一些 SqlNode 实现类未出现在上面的代码中。但它们也参与了 SQL 语句节点的解析过程,这里我们来看一下这些幕后的 SqlNode 类。
上面的 SqlNode 实现类用于处理不同的动态 SQL 逻辑,这些 SqlNode 是如何生成的呢?答案是由各种 NodeHandler 生成。我们再回到上面的代码中,可以看到这样一句代码:
11handler.handleNode(child, contents);
该代码用于处理动态 SQL 节点,并生成相应的 SqlNode。下面来简单分析一下 WhereHandler 的代码。
161/** 定义在 XMLScriptBuilder 中 */
2private class WhereHandler implements NodeHandler {
3
4 public WhereHandler() {
5 }
6
7
8 public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
9 // 调用 parseDynamicTags 解析 <where> 节点
10 MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
11 // 创建 WhereSqlNode
12 WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
13 // 添加到 targetContents
14 targetContents.add(where);
15 }
16}
如上,handleNode 方法内部会再次调用 parseDynamicTags 解析 <where>
节点中的内容(即子标签),这样又会生成一个 MixedSqlNode 对象。最终,整个 SQL 语句节点会生成一个具有树状结构的 MixedSqlNode。如下图:
到此,SQL 语句的解析过程就分析完了。现在,我们已经将 XML 配置解析了 SqlSource,但这还没有结束。SqlSource 中只能记录 SQL 语句信息,除此之外,这里还有一些额外的信息需要记录。因此,我们需要一个类能够同时存储 SqlSource 和其他的信息。这个类就是 MappedStatement。下面我们来看一下它的构建过程。
SQL 语句节点可以定义很多属性,这些属性和属性值最终存储在 MappedStatement 中。下面我们看一下 MappedStatement 的构建过程是怎样的。
401public MappedStatement addMappedStatement(
2 String id, SqlSource sqlSource, StatementType statementType,
3 SqlCommandType sqlCommandType,Integer fetchSize, Integer timeout,
4 String parameterMap, Class<?> parameterType,String resultMap,
5 Class<?> resultType, ResultSetType resultSetType, boolean flushCache,
6 boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator,
7 String keyProperty,String keyColumn, String databaseId,
8 LanguageDriver lang, String resultSets) {
9
10 if (unresolvedCacheRef) {
11 throw new IncompleteElementException("Cache-ref not yet resolved");
12 }
13
14 id = applyCurrentNamespace(id, false);
15 boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
16
17 // 创建建造器,设置各种属性(注意这里使用了MapperBuilderAssistant.currentCache)
18 MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
19 .resource(resource).fetchSize(fetchSize).timeout(timeout)
20 .statementType(statementType).keyGenerator(keyGenerator)
21 .keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId)
22 .lang(lang).resultOrdered(resultOrdered).resultSets(resultSets)
23 .resultMaps(getStatementResultMaps(resultMap, resultType, id))
24 .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
25 .resultSetType(resultSetType).useCache(valueOrDefault(useCache, isSelect))
26 .cache(currentCache);
27
28 // 获取或创建 ParameterMap
29 ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
30 if (statementParameterMap != null) {
31 statementBuilder.parameterMap(statementParameterMap);
32 }
33
34 // 构建 MappedStatement,没有什么复杂逻辑,不跟下去了
35 MappedStatement statement = statementBuilder.build();
36
37 // 添加 MappedStatement 到 configuration 的 mappedStatements 集合中
38 configuration.addMappedStatement(statement);
39 return statement;
40}
上面就是 MappedStatement的构建过程,逻辑比较简单,没什么好说的。但有一个地方需要注意,构建时用了MapperBuilderAssistant类中的currentCache,改变量是局部的,导致了名称空间相同的XML和注解的缓存配置不能共享。
映射文件解析完成后,并不意味着整个解析过程就结束了。此时还需要通过命名空间绑定 mapper 接口,这样才能将映射文件中的 SQL 语句和 mapper 接口中的方法绑定在一起,后续即可通过调用 mapper 接口方法执行与之对应的 SQL 语句。下面我们来分析一下 mapper 接口的绑定过程。
541// -☆- XMLMapperBuilder
2private void bindMapperForNamespace() {
3 // 获取映射文件的命名空间
4 String namespace = builderAssistant.getCurrentNamespace();
5 if (namespace != null) {
6 Class<?> boundType = null;
7 try {
8 // 根据命名空间获取Mapper接口(命名空间是一个全类名)
9 boundType = Resources.classForName(namespace);
10 } catch (ClassNotFoundException e) {
11 }
12 if (boundType != null) {
13 // 检测当前 mapper 类是否被绑定过
14 if (!configuration.hasMapper(boundType)) {
15 configuration.addLoadedResource("namespace:" + namespace);
16 // 绑定 mapper 类
17 configuration.addMapper(boundType);
18 }
19 }
20 }
21}
22
23// -☆- Configuration
24public <T> void addMapper(Class<T> type) {
25 // 通过 MapperRegistry 绑定 mapper 类
26 mapperRegistry.addMapper(type);
27}
28
29// -☆- MapperRegistry
30public <T> void addMapper(Class<T> type) {
31 if (type.isInterface()) {
32 if (hasMapper(type)) {
33 throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
34 }
35 boolean loadCompleted = false;
36 try {
37 /*
38 * 将 type 和 MapperProxyFactory 进行绑定(MapperProxyFactory 可为 mapper 接口生成代理类)
39 */
40 knownMappers.put(type, new MapperProxyFactory<T>(type));
41
42 // 创建注解解析器。在 MyBatis 中,有 XML 和 注解两种配置方式可选
43 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
44
45 // 解析注解中的信息
46 parser.parse();
47 loadCompleted = true;
48 } finally {
49 if (!loadCompleted) {
50 knownMappers.remove(type);
51 }
52 }
53 }
54}
以上就是 Mapper 接口的绑定过程。这里简单一下:
获取命名空间,并根据命名空间解析 mapper 类型
将 type 和 MapperProxyFactory 实例存入 knownMappers 中
解析注解中的信息
以上步骤中,第3步的逻辑较多。如果大家看懂了映射文件的解析过程,那么注解的解析过程也就不难理解了,这里就不深入分析了。好了,Mapper 接口的绑定过程就先分析到这。
在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis 的做法是抛出 IncompleteElementException
异常。外部逻辑会捕捉这个异常,并将节点对应的解析器放入 incomplet* 集合中。下面我们来看一下 MyBatis 是如何处理未完成解析的节点。
121// -☆- XMLMapperBuilder 映射文件的解析入口
2public void parse() {
3 // 省略部分代码
4
5 // 解析 mapper 节点
6 configurationElement(parser.evalNode("/mapper"));
7
8 // 处理未完成解析的节点
9 parsePendingResultMaps();
10 parsePendingCacheRefs();
11 parsePendingStatements();
12}
从上面的源码中可以知道有三种节点在解析过程中可能会出现不能完成解析的情况,相关代码逻辑类似,下面以 parsePendingCacheRefs 方法为例进行分析,看一下如何配置映射文件会导致 <cache-ref>
节点无法完成解析。
101<!-- 映射文件1 -->
2<mapper namespace="xyz.coolblog.dao.Mapper1">
3 <!-- 引用映射文件2中配置的缓存 -->
4 <cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
5</mapper>
6
7<!-- 映射文件2 -->
8<mapper namespace="xyz.coolblog.dao.Mapper2">
9 <cache/>
10</mapper>
假设 MyBatis 先解析映射文件1,然后再解析映射文件2。按照这样的解析顺序,映射文件1中的 <cache-ref>
节点就无法完成解析,因为它所引用的缓存还未被解析。当映射文件2解析完成后,MyBatis 会调用 parsePendingCacheRefs 方法处理在此之前未完成解析的 <cache-ref>
节点。具体的逻辑如下:
221private void parsePendingCacheRefs() {
2 // 获取 CacheRefResolver 列表
3 Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
4 synchronized (incompleteCacheRefs) {
5 Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
6 // 通过迭代器遍历列表
7 while (iter.hasNext()) {
8 try {
9 // 尝试解析 <cache-ref> 节点,若解析失败,则抛出 IncompleteElementException,
10 iter.next().resolveCacheRef();
11 // 移除 CacheRefResolver 对象。如果代码能执行到此处,表明已成功解析了 <cache-ref> 节点
12 iter.remove();
13 } catch (IncompleteElementException e) {
14 /*
15 * 如果再次发生 IncompleteElementException 异常,表明当前映射文件中并没有
16 * <cache-ref> 所引用的缓存。有可能所引用的缓存在后面的映射文件中,所以这里
17 * 不能将解析失败的 CacheRefResolver 从集合中删除
18 */
19 }
20 }
21 }
22}
本章节较为详细的介绍了 MyBatis 执行 SQL 的过程,包括但不限于 Mapper 接口代理类的生成、接口方法的解析、SQL 语句的解析、运行时参数的绑定、查询结果自动映射、关联查询、嵌套映射、懒加载等。下面一张图总结了MyBatis执行SQL的过程中涉及到的主要组件,把MyBatis框架分了为数层,每层都有相应的功能,其中 MyBatis 框架层又可细分为会话层
、执行器层
、JDBC处理层
等。
最外层是与我们业务代码打交道的DAO层,也称为 Mapper 层,通过动态代理技术,简化了用户的持久层操作,主要做了接口方法解析、参数转换等工作。
会话层主要封装了执行器,提供了各种语义清晰的方法,供使用者调用。执行器层用于协调其它组件以及实现一些公共逻辑,如一二级缓存、获取连接、转换和设置参数、映射结果集等,做的事情较多。JDCB处理层主要是与 JDBC 层面的接口打交道。
除此之外,一些其它组件也起到了重要的作用。如 ParameterHandler 和 ResultSetHandler,一个负责向 SQL 中设置运行时参数,另一个负责处理 SQL 执行结果,它们俩可以看做是 StatementHandler 辅助类。
最后看一下右边横跨数层的类,Configuration 是一个全局配置类,很多地方都依赖它。MappedStatement 对应 SQL 配置,包含了 SQL 配置的相关信息。BoundSql 中包含了已完成解析的 SQL 语句,以及运行时参数等。
下面我们将从一个简单案例开始,逐步分析上述提到的一些组件。
在单独使用 MyBatis 进行数据库操作时,我们通常都会先调用 SqlSession
接口的 getMapper方法为我们的 Mapper 接口生成实现类,然后就可以通过 Mapper 进行数据库操作了。
51// 1. 生成Mapper接口实现类(通过JDK动态代理的方式)
2ArticleMapper articleMapper = session.getMapper(ArticleMapper.class);
3
4// 2. 通过Mapper执行操作
5Article article = articleMapper.findOne(1);
在执行操作时,方法调用会被代理逻辑拦截。在代理逻辑中可根据方法名及方法归属接口获取到当前方法对应的 SQL 以及其他一些信息,拿到这些信息即可进行数据库操作。下面就来看看实现类是如何生成的,以及代理逻辑是怎么样的?
首先从 DefaultSqlSession 的 getMapper 方法开始看起,如下:
251// -☆- DefaultSqlSession
2public <T> T getMapper(Class<T> type) {
3 return configuration.<T>getMapper(type, this);
4}
5
6// -☆- Configuration
7public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
8 return mapperRegistry.getMapper(type, sqlSession);
9}
10
11// -☆- MapperRegistry
12public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
13 // 从 knownMappers 中获取与 type 对应的 MapperProxyFactory
14 // knownMappers 是在解析 <mappers> 节点时,调用 MapperRegistry 的 addMapper 方法初始化的
15 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
16 if (mapperProxyFactory == null) {
17 throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
18 }
19 try {
20 // 创建代理对象
21 return mapperProxyFactory.newInstance(sqlSession);
22 } catch (Exception e) {
23 throw new BindingException("Error getting mapper instance. Cause: " + e, e);
24 }
25}
经过连续的简单调用后,获取到了Mapper接口对应 MapperProxyFactory 对象,然后就可调用工厂方法为 Mapper 接口生成代理对象了
141// -☆- MapperProxyFactory
2public T newInstance(SqlSession sqlSession) {
3 /*
4 * 创建 MapperProxy 对象。
5 MapperProxy 实现了 InvocationHandler 接口,代理逻辑封装在此类中
6 */
7 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
8 return newInstance(mapperProxy);
9}
10
11protected T newInstance(MapperProxy<T> mapperProxy) {
12 // 通过 JDK 动态代理创建代理对象
13 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
14}
上面的代码首先创建了一个 MapperProxy
对象,该对象实现了 InvocationHandler
接口。然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成代理对象。
到此,关于 Mapper 接口代理对象的创建过程就分析完了。现在我们的 ArticleMapper 接口指向的代理对象已经创建完毕,下面就可以调用接口方法进行数据库操作了。由于接口方法会被代理逻辑拦截,所以下面我们把目光聚焦在代理逻辑上面,看看代理逻辑会做哪些事情。
Mapper 接口方法的代理逻辑实现的比较简单,首先会对拦截的方法进行一些检测,以决定是否执行后续的数据库操作。
261public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
2 try {
3 // 如果方法是定义在 Object 类中的,则直接调用
4 if (Object.class.equals(method.getDeclaringClass())) {
5 return method.invoke(this, args);
6
7 /*
8 * 下面的代码最早出现在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的
9 * 新特性 - 默认方法。这段代码的逻辑就不分析了,有兴趣的同学可以
10 * 去 Github 上看一下相关的相关的讨论(issue #709),链接如下:
11 *
12 * https://github.com/mybatis/mybatis-3/issues/709
13 */
14 } else if (isDefaultMethod(method)) {
15 return invokeDefaultMethod(proxy, method, args);
16 }
17 } catch (Throwable t) {
18 throw ExceptionUtil.unwrapThrowable(t);
19 }
20
21 // 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象
22 final MapperMethod mapperMethod = cachedMapperMethod(method);
23
24 // 调用 execute 方法执行 SQL
25 return mapperMethod.execute(sqlSession, args);
26}
如上,代理逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、hashCode 方法等。对于这类方法,直接执行即可。除此之外,MyBatis 从 3.4.2 版本开始,对 JDK 1.8 接口的默认方法提供了支持,具体就不分析了。完成相关检测后,紧接着从缓存中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。在分析 execute 方法之前,我们先来看一下 MapperMethod 对象的创建过程。MapperMethod 的创建过程看似普通,但却包含了一些重要的逻辑,所以不能忽视。
本节来分析一下 MapperMethod 的构造方法,看看它的构造方法中都包含了哪些逻辑。如下:
121public class MapperMethod {
2
3 private final SqlCommand command;
4 private final MethodSignature method;
5
6 public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
7 // 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息
8 this.command = new SqlCommand(config, mapperInterface, method);
9 // 创建 MethodSignature 对象,从类名中可知,该对象包含了被拦截方法的一些信息
10 this.method = new MethodSignature(config, mapperInterface, method);
11 }
12}
如上,MapperMethod 构造方法的逻辑很简单,主要是创建 SqlCommand
和 MethodSignature
对象。这两个对象分别记录了不同的信息,这些信息在后续的方法调用中都会被用到。下面我们深入到这两个类的构造方法中,探索它们的初始化逻辑。
1) 创建 SqlCommand 对象
前面说了 SqlCommand 中保存了一些和 SQL 相关的信息,那具体有哪些信息呢?答案在下面的代码中。
351public static class SqlCommand {
2
3 private final String name;
4 private final SqlCommandType type;
5
6 public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
7 final String methodName = method.getName();
8 final Class<?> declaringClass = method.getDeclaringClass();
9
10 // 查找 MappedStatement。
11 // 先拼接 statementId:接口全类名.方法名,在当前接口查找。未找到则递归父接口查找。
12 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
13
14 // 检测当前方法是否有对应的 MappedStatement
15 if (ms == null) {
16 // 检测当前方法是否有 @Flush 注解
17 if (method.getAnnotation(Flush.class) != null) {
18 // 设置 name 和 type 变量
19 name = null;
20 type = SqlCommandType.FLUSH;
21 } else {
22 // 若 ms == null 且方法无 @Flush 注解,此时抛出异常。
23 throw new BindingException("Invalid bound statement (not found): "
24 + mapperInterface.getName() + "." + methodName);
25 }
26 } else {
27 // 设置 name 和 type 变量
28 name = ms.getId();
29 type = ms.getSqlCommandType();
30 if (type == SqlCommandType.UNKNOWN) {
31 throw new BindingException("Unknown execution method for: " + name);
32 }
33 }
34 }
35}
如上,SqlCommand 的构造方法主要用于初始化它的两个成员变量。代码不是很长,逻辑也不难理解,就不多说了。继续往下看。
2) 创建 MethodSignature 对象
MethodSignature 即方法签名,顾名思义,该类保存了一些和目标方法相关的信息。比如目标方法的返回类型,目标方法的参数列表信息等。下面,我们来分析一下 MethodSignature 的构造方法。
421public static class MethodSignature {
2
3 private final boolean returnsMany;
4 private final boolean returnsMap;
5 private final boolean returnsVoid;
6 private final boolean returnsCursor;
7 private final Class<?> returnType;
8 private final String mapKey;
9 private final Integer resultHandlerIndex;
10 private final Integer rowBoundsIndex;
11 private final ParamNameResolver paramNameResolver;
12
13 public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
14
15 // 通过反射解析方法返回类型
16 Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
17 if (resolvedReturnType instanceof Class<?>) {
18 this.returnType = (Class<?>) resolvedReturnType;
19 } else if (resolvedReturnType instanceof ParameterizedType) {
20 this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
21 } else {
22 this.returnType = method.getReturnType();
23 }
24
25 // 检测返回值类型是否是 void、集合或数组、Cursor、Map 等
26 this.returnsVoid = void.class.equals(this.returnType);
27 this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
28 this.returnsCursor = Cursor.class.equals(this.returnType);
29
30 // 解析 @MapKey 注解,获取注解内容
31 this.mapKey = getMapKey(method);
32 this.returnsMap = this.mapKey != null;
33
34 // 获取 RowBounds 参数在参数列表中的位置,如果参数列表中包含多个 RowBounds 参数,此方法会抛出异常
35 this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
36 // 获取 ResultHandler 参数在参数列表中的位置
37 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
38
39 // 解析参数列表
40 this.paramNameResolver = new ParamNameResolver(configuration, method);
41 }
42}
上面的代码用于检测目标方法的返回类型,以及解析目标方法参数列表。其中,检测返回类型的目的是为避免查询方法返回错误的类型。比如我们要求接口方法返回一个对象,结果却返回了对象集合,这会导致类型转换错误。关于返回值类型的解析过程先说到这,下面分析参数列表的解析过程。
671public class ParamNameResolver {
2
3 private static final String GENERIC_NAME_PREFIX = "param";
4 private final SortedMap<Integer, String> names;
5
6 public ParamNameResolver(Configuration config, Method method) {
7 // 获取参数类型列表
8 final Class<?>[] paramTypes = method.getParameterTypes();
9
10 // 获取参数及参数对应的注解
11 final Annotation[][] paramAnnotations = method.getParameterAnnotations();
12
13 // 定义临时map
14 final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
15
16 // 获取参数个数,并遍历
17 int paramCount = paramAnnotations.length;
18 for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
19 // 检测当前的参数类型是否为 RowBounds 或 ResultHandler,是则跳过
20 if (isSpecialParameter(paramTypes[paramIndex])) {
21 continue;
22 }
23
24 // 给参数起个名字
25 // 1. 优先使用@Param注解中配置的名字
26 String name = null;
27 for (Annotation annotation : paramAnnotations[paramIndex]) {
28 if (annotation instanceof Param) {
29 hasParamAnnotation = true;
30 // 获取 @Param 注解内容
31 name = ((Param) annotation).value();
32 break;
33 }
34 }
35
36 // name 为空,表明未给参数配置 @Param 注解
37 if (name == null) {
38 // 检测是否设置了 useActualParamName 全局配置
39 if (config.isUseActualParamName()) {
40 /*
41 * 2. 通过反射获取参数名称。此种方式要求 JDK 版本为 1.8+,
42 * 且要求编译时加入 -parameters 参数,否则获取到的参数名
43 * 仍然是 arg1, arg2, ..., argN
44 */
45 name = getActualParamName(method, paramIndex);
46 }
47 if (name == null) {
48 /*
49 * 3. 使用 map.size() 返回值作为名称,思考一下为什么不这样写:
50 * name = String.valueOf(paramIndex);
51 * 因为如果参数列表中包含 RowBounds 或 ResultHandler,这两个参数
52 * 会被忽略掉,这样将导致名称不连续。
53 *
54 * 比如参数列表 (int p1, int p2, RowBounds rb, int p3)
55 * - 期望得到名称列表为 ["0", "1", "2"]
56 * - 实际得到名称列表为 ["0", "1", "3"]
57 */
58 name = String.valueOf(map.size());
59 }
60 }
61
62 // 存储 paramIndex 到 name 的映射
63 map.put(paramIndex, name);
64 }
65 names = Collections.unmodifiableSortedMap(map);
66 }
67}
以上就是方法参数列表的解析过程,解析完毕后,可得到参数下标到参数名的映射关系,这些映射关系最终存储在 ParamNameResolver
的 names 成员变量中。这些映射关系将会在后面的代码中被用到,大家留意一下。
下面写点代码测试一下 ParamNameResolver 的解析逻辑。如下:
211public class ParamNameResolverTest {
2
3
4 public void test() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
5 Configuration config = new Configuration();
6 config.setUseActualParamName(false);
7 Method method = ArticleMapper.class.getMethod("select", Integer.class, String.class, RowBounds.class, Article.class);
8
9 ParamNameResolver resolver = new ParamNameResolver(config, method);
10 Field field = resolver.getClass().getDeclaredField("names");
11 field.setAccessible(true);
12 // 通过反射获取 ParamNameResolver 私有成员变量 names
13 Object names = field.get(resolver);
14
15 System.out.println("names: " + names);
16 }
17
18 class ArticleMapper {
19 public void select( ("id") Integer id, ("author") String author, RowBounds rb, Article article) {}
20 }
21}
测试结果如下:
11names: {0=id, 1=author, 3=2} //3=2???其中的3表示第三个参数,2表示没有配置@Param注解,也没打开UseActualParamName开关,所以取了当前map.size()作为参数名
到此,关于 MapperMethod 的初始化逻辑就分析完了,继续往下分析。
前面已经分析了 MapperMethod 的初始化过程,现在 MapperMethod 创建好了。那么,接下来要做的事情是调用 MapperMethod 的 execute 方法,执行 SQL。代码如下:
651// -☆- MapperMethod
2public Object execute(SqlSession sqlSession, Object[] args) {
3 Object result;
4
5 // 根据 SQL 类型执行相应的数据库操作
6 switch (command.getType()) {
7 case INSERT: {
8 // 对用户传入的参数进行转换,下同
9 Object param = method.convertArgsToSqlCommandParam(args);
10 // 执行插入操作,rowCountResult 方法用于处理返回值
11 result = rowCountResult(sqlSession.insert(command.getName(), param));
12 break;
13 }
14 case UPDATE: {
15 Object param = method.convertArgsToSqlCommandParam(args);
16 // 执行更新操作
17 result = rowCountResult(sqlSession.update(command.getName(), param));
18 break;
19 }
20 case DELETE: {
21 Object param = method.convertArgsToSqlCommandParam(args);
22 // 执行删除操作
23 result = rowCountResult(sqlSession.delete(command.getName(), param));
24 break;
25 }
26 case SELECT:
27 // 根据目标方法的返回类型进行相应的查询操作
28 if (method.returnsVoid() && method.hasResultHandler()) {
29 /*
30 * 如果方法返回值为 void,但参数列表中包含 ResultHandler,表明使用者
31 * 想通过 ResultHandler 的方式获取查询结果,而非通过返回值获取结果
32 */
33 executeWithResultHandler(sqlSession, args);
34 result = null;
35 } else if (method.returnsMany()) {
36 // 执行查询操作,并返回多个结果
37 result = executeForMany(sqlSession, args);
38 } else if (method.returnsMap()) {
39 // 执行查询操作,并将结果封装在 Map 中返回
40 result = executeForMap(sqlSession, args);
41 } else if (method.returnsCursor()) {
42 // 执行查询操作,并返回一个 Cursor 对象
43 result = executeForCursor(sqlSession, args);
44 } else {
45 Object param = method.convertArgsToSqlCommandParam(args);
46 // 执行查询操作,并返回一个结果
47 result = sqlSession.selectOne(command.getName(), param);
48 }
49 break;
50 case FLUSH:
51 // 执行刷新操作
52 result = sqlSession.flushStatements();
53 break;
54 default:
55 throw new BindingException("Unknown execution method for: " + command.getName());
56 }
57
58 // 如果方法的返回值为基本类型,而返回值却为 null,此种情况下应抛出异常
59 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
60 throw new BindingException("Mapper method '" + command.getName()
61 + " attempted to return null from a method with a primitive return type (" + method.getReturnType()
62 + ").");
63 }
64 return result;
65}
如上,execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库操作。该方法的逻辑清晰,不需要太多的分析。不过在上面的方法中 convertArgsToSqlCommandParam 方法出现次数比较频繁,这里分析一下:
411// -☆- MapperMethod
2public Object convertArgsToSqlCommandParam(Object[] args) {
3 return paramNameResolver.getNamedParams(args);
4}
5
6public Object getNamedParams(Object[] args) {
7 final int paramCount = names.size();
8 if (args == null || paramCount == 0) {
9 return null;
10 } else if (!hasParamAnnotation && paramCount == 1) {
11 /*
12 * 如果方法参数列表无 @Param 注解,且仅有一个非特别参数,则返回该参数的值。
13 * 比如如下方法:
14 * List findList(RowBounds rb, String name)
15 * names 如下:
16 * names = {1 : "0"}
17 * 此种情况下,返回 args[names.firstKey()],即 args[1] -> name
18 */
19 return args[names.firstKey()];
20 } else {
21 final Map<String, Object> param = new ParamMap<Object>();
22 int i = 0;
23 for (Map.Entry<Integer, String> entry : names.entrySet()) {
24 // 添加 <参数名, 参数值> 键值对到 param 中
25 param.put(entry.getValue(), args[entry.getKey()]);
26 // genericParamName = param + index。比如 param1, param2, ... paramN
27 final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
28 /*
29 * 检测 names 中是否包含 genericParamName,什么情况下会包含?答案如下:
30 *
31 * 使用者显式将参数名称配置为 param1,即 @Param("param1")
32 */
33 if (!names.containsValue(genericParamName)) {
34 // 添加 <param*, value> 到 param 中
35 param.put(genericParamName, args[entry.getKey()]);
36 }
37 i++;
38 }
39 return param;
40 }
41}
上节中讲解的 ParamNameResolver 构造函数建立了形参索引和参数名的映射names,而本节的 getNamedParams 方法根据names从传入的实参对象中取参数值返回。有两种情形:
单个非特殊参数且无@Param注解。直接返回传入的实参对象。
有@Param参数或存在多个参数。逐个添加[参数名-参数值]到 ParamMap 后返回。同时为参数名添加一份固定名称paramXxx。
在JDBC中,将SELECT语句归类为查询语句,将其它一些语句(如UPDATE、DDL等)归类为更新语句。在本节中,先对查询语句的执行流程进行讲解。从上节MapperMathod代码可以看到,查询语句根据返回值类型以及是否使用ResultHandler
处理结果大致分为了以下几类:
executeWithResultHandler
executeForMany
executeForMap
executeForCursor
...
这些方法在内部都是调用了 SqlSession
中的 selectXxxx 方法,比如 selectList、selectMap、selectCursor 等。而其中最常用的 selectList 被 selectOne 方法调用。因此我们从该方法来看看查询语句执行的主体流程。
在 MapperMethod 执行查询语句时,当返回值不是List、Map或Cursor且没有使用结果处理器时,会调用 selectOne 方法进行查询。
151// -☆- DefaultSqlSession
2public <T> T selectOne(String statement, Object parameter) {
3 // selectOne内部调用 selectList 获取结果
4 List<T> list = this.<T>selectList(statement, parameter);
5
6 // 查询后对结果集数量进行判断:如果 <=0 返回 NULL, ==1 返回第一个结果, >1直接报错,单查询不应该返回多行结果
7 if (list.size() == 1) {
8 return list.get(0);
9 } else if (list.size() > 1) {
10 throw new TooManyResultsException(
11 "Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
12 } else {
13 return null;
14 }
15}
下面我们来看看 selectList 方法的实现。
211// -☆- DefaultSqlSession
2public <E> List<E> selectList(String statement, Object parameter) {
3 // 调用重载方法,设置默认的RowBounds
4 return this.selectList(statement, parameter, RowBounds.DEFAULT);
5}
6
7private final Executor executor;
8
9public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
10 try {
11 // 1. 获取 MappedStatement
12 MappedStatement ms = configuration.getMappedStatement(statement);
13
14 // 2. 调用 Executor 实现类中的 query 方法
15 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
16 } catch (Exception e) {
17 throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
18 } finally {
19 ErrorContext.instance().reset();
20 }
21}
这里注意一下执行器 Executor
,这是MyBatis中的一个重要组件。Executor 是一个接口,它的实现类如下:
具体使用哪个Excutor实现类,可以在打开会话(openSession)时指定,也可以在MyBatis全局配置文件中修改默认的执行器类型。Excutor 默认为SimpleExcutor
。如果开启了全局属性cacheEnabled,则还会被CachingExcutor
所装饰(详情见newExecutor方法)。
111// -☆- CachingExecutor
2public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
3 // 1. 获取 BoundSql
4 BoundSql boundSql = ms.getBoundSql(parameterObject);
5
6 // 2. 创建 CacheKey
7 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
8
9 // 3. 调用重载方法
10 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
11}
上面的代码用于获取 BoundSql 对象,创建 CacheKey 对象,然后再将这两个对象传给重载方法。关于 BoundSql 的获取过程较为复杂,我将在下一节进行分析。CacheKey 以及接下来即将出现的一二级缓存将会独立成文进行分析。
上面的方法和 SimpleExecutor 父类 BaseExecutor 中的实现没什么区别,有区别的地方在于这个方法所调用的重载方法。我们继续往下看。
281// -☆- CachingExecutor
2public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
3 // 1. 从 MappedStatement 中获取缓存引用(在解析SQL语句节点生成MappedStatement时,保存了解析器中currentCache)
4 Cache cache = ms.getCache();
5
6 // 若映射文件中未配置缓存或参照缓存,此时 cache = null
7 if (cache != null) {
8 // 检查是否需要刷新缓存
9 flushCacheIfRequired(ms);
10 // 当前MS开启了缓存,且未使用结果处理器
11 if (ms.isUseCache() && resultHandler == null) {
12 // 存储过程特殊处理
13 ensureNoOutParams(ms, boundSql);
14 // 2. 从事务缓存管理器的缓存区中查询二级缓存
15 List<E> list = (List<E>) tcm.getObject(cache, key);
16 if (list == null) {
17 // 3. 若缓存未命中,则调用被装饰类的 query 方法
18 list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
19
20 // 4. 存储查询结果到二级缓存暂存区
21 tcm.putObject(cache, key, list); // issue #578 and #116
22 }
23 return list;
24 }
25 }
26 // 3. 调用被装饰类的 query 方法
27 return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
28}
上面的代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方法。下面来看一下 BaseExecutor 的中签名相同的 query 方法是如何实现的。
351// -☆- BaseExecutor
2public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
3 if (closed) {
4 throw new ExecutorException("Executor was closed.");
5 }
6 if (queryStack == 0 && ms.isFlushCacheRequired()) {
7 clearLocalCache();
8 }
9 List<E> list;
10 try {
11 queryStack++;
12 // 从一级缓存中获取缓存项
13 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
14 if (list != null) {
15 // 存储过程相关处理逻辑,本文不分析存储过程,故该方法不分析了
16 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
17 } else {
18 // 一级缓存未命中,则从数据库中查询
19 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
20 }
21 } finally {
22 queryStack--;
23 }
24 if (queryStack == 0) {
25 // 从一级缓存中延迟加载嵌套查询结果
26 for (DeferredLoad deferredLoad : deferredLoads) {
27 deferredLoad.load();
28 }
29 deferredLoads.clear();
30 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
31 clearLocalCache();
32 }
33 }
34 return list;
35}
如上,上面的方法主要用于从一级缓存中查找查询结果。若缓存未命中,再向数据库进行查询。在上面的代码中,出现了一个新的类 DeferredLoad
,这个类用于延迟加载,后面将会分析。现在先来看一下 queryFromDatabase 方法的实现。
201// -☆- BaseExecutor
2private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
3 ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
4 List<E> list;
5 // 向缓存中存储一个占位符(用于解决关联查询循环问题)
6 localCache.putObject(key, EXECUTION_PLACEHOLDER);
7 try {
8 // 调用 doQuery 进行查询
9 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
10 } finally {
11 // 移除占位符
12 localCache.removeObject(key);
13 }
14 // 缓存查询结果
15 localCache.putObject(key, list);
16 if (ms.getStatementType() == StatementType.CALLABLE) {
17 localOutputParameterCache.putObject(key, parameter);
18 }
19 return list;
20}
抛开缓存操作,queryFromDatabase 最终还会调用 doQuery 进行查询。下面我们继续进行跟踪。
161// -☆- SimpleExecutor
2public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
3 Statement stmt = null;
4 try {
5 Configuration configuration = ms.getConfiguration();
6 // 1. 创建 StatementHandler
7 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
8 // 2. 创建 Statement
9 stmt = prepareStatement(handler, ms.getStatementLog());
10 // 3. 执行查询操作
11 return handler.<E>query(stmt, resultHandler);
12 } finally {
13 // 4. 关闭 Statement
14 closeStatement(stmt);
15 }
16}
我们先跳过 StatementHandler 和 Statement 创建过程,这两个对象的创建过程会在后面进行说明。这里先看看Statement的 query 方法是怎样实现的。这里选择 PreparedStatementHandler
为例进行分析,至于 SimpleStatementHandler 和 CallableStatementHandler 分别用来处理非预编译SQL和存储过程的,用的比较少,就不分析了。
首先经过 RoutingStatementHandler 进行路由,进入到 PreparedStatementHandler 中。
41
2 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
3 return delegate.query(statement, resultHandler);
4 }
PreparedStatementHandler 逻辑非常简单,调用JDBC 的原生api执行SQL,然后调用结果集处理器处理结果。
81// -☆- PreparedStatementHandler
2public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
3 PreparedStatement ps = (PreparedStatement) statement;
4 // 执行 SQL
5 ps.execute();
6 // 处理执行结果
7 return resultSetHandler.<E>handleResultSets(ps);
8}
到此,SQL是执行完毕了,但结果集处理是MyBatis中最复杂的一个部分,将会在后面重点进行讲解。现在,我们先看下之前跳过的获取 BoundSql 的过程,这也非常的重要。
在XML或注解中配置的原始SQL语句,可能带有字符串替换标记${}
或动态标签,如 <if>
、<where>
等。这些SQL语句会在应用启动时会被解析为动态SQL源,解析过程在前面的章节已经分析过,不再赘述。后续在每次执行SQL时,都必须先根据实际参数从动态SQL源解析出能被JDBC Api执行的BoundSql
,这个过程叫动态SQL解析。
简单来说,就是按部就班的执行一遍动态SQL源中的语法树节点,从每个节点中获取实际的SQL片段,最终拼接为一个完整的SQL语句。这个完整的 SQL 以及其他的一些信息最终会存储在 BoundSql 对象中。下面我们来看一下 BoundSql 类的成员变量信息,如下:
101// 一个可以直接被JDBC Api执行的完整 SQL 语句,可能会包含问号 ? 占位符
2private final String sql;
3// 参数映射列表,SQL 中的每个 #{xxx} 占位符都会被解析成相应的 ParameterMapping 对象
4private final List<ParameterMapping> parameterMappings;
5// 运行时参数,即用户传入的参数
6private final Object parameterObject;
7// 附加参数集合,用于存储一些额外的信息,比如 datebaseId 等
8private final Map<String, Object> additionalParameters;
9// additionalParameters 的元信息对象
10private final MetaObject metaParameters;
接下来,开始分析 BoundSql 的构建过程,首先从 MappedStatement
的 getBoundSql 方法看起,代码如下:
291// -☆- MappedStatement 通过实际参数获取BoundSql
2public BoundSql getBoundSql(Object parameterObject) {
3
4 // 调用 sqlSource 的 getBoundSql 获取 BoundSql
5 // 1. 如果是 RawSqlSource ,则调用其内部 StaticSqlSource 的 getBoundSql 方法直接new一个即可
6 // 2. 如果是 DynamicSqlSource ,则进行语法树解析
7 BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
8 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
9 if (parameterMappings == null || parameterMappings.isEmpty()) {
10 /*
11 * 创建新的 BoundSql,这里的 parameterMap 是 ParameterMap 类型。 由<ParameterMap> 节点进行配置,该节点已经废弃,不推荐使用。
12 * 默认情况下,parameterMap.getParameterMappings() 返回空集合
13 */
14 boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
15 }
16
17 // check for nested result maps in parameter mappings (issue #30)
18 for (ParameterMapping pm : boundSql.getParameterMappings()) {
19 String rmId = pm.getResultMapId();
20 if (rmId != null) {
21 ResultMap rm = configuration.getResultMap(rmId);
22 if (rm != null) {
23 hasNestedResultMaps |= rm.hasNestedResultMaps();
24 }
25 }
26 }
27
28 return boundSql;
29}
上述代码主要是调用了SQL源的getBoundSql方法来获取BoundSql,然后对参数映射列表做一些处理,这些配置都不常用了,这里不做深入分析,而SQL源的getBoundSql才是我们的重点。由上文分析的动态标签解析流程可知,这些的SQL源一般为 RawSqlSource
或 DynamicSqlSource
。前者直接调用其内部 StaticSqlSource
的 getBoundSql 方法直接new一个即可,后者才是真正进行语法树的解析。下面对这个 DynamicSqlSource 进行分析。
241// -☆- DynamicSqlSource
2public BoundSql getBoundSql(Object parameterObject) {
3 // 创建 DynamicContext
4 DynamicContext context = new DynamicContext(configuration, parameterObject);
5
6 // ※ 解析 SQL 片段,并将解析结果存储到 DynamicContext 中
7 rootSqlNode.apply(context);
8
9 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
10 Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
11 /*
12 * ※ 构建 StaticSqlSource,在此过程中将 sql 语句中的占位符 #{} 替换为问号 ?, 并为每个占位符构建相应的 ParameterMapping
13 */
14 SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
15
16 // 调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
17 BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
18
19 // 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中
20 for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
21 boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
22 }
23 return boundSql;
24}
如上,DynamicSqlSource 的 getBoundSql 方法的代码看起来不多,但是逻辑却并不简单。该方法由数个步骤组成,这里总结一下:
创建 DynamicContext
解析 SQL 片段,并将解析结果存储到 DynamicContext 中
解析 SQL 语句,并构建 StaticSqlSource
调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中
如上5个步骤中,第5步为常规操作,就不多说了,其他步骤将会在接下来章节中一一进行分析。按照顺序,我们先来分析 DynamicContext 的实现。
DynamicContext
是 SQL 语句构建的上下文,每个 SQL 片段解析完成后,都会将解析结果存入 DynamicContext 中。待所有的 SQL 片段解析完毕后,一条完整的 SQL 语句就会出现在 DynamicContext 对象中。下面我们来看一下 DynamicContext 类的定义。
261public class DynamicContext {
2
3 public static final String PARAMETER_OBJECT_KEY = "_parameter";
4 public static final String DATABASE_ID_KEY = "_databaseId";
5
6 // 用于存储一些额外的信息,比如运行时参数 和 databaseId
7 private final ContextMap bindings;
8 // 用于存放 SQL 片段的解析结果
9 private final StringBuilder sqlBuilder = new StringBuilder();
10
11 public DynamicContext(Configuration configuration, Object parameterObject) {
12 // 创建 ContextMap
13 if (parameterObject != null && !(parameterObject instanceof Map)) {
14 MetaObject metaObject = configuration.newMetaObject(parameterObject);
15 bindings = new ContextMap(metaObject);
16 } else {
17 bindings = new ContextMap(null);
18 }
19
20 // 存放运行时参数 parameterObject 以及 databaseId
21 bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
22 bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
23 }
24
25 // 省略部分代码
26}
如上,其中 sqlBuilder 变量用于存放 SQL 片段的解析结果,bindings 则用于存储一些额外的信息,比如运行时参数 和 databaseId 等。bindings 类型为 ContextMap,ContextMap 定义在 DynamicContext 中,是一个静态内部类。该类继承自 HashMap,并覆写了 get 方法。它的代码如下:
241static class ContextMap extends HashMap<String, Object> {
2
3 private MetaObject parameterMetaObject;
4
5 public ContextMap(MetaObject parameterMetaObject) {
6 this.parameterMetaObject = parameterMetaObject;
7 }
8
9
10 public Object get(Object key) {
11 String strKey = (String) key;
12 // 检查是否包含 strKey,若包含则直接返回
13 if (super.containsKey(strKey)) {
14 return super.get(strKey);
15 }
16
17 if (parameterMetaObject != null) {
18 // 从运行时参数中查找结果
19 return parameterMetaObject.getValue(strKey);
20 }
21
22 return null;
23 }
24}
DynamicContext 对外提供了两个接口,用于操作 sqlBuilder。分别如下:
81public void appendSql(String sql) {
2 sqlBuilder.append(sql);
3 sqlBuilder.append(" ");
4}
5
6public String getSql() {
7 return sqlBuilder.toString().trim();
8}
以上就是对 DynamicContext 的简单介绍,DynamicContext 的源码不难理解,这里就不多说了。继续往下分析。
对于一个包含了 ${} 占位符,或 <if>
、<where>
等标签的 SQL,在解析的过程中,会被分解成多个片段。每个片段都有对应的类型,每种类型的片段都有不同的解析逻辑。在源码中,片段这个概念等价于 sql 节点,即 SqlNode。SqlNode 是一个接口,它有众多的实现类。其继承体系如下:
上图只画出了部分的实现类,还有一小部分没画出来,不过这并不影响接下来的分析。在众多实现类中,StaticTextSqlNode
用于存储静态文本,TextSqlNode
用于存储带有 ${} 占位符的普通文本,IfSqlNode
则用于存储 <if>
节点的内容。MixedSqlNode
内部维护了一个 SqlNode 集合,用于存储各种各样的 SqlNode。接下来,我将会对 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等进行分析,其他的实现类请大家自行分析。
171public class MixedSqlNode implements SqlNode {
2 private final List<SqlNode> contents;
3
4 public MixedSqlNode(List<SqlNode> contents) {
5 this.contents = contents;
6 }
7
8
9 public boolean apply(DynamicContext context) {
10 // 遍历 SqlNode 集合
11 for (SqlNode sqlNode : contents) {
12 // 调用 salNode 对象本身的 apply 方法解析 sql
13 sqlNode.apply(context);
14 }
15 return true;
16 }
17}
MixedSqlNode 可以看做是 SqlNode 实现类对象的容器,凡是实现了 SqlNode 接口的类都可以存储到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 逻辑比较简单,即遍历 SqlNode 集合,并调用其他 SalNode 实现类对象的 apply 方法解析 sql。那下面我们来看看其他 SalNode 实现类的 apply 方法是怎样实现的。
141public class StaticTextSqlNode implements SqlNode {
2
3 private final String text;
4
5 public StaticTextSqlNode(String text) {
6 this.text = text;
7 }
8
9
10 public boolean apply(DynamicContext context) {
11 context.appendSql(text);
12 return true;
13 }
14}
StaticTextSqlNode 用于存储静态文本,所以它不需要什么解析逻辑,直接将其存储的 SQL 片段添加到 DynamicContext 中即可。StaticTextSqlNode 的实现比较简单,看起来很轻松。下面分析一下 TextSqlNode。
471public class TextSqlNode implements SqlNode {
2
3 private final String text;
4 private final Pattern injectionFilter;
5
6
7 public boolean apply(DynamicContext context) {
8 // 创建 ${} 占位符解析器
9 GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
10 // 解析 ${} 占位符,并将解析结果添加到 DynamicContext 中
11 context.appendSql(parser.parse(text));
12 return true;
13 }
14
15 private GenericTokenParser createParser(TokenHandler handler) {
16 // 创建占位符解析器,GenericTokenParser 是一个通用解析器,并非只能解析 ${}
17 return new GenericTokenParser("${", "}", handler);
18 }
19
20 // 对${}中的内容进行处理替换
21 private static class BindingTokenParser implements TokenHandler {
22
23 private DynamicContext context;
24 private Pattern injectionFilter;
25
26 public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
27 this.context = context;
28 this.injectionFilter = injectionFilter;
29 }
30
31
32 public String handleToken(String content) {
33 Object parameter = context.getBindings().get("_parameter");
34 if (parameter == null) {
35 context.getBindings().put("value", null);
36 } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
37 context.getBindings().put("value", parameter);
38 }
39 // 通过 ONGL 从用户传入的参数中获取结果
40 Object value = OgnlCache.getValue(content, context.getBindings());
41 String srtValue = (value == null ? "" : String.valueOf(value));
42 // 通过正则表达式检测 srtValue 有效性
43 checkInjection(srtValue);
44 return srtValue;
45 }
46 }
47}
GenericTokenParser 是一个通用的标记解析器,用于解析形如
${xxx}
,#{xxx}
等标记。GenericTokenParser 负责将标记中的内容抽取出来,并将标记内容交给相应的 TokenHandler 去处理。BindingTokenParser 负责解析标记内容,并将解析结果返回给 GenericTokenParser,用于替换${xxx}
标记。举个例子说明一下吧,如下。我们有这样一个 SQL 语句,用于从 article 表中查询某个作者所写的文章。如下:
11SELECT * FROM article WHERE author = '${author}'
假设我们我们传入的 author 值为 tianxiaobo,那么该 SQL 最终会被解析成如下的结果:
11SELECT * FROM article WHERE author = 'tianxiaobo'
并且在替换时,可以支持传入的正则表达式进行校验,防止SQL注入问题。
分析完 TextSqlNode 的逻辑,接下来,分析 IfSqlNode 的实现。
231public class IfSqlNode implements SqlNode {
2
3 private final ExpressionEvaluator evaluator;
4 private final String test;
5 private final SqlNode contents;
6
7 public IfSqlNode(SqlNode contents, String test) {
8 this.test = test;
9 this.contents = contents;
10 this.evaluator = new ExpressionEvaluator();
11 }
12
13
14 public boolean apply(DynamicContext context) {
15 // 通过 ONGL 评估 test 表达式的结果
16 if (evaluator.evaluateBoolean(test, context.getBindings())) {
17 // 若 test 表达式中的条件成立,则调用其他节点的 apply 方法进行解析
18 contents.apply(context);
19 return true;
20 }
21 return false;
22 }
23}
IfSqlNode 对应的是 <if test='xxx'>
节点,<if>
节点是日常开发中使用频次比较高的一个节点。它的具体用法我想大家都很熟悉了,这里就不多啰嗦。IfSqlNode 的 apply 方法逻辑并不复杂,首先是通过 ONGL 检测 test 表达式是否为 true,如果为 true,则调用其他节点的 apply 方法继续进行解析。需要注意的是 <if>
节点中也可嵌套其他的动态节点,并非只有纯文本。因此 contents 变量遍历指向的是 MixedSqlNode,而非 StaticTextSqlNode。
关于 IfSqlNode 就说到这,接下来分析 WhereSqlNode 的实现。
101public class WhereSqlNode extends TrimSqlNode {
2
3 /** 前缀列表 */
4 private static List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
5
6 public WhereSqlNode(Configuration configuration, SqlNode contents) {
7 // 调用父类的构造方法
8 super(configuration, contents, "WHERE", prefixList, null, null);
9 }
10}
在 MyBatis 中,WhereSqlNode 和 SetSqlNode 都是基于 TrimSqlNode 实现的,所以上面的代码看起来很简单。WhereSqlNode 对应于 <where>
节点,关于该节点的用法以及它的应用场景,大家请自行查阅资料。我在分析源码的过程中,默认大家已经知道了该节点的用途和应用场景。
接下来,我们把目光聚焦在 TrimSqlNode 的实现上。
221public class TrimSqlNode implements SqlNode {
2
3 private final SqlNode contents;
4 private final String prefix;
5 private final String suffix;
6 private final List<String> prefixesToOverride;
7 private final List<String> suffixesToOverride;
8 private final Configuration configuration;
9
10 // 省略构造方法
11
12
13 public boolean apply(DynamicContext context) {
14 // 创建具有过滤功能的 DynamicContext
15 FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
16 // 解析节点内容
17 boolean result = contents.apply(filteredDynamicContext);
18 // 过滤掉前缀和后缀
19 filteredDynamicContext.applyAll();
20 return result;
21 }
22}
如上,apply 方法首选调用了其他 SqlNode 的 apply 方法解析节点内容,这步操作完成后,FilteredDynamicContext 中会得到一条 SQL 片段字符串。接下里需要做的事情是过滤字符串前缀后和后缀,并添加相应的前缀和后缀。这个事情由 FilteredDynamicContext 负责,FilteredDynamicContext 是 TrimSqlNode 的私有内部类。我们去看一下它的代码。
501private class FilteredDynamicContext extends DynamicContext {
2
3 private DynamicContext delegate;
4 /** 构造方法会将下面两个布尔值置为 false */
5 private boolean prefixApplied;
6 private boolean suffixApplied;
7 private StringBuilder sqlBuffer;
8
9 // 省略构造方法
10
11 public void applyAll() {
12 sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
13 String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
14 if (trimmedUppercaseSql.length() > 0) {
15 // 引用前缀和后缀,也就是对 sql 进行过滤操作,移除掉前缀或后缀
16 applyPrefix(sqlBuffer, trimmedUppercaseSql);
17 applySuffix(sqlBuffer, trimmedUppercaseSql);
18 }
19 // 将当前对象的 sqlBuffer 内容添加到代理类中
20 delegate.appendSql(sqlBuffer.toString());
21 }
22
23 // 省略部分方法
24
25 private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
26 if (!prefixApplied) {
27 // 设置 prefixApplied 为 true,以下逻辑仅会被执行一次
28 prefixApplied = true;
29 if (prefixesToOverride != null) {
30 for (String toRemove : prefixesToOverride) {
31 // 检测当前 sql 字符串是否包含 toRemove 前缀,比如 'AND ', 'AND\t'
32 if (trimmedUppercaseSql.startsWith(toRemove)) {
33 // 移除前缀
34 sql.delete(0, toRemove.trim().length());
35 break;
36 }
37 }
38 }
39
40 // 插入前缀,比如 WHERE
41 if (prefix != null) {
42 sql.insert(0, " ");
43 sql.insert(0, prefix);
44 }
45 }
46 }
47
48 // 该方法逻辑与 applyPrefix 大同小异,大家自行分析
49 private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {...}
50}
在上面的代码中,我们重点关注 applyAll 和 applyPrefix 方法,其他的方法大家自行分析。applyAll 方法的逻辑比较简单,首先从 sqlBuffer 中获取 SQL 字符串。然后调用 applyPrefix 和 applySuffix 进行过滤操作。最后将过滤后的 SQL 字符串添加到被装饰的类中。applyPrefix 方法会首先检测 SQL 字符串是不是以 "AND ","OR ",或 “AND\n”, “OR\n” 等前缀开头,若是则将前缀从 sqlBuffer 中移除。然后将前缀插入到 sqlBuffer 的首部,整个逻辑就结束了。下面写点代码简单验证一下,如下:
131public class SqlNodeTest {
2
3
4 public void testWhereSqlNode() throws IOException {
5 String sqlFragment = "AND id = #{id}";
6 MixedSqlNode msn = new MixedSqlNode(Arrays.asList(new StaticTextSqlNode(sqlFragment)));
7 WhereSqlNode wsn = new WhereSqlNode(new Configuration(), msn);
8 DynamicContext dc = new DynamicContext(new Configuration(), new ParamMap<>());
9 wsn.apply(dc);
10 System.out.println("解析前:" + sqlFragment);
11 System.out.println("解析后:" + dc.getSql());
12 }
13}
测试结果如下:
21解析前:AND id = #{id}
2解析后:WHERE id = #{id}
经过前面的解析,我们已经能从 DynamicContext 获取到完整的 SQL 语句了。但这并不意味着解析过程就结束了,因为当前的 SQL 语句中还有一种占位符没有处理,即 #{}。与 ${} 占位符的处理方式不同,MyBatis 并不会直接将 #{} 占位符替换为相应的参数值。#{} 占位符的解析逻辑这里先不多说,等相应的源码分析完了,答案就明了了。
#{} 占位符的解析逻辑是包含在 SqlSourceBuilder
的 parse 方法中,该方法最终会将解析后的 SQL 以及其他的一些数据封装到 StaticSqlSource
中。下面,一起来看一下 SqlSourceBuilder 的 parse 方法。
111// -☆- SqlSourceBuilder
2public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
3 // 创建 #{} 占位符处理器
4 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
5 // 创建 #{} 占位符解析器
6 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
7 // 解析 #{} 占位符,并返回解析结果
8 String sql = parser.parse(originalSql);
9 // 封装解析结果到 StaticSqlSource 中,并返回
10 return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
11}
如上,GenericTokenParser 的用途上一节已经介绍过了,就不多说了。接下来,我们重点关注 #{} 占位符处理器 ParameterMappingTokenHandler 的逻辑。
61public String handleToken(String content) {
2 // 获取 content 的对应的 ParameterMapping
3 parameterMappings.add(buildParameterMapping(content));
4 // 返回 ?
5 return "?";
6}
ParameterMappingTokenHandler
的 handleToken 方法看起来比较简单,但实际上并非如此。GenericTokenParser 负责将 #{} 占位符中的内容抽取出来,并将抽取出的内容传给 handleToken 方法。handleToken 负责将传入的参数解析成对应的 ParameterMapping
对象,这步操作由 buildParameterMapping 方法完成。下面我们看一下 buildParameterMapping 的源码。
881private ParameterMapping buildParameterMapping(String content) {
2 // 1. 将 #{xxx} 占位符中的内容解析成 Map。
3 /* 如 #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler} 将会被转换为
4 {
5 "property": "age",
6 "typeHandler": "MyTypeHandler",
7 "jdbcType": "NUMERIC",
8 "javaType": "int"
9 }
10 parseParameterMapping 内部依赖 ParameterExpression 对字符串进行解析,ParameterExpression 的逻辑不是很复杂,这里就不分析了。大家若有兴趣,可自行分析
11 */
12 Map<String, String> propertiesMap = parseParameterMapping(content);
13
14 // 2. 获取 property ,即"age"
15 String property = propertiesMap.get("property");
16
17 // 3. 获取属性的类型
18 Class<?> propertyType;
19 // metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象
20 if (metaParameters.hasGetter(property)) {
21 propertyType = metaParameters.getGetterType(property);
22 /*
23 * parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Article 对象,此时
24 * parameterType 为 Article.class。如果用户传入的多个参数,比如 [id = 1, author = "coolblog"],
25 * MyBatis 会使用 ParamMap 封装这些参数,此时 parameterType 为 ParamMap.class。如果
26 * parameterType 有相应的 TypeHandler,这里则把 parameterType 设为 propertyType
27 */
28 } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
29 propertyType = parameterType;
30 } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
31 propertyType = java.sql.ResultSet.class;
32 } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
33 // 如果 property 为空,或 parameterType 是 Map 类型,则将 propertyType 设为 Object.class
34 propertyType = Object.class;
35 } else {
36 // 代码逻辑走到此分支中,表明 parameterType 是一个自定义的类,比如 Article,此时为该类创建一个元信息对象
37 MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
38 // 检测参数对象有没有与 property 想对应的 getter 方法
39 if (metaClass.hasGetter(property)) {
40 // 获取成员变量的类型
41 propertyType = metaClass.getGetterType(property);
42 } else {
43 propertyType = Object.class;
44 }
45 }
46
47 // -------------------------- 分割线 ---------------------------
48
49 ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
50
51 // 将 propertyType 赋值给 javaType
52 Class<?> javaType = propertyType;
53 String typeHandlerAlias = null;
54
55 // 遍历 propertiesMap,逐个解析参数
56 for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
57 String name = entry.getKey();
58 String value = entry.getValue();
59 if ("javaType".equals(name)) {
60 // 如果用户明确配置了 javaType,则以用户的配置为准
61 javaType = resolveClass(value);
62 builder.javaType(javaType);
63 } else if ("jdbcType".equals(name)) {
64 // 解析 jdbcType
65 builder.jdbcType(resolveJdbcType(value));
66 } else if ("mode".equals(name)) {...}
67 else if ("numericScale".equals(name)) {...}
68 else if ("resultMap".equals(name)) {...}
69 else if ("typeHandler".equals(name)) {
70 typeHandlerAlias = value;
71 }
72 else if ("jdbcTypeName".equals(name)) {...}
73 else if ("property".equals(name)) {...}
74 else if ("expression".equals(name)) {
75 throw new BuilderException("Expression based parameters are not supported yet");
76 } else {
77 throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
78 + "}. Valid properties are " + parameterProperties);
79 }
80 }
81 if (typeHandlerAlias != null) {
82 // 解析 TypeHandler
83 builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
84 }
85
86 // 构建 ParameterMapping 对象
87 return builder.build();
88}
如上,buildParameterMapping 代码很多,逻辑看起来很复杂。但是它做的事情却不是很多,只有3件事情。如下:
解析 content
解析 propertyType,对应分割线之上的代码
构建 ParameterMapping 对象,对应分割线之下的代码
buildParameterMapping 代码比较多,不太好理解,下面写个示例演示一下。如下:
221public class SqlSourceBuilderTest {
2
3
4 public void test() {
5 // 带有复杂 #{} 占位符的参数,接下里会解析这个占位符
6 String sql = "SELECT * FROM Author WHERE age = #{age,javaType=int,jdbcType=NUMERIC}";
7 SqlSourceBuilder sqlSourceBuilder = new SqlSourceBuilder(new Configuration());
8 SqlSource sqlSource = sqlSourceBuilder.parse(sql, Author.class, new HashMap<>());
9 BoundSql boundSql = sqlSource.getBoundSql(new Author());
10
11 System.out.println(String.format("SQL: %s\n", boundSql.getSql()));
12 System.out.println(String.format("ParameterMappings: %s", boundSql.getParameterMappings()));
13 }
14}
15
16public class Author {
17 private Integer id;
18 private String name;
19 private Integer age;
20
21 // 省略 getter/setter
22}
测试结果如下:
31SQL: SELECT * FROM Author WHERE age = ?
2
3ParameterMappings: [ParameterMapping{property='age', mode=IN, javaType=class java.lang.Integer, jdbcType=NUMERIC, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}]
正如测试结果所示,SQL 中的 #{age, …} 占位符被替换成了问号 ?。#{age, …} 也被解析成了一个 ParameterMapping 对象。
本节的最后,我们再来看一下 StaticSqlSource 的创建过程。如下:
221public class StaticSqlSource implements SqlSource {
2
3 private final String sql;
4 private final List<ParameterMapping> parameterMappings;
5 private final Configuration configuration;
6
7 public StaticSqlSource(Configuration configuration, String sql) {
8 this(configuration, sql, null);
9 }
10
11 public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
12 this.sql = sql;
13 this.parameterMappings = parameterMappings;
14 this.configuration = configuration;
15 }
16
17
18 public BoundSql getBoundSql(Object parameterObject) {
19 // 创建 BoundSql 对象
20 return new BoundSql(configuration, sql, parameterMappings, parameterObject);
21 }
22}
上面代码没有什么太复杂的地方,从上面代码中可以看出 BoundSql 的创建过程也很简单。正因为前面经历了这么复杂的解析逻辑,BoundSql 的创建过程才会如此简单。到此,关于 BoundSql 构建的过程就分析完了,稍作休息,我们进行后面的分析。
StatementHandler
接口是 Mybatis 源码与 JDBC 接口的边界,往上调用 MyBatis 的参数处理器和结果集处理器填充参数和处理结果集,往下直接操作 JDBC Api 来创建 Statement 对象并执行SQL语句。其实现类与 JDBC 中三类 Stetement 十分相似,继承体系如下图。
首先派生一个抽象类 BaseStatementHandler
处理公共逻辑,然后三个具体的实现类分别对应 JDBC 三种不同的 Statement 。除外之外,额外增加了一个实现类 RoutingStatementHandler,起路由作用(其实并没有什么卵用,可能是为了后续扩展吧)。
下面看下 StatementHandler 的创建过程,为了实现拦截逻辑,和执行器等组件的创建过程类似,统一放在了 Configuration 中创建。
121// -☆- Configuration
2public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
3 Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
4
5 // 创建具有路由功能的 RoutingStatementHandler
6 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
7
8 // 应用插件到 StatementHandler 上
9 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
10
11 return statementHandler;
12}
关于 MyBatis 的插件机制,后面独立成文进行讲解,这里就不分析了。下面分析一下 RoutingStatementHandler。
251public class RoutingStatementHandler implements StatementHandler {
2
3 private final StatementHandler delegate;
4
5 public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
6 ResultHandler resultHandler, BoundSql boundSql) {
7
8 // 根据 StatementType 创建不同的 StatementHandler
9 switch (ms.getStatementType()) {
10 case STATEMENT:
11 delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
12 break;
13 case PREPARED:
14 delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
15 break;
16 case CALLABLE:
17 delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
18 break;
19 default:
20 throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
21 }
22 }
23
24 // 其他方法逻辑均由别的 StatementHandler 代理完成,就不贴代码了
25}
基本就是根据之前解析好的 StatementType 类型创建对应的 StatementHandler ,没有什么特殊的逻辑。如果未在Mapper文件中的SQL语句标签修改 statementType 属性,则默认创建 PreparedStatementHandler 。关于 StatementHandler 创建的过程就先分析到这,StatementHandler 创建完成了,后续要做到事情是创建 Statement,以及将运行时参数和 Statement 进行绑定。
JDBC 提供了三种 Statement 接口,分别是 Statement、PreparedStatement 和 CallableStatement。他们的关系如下:
其中,Statement 接口提供了执行 SQL,获取执行结果等基本功能。PreparedStatement 在此基础上,对 IN 类型的参数提供了支持,使得我们可以使用运行时参数替换 SQL 中的问号 ? 占位符,而不用手动拼接 SQL。CallableStatement 则是 在 PreparedStatement 基础上,对 OUT 类型的参数提供了支持,该种类型的参数用于保存存储过程输出的结果。
下面以最常用的 PreparedStatement 的创建为例分析,根据前文分析的 selectOne 方法可知,Statement 是在 Executor 的 prepareStatement 方法中创建的,先来看看这个方法。
111// -☆- SimpleExecutor
2private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
3 Statement stmt;
4 // 获取数据库连接
5 Connection connection = getConnection(statementLog);
6 // 创建 Statement
7 stmt = handler.prepare(connection, transaction.getTimeout());
8 // 为 Statement 设置 IN 参数
9 handler.parameterize(stmt);
10 return stmt;
11}
首先获取连接,然后创建 Statement ,最后设置参数,等待后续的执行操作,这和 JDBC 操作非常相似。下面来看看 PreparedStatement
具体是怎样创建的。
351// -☆- PreparedStatementHandler
2public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
3 Statement statement = null;
4 try {
5 // 创建 Statement
6 statement = instantiateStatement(connection);
7 // 设置超时和 FetchSize
8 setStatementTimeout(statement, transactionTimeout);
9 setFetchSize(statement);
10 return statement;
11 } catch (SQLException e) {
12 closeStatement(statement);
13 throw e;
14 } catch (Exception e) {
15 closeStatement(statement);
16 throw new ExecutorException("Error preparing statement. Cause: " + e, e);
17 }
18}
19
20protected Statement instantiateStatement(Connection connection) throws SQLException {
21 String sql = boundSql.getSql();
22 // 根据条件调用不同的 prepareStatement 方法创建 PreparedStatement
23 if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
24 String[] keyColumnNames = mappedStatement.getKeyColumns();
25 if (keyColumnNames == null) {
26 return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
27 } else {
28 return connection.prepareStatement(sql, keyColumnNames);
29 }
30 } else if (mappedStatement.getResultSetType() != null) {
31 return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
32 } else {
33 return connection.prepareStatement(sql);
34 }
35}
如上,PreparedStatement 的创建过程没什么复杂的地方,就不多说了。下面分析运行时参数是如何被设置到 SQL 中的过程。
621// -☆- PreparedStatementHandler
2public void parameterize(Statement statement) throws SQLException {
3 // 通过参数处理器 ParameterHandler 设置运行时参数到 PreparedStatement 中
4 parameterHandler.setParameters((PreparedStatement) statement);
5}
6
7public class DefaultParameterHandler implements ParameterHandler {
8 private final TypeHandlerRegistry typeHandlerRegistry;
9 private final MappedStatement mappedStatement;
10 private final Object parameterObject;
11 private final BoundSql boundSql;
12 private final Configuration configuration;
13
14 public void setParameters(PreparedStatement ps) {
15 // 从 BoundSql 中获取 ParameterMapping 列表,每个 ParameterMapping 与原始 SQL 中的 #{xxx} 占位符一一对应
16 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
17 if (parameterMappings != null) {
18 for (int i = 0; i < parameterMappings.size(); i++) {
19 ParameterMapping parameterMapping = parameterMappings.get(i);
20 // 检测参数类型,排除掉 mode 为 OUT 类型的 parameterMapping
21 if (parameterMapping.getMode() != ParameterMode.OUT) {
22 Object value;
23 // 获取属性名
24 String propertyName = parameterMapping.getProperty();
25 // 检测 BoundSql 的 additionalParameters 是否包含 propertyName
26 if (boundSql.hasAdditionalParameter(propertyName)) {
27 value = boundSql.getAdditionalParameter(propertyName);
28 } else if (parameterObject == null) {
29 value = null;
30
31 // 检测运行时参数是否有相应的类型解析器
32 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
33 // 若运行时参数的类型有相应的类型处理器 TypeHandler,则将 parameterObject 设为当前属性的值。
34 value = parameterObject;
35 } else {
36 // 为用户传入的参数 parameterObject 创建元信息对象
37 MetaObject metaObject = configuration.newMetaObject(parameterObject);
38 // 从用户传入的参数中获取 propertyName 对应的值
39 value = metaObject.getValue(propertyName);
40 }
41
42 // ---------------------分割线---------------------
43
44 TypeHandler typeHandler = parameterMapping.getTypeHandler();
45 JdbcType jdbcType = parameterMapping.getJdbcType();
46 if (value == null && jdbcType == null) {
47 // 此处 jdbcType = JdbcType.OTHER
48 jdbcType = configuration.getJdbcTypeForNull();
49 }
50 try {
51 // 由类型处理器 typeHandler 向 ParameterHandler 设置参数
52 typeHandler.setParameter(ps, i + 1, value, jdbcType);
53 } catch (TypeException e) {
54 throw new TypeException(...);
55 } catch (SQLException e) {
56 throw new TypeException(...);
57 }
58 }
59 }
60 }
61 }
62}
如上代码,分割线以上的大段代码用于获取 #{xxx} 占位符属性所对应的运行时参数。分割线以下的代码则是获取 #{xxx} 占位符属性对应的 TypeHandler,并在最后通过 TypeHandler 将运行时参数值设置到 PreparedStatement 中。
下面对之前参数转换和设置的过程做一个小结,假设我们有这样一条 SQL 语句:
11SELECT * FROM author WHERE name = #{name} AND age = #{age}
这个 SQL 语句中包含两个 #{} 占位符,在运行时这两个占位符会被解析成两个 ParameterMapping 对象。如下:
21ParameterMapping{property='name', mode=IN, javaType=class java.lang.String, jdbcType=null, }
2ParameterMapping{property='age', mode=IN, javaType=class java.lang.Integer, jdbcType=null, }
#{} 占位符解析完毕后,得到的 SQL 如下:
11SELECT * FROM Author WHERE name = ? AND age = ?
这里假设下面这个方法与上面的 SQL 对应:
11Author findByNameAndAge( ("name") String name, ("age") Integer age)
该方法的参数列表会被 ParamNameResolver 解析成一个 map,如下:
41{
2 0: "name",
3 1: "age"
4}
假设该方法在运行时有如下的调用:
11findByNameAndAge("tianxiaobo", 20)
此时,需要再次借助 ParamNameResolver 力量。这次我们将参数名和运行时的参数值绑定起来,得到如下的映射关系。
61{
2 "name": "tianxiaobo",
3 "age": 20,
4 "param1": "tianxiaobo",
5 "param2": 20
6}
下一步,我们要将运行时参数设置到 SQL 中。由于原 SQL 经过解析后,占位符信息已经被擦除掉了,我们无法直接将运行时参数 SQL 中。不过好在,这些占位符信息被记录在了 ParameterMapping 中了,MyBatis 会将 ParameterMapping 会按照 #{} 的解析顺序存入到 List 中。这样我们通过 ParameterMapping 在列表中的位置确定它与 SQL 中的哪个 ?
占位符相关联。同时通过 ParameterMapping 中的 property 字段,我们到“参数名与参数值”映射表中查找具体的参数值。这样,我们就可以将参数值准确的设置到 SQL 中了,此时 SQL 如下:
11SELECT * FROM Author WHERE name = "tianxiaobo" AND age = 20
整个流程如下图所示。
当运行时参数被设置到 SQL 中 后,下一步要做的事情是执行 SQL,然后处理 SQL 执行结果。对于更新操作,数据库一般返回一个 int 行数值,表示受影响行数,这个处理起来比较简单。但对于查询操作,返回的结果类型多变,处理方式也很复杂。接下来,我们就来看看 MyBatis 是如何处理查询结果的。
MyBatis 可以将查询结果,即结果集 ResultSet 自动映射成实体类对象。这样使用者就无需再手动操作结果集,并将数据填充到实体类对象中。这可大大降低开发的工作量,提高工作效率。
在 MyBatis 中,结果集的处理工作由结果集处理器 ResultSetHandler
执行。ResultSetHandler 是一个接口,它只有一个实现类 DefaultResultSetHandler
。结果集的处理入口方法是 handleResultSets,下面来看一下该方法的实现。
531public List<Object> handleResultSets(Statement stmt) throws SQLException {
2
3 final List<Object> multipleResults = new ArrayList<Object>();
4
5 int resultSetCount = 0;
6 // 获取第一个结果集
7 ResultSetWrapper rsw = getFirstResultSet(stmt);
8
9 List<ResultMap> resultMaps = mappedStatement.getResultMaps();
10 int resultMapCount = resultMaps.size();
11 validateResultMapsCount(rsw, resultMapCount);
12
13 while (rsw != null && resultMapCount > resultSetCount) {
14 ResultMap resultMap = resultMaps.get(resultSetCount);
15 // 处理结果集
16 handleResultSet(rsw, resultMap, multipleResults, null);
17 // 获取下一个结果集
18 rsw = getNextResultSet(stmt);
19 cleanUpAfterHandlingResultSet();
20 resultSetCount++;
21 }
22
23 // 以下逻辑均与多结果集有关,就不分析了,代码省略
24 String[] resultSets = mappedStatement.getResultSets();
25 if (resultSets != null) {...}
26
27 return collapseSingleResultList(multipleResults);
28}
29
30private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
31 // 获取结果集
32 ResultSet rs = stmt.getResultSet();
33 while (rs == null) {
34 /*
35 * 移动 ResultSet 指针到下一个上,有些数据库驱动可能需要使用者
36 * 先调用 getMoreResults 方法,然后才能调用 getResultSet 方法
37 * 获取到第一个 ResultSet
38 */
39 if (stmt.getMoreResults()) {
40 rs = stmt.getResultSet();
41 } else {
42 if (stmt.getUpdateCount() == -1) {
43 break;
44 }
45 }
46 }
47 /*
48 * 这里并不直接返回 ResultSet,而是将其封装到 ResultSetWrapper 中。
49 * ResultSetWrapper 中包含了 ResultSet 一些元信息,比如列名称、每列对应的 JdbcType、
50 * 以及每列对应的 Java 类名(class name,譬如 java.lang.String)等。
51 */
52 return rs != null ? new ResultSetWrapper(rs, configuration) : null;
53}
如上,该方法首先从 Statement 中获取第一个结果集,然后调用 handleResultSet 方法对该结果集进行处理。一般情况下,如果我们不调用存储过程,不会涉及到多结果集的问题。由于存储过程并不是很常用,所以关于多结果集的处理逻辑我就不分析了。下面,我们把目光聚焦在单结果集的处理逻辑上。
271private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
2 try {
3 if (parentMapping != null) {
4 // 多结果集相关逻辑,不分析了
5 handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
6 } else {
7 /*
8 * 检测 resultHandler 是否为空。ResultHandler 是一个接口,使用者可实现该接口,
9 * 这样我们可以通过 ResultHandler 自定义接收查询结果的动作。比如我们可将结果存储到
10 * List、Map 亦或是 Set,甚至丢弃,这完全取决于大家的实现逻辑。
11 */
12 if (resultHandler == null) {
13 // 创建默认的结果处理器(默认是将结果存储在List中)
14 DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
15 // 处理结果集的行数据
16 handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
17 multipleResults.add(defaultResultHandler.getResultList());
18 } else {
19 // 处理结果集的行数据
20 handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
21 }
22 }
23 } finally {
24 // issue #228 (close resultsets)
25 closeResultSet(rsw.getResultSet());
26 }
27}
在上面代码中,出镜率最高的 handleRowValues 方法,该方法用于处理结果集中的数据。下面来看一下这个方法的逻辑。
131public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
2 RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
3
4 if (resultMap.hasNestedResultMaps()) {
5 ensureNoRowBounds();
6 checkResultHandler();
7 // 处理嵌套映射
8 handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
9 } else {
10 // 处理简单映射
11 handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
12 }
13}
如上,handleRowValues 方法中针对两种映射方式进行了处理。一种是嵌套映射,另一种是简单映射。嵌套是指 ResultMap 的子标签也存在 ResultMap,有内联嵌套映射和外引用嵌套映射两种情形,后面将会进行分析。这里先来简单映射的处理逻辑。
161private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
2 ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
3
4 DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
5 // 1. 根据 RowBounds 定位到指定行记录
6 skipRows(rsw.getResultSet(), rowBounds);
7 // 2. 检测是否还有更多行的数据需要处理
8 while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
9 // 3. 获取经过鉴别器处理后的 ResultMap
10 ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
11 // 4. 从 resultSet 中获取结果(即由某行数据封装出来的对象)
12 Object rowValue = getRowValue(rsw, discriminatedResultMap);
13 // 5. 存储结果到 ResultHandler 中
14 storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
15 }
16}
在如上几个步骤中,鉴别器相关的逻辑就不分析了,不是很常用。先来分析第一个步骤对应的代码逻辑。如下:
141private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
2 // 检测 rs 的类型,不同的类型行数据定位方式是不同的
3 if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
4 if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
5 // 直接定位到 rowBounds.getOffset() 位置处
6 rs.absolute(rowBounds.getOffset());
7 }
8 } else {
9 for (int i = 0; i < rowBounds.getOffset(); i++) {
10 // 通过多次调用 rs.next() 方法实现行数据定位。 当 Offset 数值很大时,这种效率很低下
11 rs.next();
12 }
13 }
14}
这段逻辑主要用于处理 RowBounds
,来跳过前 rowBounds.getOffset()
行,效率并不高,能不能尽量不用。
第二个步骤主要是通过 JDBCApi resultSet.next()
来遍历剩余的结果集,需要注意的是,如果结果集被关闭或者解析被终止或解析到足够的结果行则停止遍历。
111// resultSet.isClosed() 用于检测结果集是否关闭,只有在未关闭时才继续解析后续结果
2// shouldProcessMoreRows用于进行其它两项检测
3while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
4 // ...
5}
6
7// context.isStopped() 判断上下文中的停止标记,可以在ResultHandler中获取并修改
8// context.getResultCount() < rowBounds.getLimit() 解析到足够的记录数后退出解析
9private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
10 return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
11}
第五个步骤主要是存储解析出来的对象到 Resulthandler 中,并同步上下文信息。
281 private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
2 if (parentMapping != null) {
3 // 多结果集相关,不分析了
4 linkToParents(rs, parentMapping, rowValue);
5 } else {
6 // 存储结果
7 callResultHandler(resultHandler, resultContext, rowValue);
8 }
9 }
10
11 private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
12 // 同步 resultContext
13 resultContext.nextResultObject(rowValue);
14
15 // 调用 resultHandler 的 handleResult 方法进行存储(或者可以自定义其它处理方式)
16 ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
17 }
18
19 public void nextResultObject(T resultObject) {
20 resultCount++; // 已解析记录数
21 this.resultObject = resultObject; // 当前解析的结果
22 }
23
24
25 public void handleResult(ResultContext<?> context) {
26 // DefaultResultHandler的结果处理方式 ==> 添加结果到 list 中
27 list.add(context.getResultObject());
28 }
除此之外,如果 Mapper 接口方法返回值为 Map 类型,此时则需要另一种 ResultHandler 实现类处理结果,即 DefaultMapResultHandler。
91// DefaultMapResultHandler 把结果存储在MAP中
2
3 public void handleResult(ResultContext<? extends V> context) {
4 final V value = context.getResultObject();
5 final MetaObject mo = MetaObject.forObject(value, objectFactory, objectWrapperFactory, reflectorFactory);
6 // TODO is that assignment always true?
7 final K key = (K) mo.getValue(mapKey);
8 mappedResults.put(key, value);
9 }
最后再着重分析下第四步, ResultSet 的映射过程,如下:
241private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
2 // 用于处理懒加载
3 final ResultLoaderMap lazyLoader = new ResultLoaderMap();
4
5 // 1. 创建实体类对象,比如 Article 对象
6 Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
7 if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
8 final MetaObject metaObject = configuration.newMetaObject(rowValue);
9 boolean foundValues = this.useConstructorMappings;
10
11 // 检测是否应该自动映射结果集
12 if (shouldApplyAutomaticMappings(resultMap, false)) {
13 // 2.进行自动映射
14 foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
15 }
16
17 // 3. 手动映射:根据 <resultMap> 节点中配置的映射关系进行映射
18 foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
19
20 foundValues = lazyLoader.size() > 0 || foundValues;
21 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
22 }
23 return rowValue;
24}
在上面的方法中,重要的逻辑已经注释出来了。有三处代码的逻辑比较复杂,接下来按顺序进行分节说明。首先分析实体类的创建过程。
MyBatis支持多种方式创建实体类对象,并在在创建时织入懒加载处理逻辑。代码如下:
301// -☆- DefaultResultSetHandler
2private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
3
4 this.useConstructorMappings = false;
5 final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
6 final List<Object> constructorArgs = new ArrayList<Object>();
7
8 // 1. 调用重载方法创建实体类对象
9 Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
10
11 // 2. 下面代码用于织入懒加载处理逻辑
12 if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
13 // 遍历 ResultMappings 检查是否有懒加载
14 final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
15 for (ResultMapping propertyMapping : propertyMappings) {
16 // 如果开启了延迟加载,则为 resultObject 生成代理类
17 if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
18 /*
19 * 创建代理类,默认使用 Javassist 框架生成代理类。由于实体类通常不会实现接口,
20 * 所以不能使用 JDK 动态代理 API 为实体类生成代理。
21 */
22 resultObject = configuration.getProxyFactory()
23 .createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
24 break;
25 }
26 }
27 }
28 this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
29 return resultObject;
30}
重点注意,懒加载的处理逻辑是在此处织入的,后续章节会对懒加载再进行详细分析,先来看看 MyBatis 创建实体类对象的具体过程。
291private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException {
2
3 final Class<?> resultType = resultMap.getType();
4 final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
5 // 获取 <constructor> 节点对应的 ResultMapping
6 final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
7
8 /*
9 * 1. 检测是否有与返回值类型相对应的 TypeHandler,若有则直接从
10 * 通过 TypeHandler 从结果集中提取数据,并生成返回值对象
11 */
12 if (hasTypeHandlerForResultObject(rsw, resultType)) {
13 // 通过 TypeHandler 获取提取,并生成返回值对象
14 return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
15 } else if (!constructorMappings.isEmpty()) {
16 /*
17 * 2. 通过 <constructor> 节点配置的映射信息从 ResultSet 中提取数据,
18 * 然后将这些数据传给指定构造方法,即可创建实体类对象
19 */
20 return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
21 } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
22 // 3. 通过 ObjectFactory 调用目标类的默认构造方法创建实例
23 return objectFactory.create(resultType);
24 } else if (shouldApplyAutomaticMappings(resultMap, false)) {
25 // 4. 通过自动映射查找合适的构造方法创建实例
26 return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
27 }
28 throw new ExecutorException("Do not know how to create an instance of " + resultType);
29}
如上,createResultObject 方法中包含了4种创建实体类对象的方式。一般情况下,若无特殊要求,MyBatis 会通过 ObjectFactory 调用默认构造方法创建实体类对象。ObjectFactory 是一个接口,大家可以实现这个接口,以按照自己的逻辑控制对象的创建过程。到此,实体类对象已经创建好了,接下里要做的事情是将结果集中的数据映射到实体类对象中。
在 MyBatis 中,全局自动映射行为有三种等级。如下:
NONE
:禁用自动映射。仅对手动配置的列进行映射。
PARTIAL
:默认值。如果存在嵌套映射,则关闭自动映射,防止数据映射错误。
FULL
:对所有未进行手动配置的列进行自动映射。
这可以通过配置 <resultMap>
节点的 autoMapping
属性来进行修改。下面来看看具体的代码实现:
151private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
2 // 检测 <resultMap> 是否配置了 autoMapping 属性
3 if (resultMap.getAutoMapping() != null) {
4 // 首先以 autoMapping 属性为准
5 return resultMap.getAutoMapping();
6 } else {
7 if (isNested) {
8 // 对于嵌套 resultMap,仅当全局的映射行为为 FULL 时,才进行自动映射
9 return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
10 } else {
11 // 对于普通的 resultMap,只要全局的映射行为不为 NONE,即可进行自动映射
12 return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
13 }
14 }
15}
如上,该方法用于检测是否应为当前结果集应用自动映射,逻辑不难理解,接下来分析 MyBatis 如何进行自动映射。
221private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
2
3 // 1. 获取 UnMappedColumnAutoMapping 列表(即为没有配置手动映射的列创建自动映射)
4 List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
5
6 // 2. 进行自动映射
7 boolean foundValues = false;
8 if (!autoMapping.isEmpty()) {
9 for (UnMappedColumnAutoMapping mapping : autoMapping) {
10 // 2.1 通过 TypeHandler 从结果集中获取指定列的数据
11 final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
12 if (value != null) {
13 foundValues = true;
14 }
15 if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
16 // 2.2 通过元信息对象设置 value 到实体类对象的指定字段上
17 metaObject.setValue(mapping.property, value);
18 }
19 }
20 }
21 return foundValues;
22}
首先对未手工配置的列生成映射配置UnMappedColumnAutoMapping
,该类定义在 DefaultResultSetHandler 内部,如下:
131private static class UnMappedColumnAutoMapping {
2 private final String column;
3 private final String property;
4 private final TypeHandler<?> typeHandler;
5 private final boolean primitive;
6
7 public UnMappedColumnAutoMapping(String column, String property, TypeHandler<?> typeHandler, boolean primitive) {
8 this.column = column;
9 this.property = property;
10 this.typeHandler = typeHandler;
11 this.primitive = primitive;
12 }
13}
然后使用该配置逐一进行映射。映射过程即通过 TypeHandler 从结果集获取值,然后通过 value 的 MetaObject 对象设置该值到对象中。下面再来看一下生成 UnMappedColumnAutoMapping 集合的过程,如下:
631// -☆- DefaultResultSetHandler
2private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
3 // 生成key,从缓存中获取 UnMappedColumnAutoMapping 列表
4 final String mapKey = resultMap.getId() + ":" + columnPrefix;
5 List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
6
7 // 缓存未命中
8 if (autoMapping == null) {
9 autoMapping = new ArrayList<UnMappedColumnAutoMapping>();
10
11 // 1. 从结果集获取未手工配置的列名
12 final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
13 for (String columnName : unmappedColumnNames) {
14
15 // 2. 列名前缀处理
16 String propertyName = columnName;
17 if (columnPrefix != null && !columnPrefix.isEmpty()) {
18 if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
19 // 属性名去掉前缀
20 propertyName = columnName.substring(columnPrefix.length());
21 } else {
22 // 不映射非对应前缀的列名
23 continue;
24 }
25 }
26
27 // 3. 匹配属性名。根据配置是否进行下划线转为驼峰。比如 AUTHOR_NAME -> authorName
28 final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
29
30 // 如果实体类存在对应的属性则继续生成映射配置,否则视配置进行处理
31 if (property != null && metaObject.hasSetter(property)) {
32 // 再次检测该列名是否已经手工配置了(防止去掉前缀后属性名撞车,覆盖手工配置的属性)
33 if (resultMap.getMappedProperties().contains(property)) {
34 continue;
35 }
36
37 // 4. 获取属性对应的类型
38 final Class<?> propertyType = metaObject.getSetterType(property);
39
40 // 判断该类型是否有对应的类型处理器
41 if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
42 // 5. 获取类型处理器
43 final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
44
45 // 6. 封装上面获取到的信息到 UnMappedColumnAutoMapping 对象中
46 autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
47 } else {
48 // 如果没有对应的类型处理器,MyBatis不知道怎么映射。根据配置选择抛出异常、仅写日志或啥也不做。默认为啥也不做。
49 configuration.getAutoMappingUnknownColumnBehavior()
50 .doAction(mappedStatement, columnName, property, propertyType);
51 }
52 } else {
53 // 列名没有对应的属性名,也不知道要怎么映射。
54 configuration.getAutoMappingUnknownColumnBehavior()
55 .doAction(mappedStatement, columnName, (property != null) ? property : propertyName, null);
56 }
57 }
58
59 // 写入缓存
60 autoMappingsCache.put(mapKey, autoMapping);
61 }
62 return autoMapping;
63}
以上步骤中,除了第一步,其他都是常规操作,无需过多说明。下面来分析第一个步骤的逻辑,如下:
451// -☆- ResultSetWrapper
2public List<String> getUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
3 // 先到缓存看看(unMappedColumnNamesMap是RSW的成员变量)
4 List<String> unMappedColumnNames = unMappedColumnNamesMap.get(getMapKey(resultMap, columnPrefix));
5
6 // 缓存没有
7 if (unMappedColumnNames == null) {
8 // 1. 加载已映射与未映射列名到缓存
9 loadMappedAndUnmappedColumnNames(resultMap, columnPrefix);
10
11 // 2. 获取未映射列名
12 unMappedColumnNames = unMappedColumnNamesMap.get(getMapKey(resultMap, columnPrefix));
13 }
14 return unMappedColumnNames;
15}
16
17private void loadMappedAndUnmappedColumnNames(ResultMap resultMap, String columnPrefix) throws SQLException {
18 List<String> mappedColumnNames = new ArrayList<String>();
19 List<String> unmappedColumnNames = new ArrayList<String>();
20
21 // 前缀转为大写,忽略大小写匹配
22 final String upperColumnPrefix = columnPrefix == null ? null : columnPrefix.toUpperCase(Locale.ENGLISH);
23
24 // 1. 为 <resultMap> 中的列名拼接前缀
25 final Set<String> mappedColumns = prependPrefixes(resultMap.getMappedColumns(), upperColumnPrefix);
26
27 // 遍历 columnNames,columnNames 是 ResultSetWrapper 的成员变量,保存了当前结果集中的所有列名
28 for (String columnName : columnNames) {
29 // 结果集中列名转为大写,忽略大小写匹配
30 final String upperColumnName = columnName.toUpperCase(Locale.ENGLISH);
31
32 // 检测已映射列名集合中是否包含当前列名
33 if (mappedColumns.contains(upperColumnName)) {
34 // 2.1 包含则存入 mappedColumnNames 集合
35 mappedColumnNames.add(upperColumnName);
36 } else {
37 // 2.2 不包含则存入 unmappedColumnNames 集合
38 unmappedColumnNames.add(columnName);
39 }
40 }
41
42 // 缓存列名集合
43 mappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), mappedColumnNames);
44 unMappedColumnNamesMap.put(getMapKey(resultMap, columnPrefix), unmappedColumnNames);
45}
如上,已映射列名与未映射列名的分拣逻辑并不复杂。
到此为止,自动映射配置的创建过程已分析完毕,接下来看看手工配置的映射。
751// -☆- DefaultResultSetHandler
2private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
3
4 // 1. 获取手工配置的列名(自动映射可能分拣过,有缓存)
5 final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
6
7 boolean foundValues = false;
8
9 // 获取 ResultMap 中所有的属性映射,并遍历
10 final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
11 for (ResultMapping propertyMapping : propertyMappings) {
12 // 2. 拼接列名前缀,得到完整列名
13 String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
14 // 如果是关联查询,可能是 {prop1=col1, prop2=col2} 形式,不能这样简单处理,先重置拼接的列名
15 if (propertyMapping.getNestedResultMapId() != null) {
16 column = null;
17 }
18
19 /*
20 * 下面的 if 分支由三个或条件组合而成,三个条件的含义如下:
21 * 条件一:检测 column 是否为 {prop1=col1, prop2=col2} 形式,该种形式的 column 一般用于关联查询
22 * 条件二:检测当前列名是否被包含在已映射的列名集合中,若包含则可进行数据集映射操作
23 * 条件三:多结果集相关,暂不分析
24 */
25 if (propertyMapping.isCompositeResult()
26 || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
27 || propertyMapping.getResultSet() != null) {
28
29 // 2. 从结果集中获取指定列的数据
30 Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
31
32 // 3. 获取映射的属性名 issue #541 make property optional
33 final String property = propertyMapping.getProperty();
34 // 3.1 ResultMapping 未配置 property ,不知道映射到哪里去,直接跳过
35 if (property == null) {
36 continue;
37
38 // 3,2 若获取到的值为 DEFERED,则延迟加载该值
39 } else if (value == DEFERED) {
40 foundValues = true;
41 continue;
42 }
43
44 // 设置 foundValues 标记
45 if (value != null) {
46 foundValues = true;
47 }
48
49 // 3.3 将获取到的值设置到实体类对象中 (如果不是基本类型,根据参数判断是否要把NULL设置进去)
50 if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property)
51 .isPrimitive())) {
52
53 // 将获取到的值设置到实体类对象中
54 metaObject.setValue(property, value);
55 }
56 }
57 }
58 return foundValues;
59}
60
61private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
62 if (propertyMapping.getNestedQueryId() != null) {
63 // 获取关联查询结果(后续章节分析)
64 return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
65 } else if (propertyMapping.getResultSet() != null) {
66 // 多结果集逻辑,暂不分析
67 addPendingChildRelation(rs, metaResultObject, propertyMapping);
68 return DEFERED;
69 } else {
70 // 使用类型处理器从结果集获取列数据
71 final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
72 final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
73 return typeHandler.getResult(rs, column);
74 }
75}
如上,首先从 ResultSetWrapper 中获取已映射列名集合 mappedColumnNames,从 ResultMap 获取映射对象 ResultMapping 集合。然后遍历 ResultMapping 集合,再此过程中调用 getPropertyMappingValue 获取指定指定列的数据,最后将获取到的数据设置到实体类对象中。到此,基本的结果集映射过程就分析完了。下面章节将分析之前提到的获取关联查询的结果。
在进行数据库查询时,经常会碰到一对一和一对多的查询场景。如查询 USER_INFO(pk:USER_ID) 中用户的基本信息时,去同步查询 USER_FUND(pk:USER_ID) 中该用户的总资产,就是一对一查询。如果同步查询 USER_FUND_DETAIL(pk:USER_ID,FUND_CLS) 中用户的资产明细,则是一对多查询。MyBatis为这两种场景提供了四种解决方案:
定制VO类。为每个特殊的结果集定义一个对应的VO对象,直接使用简单映射配置来映射所有列。优点是映射配置简单,缺点是VO类增多、内存数据冗余。
嵌套映射。在 ResultMap 中嵌套另一个 ResultMap 来映射到无类型处理器的成员变量中。虽然减少了VO类和内存数据冗余,但映射配置变得复杂。
关联查询。通过多次单表查询来替代复杂的关联查询,把多次查询结果通过多次简单映射来映射到一个复杂的对象中。优点是SQL语句简单,缺点是存在"1+N"问题,性能影响较大。
多结果集。通过存储过程返回多个结果集,映射到多个VO对象。
其中,定制VO类使用的是简单映射,前文已经做了详细分析,嵌套映射将会在下一章节讲解,而多结果集使用较少,本文暂不分析。下面先来看看上文提到的关联查询。关联查询相关的标签有<association>
和<collection>
,分别用作一对一和一对多查询,如果你对这两种方式的使用还不太了解,可以先阅读本系列文章的基础使用篇。
我们从之前提到的 getNestedQueryMappingValue 方法开始分析,如下:
491private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
2 // 1. 获取关联查询需要用到的信息及关联查询对应的 MappedStatement。
3 // 关联查询ID:命名空间 + <association> 的 select 属性值
4 final String nestedQueryId = propertyMapping.getNestedQueryId();
5 // 将关联查询的值映射到该属性
6 final String property = propertyMapping.getProperty();
7 // 根据 nestedQueryId 获取关联查询的 MappedStatement
8 final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
9 // 关联查询参数类型(单一参数则为对应的实际类型,复合参数则默认为Map,可通过关联查询的MS的parameterType修改为VO类
10 final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
11
12 // 2. 从结果集获取关联查询参数(逻辑与主查询的参数转换类似,参数值由 column 属性指定结果集中的列数据)
13 final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
14
15 Object value = null;
16
17 // 仅在参数不为NULL的时候才进行关联查询
18 if (nestedQueryParameterObject != null) {
19 // 3. 查询前准备:获取 BoundSql、创建缓存key、获取结果类型
20 final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
21 final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
22 final Class<?> targetType = propertyMapping.getJavaType();
23
24 // 检查一级缓存是否保存了关联查询结果
25 if (executor.isCached(nestedQuery, key)) {
26 // 4.1 如果一级缓存存在,则走延迟加载逻辑。
27 // 一般是添加到执行器的延迟加载队列,与前面设置的缓存占位符配合,可解决关联查询死循环问题。
28 // 如果缓存中确实存在查询结果且不是缓存占位符,则直接设置到对象中
29 executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
30 value = DEFERED;
31 } else {
32 // 创建结果加载器(用于执行查询和映射结果列,同时,也为了方便后面懒加载触发时执行查询)
33 final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
34
35 // 检测当前属性是否需要懒加载
36 if (propertyMapping.isLazy()) {
37 // 4.2 添加懒加载相关的对象到 loaderMap 集合中
38 lazyLoader.addLoader(property, metaResultObject, resultLoader);
39 value = DEFERED;
40 } else {
41 // 4.3 直接执行关联查询
42 value = resultLoader.loadResult();
43 }
44 }
45 }
46
47 // 5. 返回关联查询结果或 DEFERRED
48 return value;
49}
首先看看从结果集获取关联查询参数的过程。
451 private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
2 // 判断是否为复合参数
3 if (resultMapping.isCompositeResult()) {
4 // 复合参数转换
5 return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
6 } else {
7 // 单一参数转换(直接查找类型处理器从结果集取列数据)
8 return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
9 }
10 }
11
12 private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
13 final TypeHandler<?> typeHandler;
14 // 1. 获取类型处理器
15 if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
16 typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
17 } else {
18 typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
19 }
20 // 2. 使用类型处理器从结果集取列数据
21 return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
22 }
23
24 private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType, String columnPrefix) throws SQLException {
25 // 1. 创建参数对象(HashMap或实体类),并创建对应的 MetaObject 对象
26 final Object parameterObject = instantiateParameterObject(parameterType);
27 final MetaObject metaObject = configuration.newMetaObject(parameterObject);
28
29 // 2. 遍历复合参数的映射配置,从结果集取列数据填充参数对象
30 boolean foundValues = false;
31 for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
32 final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
33 final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
34 final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
35 // issue #353 & #560 do not execute nested query if key is null
36 if (propValue != null) {
37 metaObject.setValue(innerResultMapping.getProperty(), propValue);
38 foundValues = true;
39 }
40 }
41
42 // 如果取到了列数据则返回封装的参数,否则返回NULL
43 return foundValues ? parameterObject : null;
44 }
45
接着分析关联查询的懒加载机制。懒加载在此处仅是将关联查询相关信息添加到 loaderMap 集合中而已。
121// -☆- ResultLoaderMap
2public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
3 // 将属性名转为大写
4 String upperFirst = getUppercaseFirstProperty(property);
5 if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
6 throw new ExecutorException("Nested lazy loaded result property '" + property +
7 "' for query id '" + resultLoader.mappedStatement.getId() +
8 " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
9 }
10 // 创建 LoadPair,并将 <大写属性名,LoadPair对象> 键值对添加到 loaderMap 中
11 loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
12}
那么懒加载是如何触发的呢?又是谁触发的呢?答案在实体类的代理对象, 回顾之前创建实体类对象时,MyBatis 会为需要延迟加载的类生成代理类,代理逻辑会拦截实体类的方法调用。
671private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
2 // ...
3
4 // 默认使用 Javassist 框架生成代理类
5 resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
6
7 // ...
8
9 return resultObject;
10}
11
12// ========== JavassistProxyFactory.java ================
13
14
15public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
16 // 调用内部类 EnhancedResultObjectProxyImpl 创建代理,该类实现类 MethodHandler 接口
17 return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
18}
19
20public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
21 final Class<?> type = target.getClass();
22 // 创建方法拦截处理器
23 EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
24
25 // 调用重载函数创建代理对象
26 Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
27
28 // 拷贝代理对象的属性到 resultObject
29 PropertyCopier.copyBeanProperties(type, target, enhanced);
30 return enhanced;
31}
32
33static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
34
35 // 1. 创建 javassist 的 ProxyFactory
36 ProxyFactory enhancer = new ProxyFactory();
37
38 // 2. 设置被代理的类
39 enhancer.setSuperclass(type);
40
41 // Java序列化机制特殊处理
42 try {
43 type.getDeclaredMethod(WRITE_REPLACE_METHOD);
44 // ObjectOutputStream will call writeReplace of objects returned by writeReplace
45 if (LogHolder.log.isDebugEnabled()) {
46 LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
47 }
48 } catch (NoSuchMethodException e) {
49 enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
50 } catch (SecurityException e) {
51 // nothing to do here
52 }
53
54 // 3. 创建代理
55 Object enhanced;
56 Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
57 Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
58 try {
59 enhanced = enhancer.create(typesArray, valuesArray);
60 } catch (Exception e) {
61 throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
62 }
63
64 // 4. 设置方法拦截处理器 为 EnhancedResultObjectProxyImpl 对象
65 ((Proxy) enhanced).setHandler(callback);
66 return enhanced;
67}
EnhancedResultObjectProxyImpl 类的具体代理逻辑,我们可以看看它的invoke方法。
491// -☆- EnhancedResultObjectProxyImpl
2public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
3 final String methodName = method.getName();
4 try {
5 synchronized (lazyLoader) {
6 if (WRITE_REPLACE_METHOD.equals(methodName)) {
7 // 针对JAVA序列化机制 writeReplace 方法的处理逻辑,与延迟加载无关,不分析了
8 if (WRITE_REPLACE_METHOD.equals(methodName)) {
9 Object original;
10 if (constructorArgTypes.isEmpty()) {
11 original = objectFactory.create(type);
12 } else {
13 original = objectFactory.create(type, constructorArgTypes, constructorArgs);
14 }
15 PropertyCopier.copyBeanProperties(type, enhanced, original);
16 if (lazyLoader.size() > 0) {
17 return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
18 } else {
19 return original;
20 }
21 } else {
22 if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
23 // 1. 如果 aggressive 为 true,或触发方法(默认为equals,clone,hashCode,toString)被调用,则触发所有的属性懒加载
24 if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
25 lazyLoader.loadAll();
26
27 // 2. 如果使用者显示调用了 setter 方法,则将相应的懒加载类从 loaderMap 中移除
28 } else if (PropertyNamer.isSetter(methodName)) {
29 final String property = PropertyNamer.methodToProperty(methodName);
30 lazyLoader.remove(property);
31
32 /// 3. 某个属性的 getter 方法被调用,则触发该属性的延懒加载
33 } else if (PropertyNamer.isGetter(methodName)) {
34 final String property = PropertyNamer.methodToProperty(methodName);
35 // 检测该属性是否有相应的 LoadPair 对象
36 if (lazyLoader.hasLoader(property)) {
37 lazyLoader.load(property);
38 }
39 }
40 }
41 }
42 }
43
44 // 调用被代理类的方法
45 return methodProxy.invoke(enhanced, args);
46 } catch (Throwable t) {
47 throw ExceptionUtil.unwrapThrowable(t);
48 }
49}
接下来,我们来看看延迟加载逻辑是怎样实现的。
581// -☆- ResultLoaderMap
2public boolean load(String property) throws SQLException {
3 // 从 loaderMap 中移除 property 所对应的 LoadPair
4 LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
5 if (pair != null) {
6 // 加载结果
7 pair.load();
8 return true;
9 }
10 return false;
11}
12
13// -☆- LoadPair
14public void load() throws SQLException {
15 if (this.metaResultObject == null) {
16 throw new IllegalArgumentException("metaResultObject is null");
17 }
18 if (this.resultLoader == null) {
19 throw new IllegalArgumentException("resultLoader is null");
20 }
21
22 // 调用重载方法
23 this.load(null);
24}
25
26public void load(final Object userObject) throws SQLException {
27 // 若 metaResultObject 和 resultLoader 为 null,则创建相关对象。
28 if (this.metaResultObject == null || this.resultLoader == null) {
29 if (this.mappedParameter == null) {
30 throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
31 + "required parameter of mapped statement ["
32 + this.mappedStatement + "] is not serializable.");
33 }
34
35 final Configuration config = this.getConfiguration();
36 final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
37 if (ms == null) {
38 throw new ExecutorException("Cannot lazy load property [" + this.property
39 + "] of deserialized object [" + userObject.getClass()
40 + "] because configuration does not contain statement ["
41 + this.mappedStatement + "]");
42 }
43
44 this.metaResultObject = config.newMetaObject(userObject);
45 this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
46 metaResultObject.getSetterType(this.property), null, null);
47 }
48
49 // 线程安全检测
50 if (this.serializationCheck == null) {
51 final ResultLoader old = this.resultLoader;
52 // 重新创建新的 ResultLoader 和 ClosedExecutor,ClosedExecutor 是非线程安全的
53 this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement, old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
54 }
55
56 // 调用 ResultLoader 的 loadResult 方法加载结果,并通过 metaResultObject 设置结果到实体类对象中
57 this.metaResultObject.setValue(property, this.resultLoader.loadResult());
58}
上面的代码比较多,但是没什么特别的逻辑,下面看一下 ResultLoader 的 loadResult 方法逻辑是怎样的。
231public Object loadResult() throws SQLException {
2 // 执行关联查询
3 List<Object> list = selectList();
4 // 抽取结果
5 resultObject = resultExtractor.extractObjectFromList(list, targetType);
6 return resultObject;
7}
8
9private <E> List<E> selectList() throws SQLException {
10 Executor localExecutor = executor;
11 if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
12 localExecutor = newExecutor();
13 }
14 try {
15 // 通过 Executor 执行查询,这个之前已经分析过了
16 return localExecutor.<E>query(mappedStatement, parameterObject, RowBounds.DEFAULT,
17 Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
18 } finally {
19 if (localExecutor != executor) {
20 localExecutor.close(false);
21 }
22 }
23}
如上,我们在 ResultLoader 中终于看到了执行关联查询的代码,即 selectList 方法中的逻辑。该方法在内部通过 Executor 进行查询。至于查询结果的抽取过程,代码贴在下面,了解下即可。
361public Object extractObjectFromList(List<Object> list, Class<?> targetType) {
2 Object value = null;
3 // 如果 targetType 是list,则直接返回
4 if (targetType != null && targetType.isAssignableFrom(list.getClass())) {
5 value = list;
6
7 // 其它类型集合
8 } else if (targetType != null && objectFactory.isCollection(targetType)) {
9 value = objectFactory.create(targetType);
10 MetaObject metaObject = configuration.newMetaObject(value);
11 metaObject.addAll(list);
12
13 // 数组。list转数组
14 } else if (targetType != null && targetType.isArray()) {
15 Class<?> arrayComponentType = targetType.getComponentType();
16 Object array = Array.newInstance(arrayComponentType, list.size());
17 if (arrayComponentType.isPrimitive()) {
18 for (int i = 0; i < list.size(); i++) {
19 Array.set(array, i, list.get(i));
20 }
21 value = array;
22 } else {
23 value = list.toArray((Object[])array);
24 }
25
26 // 其它类型。取第一个值,没有则为null,如果>1则报错
27 } else {
28 if (list != null && list.size() > 1) {
29 throw new ExecutorException("Statement returned more than one row, where no more than one was expected.");
30 } else if (list != null && list.size() == 1) {
31 value = list.get(0);
32 }
33 }
34 return value;
35}
36
嵌套映射指将二维表形式的查询结果映射到复杂VO对象,并对重复出现的数据进行折叠。嵌套映射分为两种,一种是一对一
的嵌套映射。如查询Blog及所属的User,返回结果集如下所示,这时将行中的id、title映射到Blog对象,将user_id和user_name映射到Blog的成员变量user。
还有一种是一对多
的嵌套映射,如查询博客及博客下的所有评论,返回数据格式如下图所示,这时将行中的id、title映射到Blog对象,将comment_id和comment_body映射到Blog的成员变量List<Comment>。
mybatis是如何知道哪一列映射到哪个对象的呢?
这是在ResultMap元素中配置的,并且可以进行
前缀匹配
(columnPrefix在匹配时会添加该前缀与列名匹配)和限定不为空的列
(notNullColumn可以指定一个或多个列,以逗号分隔,如果全部为空,则会忽略该行数据)。注意:在嵌套映射的场景下,autoMapping=false,自动映射默认关闭。
提示:可以在ResultMap中指定id列,则在进行嵌套映射时,优先使用id列进行结果行分组。如果没有指定id列,则使用所有的result配置创建RowKey。例如,上面一对多映射中,将id一致的记录视为同一个Blog对象。
下面是 MyBatis 处理嵌套映射的流程图,可以看到嵌套映射时首先会创建一个RowKey,去暂存区读数据,如果不存在则创建对象(Blog),并进行自动映射和手动映射,然后进行复合属性填充。复合属性填充依旧先创建RowKey,流程与前类似。
在读取暂存区的时候,如果根据RowKey找到对象,则表示该对象在之前已经创建过了,直接进入复合属性填充即可。
下面进行源码跟踪验证,代码和配置摘要如下:
91/**
2 * 跟踪代码:嵌套结果映射+循环映射
3 */
4
5public void testNestedMapping() {
6 Blog blog = blogMapper.findByIdNestedMappingComments(1); // blog嵌套映射comments,comment内部又映射到当前blog
7 System.out.println(blog);
8}
9
201<resultMap id="blogNestedMappingMap" type="org.example.model.Blog" autoMapping="true">
2 <id column="id" property="id"/>
3 <result column="title" property="title"/>
4 <!-- <result column="body" property="body"/>-->
5 <collection property="comments" ofType="org.example.model.Comment" columnPrefix="comment_">
6 <id column="id" property="id"/>
7 <result column="blog_id" property="blogId"/>
8 <result column="content" property="content"/>
9 <association property="blog" resultMap="blogNestedMappingMap"/>
10 </collection>
11</resultMap>
12
13<!-- 据ID查询博客 嵌套映射comments-->
14<select id="findByIdNestedMappingComments" resultMap="blogNestedMappingMap">
15 SELECT blog.id, blog.title, blog.body, comment.id comment_id, comment.blog_id comment_blog_id, comment.content comment_content
16 FROM blog
17 LEFT JOIN comment ON blog.id = comment.blog_id
18 WHERE blog.id = #{id}
19</select>
20
在 DefaultResultSetHandler 的 handleRowValues 打上断点,开始进行跟踪。如果存在嵌套结果集映射,则调用 handleRowValuesForNestedResultMap
处理嵌套结果映射,否则进行之前讲解的简单结果映射。
131public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
2 // 判断是否存在嵌套结果映射
3 if (resultMap.hasNestedResultMaps()) {
4 ensureNoRowBounds();
5 checkResultHandler();
6 // 处理嵌套映射
7 handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
8 } else {
9 // 处理简单映射
10 handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
11 }
12}
13
处理嵌套结果映射时,对照流程图可以看到,首先创建RowKey,尝试从暂存区nestedResultObjects
读取未映射完成的对象partialObject
(如暂未映射comments的Blog对象)。把该对象传给getRowValue
继续进行映射,映射完成后继续把该对象保存起来(storeObject)。
471private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
2 final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
3 ResultSet resultSet = rsw.getResultSet();
4
5 // 根据 RowBounds 定位到指定行记录
6 skipRows(resultSet, rowBounds);
7
8 Object rowValue = previousRowValue;
9
10 // 遍历所需解析的结果行
11 while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
12 final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
13
14 // 1. 创建RowKey
15 final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
16
17 // 2. 从暂存区读取RowKey对应的主对象(为null表示需创建新的主对象)
18 Object partialObject = nestedResultObjects.get(rowKey);
19
20 // 判断结果集是否有序
21 // 如果有序,则一个对象的所有成员数据都出现在连续的行,不会跳行,该对象解析完成后,可以清空暂存区。
22 // 如果无序,则暂存区必须始终保存所有正在解析的对象,防止后面的行出现新成员数据。
23 if (mappedStatement.isResultOrdered()) {
24 if (partialObject == null && rowValue != null) {
25 nestedResultObjects.clear();
26 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
27 }
28 rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
29 } else {
30 // 3. 解析当前行(有两种情形:1. 如果主对象不存在,则需创建主对象,映射主对象属性,再映射嵌套属性。2. 如果主对象存在,直接映射嵌套属性即可)
31 rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
32
33 // 4. 保存结果对象(仅在创建新的主对象时)
34 if (partialObject == null) {
35 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
36 }
37 }
38 }
39
40 if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
41 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
42 previousRowValue = null;
43 } else if (rowValue != null) {
44 previousRowValue = rowValue;
45 }
46}
47
在getRowValue
中(嵌套映射的重载形式),如果 partialObject != null,也就是根据RowKey查找到了对象,则直接进行嵌套属性映射(一般在一对多映射的子属性第二次及以上映射)。
如果在暂存区没有找到映射的对象,则先创建对象,进行自动映射和手动映射后再进行嵌套属性映射。
491private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
2 final String resultMapId = resultMap.getId();
3 Object rowValue = partialObject;
4
5 /* 主对象已存在,仅映射嵌套的属性 */
6 if (rowValue != null) {
7 final MetaObject metaObject = configuration.newMetaObject(rowValue);
8
9 // 映射嵌套的属性。注意:映射前保存当前对象到祖宗对象集(ancestorObjects)中,后面处理嵌套映射时需要
10 putAncestor(rowValue, resultMapId);
11 applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
12 ancestorObjects.remove(resultMapId);
13
14 /* 创建新的主对象,先映射主对象属性,再映射嵌套的属性 */
15 } else {
16 final ResultLoaderMap lazyLoader = new ResultLoaderMap();
17
18 // 1. 创建主对象
19 rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
20 if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
21 final MetaObject metaObject = configuration.newMetaObject(rowValue);
22 boolean foundValues = this.useConstructorMappings;
23
24 // 2. 自动属性映射
25 if (shouldApplyAutomaticMappings(resultMap, true)) {
26 foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
27 }
28
29 // 3. 手工配置属性映射
30 foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
31
32 // 4. 嵌套属性映射
33 putAncestor(rowValue, resultMapId);
34 foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
35 ancestorObjects.remove(resultMapId);
36
37 // 处理属性值为空是否返回空对象的逻辑
38 foundValues = lazyLoader.size() > 0 || foundValues;
39 rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
40 }
41
42 // 将新创建的主对象存入暂存区,后面就不需要再创建和映射主对象了。
43 if (combinedKey != CacheKey.NULL_CACHE_KEY) {
44 nestedResultObjects.put(combinedKey, rowValue);
45 }
46 }
47 return rowValue;
48}
49
在applyNestedResultMappings
中映射嵌套属性时,又回到了流程图中创建RowKey的逻辑,不过这次的Key是combinedKey
。
591private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
2 boolean foundValues = false;
3 // 遍历所有映射配置
4 for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
5
6 // 获取嵌套映射ID。如果存在,则进行映射。
7 final String nestedResultMapId = resultMapping.getNestedResultMapId();
8 if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
9
10 try {
11 // 获取列前缀和嵌套映射配置
12 final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
13 final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
14
15 // 处理循环嵌套映射问题(如果nestedResultMapId与祖宗的某个ResultMapId一致,则出现了循环,就不要创建新的主对象了,直接链接到祖宗对象)
16 if (resultMapping.getColumnPrefix() == null) {
17 // try to fill circular reference only when columnPrefix
18 // is not specified for the nested result map (issue #215)
19 Object ancestorObject = ancestorObjects.get(nestedResultMapId);
20 if (ancestorObject != null) {
21 if (newObject) {
22 linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
23 }
24 continue;
25 }
26 }
27
28 /* 下面对嵌套的属性进行映射, 和 handleRowValuesForNestedResultMap 类似,构成递归*/
29
30 // 1. 创建RowKey(并拼接combinedKey)
31 final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
32 final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
33
34 // 2. 从暂存区读取RowKey对应的主对象(为null表示需创建新的主对象)
35 Object rowValue = nestedResultObjects.get(combinedKey);
36 boolean knownValue = rowValue != null;
37
38 // 3. 判断是否需要为成员数据创建集合
39 instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
40
41 // 4. 映射数据到主对象的单个成员或集合成员(仅在存在任意非空列时)
42 if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
43 // 解析所需的数据(递归,因为嵌套属性可能还有嵌套)
44 rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
45
46 // 将解析出来的数据链接到主对象
47 if (rowValue != null && !knownValue) {
48 linkObjects(metaObject, resultMapping, rowValue);
49 foundValues = true;
50 }
51 }
52 } catch (SQLException e) {
53 throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);
54 }
55 }
56 }
57 return foundValues;
58}
59
在上面源码跟踪的最后一步,发现又回到了创建RowKey的起始位置。观察上面案例,blogNestedMappingMap中映射comments时,coments内部又引用了blogNestedMappingMap进行blog的映射,这样会不会产生循环映射呢?
MyBatis使用ancestorObjects
暂存区解决的循环映射的问题,在进行嵌套属性映射前,以当前resultMapId为key,当前对象为value存入ancestorObjects容器,在嵌套属性映射完成后,再从容器中删除。
而在 applyNestedResultMappings 中,获取嵌套映射MapnestedResultMap
后,先去ancestorObjects中查找,是否与父对象的映射一致,如果是则直接进行linkObjects,不必要再进行combinedKey的创建及后续的getRowValue了。
MyBatis 中更新语句指的是除查询之外的所有语句,包括插入
、删除
、修改
及数据库定义语句(DDL)
等。它们在处理上大同小异,与查询语句相比,最大的区别是查询结果的映射变的非常简单,其次是在缓存方面,更新语句刷新缓存的时机也不同,当然还有其它一些不同点,都将会在这节一一讲解。
首先,我们还是从 MapperMethod 的 execute 方法开始看起,这里根据不同的命令类型,处理参数后路由到 SqlSession 的不同入口。
341// -☆- MapperMethod
2public Object execute(SqlSession sqlSession, Object[] args) {
3 Object result;
4 switch (command.getType()) {
5 // 执行插入语句
6 case INSERT: {
7 Object param = method.convertArgsToSqlCommandParam(args);
8 result = rowCountResult(sqlSession.insert(command.getName(), param));
9 break;
10 }
11 // 执行更新语句
12 case UPDATE: {
13 Object param = method.convertArgsToSqlCommandParam(args);
14 result = rowCountResult(sqlSession.update(command.getName(), param));
15 break;
16 }
17 // 执行删除语句
18 case DELETE: {
19 Object param = method.convertArgsToSqlCommandParam(args);
20 result = rowCountResult(sqlSession.delete(command.getName(), param));
21 break;
22 }
23 case SELECT:
24 // ...
25 break;
26 case FLUSH:
27 // ...
28 break;
29 default:
30 throw new BindingException("Unknown execution method for: " + command.getName());
31 }
32 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {...}
33 return result;
34}
因为三种类型的语句对JDBC来说是不区分的,因此都在内部调用 SqlSession 的 update 方法来进行下一步的处理。在 update 方法中,从全局配置中获取 MappedStatement 后,调用执行器的 update 方法来执行SQL。
221// -☆- DefaultSqlSession
2public int insert(String statement, Object parameter) {
3 return update(statement, parameter);
4}
5
6public int delete(String statement, Object parameter) {
7 return update(statement, parameter);
8}
9
10public int update(String statement, Object parameter) {
11 try {
12 dirty = true;
13 // 获取 MappedStatement
14 MappedStatement ms = configuration.getMappedStatement(statement);
15 // 调用 Executor 的 update 方法
16 return executor.update(ms, wrapCollection(parameter));
17 } catch (Exception e) {
18 throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
19 } finally {
20 ErrorContext.instance().reset();
21 }
22}
如果全局属性 cacheEnabled
开启,则会先进入到执行器的装饰器类 CachingExecutor
,再进入到基类 BaseExecutor
,最后调用子类的具体实现。装饰器类和基类中都仅执行了各自的缓存刷新逻辑,是否刷新取决于具体的配置。
491// -☆- CachingExecutor
2public int update(MappedStatement ms, Object parameterObject) throws SQLException {
3 // 1. 刷新二级缓存
4 flushCacheIfRequired(ms);
5
6 // 2. 委托给被装饰的类执行
7 return delegate.update(ms, parameterObject);
8}
9
10// -☆- BaseExecutor
11public int update(MappedStatement ms, Object parameter) throws SQLException {
12 if (closed) {
13 throw new ExecutorException("Executor was closed.");
14 }
15
16 // 1. 刷新一级缓存
17 clearLocalCache();
18
19 // 2. 调用子类的具体实现
20 return doUpdate(ms, parameter);
21}
22
23
24// ------------------------------------------------------------------
25// *************CachingExecutor 中二级缓存刷新逻辑*********************
26// 当全局缓存开关打开后才会开启二级缓存,应用装饰类
27// 每次执行SQL时是否刷新二级缓存,取决于MS的flushCache属性
28// SELECT命令默认为false,INSERT/UPDATE/DELETE命令默认为true
29 private void flushCacheIfRequired(MappedStatement ms) {
30 Cache cache = ms.getCache();
31 if (cache != null && ms.isFlushCacheRequired()) {
32 tcm.clear(cache);
33 }
34 }
35
36// *********** BaseExecutor 中一级缓存刷新逻辑 ***************
37// UPDATE语句/提交事务/回滚事务始终刷新一级缓存
38// SELECT语句,一般在主查询前,如果配置了 flushCache 属性为true,则会刷新一级缓存。
39// 还有在主查询之后,如果 缓存范围为 STATEMENT,则会刷新一级缓存
40// 注意:子查询始终不会刷新一级缓存。
41查询语句要分缓存作用范围 SESSION 和 STATEMENT
42// SESSION:
43
44 public void clearLocalCache() {
45 if (!closed) {
46 localCache.clear();
47 localOutputParameterCache.clear();
48 }
49 }
下面分析 BaseExecutor
的 doUpdate 方法,该方法是一个抽象方法,默认情况下,使用的实现类是 SimpleExecutor ,这可以通过全局属性配置或在打开会话时进行设置。
151// -☆- SimpleExecutor
2public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
3 Statement stmt = null;
4 try {
5 Configuration configuration = ms.getConfiguration();
6 // 1. 创建 StatementHandler
7 StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
8 // 2. 创建 Statement(包括获取连接与设置参数)
9 stmt = prepareStatement(handler, ms.getStatementLog());
10 // 3. 调用 StatementHandler 的 update 方法
11 return handler.update(stmt);
12 } finally {
13 closeStatement(stmt);
14 }
15}
前两步已经分析过,这里就不重复分析了。下面分析 PreparedStatementHandler 的 update 方法。
151// -☆- PreparedStatementHandler
2public int update(Statement statement) throws SQLException {
3 PreparedStatement ps = (PreparedStatement) statement;
4 // 1. 执行 SQL
5 ps.execute();
6
7 // 2. 从结果集获取受影响行数
8 int rows = ps.getUpdateCount();
9
10 // 3. 获取自增主键的值,并将值填入到参数对象中
11 Object parameterObject = boundSql.getParameterObject();
12 KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
13 keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
14 return rows;
15}
如上,前两步调用 JDBCApi 执行 SQL 和获取更新结果(影响的行数),逻辑非常简单。第三步为自增主键值的回填,实现逻辑封装在 KeyGenerator 的实现类中,下面一起来看看。
KeyGenerator
是一个接口,目前它有三个实现类:
Jdbc3KeyGenerator
:用于获取插入时自增列自动增长生成的数据。
SelectKeyGenerator
:某些数据库不支持自增主键,需要手动填写主键字段,此时需要借助 SelectKeyGenerator 获取主键值。
NoKeyGenerator
:这是一个空实现,没什么可说的。
先来分析 Jdbc3KeyGenerator 的源码,配置可参考本系列文档的基础使用篇。
2441public class Jdbc3KeyGenerator implements KeyGenerator {
2
3 /**
4 * A shared instance.
5 *
6 * @since 3.4.3
7 */
8 public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
9
10 private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. "
11 + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.";
12
13
14 public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
15 // do nothing
16 // 使用数据库生成主键不需要提前查询
17 }
18
19
20 public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
21 // 支持批量生成主键
22 processBatch(ms, stmt, parameter);
23 }
24
25 public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
26 // 1. 获取 MS 的 keyPropertie属性
27 final String[] keyProperties = ms.getKeyProperties();
28 if (keyProperties == null || keyProperties.length == 0) {
29 return;
30 }
31
32 // 2. 获取GK结果集
33 // 使用JDBCApi获取主查询返回的 GeneratedKeys 结果集(简称GK结果集)
34 try (ResultSet rs = stmt.getGeneratedKeys()) {
35 // 获取GK结果集 ResultSet 的元数据
36 final ResultSetMetaData rsmd = rs.getMetaData();
37 // 获取全局配置
38 final Configuration configuration = ms.getConfiguration();
39 if (rsmd.getColumnCount() < keyProperties.length) {
40 // Error?
41 } else {
42 // 3. 从GK结果集取数据回填
43 assignKeys(configuration, rs, rsmd, keyProperties, parameter);
44 }
45 } catch (Exception e) {
46 throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
47 }
48 }
49
50 "unchecked") (
51 private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
52 Object parameter) throws SQLException {
53
54 if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
55 // 1. Multi-param or single param with @Param
56 assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
57 } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
58 && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
59 // 2. Multi-param or single param with @Param in batch operation
60 assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, ((ArrayList<ParamMap<?>>) parameter));
61 } else {
62 // 3. Single param without @Param
63 assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
64 }
65 }
66
67 // Single param without @Param
68 private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
69 String[] keyProperties, Object parameter) throws SQLException {
70 Collection<?> params = collectionize(parameter);
71 if (params.isEmpty()) {
72 return;
73 }
74 List<KeyAssigner> assignerList = new ArrayList<>();
75 for (int i = 0; i < keyProperties.length; i++) {
76 assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
77 }
78 Iterator<?> iterator = params.iterator();
79 while (rs.next()) {
80 if (!iterator.hasNext()) {
81 throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
82 }
83 Object param = iterator.next();
84 assignerList.forEach(x -> x.assign(rs, param));
85 }
86 }
87
88 // Multi-param or single param with @Param in batch operation
89 private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
90 String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
91 Iterator<ParamMap<?>> iterator = paramMapList.iterator();
92 List<KeyAssigner> assignerList = new ArrayList<>();
93 long counter = 0;
94 while (rs.next()) {
95 if (!iterator.hasNext()) {
96 throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
97 }
98 ParamMap<?> paramMap = iterator.next();
99 if (assignerList.isEmpty()) {
100 for (int i = 0; i < keyProperties.length; i++) {
101 assignerList
102 .add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
103 .getValue());
104 }
105 }
106 assignerList.forEach(x -> x.assign(rs, paramMap));
107 counter++;
108 }
109 }
110
111 // Multi-param or single param with @Param
112 private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
113 String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
114 if (paramMap.isEmpty()) {
115 return;
116 }
117 Map<String, Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
118 for (int i = 0; i < keyProperties.length; i++) {
119 Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i],
120 keyProperties, true);
121 Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = assignerMap.computeIfAbsent(entry.getKey(),
122 k -> entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
123 iteratorPair.getValue().add(entry.getValue());
124 }
125 long counter = 0;
126 while (rs.next()) {
127 for (Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
128 if (!pair.getKey().hasNext()) {
129 throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
130 }
131 Object param = pair.getKey().next();
132 pair.getValue().forEach(x -> x.assign(rs, param));
133 }
134 counter++;
135 }
136 }
137
138 private Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd,
139 int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
140 boolean singleParam = paramMap.values().stream().distinct().count() == 1;
141 int firstDot = keyProperty.indexOf('.');
142 if (firstDot == -1) {
143 if (singleParam) {
144 return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
145 }
146 throw new ExecutorException("Could not determine which parameter to assign generated keys to. "
147 + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
148 + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
149 + paramMap.keySet());
150 }
151 String paramName = keyProperty.substring(0, firstDot);
152 if (paramMap.containsKey(paramName)) {
153 String argParamName = omitParamName ? null : paramName;
154 String argKeyProperty = keyProperty.substring(firstDot + 1);
155 return entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
156 } else if (singleParam) {
157 return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
158 } else {
159 throw new ExecutorException("Could not find parameter '" + paramName + "'. "
160 + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
161 + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
162 + paramMap.keySet());
163 }
164 }
165
166 private Entry<String, KeyAssigner> getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd,
167 int columnPosition, Map<String, ?> paramMap, String keyProperty, boolean omitParamName) {
168 // Assume 'keyProperty' to be a property of the single param.
169 String singleParamName = nameOfSingleParam(paramMap);
170 String argParamName = omitParamName ? null : singleParamName;
171 return entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
172 }
173
174 private static String nameOfSingleParam(Map<String, ?> paramMap) {
175 // There is virtually one parameter, so any key works.
176 return paramMap.keySet().iterator().next();
177 }
178
179 private static Collection<?> collectionize(Object param) {
180 if (param instanceof Collection) {
181 return (Collection<?>) param;
182 } else if (param instanceof Object[]) {
183 return Arrays.asList((Object[]) param);
184 } else {
185 return Arrays.asList(param);
186 }
187 }
188
189 private static <K, V> Entry<K, V> entry(K key, V value) {
190 // Replace this with Map.entry(key, value) in Java 9.
191 return new AbstractMap.SimpleImmutableEntry<>(key, value);
192 }
193
194 private class KeyAssigner {
195 private final Configuration configuration;
196 private final ResultSetMetaData rsmd;
197 private final TypeHandlerRegistry typeHandlerRegistry;
198 private final int columnPosition;
199 private final String paramName;
200 private final String propertyName;
201 private TypeHandler<?> typeHandler;
202
203 protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
204 String propertyName) {
205 super();
206 this.configuration = configuration;
207 this.rsmd = rsmd;
208 this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
209 this.columnPosition = columnPosition;
210 this.paramName = paramName;
211 this.propertyName = propertyName;
212 }
213
214 protected void assign(ResultSet rs, Object param) {
215 if (paramName != null) {
216 // If paramName is set, param is ParamMap
217 param = ((ParamMap<?>) param).get(paramName);
218 }
219 MetaObject metaParam = configuration.newMetaObject(param);
220 try {
221 if (typeHandler == null) {
222 if (metaParam.hasSetter(propertyName)) {
223 Class<?> propertyType = metaParam.getSetterType(propertyName);
224 typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
225 JdbcType.forCode(rsmd.getColumnType(columnPosition)));
226 } else {
227 throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
228 + metaParam.getOriginalObject().getClass().getName() + "'.");
229 }
230 }
231 if (typeHandler == null) {
232 // Error?
233 } else {
234 Object value = typeHandler.getResult(rs, columnPosition);
235 metaParam.setValue(propertyName, value);
236 }
237 } catch (SQLException e) {
238 throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
239 e);
240 }
241 }
242 }
243}
244
再来看看 SelectKeyGenerator 的源码,在执行更新语句前事先查询出Key,或在更新语句执行完后回填key。
1091public class SelectKeyGenerator implements KeyGenerator {
2
3 public static final String SELECT_KEY_SUFFIX = "!selectKey";
4 private final boolean executeBefore;
5 private final MappedStatement keyStatement;
6
7 public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
8 this.executeBefore = executeBefore;
9 this.keyStatement = keyStatement;
10 }
11
12
13 public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
14 // 在 BaseStatementHandler 的构造器中调用
15 // 如果是“执行前调用”则执行SelectKey逻辑
16 if (executeBefore) {
17 processGeneratedKeys(executor, ms, parameter);
18 }
19 }
20
21
22 public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
23 // 在 Statement 执行SQL语句之后调用
24 // 如果是“执行后调用”则执行SelectKey逻辑
25 if (!executeBefore) {
26 processGeneratedKeys(executor, ms, parameter);
27 }
28 }
29
30 private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
31 try {
32 if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
33
34 // 1, 获取MS的 keyProperty 属性、全局配置,并为实参创建MetaObject对象
35 String[] keyProperties = keyStatement.getKeyProperties();
36 final Configuration configuration = ms.getConfiguration();
37 final MetaObject metaParam = configuration.newMetaObject(parameter);
38
39
40 if (keyProperties != null) {
41 // 2. 创建执行器,执行SQL语句。Do not close keyExecutor. The transaction will be closed by parent executor.
42 Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
43 List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
44
45 // 3. 获取 SelectKey 查询结果,并创建MetaObject对象
46 if (values.size() == 0) {
47 throw new ExecutorException("SelectKey returned no data.");
48 } else if (values.size() > 1) {
49 throw new ExecutorException("SelectKey returned more than one value.");
50 } else {
51 MetaObject metaResult = configuration.newMetaObject(values.get(0));
52
53 // 4. 设置 SelectKey 查询结果到主查询结果中
54 if (keyProperties.length == 1) {
55 // 4.1 映射到单个属性
56 if (metaResult.hasGetter(keyProperties[0])) {
57 // SelectKey 查询结果有多列,取对应列
58 setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
59 } else {
60 // SelectKey 查询结果刚好是需要的列
61 // no getter for the property - maybe just a single value object.so try that
62 setValue(metaParam, keyProperties[0], values.get(0));
63 }
64 } else {
65 // 4.2 映射到多个属性
66 handleMultipleProperties(keyProperties, metaParam, metaResult);
67 }
68 }
69 }
70 }
71 } catch (ExecutorException e) {
72 throw e;
73 } catch (Exception e) {
74 throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
75 }
76 }
77
78 private void handleMultipleProperties(String[] keyProperties,
79 MetaObject metaParam, MetaObject metaResult) {
80 // 1. 获取 MS 的 keyColumn 属性
81 String[] keyColumns = keyStatement.getKeyColumns();
82
83 if (keyColumns == null || keyColumns.length == 0) {
84 // 2.1 keyColumn 属性未配置,则用 keyProperty 从结果集取值映射
85 // no key columns specified, just use the property names
86 for (String keyProperty : keyProperties) {
87 setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));
88 }
89 } else {
90 //2.2 keyColumn 属性配置了,进行对应位置映射
91 if (keyColumns.length != keyProperties.length) {
92 throw new ExecutorException("If SelectKey has key columns, the number must match the number of key properties.");
93 }
94 for (int i = 0; i < keyProperties.length; i++) {
95 setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));
96 }
97 }
98 }
99
100 private void setValue(MetaObject metaParam, String property, Object value) {
101 if (metaParam.hasSetter(property)) {
102 // 使用MetaObject工具类设置属性
103 metaParam.setValue(property, value);
104 } else {
105 throw new ExecutorException("No setter found for the keyProperty '" + property + "' in " + metaParam.getOriginalObject().getClass().getName() + ".");
106 }
107 }
108}
109
更新语句的执行结果是一个整型值,表示本次更新所影响的行数,处理逻辑非常简单。
211// -☆- MapperMethod
2private Object rowCountResult(int rowCount) {
3 final Object result;
4
5 if (method.returnsVoid()) {
6 // 方法返回类型为 void,则不用返回结果,这里将结果置空
7 result = null;
8 } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
9 // 方法返回类型为 Integer 或 int,直接赋值返回即可
10 result = rowCount;
11 } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
12 // 如果返回值类型为 Long 或者 long,这里强转一下即可
13 result = (long) rowCount;
14 } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
15 // 方法返回类型为布尔类型,若 rowCount > 0,则返回 ture,否则返回 false
16 result = rowCount > 0;
17 } else {
18 throw new BindingException(...);
19 }
20 return result;
21}
MyBatis 支持三种类型的数据源配置,分别是UNPOOLED
、POOLED
和 JNDI
。其中UNPOOED 是一种无连接缓存的数据源实现,每次都向数据库获取新连接。而 POOLED 在 UNPOOLED 的基础上加入了连接池技术,获取连接时更加高效。另外,为了能够在 EJB 或应用服务器上运行,还引入了 JNDI 类型的配置,但使用较少,稍作了解即可。
MyBatis 在解析 environment 节点时,会一并解析内嵌的 dataSource 节点,根据不同类型的配置,创建不同类型的数据源工厂。
191 private DataSourceFactory dataSourceElement(XNode context) throws Exception {
2 if (context != null) {
3 // 获取数据源类型
4 String type = context.getStringAttribute("type");
5
6 // 获取数据源属性
7 Properties props = context.getChildrenAsProperties();
8
9 // 创建数据源工厂类
10 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
11
12 // 设置数据源的属性
13 factory.setProperties(props);
14
15 // 返回数据源工厂(后续会使用该工厂获取数据源并构建 Environment 对象,设置到 configuration 中)
16 return factory;
17 }
18 throw new BuilderException("Environment declaration requires a DataSourceFactory.");
19 }
如果type属性配置的类型是 UNPOOLED ,则会创建 UnpooledDataSourceFactory ,下面来看看它的源码。
731public class UnpooledDataSourceFactory implements DataSourceFactory {
2
3 private static final String DRIVER_PROPERTY_PREFIX = "driver.";
4 private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();
5
6 protected DataSource dataSource;
7
8 public UnpooledDataSourceFactory() {
9 // 创建一个无连接池的数据源实现
10 this.dataSource = new UnpooledDataSource();
11 }
12
13
14 public void setProperties(Properties properties) {
15 Properties driverProperties = new Properties();
16
17 // 为 dataSource 创建元信息对象
18 MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
19
20 // 遍历子节点配置的属性
21 for (Object key : properties.keySet()) {
22 // 获取属性名
23 String propertyName = (String) key;
24
25 // -1 驱动的集合属性:以 driver. 开头,可以配置多个
26 if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
27 // 获取属性值,先存到 driverProperties 中
28 String value = properties.getProperty(propertyName);
29 driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
30
31 // -2 普通属性:检查是否有 Setter 方法
32 } else if (metaDataSource.hasSetter(propertyName)) {
33 // 获取属性值并进行类型转换
34 String value = (String) properties.get(propertyName);
35 Object convertedValue = convertValue(metaDataSource, propertyName, value);
36
37 // 通过工具类设置到数据源实例中
38 metaDataSource.setValue(propertyName, convertedValue);
39
40 // -3 无法设置的属性,直接报错
41 } else {
42 throw new DataSourceException("Unknown DataSource property: " + propertyName);
43 }
44 }
45
46 // 设置数据源的驱动属性(集合类型)
47 if (driverProperties.size() > 0) {
48 metaDataSource.setValue("driverProperties", driverProperties);
49 }
50 }
51
52
53 public DataSource getDataSource() {
54 return dataSource;
55 }
56
57 private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
58 Object convertedValue = value;
59 // 获取属性对应的Setter方法类型
60 Class<?> targetType = metaDataSource.getSetterType(propertyName);
61
62 // 强转为对应类型
63 if (targetType == Integer.class || targetType == int.class) {
64 convertedValue = Integer.valueOf(value);
65 } else if (targetType == Long.class || targetType == long.class) {
66 convertedValue = Long.valueOf(value);
67 } else if (targetType == Boolean.class || targetType == boolean.class) {
68 convertedValue = Boolean.valueOf(value);
69 }
70 return convertedValue;
71 }
72
73}
如果type属性配置的类型是 POOLED,则会创建 PooledDataSourceFactory。PooledDataSourceFactory 继承自 UnpooledDataSourceFactory,复用了父类的逻辑,因此它的实现很简单。
71public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
2
3 public PooledDataSourceFactory() {
4 // 创建支持连接池的数据源实例
5 this.dataSource = new PooledDataSource();
6 }
7}
如果type属性配置的类型是 JNDI,则会创建 JndiDataSourceFactory,从容器上下文查找数据源。
671public class JndiDataSourceFactory implements DataSourceFactory {
2
3 public static final String INITIAL_CONTEXT = "initial_context";
4 public static final String DATA_SOURCE = "data_source";
5 public static final String ENV_PREFIX = "env.";
6
7 private DataSource dataSource;
8
9
10 public void setProperties(Properties properties) {
11 try {
12 InitialContext initCtx;
13
14 // 获取 env. 开头的属性
15 Properties env = getEnvProperties(properties);
16
17 // 初始化容器上下文
18 if (env == null) {
19 initCtx = new InitialContext();
20 } else {
21 initCtx = new InitialContext(env);
22 }
23
24 // -1 先使用 initial_context 属性查找 Context,再从 Context 中查找数据源
25 if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE)) {
26 Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
27 dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
28
29 // -2 直接使用 data_source 属性查找容器中的数据源
30 } else if (properties.containsKey(DATA_SOURCE)) {
31 dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
32 }
33
34 } catch (NamingException e) {
35 throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
36 }
37 }
38
39
40 public DataSource getDataSource() {
41 return dataSource;
42 }
43
44 private static Properties getEnvProperties(Properties allProps) {
45 final String PREFIX = ENV_PREFIX;
46 Properties contextProperties = null;
47
48 // 遍历子节点配置的属性
49 for (Entry<Object, Object> entry : allProps.entrySet()) {
50 // 获取key和value
51 String key = (String) entry.getKey();
52 String value = (String) entry.getValue();
53
54 // 如果是以 env. 开头,则设置到 contextProperties 集合
55 if (key.startsWith(PREFIX)) {
56 if (contextProperties == null) {
57 contextProperties = new Properties();
58 }
59 contextProperties.put(key.substring(PREFIX.length()), value);
60 }
61 }
62
63 return contextProperties;
64 }
65
66}
67
UnpooledDataSource 是对 JDBC 获取连接的一层简单封装,不具有池化特性,无需提供连接池功能,因此它的实现非常简单。
851// -☆- UnpooledDataSource
2public Connection getConnection() throws SQLException {
3 return doGetConnection(username, password);
4}
5
6private Connection doGetConnection(String username, String password) throws SQLException {
7 Properties props = new Properties();
8
9 // 把创建数据源工厂时设置的驱动属性添加到 props
10 if (driverProperties != null) {
11 props.putAll(driverProperties);
12 }
13
14 // 添加 user 配置
15 if (username != null) {
16 props.setProperty("user", username);
17 }
18
19 // 添加 password 配置
20 if (password != null) {
21 props.setProperty("password", password);
22 }
23
24 // 调用重载方法
25 return doGetConnection(props);
26}
27
28private Connection doGetConnection(Properties properties) throws SQLException {
29 // 1. 初始化驱动
30 initializeDriver();
31
32 // 2. 获取连接
33 Connection connection = DriverManager.getConnection(url, properties);
34
35 // 3. 配置连接,包括自动提交以及事务等级
36 configureConnection(connection);
37 return connection;
38}
39
40private synchronized void initializeDriver() throws SQLException {
41 // 检测缓存中是否包含了与 driver 对应的驱动实例
42 if (!registeredDrivers.containsKey(driver)) {
43 Class<?> driverType;
44 try {
45 // 1. 获取驱动类型
46 if (driverClassLoader != null) {
47 // 使用 driverClassLoader 加载驱动
48 driverType = Class.forName(driver, true, driverClassLoader);
49 } else {
50 // 通过其他 ClassLoader 加载驱动
51 driverType = Resources.classForName(driver);
52 }
53
54 // 2. 通过反射创建驱动实例
55 Driver driverInstance = (Driver) driverType.newInstance();
56
57 /*
58 * 3. 注册驱动,注意这里是将 Driver 代理类 DriverProxy 对象注册到 DriverManager 中的,
59 * 而非 Driver 对象本身。DriverProxy 中并没什么特别的逻辑,就不分析。
60 */
61 DriverManager.registerDriver(new DriverProxy(driverInstance));
62
63 // 缓存驱动类名和实例
64 registeredDrivers.put(driver, driverInstance);
65 } catch (Exception e) {
66 throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
67 }
68 }
69}
70
71private void configureConnection(Connection conn) throws SQLException {
72 if (defaultNetworkTimeout != null) {
73 conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
74 }
75
76 if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
77 // 设置自动提交
78 conn.setAutoCommit(autoCommit);
79 }
80
81 if (defaultTransactionIsolationLevel != null) {
82 // 设置事务隔离级别
83 conn.setTransactionIsolation(defaultTransactionIsolationLevel);
84 }
85}
如上,将一些配置信息放入到 Properties 对象中,然后将数据库连接和 Properties 对象传给 DriverManager 的 getConnection 方法即可获取到数据库连接。
PooledDataSource 是一个支持连接池的数据源实现,从性能上来说,要优于 UnpooledDataSource。
为了实现连接池的功能,PooledDataSource 抽象了两个辅助类 PoolState
和 PooledConnection
。PoolState 用于记录连接池运行时的状态,比如连接获取次数,无效连接数量等。除此之外,还定义了两个 PooledConnection 集合,分别用于存储空闲连接和活跃连接。
271public class PoolState {
2 protected PooledDataSource dataSource;
3
4 // 空闲连接列表
5 protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
6 // 活跃连接列表
7 protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
8
9 // 从连接池中获取连接的次数
10 protected long requestCount = 0;
11 // 请求连接总耗时(单位:毫秒)
12 protected long accumulatedRequestTime = 0;
13 // 连接执行时间总耗时
14 protected long accumulatedCheckoutTime = 0;
15 // 执行时间超时的连接数
16 protected long claimedOverdueConnectionCount = 0;
17 // 超时时间累加值
18 protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
19 // 等待时间累加值
20 protected long accumulatedWaitTime = 0;
21 // 等待次数
22 protected long hadToWaitCount = 0;
23 // 无效连接数
24 protected long badConnectionCount = 0;
25
26 // ...
27}
PooledConnection 内部包含一个真实的连接和一个 Connection 的代理,代理的拦截逻辑为 PooledConnection 的 invoke 方法,后面将会讲解。除此之外,其内部也定义了一些字段,用于记录数据库连接的一些运行时状态。
401class PooledConnection implements InvocationHandler {
2 private static final String CLOSE = "close";
3 private static final Class<?>[] IFACES = new Class<?>[]{Connection.class};
4
5 private final int hashCode;
6 private final PooledDataSource dataSource;
7
8 // 真实的数据库连接
9 private final Connection realConnection;
10 // 数据库连接代理
11 private final Connection proxyConnection;
12
13 // 从连接池中取出连接时的时间戳
14 private long checkoutTimestamp;
15 // 数据库连接创建时间
16 private long createdTimestamp;
17 // 数据库连接最后使用时间
18 private long lastUsedTimestamp;
19 // connectionTypeCode = (url + username + password).hashCode()
20 private int connectionTypeCode;
21 // 表示连接是否有效
22 private boolean valid;
23
24 public PooledConnection(Connection connection, PooledDataSource dataSource) {
25 this.hashCode = connection.hashCode();
26 this.realConnection = connection;
27 this.dataSource = dataSource;
28 this.createdTimestamp = System.currentTimeMillis();
29 this.lastUsedTimestamp = System.currentTimeMillis();
30 this.valid = true;
31
32 // 创建 Connection 的代理类对象
33 this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
34 }
35
36
37 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {...}
38
39 //...
40}
PooledDataSource 会对数据库连接进行缓存,获取连接时可能会遇到多种情况,请看图。
下面我们深入到源码中一探究竟。
1201public Connection getConnection() throws SQLException {
2 // 返回 Connection 的代理对象
3 return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
4}
5
6private PooledConnection popConnection(String username, String password) throws SQLException {
7 boolean countedWait = false;
8 PooledConnection conn = null;
9 long t = System.currentTimeMillis();
10 int localBadConnectionCount = 0;
11
12 while (conn == null) {
13 synchronized (state) {
14 // 检测空闲连接集合(idleConnections)是否为空
15 if (!state.idleConnections.isEmpty()) {
16 // idleConnections 不为空,表示有空闲连接可以使用
17 conn = state.idleConnections.remove(0);
18 } else {
19 /*
20 * 暂无空闲连接可用,但如果活跃连接数还未超出限制
21 *(poolMaximumActiveConnections),则可创建新的连接
22 */
23 if (state.activeConnections.size() < poolMaximumActiveConnections) {
24 // 创建新连接
25 conn = new PooledConnection(dataSource.getConnection(), this);
26
27 } else { // 连接池已满,不能创建新连接
28 // 取出运行时间最长的连接
29 PooledConnection oldestActiveConnection = state.activeConnections.get(0);
30 // 获取运行时长
31 long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
32 // 检测运行时长是否超出限制,即超时
33 if (longestCheckoutTime > poolMaximumCheckoutTime) {
34 // 累加超时相关的统计字段
35 state.claimedOverdueConnectionCount++;
36 state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
37 state.accumulatedCheckoutTime += longestCheckoutTime;
38
39 // 从活跃连接集合中移除超时连接
40 state.activeConnections.remove(oldestActiveConnection);
41 // 若连接未设置自动提交,此处进行回滚操作
42 if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
43 try {
44 oldestActiveConnection.getRealConnection().rollback();
45 } catch (SQLException e) {...}
46 }
47 /*
48 * 创建一个新的 PooledConnection,注意,
49 * 此处复用 oldestActiveConnection 的 realConnection 变量
50 */
51 conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
52 /*
53 * 复用 oldestActiveConnection 的一些信息,注意 PooledConnection 中的
54 * createdTimestamp 用于记录 Connection 的创建时间,而非 PooledConnection
55 * 的创建时间。所以这里要复用原连接的时间信息。
56 */
57 conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
58 conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
59
60 // 设置连接为无效状态
61 oldestActiveConnection.invalidate();
62
63 } else { // 运行时间最长的连接并未超时
64 try {
65 if (!countedWait) {
66 state.hadToWaitCount++;
67 countedWait = true;
68 }
69 long wt = System.currentTimeMillis();
70 // 当前线程进入等待状态
71 state.wait(poolTimeToWait);
72 state.accumulatedWaitTime += System.currentTimeMillis() - wt;
73 } catch (InterruptedException e) {
74 break;
75 }
76 }
77 }
78 }
79 if (conn != null) {
80 /*
81 * 检测连接是否有效,isValid 方法除了会检测 valid 是否为 true,
82 * 还会通过 PooledConnection 的 pingConnection 方法执行 SQL 语句,
83 * 检测连接是否可用。pingConnection 方法的逻辑不复杂,大家可以自行分析。
84 * 另外,官方文档在介绍 POOLED 类型数据源时,也介绍了连接有效性检测方面的
85 * 属性,有三个:poolPingQuery,poolPingEnabled 和
86 * poolPingConnectionsNotUsedFor。关于这三个属性,大家可以查阅官方文档
87 */
88 if (conn.isValid()) {
89 if (!conn.getRealConnection().getAutoCommit()) {
90 // 进行回滚操作
91 conn.getRealConnection().rollback();
92 }
93 conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
94 // 设置统计字段
95 conn.setCheckoutTimestamp(System.currentTimeMillis());
96 conn.setLastUsedTimestamp(System.currentTimeMillis());
97 state.activeConnections.add(conn);
98 state.requestCount++;
99 state.accumulatedRequestTime += System.currentTimeMillis() - t;
100 } else {
101 // 连接无效,此时累加无效连接相关的统计字段
102 state.badConnectionCount++;
103 localBadConnectionCount++;
104 conn = null;
105 if (localBadConnectionCount > (poolMaximumIdleConnections
106 + poolMaximumLocalBadConnectionTolerance)) {
107 throw new SQLException(...);
108 }
109 }
110 }
111 }
112
113 }
114
115 if (conn == null) {
116 throw new SQLException(...);
117 }
118
119 return conn;
120}
相比于获取连接,回收连接的逻辑要简单的多。回收连接成功与否只取决于空闲连接集合的状态,所需处理情况很少,因此比较简单。
441protected void pushConnection(PooledConnection conn) throws SQLException {
2 synchronized (state) {
3 // 从活跃连接池中移除连接
4 state.activeConnections.remove(conn);
5 if (conn.isValid()) {
6 // 空闲连接集合未满
7 if (state.idleConnections.size() < poolMaximumIdleConnections
8 && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
9 state.accumulatedCheckoutTime += conn.getCheckoutTime();
10
11 // 回滚未提交的事务
12 if (!conn.getRealConnection().getAutoCommit()) {
13 conn.getRealConnection().rollback();
14 }
15
16 // 创建新的 PooledConnection
17 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
18 state.idleConnections.add(newConn);
19 // 复用时间信息
20 newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
21 newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
22
23 // 将原连接置为无效状态
24 conn.invalidate();
25
26 // 通知等待的线程
27 state.notifyAll();
28
29 } else { // 空闲连接集合已满
30 state.accumulatedCheckoutTime += conn.getCheckoutTime();
31 // 回滚未提交的事务
32 if (!conn.getRealConnection().getAutoCommit()) {
33 conn.getRealConnection().rollback();
34 }
35
36 // 关闭数据库连接
37 conn.getRealConnection().close();
38 conn.invalidate();
39 }
40 } else {
41 state.badConnectionCount++;
42 }
43 }
44}
上面代码首先将连接从活跃连接集合中移除,然后再根据空闲集合是否有空闲空间进行后续处理。如果空闲集合未满,此时复用原连接的字段信息创建新的连接,并将其放入空闲集合中即可。若空闲集合已满,此时无需回收连接,直接关闭即可。
我们知道获取连接的方法 popConnection 是由 getConnection 方法调用的,那回收连接的方法 pushConnection 是由谁调用的呢?答案是 PooledConnection 中的代理逻辑。相关代码如下:
221// -☆- PooledConnection
2public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
3 String methodName = method.getName();
4 // 检测 close 方法是否被调用,若被调用则拦截之
5 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
6 // 将回收连接中,而不是直接将连接关闭
7 dataSource.pushConnection(this);
8 return null;
9 } else {
10 try {
11 // 检查连接是否有效(注意:执行Object中定义的方法不检查,如toString不应该报错)
12 if (!Object.class.equals(method.getDeclaringClass())) {
13 checkConnection();
14 }
15
16 // 调用真实连接的目标方法
17 return method.invoke(realConnection, args);
18 } catch (Throwable t) {
19 throw ExceptionUtil.unwrapThrowable(t);
20 }
21 }
22}
代理对象中的方法被调用时,如果判断是 close 方法,那么MyBatis 并不会直接调用真实连接的 close 方法关闭连接,而是调用 pushConnection 方法回收连接。同时会唤醒处于睡眠中的线程,使其恢复运行。
为了缓解数据库查询压力,提高查询性能,MyBatis提供了一级缓存和二级缓存。
一级缓存的结构非常简单,仅为一个基于HashMap 的缓存类 PerpetualCache
,直接定义在BaseExecutor 中,没有附加任何的装饰器。
651// 一个基于HashMap的缓存实现
2public class PerpetualCache implements Cache {
3 private final String id;
4 private Map<Object, Object> cache = new HashMap<>();
5
6 public PerpetualCache(String id) {
7 this.id = id;
8 }
9
10
11 public String getId() {
12 return id;
13 }
14
15
16 public int getSize() {
17 return cache.size();
18 }
19
20
21 public void putObject(Object key, Object value) {
22 cache.put(key, value);
23 }
24
25
26 public Object getObject(Object key) {
27 return cache.get(key);
28 }
29
30
31 public Object removeObject(Object key) {
32 return cache.remove(key);
33 }
34
35
36 public void clear() {
37 cache.clear();
38 }
39
40
41 public boolean equals(Object o) {
42 if (getId() == null) {
43 throw new CacheException("Cache instances require an ID.");
44 }
45 if (this == o) {
46 return true;
47 }
48 if (!(o instanceof Cache)) {
49 return false;
50 }
51
52 Cache otherCache = (Cache) o;
53 return getId().equals(otherCache.getId());
54 }
55
56
57 public int hashCode() {
58 if (getId() == null) {
59 throw new CacheException("Cache instances require an ID.");
60 }
61 return getId().hashCode();
62 }
63
64}
65
为什么是基于 HashMap 而非ConcurrentHashMap?因为一级缓存是定义在执行器中的,而执行器是与会话绑定的,不存在并发情形。
首先,在创建执行器时,会初始化内部的一级缓存,名称固定为 LocalCache。
91public abstract class BaseExecutor implements Executor {
2 protected PerpetualCache localCache;
3 // 省略其他字段
4
5 protected BaseExecutor(Configuration configuration, Transaction transaction) {
6 this.localCache = new PerpetualCache("LocalCache");
7 // 省略其他字段初始化方法
8 }
9}
后续在每次查询前,都会去执行 BaseExecutor 中的一级缓存逻辑,查询一级缓存。
571
2public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
3 // 获取 BoundSql
4 BoundSql boundSql = ms.getBoundSql(parameter);
5
6 // 创建缓存Key
7 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
8
9 // 调用重载函数
10 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
11}
12
13
14public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
15 ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
16 if (closed) {
17 throw new ExecutorException("Executor was closed.");
18 }
19
20 // 在主查询前,如果需要清空一级缓存,则先清空
21 if (queryStack == 0 && ms.isFlushCacheRequired()) {
22 clearLocalCache();
23 }
24 List<E> list;
25 try {
26 queryStack++;
27 // 从一级缓存中获取缓存项
28 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
29 if (list != null) {
30 // 存储过程相关处理逻辑,本文不分析存储过程,故该方法不分析了
31 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
32 } else {
33 // 一级缓存未命中,则从数据库中查询
34 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
35 }
36 } finally {
37 queryStack--;
38 }
39
40 // 再次回到主查询
41 if (queryStack == 0) {
42 // 从一级缓存中延迟加载嵌套查询结果
43 for (DeferredLoad deferredLoad : deferredLoads) {
44 deferredLoad.load();
45 }
46 // 清空当前延迟加载列表
47 // issue #601
48 deferredLoads.clear();
49
50 // STATEMENT范围的一级缓存在每次主查询结束后都要清空
51 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
52 // issue #482
53 clearLocalCache();
54 }
55 }
56 return list;
57}
若一级缓存未命中,BaseExecutor 会调用 queryFromDatabase 查询数据库,并将查询结果写入缓存中。
211private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
2 List<E> list;
3 // 向缓存中存储一个占位符
4 localCache.putObject(key, EXECUTION_PLACEHOLDER);
5 try {
6 // 查询数据库
7 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
8 } finally {
9 // 移除占位符
10 localCache.removeObject(key);
11 }
12 // 存储查询结果
13 localCache.putObject(key, list);
14
15 // 存储过程相关逻辑,忽略
16 if (ms.getStatementType() == StatementType.CALLABLE) {
17 localOutputParameterCache.putObject(key, parameter);
18 }
19 return list;
20}
21
在查询一级缓存之前,MyBatis会优先去找到应用级别的缓存,即二级缓存。二级缓存本质上也是一个 PerpetualCache
,但在此基础上添加了一些装饰器。常用的装饰器如下。
下面写段测试代码看看二级缓存的具体内存结构是哪样的。
161// 缓存组件和结构探究
2
3public void cacheTest01() {
4 // 获取启动时创建的二级缓存
5 Cache cache = configuration.getCache("org.example.dao.UserDao");
6
7 // put一个值
8 User user = new User();
9 user.setId(1);
10 user.setUsername("hyx-Name");
11 cache.putObject("hyx", user);
12
13 // 取出来看看
14 Object value = cache.getObject("hyx");
15 System.out.println(value);
16}
跟踪代码可以看到,二级缓存是通过责任链+装饰器模式构建的一个多功能缓存实现。
下面是MyBatis提供的一些缓存装饰器的简要说明,后续将会对部分装饰器进行详细分析。
缓存装饰器 | 说明 |
---|---|
LruCache | 基于 LinkedHashMap 的 LRU(最近最少使用) 淘汰策略 |
FifoCache | 基于Deque 实现的先进先出的淘汰策略 |
SoftCache/WeakCache | 基于JDK的软引用和弱引用实现的淘汰策略 |
BlockingCache | 基于Java重入锁实现的线程阻塞特性 |
SynchronizedCache | 基于synchronized方法实现的线程同步 |
SerializedCache | 基于ObjectOutputStream和ObjectInputStream的缓存结果序列化和反序列化 |
ScheduledCache | 基于System.currentTimeMillis()的过期清理 |
LoggingCache | 基于请求次数/命中次数的命中率统计 |
TransactionalCache | 用于解决事务安全问题 |
1) LruCache
LruCache 是一种具有 LRU 策略的缓存实现类,该特性是基于 LinkedHashMap 实现的。代码如下。
761public class LruCache implements Cache {
2 private final Cache delegate;
3 private Map<Object, Object> keyMap;
4 private Object eldestKey;
5
6 public LruCache(Cache delegate) {
7 this.delegate = delegate;
8 setSize(1024);
9 }
10
11 public int getSize() {
12 return delegate.getSize();
13 }
14
15 public void setSize(final int size) {
16 // 初始化 keyMap,并设置访问时刷新顺序。(元素被访问后将会被移至末尾)
17 keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
18 private static final long serialVersionUID = 4267176411845948333L;
19
20 // 覆盖 LinkedHashMap 的 removeEldestEntry 方法
21
22 protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
23 boolean tooBig = size() > size;
24 if (tooBig) {
25 // 获取将要被移除缓存项的键值
26 eldestKey = eldest.getKey();
27 }
28 return tooBig;
29 }
30 };
31 }
32
33
34 public void putObject(Object key, Object value) {
35 // 存储缓存项
36 delegate.putObject(key, value);
37
38 // 维护keyMap
39 cycleKeyList(key);
40 }
41
42
43 public Object getObject(Object key) {
44 // 刷新 key 在 keyMap 中的位置
45 keyMap.get(key);
46
47 // 从被装饰类中获取相应缓存项
48 return delegate.getObject(key);
49 }
50
51
52 public Object removeObject(Object key) {
53 // 从被装饰类中移除相应的缓存项
54 return delegate.removeObject(key);
55 }
56
57
58 public void clear() {
59 delegate.clear();
60 keyMap.clear();
61 }
62
63 private void cycleKeyList(Object key) {
64 // 存储 key 到 keyMap 中
65 keyMap.put(key, key);
66
67 // 判断是否需要移除旧值(这个在被覆盖的removeEldestEntry方法中设置)
68 if (eldestKey != null) {
69 // 从被装饰类中移除相应的缓存项
70 delegate.removeObject(eldestKey);
71 eldestKey = null;
72 }
73 }
74
75 // 省略部分代码
76}
如上,LruCache 的 keyMap 属性是实现 LRU 策略的关键,该属性类型继承自 LinkedHashMap,并覆盖了 removeEldestEntry 方法。LinkedHashMap 可保持键值对的插入顺序,当插入一个新的键值对时,LinkedHashMap 内部的 tail 节点会指向最新插入的节点。head 节点则指向第一个被插入的键值对,也就是最久未被访问的那个键值对。默认情况下,LinkedHashMap 仅维护键值对的插入顺序。若要基于 LinkedHashMap 实现 LRU 缓存,还需通过构造方法将 LinkedHashMap 的 accessOrder 属性设为 true,此时 LinkedHashMap 会维护键值对的访问顺序。比如,上面代码中 getObject 方法中执行了这样一句代码 keyMap.get(key),目的是刷新 key 对应的键值对在 LinkedHashMap 的位置。LinkedHashMap 会将 key 对应的键值对移动到链表的尾部,尾部节点表示最久刚被访问过或者插入的节点。除了需将 accessOrder 设为 true,还需覆盖 removeEldestEntry 方法。LinkedHashMap 在插入新的键值对时会调用该方法,以决定是否在插入新的键值对后,移除老的键值对。在上面的代码中,当被装饰类的容量超出了 keyMap 的所规定的容量(由构造方法传入)后,keyMap 会移除最长时间未被访问的键,并保存到 eldestKey 中,然后由 cycleKeyList 方法将 eldestKey 传给被装饰类的 removeObject 方法,移除相应的缓存项目。
2) SynchronizedCache
二级缓存是从 MappedStatement 中获取的,而非由 CachingExecutor 创建,由于 MappedStatement 存在于全局配置中,可被多个 CachingExecutor 获取到,这样就会出现线程安全问题。MyBatis为解决这个问题,在创建二级缓存实例时,应用了一个 SynchronizedCache 装饰器,通过同步方法解决线程安全问题。
501public class SynchronizedCache implements Cache {
2
3 private final Cache delegate;
4
5 public SynchronizedCache(Cache delegate) {
6 this.delegate = delegate;
7 }
8
9
10 public String getId() {
11 return delegate.getId();
12 }
13
14 // 使用同步方法进行同步,下同
15
16 public synchronized int getSize() {
17 return delegate.getSize();
18 }
19
20
21 public synchronized void putObject(Object key, Object object) {
22 delegate.putObject(key, object);
23 }
24
25
26 public synchronized Object getObject(Object key) {
27 return delegate.getObject(key);
28 }
29
30
31 public synchronized Object removeObject(Object key) {
32 return delegate.removeObject(key);
33 }
34
35
36 public synchronized void clear() {
37 delegate.clear();
38 }
39
40
41 public int hashCode() {
42 return delegate.hashCode();
43 }
44
45
46 public boolean equals(Object obj) {
47 return delegate.equals(obj);
48 }
49
50}
3) BlockingCache
BlockingCache 实现了阻塞特性,该特性是基于 Java 重入锁实现的。同一时刻下,BlockingCache 仅允许一个线程访问指定 key 的缓存项,其他线程将会被阻塞住。下面我们来看一下 BlockingCache 的源码。
801public class BlockingCache implements Cache {
2 private long timeout;
3 private final Cache delegate;
4 private final ConcurrentHashMap<Object, ReentrantLock> locks;
5
6 public BlockingCache(Cache delegate) {
7 this.delegate = delegate;
8 this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
9 }
10
11
12 public void putObject(Object key, Object value) {
13 try {
14 // 缓存缓存项
15 delegate.putObject(key, value);
16 } finally {
17 // 释放锁
18 releaseLock(key);
19 }
20 }
21
22
23 public Object getObject(Object key) {
24 // 请求锁
25 acquireLock(key);
26
27 // 查询
28 Object value = delegate.getObject(key);
29
30 // 若缓存命中,则释放锁。需要注意的是,未命中则不释放锁,这个会在分析事务缓存装饰器时讲解
31 if (value != null) {
32 // 释放锁
33 releaseLock(key);
34 }
35 return value;
36 }
37
38
39 public Object removeObject(Object key) {
40 // 释放锁
41 releaseLock(key);
42 return null;
43 }
44
45 private ReentrantLock getLockForKey(Object key) {
46 ReentrantLock lock = new ReentrantLock();
47 // 存储 <key, Lock> 键值对到 locks 中
48 ReentrantLock previous = locks.putIfAbsent(key, lock);
49 return previous == null ? lock : previous;
50 }
51
52 private void acquireLock(Object key) {
53 Lock lock = getLockForKey(key);
54 if (timeout > 0) {
55 try {
56 // 尝试加锁
57 boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
58 if (!acquired) {
59 throw new CacheException("...");
60 }
61 } catch (InterruptedException e) {
62 throw new CacheException("...");
63 }
64 } else {
65 // 加锁
66 lock.lock();
67 }
68 }
69
70 private void releaseLock(Object key) {
71 // 获取与当前 key 对应的锁
72 ReentrantLock lock = locks.get(key);
73 if (lock.isHeldByCurrentThread()) {
74 // 释放锁
75 lock.unlock();
76 }
77 }
78
79 // 省略部分代码
80}
如上,查询缓存时,getObject 方法会先获取与 key 对应的锁,并加锁。若缓存命中,getObject 方法会释放锁,否则将一直锁定。getObject 方法若返回 null,表示缓存未命中。此时 MyBatis 会进行数据库查询,并调用 putObject 方法存储查询结果。同时,putObject 方法会将指定 key 对应的锁进行解锁,这样被阻塞的线程即可恢复运行。
上面的描述有点啰嗦,倒是 BlockingCache 类的注释说到比较简单明了。这里引用一下:
It sets a lock over a cache key when the element is not found in cache. This way, other threads will wait until this element is filled instead of hitting the database.
这段话的意思是,当指定 key 对应元素不存在于缓存中时,BlockingCache 会根据 lock 进行加锁。此时,其他线程将会进入等待状态,直到与 key 对应的元素被填充到缓存中。而不是让所有线程都去访问数据库。
在上面代码中,removeObject 方法的逻辑很奇怪,仅调用了 releaseLock 方法释放锁,却没有调用被装饰类的 removeObject 方法移除指定缓存项。这样做是为什么呢?大家可以先思考,答案将在分析二级缓存的相关逻辑时分析。
在创建执行器时,如果开启了全局缓存配置cacheEnabled,则会对创建的执行器使用CachingExecutor进行装饰。
111 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
2 // ...
3
4 // 应用缓存装饰类
5 if (cacheEnabled) {
6 executor = new CachingExecutor(executor);
7 }
8
9 // ...
10 return executor;
11 }
当我们执行query等方法时,会进入到 CachingExecutor 的相应方法之中。在该方法中,第三步重载的query方法被替换为CachingExecutor中的重载方法,注入了二级缓存逻辑。
111// -☆- CachingExecutor
2
3public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
4 // 获取 BoundSql
5 BoundSql boundSql = ms.getBoundSql(parameterObject);
6 // 创建 CacheKey
7 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
8 // 调用CachingExecutor中的重载方法
9 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
10}
11
从缓存事务管理器中查找二级缓存,如果未找到,再通过delegate调用之前BaseExecutor中被重写的query方法,去一级缓存查找。
311public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
2 throws SQLException {
3 // 从 MappedStatement 中获取 Cache(注意这里的 Cache 并非是在 CachingExecutor 中创建的)
4 Cache cache = ms.getCache();
5
6 // 如果配置文件中没有配置 <cache>,则 cache 为空
7 if (cache != null) {
8 flushCacheIfRequired(ms);
9 // 判断语句开关未被关闭,且未使用自定义结果处理器
10 if (ms.isUseCache() && resultHandler == null) {
11 ensureNoOutParams(ms, boundSql);
12
13 // 访问二级缓存
14 List<E> list = (List<E>) tcm.getObject(cache, key);
15
16 // 缓存未命中
17 if (list == null) {
18 // 向一级缓存或者数据库进行查询
19 list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
20
21 // 缓存查询结果
22 tcm.putObject(cache, key, list);
23 }
24 return list;
25 }
26 }
27
28 // 直接向一级缓存或者数据库进行查询
29 return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
30}
31
前文分析可知,Mybatis通过 SynchronizedCache 装饰器做了同步操作,解决了二级缓存的线程安全问题。但同一个缓存实例依然被多个事务所共享,如下情形依然可能出现脏读。
如上图所示,事务A和事务B分别开启事务;事务A先对记录A进行修改,在未提交事务的情况下又读取记录A,并写入缓存;此时,事务B去查询记录A,直接从缓存中取事务A存入的脏数据,这个数据可能被事务A回滚掉。
要想解决脏读问题,必须将各个事务未提交时的缓存数据单独存放在别处。MyBatis在创建CachingExecutor时初始化了一个事务缓存管理器 TransactionalCacheManager
,其内部通过事务缓存装饰器 TransactionalCache
对事务未提交时的缓存数据进行了暂存。
81public class CachingExecutor implements Executor {
2
3 private final Executor delegate;
4 // 初始化事务缓存管理器
5 private final TransactionalCacheManager tcm = new TransactionalCacheManager();
6
7 // ...
8}
下面是事务缓存管理器的代码,负责对真实缓存实例Cache进行装饰,并维护与事务缓存实例 TransactionalCache 间的映射关系。
411/** 事务缓存管理器 */
2public class TransactionalCacheManager {
3 // Cache 与 TransactionalCache 的映射关系表
4 private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
5
6 public void clear(Cache cache) {
7 // 获取 TransactionalCache 对象,并调用该对象的 clear 方法,下同
8 getTransactionalCache(cache).clear();
9 }
10
11 public Object getObject(Cache cache, CacheKey key) {
12 return getTransactionalCache(cache).getObject(key);
13 }
14
15 public void putObject(Cache cache, CacheKey key, Object value) {
16 getTransactionalCache(cache).putObject(key, value);
17 }
18
19 public void commit() {
20 for (TransactionalCache txCache : transactionalCaches.values()) {
21 txCache.commit();
22 }
23 }
24
25 public void rollback() {
26 for (TransactionalCache txCache : transactionalCaches.values()) {
27 txCache.rollback();
28 }
29 }
30
31 private TransactionalCache getTransactionalCache(Cache cache) {
32 // 从映射表中获取 TransactionalCache
33 TransactionalCache txCache = transactionalCaches.get(cache);
34 if (txCache == null) {
35 // TransactionalCache 也是一种装饰类,为 Cache 增加事务功能
36 txCache = new TransactionalCache(cache);
37 transactionalCaches.put(cache, txCache);
38 }
39 return txCache;
40 }
41}
TransactionalCache是一个对事务中未提交数据进行暂存的缓存装饰类。下面分析一下该类的逻辑。
981public class TransactionalCache implements Cache {
2 private final Cache delegate;
3 // 缓存区清除标记(为true时表示缓存区不可读)
4 private boolean clearOnCommit;
5
6 // 待缓存数据:在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
7 private final Map<Object, Object> entriesToAddOnCommit;
8
9 // 未命中的缓存Key:在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
10 private final Set<Object> entriesMissedInCache;
11
12 // 省略部分代码
13
14
15 public Object getObject(Object key) {
16 // 查询 delegate 所代表的缓存
17 Object object = delegate.getObject(key);
18 if (object == null) {
19 // 缓存未命中,则将 key 存入到 entriesMissedInCache 中
20 entriesMissedInCache.add(key);
21 }
22
23 // 特别注意:查询缓存时是直接查询缓存区,但clear时并未实时清空,就有可能查出应已被清空的缓存,因此需要判断缓存区清除标记
24 if (clearOnCommit) {
25 return null;
26 } else {
27 return object;
28 }
29 }
30
31
32 public void putObject(Object key, Object object) {
33 // 将键值对存入到 entriesToAddOnCommit 中,而非 delegate 缓存中
34 entriesToAddOnCommit.put(key, object);
35 }
36
37
38 public Object removeObject(Object key) {
39 return null;
40 }
41
42
43 public void clear() {
44 clearOnCommit = true;
45 // 清空 entriesToAddOnCommit,但不清空 delegate 缓存
46 entriesToAddOnCommit.clear();
47 }
48
49 public void commit() {
50 // 根据 clearOnCommit 的值决定是否清空 delegate
51 if (clearOnCommit) {
52 delegate.clear();
53 }
54
55 // 刷新未缓存的结果到 delegate 缓存中
56 flushPendingEntries();
57
58 // 重置 entriesToAddOnCommit 和 entriesMissedInCache
59 reset();
60 }
61
62 public void rollback() {
63 unlockMissedEntries();
64 reset();
65 }
66
67 private void reset() {
68 clearOnCommit = false;
69 // 清空集合
70 entriesToAddOnCommit.clear();
71 entriesMissedInCache.clear();
72 }
73
74 private void flushPendingEntries() {
75 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
76 // 将 entriesToAddOnCommit 中的内容转存到 delegate 中
77 delegate.putObject(entry.getKey(), entry.getValue());
78 }
79 for (Object entry : entriesMissedInCache) {
80 if (!entriesToAddOnCommit.containsKey(entry)) {
81 // 存入空值
82 delegate.putObject(entry, null);
83 }
84 }
85 }
86
87 private void unlockMissedEntries() {
88 for (Object entry : entriesMissedInCache) {
89 try {
90 // 调用 removeObject 进行解锁
91 delegate.removeObject(entry);
92 } catch (Exception e) {
93 log.warn("...");
94 }
95 }
96 }
97
98}
缓存实例被 TransactionalCache 装饰后,查询出来的数据不会直接缓存到 delegate 所表示的缓存中,而是先暂存到 entriesToAddOnCommit 集合,当事务提交时,再将事务缓存中的缓存项转存到共享缓存中,这样就不会读取到其它事务未提交的脏数据了。下面看下加入事务缓存后的流程图。
如上图所示,时刻1,事务A和事务B分别开启事务;时刻2,事务 A 和 B 同时查询记录 A。此时共享缓存中还没没有数据,所以两个事务均会向数据库发起查询请求,并将查询结果存储到各自的事务缓存中。时刻3,事务 A 更新记录 A,这里把更新后的记录 A 记为 A′。时刻4,两个事务再次进行查询。此时,事务 A 读取到的记录为修改后的值,而事务 B 读取到的记录仍为原值。时刻5,事务 A 被提交,并将事务缓存 A 中的内容转存到共享缓存中。时刻6,事务 B 再次查询记录 A,由于共享缓存中有相应的数据,所以直接取缓存数据即可。因此得到记录 A′,而非记录 A。但由于事务 A 已经提交,所以事务 B 读取到的记录 A′ 并非是脏数据。
扩展:MyBatis的二级缓存不支持可重复读。
MyBatis 引入事务缓存解决了脏读问题,事务间只能读取到其他事务提交后的内容,这相当于事务隔离级别中的“读已提交(Read Committed)”。但需要注意的时,MyBatis 缓存事务机制只能解决脏读问题,并不能解决“不可重复读”问题。再回到上图,事务 B 在被提交前进行了三次查询。前两次查询得到的结果为记录 A,最后一次查询得到的结果为 A′。最有一次的查询结果与前两次不同,这就会导致“不可重复读”的问题。MyBatis 的缓存事务机制最高只支持“读已提交”,并不能解决“不可重复读”问题。即使数据库使用了更高的隔离级别解决了这个问题,但因 MyBatis 缓存事务机制级别较低。此时仍然会导致“不可重复读”问题的发生,这个在日常开发中需要注意一下。
接下来,我们再来分析下 entriesMissedInCache
集合,这个集合是用于存储未命中缓存的查询请求对应的 CacheKey。单独分析与 entriesMissedInCache 相关的逻辑没什么意义,要搞清 entriesMissedInCache 的实际用途,需要把它和 BlockingCache 的逻辑结合起来进行分析。在 BlockingCache,同一时刻仅允许一个线程通过 getObject 方法查询指定 key 对应的缓存项。如果缓存未命中,getObject 方法不会释放锁,导致其他线程被阻塞住。其他线程要想恢复运行,必须进行解锁,解锁逻辑由 BlockingCache 的 putObject 和 removeObject 方法执行。其中 putObject 会在 TransactionalCache 的 flushPendingEntries 方法中被调用,removeObject 方法则由 TransactionalCache 的 unlockMissedEntries 方法调用。flushPendingEntries 和 unlockMissedEntries 最终都会遍历 entriesMissedInCache 集合,并将集合元素传给 BlockingCache 的相关方法。这样可以解开指定 key 对应的锁,让阻塞线程恢复运行。
由于不同的SQL语句、查询参数以及分页条件都会导致查询结果不一致,因此MyBatis为此创建一个复合键Cacehkey,定义如下。
241public class CacheKey implements Cloneable, Serializable {
2 private static final int DEFAULT_MULTIPLYER = 37;
3 private static final int DEFAULT_HASHCODE = 17;
4
5 // 乘子,默认为37(恒定不变)
6 private final int multiplier;
7 // CacheKey 的 hashCode,综合了各种影响因子
8 private int hashcode;
9 // 校验和
10 private long checksum;
11 // 影响因子个数
12 private int count;
13 // 影响因子集合
14 private List<Object> updateList;
15
16 public CacheKey() {
17 this.hashcode = DEFAULT_HASHCODE;
18 this.multiplier = DEFAULT_MULTIPLYER;
19 this.count = 0;
20 this.updateList = new ArrayList<Object>();
21 }
22
23 // 省略其他方法
24}
提供了 update 方法,用于加入新的影响因子,并进行计算。
151public void update(Object object) {
2 int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
3 // 自增 count
4 count++;
5 // 计算校验和
6 checksum += baseHashCode;
7 // 更新 baseHashCode
8 baseHashCode *= count;
9
10 // 计算 hashCode
11 hashcode = multiplier * hashcode + baseHashCode;
12
13 // 保存影响因子
14 updateList.add(object);
15}
当不断有新的影响因子参与计算时,hashcode 和 checksum 将会变得愈发复杂和随机。这样可降低冲突率,使 CacheKey 可在缓存中更均匀的分布。CacheKey 最终要作为键存入 HashMap,因此它需要覆盖 equals 和 hashCode 方法。下面我们来看一下这两个方法的实现。
391public boolean equals(Object object) {
2 // 检测是否为同一个对象
3 if (this == object) {
4 return true;
5 }
6 // 检测 object 是否为 CacheKey
7 if (!(object instanceof CacheKey)) {
8 return false;
9 }
10 final CacheKey cacheKey = (CacheKey) object;
11
12 // 检测 hashCode 是否相等
13 if (hashcode != cacheKey.hashcode) {
14 return false;
15 }
16 // 检测校验和是否相同
17 if (checksum != cacheKey.checksum) {
18 return false;
19 }
20 // 检测 coutn 是否相同
21 if (count != cacheKey.count) {
22 return false;
23 }
24
25 // 如果上面的检测都通过了,下面分别对每个影响因子进行比较
26 for (int i = 0; i < updateList.size(); i++) {
27 Object thisObject = updateList.get(i);
28 Object thatObject = cacheKey.updateList.get(i);
29 if (!ArrayUtil.equals(thisObject, thatObject)) {
30 return false;
31 }
32 }
33 return true;
34}
35
36public int hashCode() {
37 // 返回 hashcode 变量
38 return hashcode;
39}
equals 方法的检测逻辑比较严格,对 CacheKey 中多个成员变量进行了检测,已保证两者相等。hashCode 方法比较简单,返回 hashcode 变量即可。下面是一个CacehKey 的内存结构。
在从SqlSession进入到执行器时,首先会进入到 BaseExecutor 的 query 方法,创建CacheKey,然后再调用重载方法进行后续处理。如果开启了二级缓存全局配置,则会进入到 CachingExecutor 的重写方法中,不过逻辑基本一致,创建CacheKey时也是直接调用代理对象的createCacheKey方法。(注意区分,CachingExecutor 中后续调用的重载方法query被重写了,植入了二级缓存逻辑)
101
2public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
3 BoundSql boundSql = ms.getBoundSql(parameter);
4
5 // 创建CacheKey
6 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
7
8 // 调用重载方法
9 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
10}
下面我们来看一下 createCacheKey 方法的逻辑:
421public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
2 if (closed) {
3 throw new ExecutorException("Executor was closed.");
4 }
5 // 创建 CacheKey 对象
6 CacheKey cacheKey = new CacheKey();
7 // 将 MappedStatement 的 id 作为影响因子进行计算
8 cacheKey.update(ms.getId());
9 // RowBounds 用于分页查询,下面将它的两个字段作为影响因子进行计算
10 cacheKey.update(rowBounds.getOffset());
11 cacheKey.update(rowBounds.getLimit());
12 // 获取 sql 语句,并进行计算
13 cacheKey.update(boundSql.getSql());
14 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
15 TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
16 for (ParameterMapping parameterMapping : parameterMappings) {
17 if (parameterMapping.getMode() != ParameterMode.OUT) {
18 Object value; // 运行时参数
19 // 当前大段代码用于获取 SQL 中的占位符 #{xxx} 对应的运行时参数,
20 // 前文有类似分析,这里忽略了
21 String propertyName = parameterMapping.getProperty();
22 if (boundSql.hasAdditionalParameter(propertyName)) {
23 value = boundSql.getAdditionalParameter(propertyName);
24 } else if (parameterObject == null) {
25 value = null;
26 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
27 value = parameterObject;
28 } else {
29 MetaObject metaObject = configuration.newMetaObject(parameterObject);
30 value = metaObject.getValue(propertyName);
31 }
32
33 // 让运行时参数参与计算
34 cacheKey.update(value);
35 }
36 }
37 if (configuration.getEnvironment() != null) {
38 // 获取 Environment id 遍历,并让其参与计算
39 cacheKey.update(configuration.getEnvironment().getId());
40 }
41 return cacheKey;
42}
如上,在计算 CacheKey 的过程中,有很多影响因子参与了计算。比如 MappedStatement 的 id 字段,SQL 语句,分页参数,运行时变量,Environment 的 id 字段等。通过让这些影响因子参与计算,可以很好的区分不同查询请求。所以,我们可以简单的把 CacheKey 看做是一个查询请求的 id。有了 CacheKey,我们就可以使用它读写缓存了。
为了增加框架的灵活性,让开发者可以根据实际需求对框架进行扩展,MyBatis 在 Configuration 中对下面四个类进行统一实例化,以植入拦截逻辑。
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
如果我们想要拦截 Executor 的 query 方法,那么可以这样定义插件,首先实现 Interceptor
接口,然后配置插件的拦截点。
101 ({
2 (
3 type = Executor.class,
4 method = "query",
5 args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
6 )
7})
8public class ExamplePlugin implements Interceptor {
9 // 省略逻辑
10}
最后将插件在 mybatis.xml 文件中声明,Mybatis在启动时就会将该插件加入到拦截器链(InterceptorChain
)。由于是在Configuration中统一创建的,因此有机会做动态代理,植入拦截逻辑。下面我们来看看它的源码。
以 Executor 为例,分析 MyBatis 是如何为 Executor 实例植入插件逻辑的。Executor 实例是在开启 SqlSession 时被创建的,因此,下面我们从源头进行分析,先来看一下 SqlSession 开启的过程。
211// -☆- DefaultSqlSessionFactory
2public SqlSession openSession() {
3 return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
4}
5
6private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
7 Transaction tx = null;
8 try {
9 // 依次创建环境、事务工厂、事务、执行器、会话
10 final Environment environment = configuration.getEnvironment();
11 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
12 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
13 final Executor executor = configuration.newExecutor(tx, execType);
14 return new DefaultSqlSession(configuration, executor, autoCommit);
15 } catch (Exception e) {
16 closeTransaction(tx); // may have fetched a connection so lets call close()
17 throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
18 } finally {
19 ErrorContext.instance().reset();
20 }
21}
Executor 的创建过程封装在 Configuration 中,我们跟进去看看看。
261// -☆- Configuration
2public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
3 // 执行器默认为 SIMPLE
4 executorType = executorType == null ? defaultExecutorType : executorType;
5 executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
6
7 Executor executor;
8 if (ExecutorType.BATCH == executorType) {
9 // 创建批处理执行器
10 executor = new BatchExecutor(this, transaction);
11 } else if (ExecutorType.REUSE == executorType) {
12 // 创建复用执行器
13 executor = new ReuseExecutor(this, transaction);
14 } else {
15 // 创建简单执行器
16 executor = new SimpleExecutor(this, transaction);
17 }
18
19 // 应用缓存装饰类
20 if (cacheEnabled) {
21 executor = new CachingExecutor(executor);
22 }
23
24 // 应用拦截器链
25 executor = (Executor) interceptorChain.pluginAll(executor);
26 return executor;
如上,newExecutor 方法在创建好 Executor 实例后,紧接着通过拦截器链 interceptorChain 为 Executor 实例植入代理逻辑。那下面我们看一下 InterceptorChain 的代码是怎样的。
231public class InterceptorChain {
2
3 private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
4
5 public Object pluginAll(Object target) {
6 // 遍历拦截器集合
7 for (Interceptor interceptor : interceptors) {
8 // 调用拦截器的 plugin 方法植入相应的插件逻辑
9 target = interceptor.plugin(target);
10 }
11 return target;
12 }
13
14 /** 添加插件实例到 interceptors 集合中 */
15 public void addInterceptor(Interceptor interceptor) {
16 interceptors.add(interceptor);
17 }
18
19 /** 获取插件列表 */
20 public List<Interceptor> getInterceptors() {
21 return Collections.unmodifiableList(interceptors);
22 }
23}
以上是 InterceptorChain 的全部代码,比较简单。它的 pluginAll 方法会调用具体插件的 plugin 方法植入相应的插件逻辑。如果有多个插件,则会多次调用 plugin 方法,最终生成一个层层嵌套的代理类。形如下面:
当 Executor 的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为 plugin3 → plugin2 → Plugin1 → Executor
。plugin 方法是由具体的插件类实现,不过该方法代码一般比较固定,所以下面找个示例分析一下。
271// -☆- ExamplePlugin
2public Object plugin(Object target) {
3 return Plugin.wrap(target, this);
4}
5
6// -☆- Plugin
7public static Object wrap(Object target, Interceptor interceptor) {
8 /*
9 * 获取插件类 @Signature 注解内容,并生成相应的映射结构。形如下面:
10 * {
11 * Executor.class : [query, update, commit],
12 * ParameterHandler.class : [getParameterObject, setParameters]
13 * }
14 */
15 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
16 Class<?> type = target.getClass();
17 // 获取目标类实现的接口
18 Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
19 if (interfaces.length > 0) {
20 // 通过 JDK 动态代理为目标类生成代理类(InvocationHandler为Plugin自身)
21 return Proxy.newProxyInstance(
22 type.getClassLoader(),
23 interfaces,
24 new Plugin(target, interceptor, signatureMap));
25 }
26 return target;
27}
如上,plugin 方法在内部调用了 Plugin 类的 wrap 方法,用于为目标对象生成代理。Plugin 类实现了 InvocationHandler 接口,因此它可以作为参数传给 Proxy 的 newProxyInstance 方法。接下来,我们来看看插件拦截逻辑是怎样的。
Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
221// -☆- Plugin
2public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
3 try {
4 /*
5 * 获取被拦截方法列表,比如:
6 * signatureMap.get(Executor.class),可能返回 [query, update, commit]
7 */
8 Set<Method> methods = signatureMap.get(method.getDeclaringClass());
9
10 // 检测方法列表是否包含被拦截的方法
11 if (methods != null && methods.contains(method)) {
12
13 // 执行插件逻辑
14 return interceptor.intercept(new Invocation(target, method, args));
15 }
16
17 // 执行被拦截的方法
18 return method.invoke(target, args);
19 } catch (Exception e) {
20 throw ExceptionUtil.unwrapThrowable(e);
21 }
22}
invoke 方法的代码比较少,逻辑不难理解。首先,invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept 中,该方法的参数类型为 Invocation。Invocation 主要用于存储目标类,方法以及方法参数列表。下面简单看一下该类的定义。
191public class Invocation {
2
3 private final Object target;
4 private final Method method;
5 private final Object[] args;
6
7 public Invocation(Object target, Method method, Object[] args) {
8 this.target = target;
9 this.method = method;
10 this.args = args;
11 }
12
13 // 省略部分代码
14
15 public Object proceed() throws InvocationTargetException, IllegalAccessException {
16 // 调用被拦截的方法
17 return method.invoke(target, args);
18 }
19}
关于插件的执行逻辑就分析到这,整个过程不难理解,大家简单看看即可。
为了更好的向大家介绍 MyBatis 的插件机制,下面我将手写一个针对 MySQL 的分页插件。
541 ({
2 (
3 type = Executor.class, // 目标类
4 method = "query", // 目标方法
5 args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
6 )
7})
8public class MySqlPagingPlugin implements Interceptor {
9 private static final Integer MAPPED_STATEMENT_INDEX = 0;
10 private static final Integer PARAMETER_INDEX = 1;
11 private static final Integer ROW_BOUNDS_INDEX = 2;
12
13
14 public Object intercept(Invocation invocation) throws Throwable {
15 Object[] args = invocation.getArgs();
16 RowBounds rb = (RowBounds) args[ROW_BOUNDS_INDEX];
17
18 // 无需分页
19 if (rb == RowBounds.DEFAULT) {
20 return invocation.proceed();
21 }
22
23 // 将原 RowBounds 参数设为 RowBounds.DEFAULT,关闭 MyBatis 内置的分页机制
24 args[ROW_BOUNDS_INDEX] = RowBounds.DEFAULT;
25
26 MappedStatement ms = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
27 BoundSql boundSql = ms.getBoundSql(args[PARAMETER_INDEX]);
28
29 // 获取 SQL 语句,拼接 limit 语句
30 String sql = boundSql.getSql();
31 String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
32 sql = sql + " " + limit;
33
34 // 创建一个 StaticSqlSource,并将拼接好的 sql 传入
35 SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());
36
37 // 通过反射获取并设置 MappedStatement 的 sqlSource 字段(????没问题吗?)
38 Field field = MappedStatement.class.getDeclaredField("sqlSource");
39 field.setAccessible(true);
40 field.set(ms, sqlSource);
41
42 // 执行被拦截方法
43 return invocation.proceed();
44 }
45
46
47 public Object plugin(Object target) {
48 return Plugin.wrap(target, this);
49 }
50
51
52 public void setProperties(Properties properties) {
53 }
54}
为了记录SQL执行日志,MyBatis定义了一套日志打印的接口Log
、日志工厂LogFactory
以及日志相关的异常LogException
,并提供了多种日志实现的适配器,在启动时通过静态代码块来初始化某个具体的实现。
先来看看日志接口Log
,它里面抽象了日志打印的5种方法和2种判断方法,方便于面向接口编程。
101public interface Log {
2 boolean isDebugEnabled();
3 boolean isTraceEnabled();
4 void error(String s, Throwable e);
5 void error(String s);
6 void debug(String s);
7 void trace(String s);
8 void warn(String s);
9}
10
MyBatis为常用的几种日志实现提供了适配器,按选择的先后顺序排列如下:Slf4jImpl
、JakartaCommonsLoggingImpl
、Log4j2Impl
、Log4jImpl
、Jdk14LoggingImpl
、NoLoggingImpl
以及备选的StdOutImpl
,从名称不难看出它们所对应的日志实现。下面以 Slf4jImpl 为例看看日志实现是如何适配到Log接口的。
601// 定义一个 Slf4jImpl 适配器,实现Log接口
2public class Slf4jImpl implements Log {
3 private Log log;
4
5 public Slf4jImpl(String clazz) {
6 // 尝试获取日志实现
7 Logger logger = LoggerFactory.getLogger(clazz);
8
9 // 判断是否为 LocationAwareLogger 的子类
10 if (logger instanceof LocationAwareLogger) {
11 try {
12 // check for slf4j >= 1.6 method signature
13 logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
14 log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
15 return;
16 } catch (SecurityException | NoSuchMethodException e) {
17 // fail-back to Slf4jLoggerImpl
18 }
19 }
20
21 // Logger is not LocationAwareLogger or slf4j version < 1.6
22 log = new Slf4jLoggerImpl(logger);
23 }
24
25
26 public boolean isDebugEnabled() {
27 return log.isDebugEnabled();
28 }
29
30
31 public boolean isTraceEnabled() {
32 return log.isTraceEnabled();
33 }
34
35
36 public void error(String s, Throwable e) {
37 log.error(s, e);
38 }
39
40
41 public void error(String s) {
42 log.error(s);
43 }
44
45
46 public void debug(String s) {
47 log.debug(s);
48 }
49
50
51 public void trace(String s) {
52 log.trace(s);
53 }
54
55
56 public void warn(String s) {
57 log.warn(s);
58 }
59
60}
日志实现的实例化是在日志工厂LogFactory
的静态方法中进行的,在项目启动时,按上文所述顺序对第一个发现的日志实现进行实例化,后续日志实现将会被忽略。
991public final class LogFactory {
2 // Marker to be used by logging implementations that support markers.
3 public static final String MARKER = "MYBATIS";
4
5 // 被选中日志实现的构造器
6 private static Constructor<? extends Log> logConstructor;
7
8 static {
9 // 按顺序尝试实例化日志实现
10 tryImplementation(LogFactory::useSlf4jLogging);
11 tryImplementation(LogFactory::useCommonsLogging);
12 tryImplementation(LogFactory::useLog4J2Logging);
13 tryImplementation(LogFactory::useLog4JLogging);
14 tryImplementation(LogFactory::useJdkLogging);
15 tryImplementation(LogFactory::useNoLogging);
16 }
17
18 private LogFactory() {
19 // disable construction
20 }
21
22 public static Log getLog(Class<?> aClass) {
23 return getLog(aClass.getName());
24 }
25
26 public static Log getLog(String logger) {
27 try {
28 // 使用选中的构造器实例化日志对象
29 return logConstructor.newInstance(logger);
30 } catch (Throwable t) {
31 throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
32 }
33 }
34
35 // 选中自定义日志,后文将讲到
36 public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
37 setImplementation(clazz);
38 }
39
40 // 选中Slf4jImpl日志
41 public static synchronized void useSlf4jLogging() {
42 setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
43 }
44
45 public static synchronized void useCommonsLogging() {
46 setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
47 }
48
49 public static synchronized void useLog4JLogging() {
50 setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
51 }
52
53 public static synchronized void useLog4J2Logging() {
54 setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
55 }
56
57 public static synchronized void useJdkLogging() {
58 setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
59 }
60
61 public static synchronized void useStdOutLogging() {
62 setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
63 }
64
65 public static synchronized void useNoLogging() {
66 setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
67 }
68
69 // 尝试设置日志实现(注意:如果日志已经存在则跳过)
70 private static void tryImplementation(Runnable runnable) {
71 if (logConstructor == null) {
72 try {
73 runnable.run();
74 } catch (Throwable t) {
75 // ignore
76 }
77 }
78 }
79
80 // 选中日志实现
81 private static void setImplementation(Class<? extends Log> implClass) {
82 try {
83 // 获取选中日志实现的构造器
84 Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
85
86 // 打印日志被选中的log
87 Log log = candidate.newInstance(LogFactory.class.getName());
88 if (log.isDebugEnabled()) {
89 log.debug("Logging initialized using '" + implClass + "' adapter.");
90 }
91
92 // 设置到静态变量中
93 logConstructor = candidate;
94 } catch (Throwable t) {
95 throw new LogException("Error setting Log implementation. Cause: " + t, t);
96 }
97 }
98
99}
此外,还可以通过全局属性logImpl
直接指定所需的日志实现,这将会在 parseConfiguration 方法中进行解析。
201 private void parseConfiguration(XNode root) {
2 try {
3 // ...
4
5 // 加载自定义日志实现
6 loadCustomLogImpl(settings);
7
8 // ...
9 } catch (Exception e) {
10 throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
11 }
12 }
13
14 private void loadCustomLogImpl(Properties props) {
15 // 加载 logImpl 属性并加载字节码(可以配置MyBatis日志适配器的别名,如log4j)
16 Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
17
18 // 设置日志实现
19 configuration.setLogImpl(logImpl);
20 }
逐步调用到上文提到的 useCustomLogging 方法,内部依然是通过 setImplementation 来设置日志实现的构造器的。
121 public void setLogImpl(Class<? extends Log> logImpl) {
2 if (logImpl != null) {
3 this.logImpl = logImpl;
4 // 调用日志工厂设置日志实现
5 LogFactory.useCustomLogging(this.logImpl);
6 }
7 }
8
9 public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
10 // 复用 setImplementation
11 setImplementation(clazz);
12 }
光有日志实现还不够,必须得用起来才能打印所需的日志。MyBatis为需要打印日志的一些类定义了 InvocationHandler,如果需要打印日志,只需要给对应的对象做动态代理即可。
下面我们以 ConnectionLogger 为例,解析日志打印的代理逻辑。其它三个类逻辑类似,请自行分析。
811public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
2
3 private final Connection connection;
4
5 private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
6 super(statementLog, queryStack);
7 this.connection = conn;
8 }
9
10
11 public Object invoke(Object proxy, Method method, Object[] params)
12 throws Throwable {
13 try {
14 // 过滤掉Object中定义的方法
15 if (Object.class.equals(method.getDeclaringClass())) {
16 return method.invoke(this, params);
17 }
18
19 // 1. 对 prepareStatement 打印日志
20 if ("prepareStatement".equals(method.getName())) {
21 // 打印debug日志
22 if (isDebugEnabled()) {
23 debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
24 }
25 // invoke
26 PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
27
28 // 给创建的 PreparedStatement 做动态代理,也打印日志
29 stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
30 return stmt;
31
32 // 2. 对 prepareCall 打印日志
33 } else if ("prepareCall".equals(method.getName())) {
34 if (isDebugEnabled()) {
35 debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
36 }
37 PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
38 stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
39 return stmt;
40
41 // 3. 对 createStatement 打印日志
42 } else if ("createStatement".equals(method.getName())) {
43 Statement stmt = (Statement) method.invoke(connection, params);
44 stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
45 return stmt;
46
47 // 其它方法无需打印日志
48 } else {
49
50 return method.invoke(connection, params);
51 }
52 } catch (Throwable t) {
53 throw ExceptionUtil.unwrapThrowable(t);
54 }
55 }
56
57 /**
58 * Creates a logging version of a connection.
59 *
60 * @param conn - the original connection
61 * @return - the connection with logging
62 */
63 public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
64 // 1. 创建 Connection 对象的 InvocationHandler
65 InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
66 ClassLoader cl = Connection.class.getClassLoader();
67
68 // 2. 返回 Connection 对象的代理类 注入日志打印逻辑
69 return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
70 }
71
72 /**
73 * return the wrapped connection.
74 *
75 * @return the connection
76 */
77 public Connection getConnection() {
78 return connection;
79 }
80
81}
通过代码分析可以知道,PreparedStatementLogger
、StatementLogger
和ResultSetLogger
的代理对象创建(newInstance)都是在ConnectionLogger中完成的,那么ConnectionLogger的代理对象在什么时候创建的呢?
回顾之前的执行器相关分析代码,可以发现,在执行SQL前,需要先获取连接创建 Statement,而在创建连接后,如果判断日志是debug级别,则会创建连接的代理对象返回,在调用相关方法时就会被拦截执行打印日志逻辑。
121 protected Connection getConnection(Log statementLog) throws SQLException {
2 Connection connection = transaction.getConnection();
3
4 // 判断日志是否为debug级别
5 if (statementLog.isDebugEnabled()) {
6 // 创建连接的代理对象返回,在拦截逻辑打印日志
7 return ConnectionLogger.newInstance(connection, statementLog, queryStack);
8 } else {
9 // 返回无日志打印的连接
10 return connection;
11 }
12 }
元信息类MetaClass
是用来解析目标类的一些元信息,比如类的成员变量,getter/setter 方法等。其构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass方法进行创建。
211public class MetaClass {
2 // 反射器工厂
3 private final ReflectorFactory reflectorFactory;
4 // 反射器
5 private final Reflector reflector;
6
7 // 私有的构造方法
8 private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
9 this.reflectorFactory = reflectorFactory;
10 // 根据类型创建 Reflector
11 this.reflector = reflectorFactory.findForClass(type);
12 }
13
14 // 公有的forClass方法
15 public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
16 // 调用构造方法
17 return new MetaClass(type, reflectorFactory);
18 }
19
20 // 省略其他方法
21}
上面代码出现了两个新的类ReflectorFactory
和Reflector
,MetaClass 通过引入这些新类帮助它完成功能。下面我们以MetaClass的hasSetter方法为例看一下它的源码就知道是怎么回事了。
211// -☆- MetaClass
2public boolean hasSetter(String name) {
3 // 属性分词器,用于解析属性名
4 PropertyTokenizer prop = new PropertyTokenizer(name);
5
6 // hasNext 返回 true,则表明 name 是一个复合属性,后面会进行分析
7 if (prop.hasNext()) {
8 // 调用 reflector 的 hasSetter 方法
9 if (reflector.hasSetter(prop.getName())) {
10 // 为属性创建创建 MetaClass
11 MetaClass metaProp = metaClassForProperty(prop.getName());
12 // 再次调用 hasSetter
13 return metaProp.hasSetter(prop.getChildren());
14 } else {
15 return false;
16 }
17 } else {
18 // 调用 reflector 的 hasSetter 方法
19 return reflector.hasSetter(prop.getName());
20 }
21}
从上面的代码中,我们可以看出 MetaClass 中的 hasSetter 方法最终调用了 Reflector
的 hasSetter 方法。关于 Reflector 的 hasSetter 方法,这里先不分析,Reflector 这个类的逻辑较为复杂,本章会在随后进行详细说明。下面来简单介绍一下上面代码中出现的几个类:
ReflectorFactory:顾名思义,Reflector 的工厂类,兼有缓存 Reflector 对象的功能。
Reflector:反射器,用于解析和存储目标类中的元信息。
PropertyTokenizer:属性名分词器,用于处理较为复杂的属性名。
MetaObject是MyBatis底层的一个反射工具类,主要结构和功能如下:
MetaObject基本使用
421 // 1. 查找属性:忽略大小写,支持驼峰,支持子属性
2 // 2. 获取属性值:
3 // 2.1 基于点获取子属性 "user.name"
4 // 2.2 基于索引获取列表值 "users[1].id"
5 // 2.1 基于key获取map值 "user[map]"
6 // 3. 设置属性
7 // 3.1 可设置子属性值
8 // 3.2 支持自动创建子属性(必须带有空参构造方法,且不能是集合)
9
10 public void testMetaObject() {
11 // 装饰Blog
12 Blog blog = new Blog();
13 Configuration configuration = new Configuration();
14 MetaObject metaObject = configuration.newMetaObject(blog);
15
16 // 数组不能直接创建,需要我们手动创建
17 ArrayList<Comment> comments = new ArrayList<>();
18 comments.add(new Comment());
19
20 // 设置属性
21 metaObject.setValue("id", 666);
22 metaObject.setValue("author.id", 1);
23 metaObject.setValue("comments", comments);
24 metaObject.setValue("comments[0].content", "不错的博客!");
25 metaObject.setValue("labels", new HashMap<>());
26 metaObject.setValue("labels[red]", "红");
27
28 // 获取属性
29 System.out.println(metaObject.getValue("id")); // 666
30 System.out.println(metaObject.getValue("author.id")); // 1
31 System.out.println(metaObject.getValue("comments")); // [Comment{user=null, content='不错的博客!'}]
32 System.out.println(metaObject.getValue("comments[0].content")); // 不错的博客!
33 System.out.println(metaObject.getValue("labels")); // {red=红}
34 System.out.println(metaObject.getValue("labels[red]")); // 红
35
36 // 使用BeanWrapper获取属性
37 BeanWrapper beanWrapper = new BeanWrapper(metaObject, blog);
38 beanWrapper.get(new PropertyTokenizer("comments")); // 获取到 comments 集合
39 beanWrapper.get(new PropertyTokenizer("comments[0]")); // 获取到 comments[0] ,可以通过索引获取
40 beanWrapper.get(new PropertyTokenizer("comments[0].content")); // 获取到 comments[0] ,不支持获取子属性
41 }
42
MetaObject源码分析
MetaObject获取属性的流程图如下,在上述案例打上断点跟踪调试看看吧!
从MetaObject的getValue方法进入后,首先使用PropertyTokenizer
类进行分词,再判断是否有子属性。
如果有:使用 IndexedName 获取新的 MetaObject对象,传入 children 表达式进行递归。
如果没有:则递归结束,调用BeanWrapper
类的 get 方法获取当前对象的指定属性。
递归完成后如下图所示,简化为使用username从User对象中取值。
提示:在递归过程中,
metaObjectForProperty(prop.getIndexedName())
方法内部需要获取cmments[0]、user等对象构建新的MetaObject,可能多次调用objectWrapper.get(prop)
,要注意区分!
在BeanWrapper
类中,可获取当前对象的属性值,这里没有index
,走下面的逻辑。
再往下就是JDK反射的一些包装了,这里不详细展开讲解,可自行跟踪理解。
如果在BeanWrapper
中有index
,如comments[0],则会先调用MetaObject.getValue获取 collection 对象(传入prop.getName()
)。
获取到集合对象后,根据不同的集合类型,用 index 从集合中拿数据返回。
Reflector
这个类的用途主要是是通过反射获取目标类的 getter 方法及其返回值类型,setter 方法及其参数值类型等元信息。并将获取到的元信息缓存到相应的集合中,供后续使用。
ReflectorFactory 是一个接口,MyBatis 中目前只有一个实现类 DefaultReflectorFactory
。DefaultReflectorFactory 用于创建 Reflector,同时兼有缓存的功能,它的源码如下。
391public class DefaultReflectorFactory implements ReflectorFactory {
2 // 是否进行缓存,默认为true
3 private boolean classCacheEnabled = true;
4
5 // 目标类和反射器映射缓存
6 private final ConcurrentMap<Class<?>, Reflector> reflectorMap = new ConcurrentHashMap<Class<?>, Reflector>();
7
8 // 省略部分代码
9
10 // 创建或返回缓存的反射器
11 public Reflector findForClass(Class<?> type) {
12 if (classCacheEnabled) {
13 // 从缓存中获取 Reflector 对象
14 Reflector cached = reflectorMap.get(type);
15
16 // 缓存为空,则创建一个新的 Reflector 实例
17 if (cached == null) {
18 cached = new Reflector(type);
19 // 将 <type, cached> 映射缓存到 map 中,方便下次取用
20 reflectorMap.put(type, cached);
21 }
22 return cached;
23 } else {
24 // 直接创建一个新的 Reflector 实例
25 return new Reflector(type);
26 }
27 }
28}
29
30================= 扩展: findForClass方法优化 ========================
31 public Reflector findForClass(Class<?> type) {
32 if (classCacheEnabled) {
33 // synchronized (type) removed see issue #461
34 return reflectorMap.computeIfAbsent(type, Reflector::new);
35 } else {
36 return new Reflector(type);
37 }
38 }
39
DefaultReflectorFactory 的findForClass方法逻辑不是很复杂,包含两个缓存操作,和一个对象创建操作。代码注释的比较清楚了,就不多说了。接下来,来分析一下反射器 Reflector。
Reflector 构造方法中包含了很多初始化逻辑,目标类的元信息解析过程也是在构造方法中完成的,这些元信息最终会被保存到 Reflector 的成员变量中。下面我们先来看看 Reflector 的构造方法和相关的成员变量定义,代码如下:
451public class Reflector {
2 private final Class<?> type;
3 private final String[] readablePropertyNames;
4 private final String[] writeablePropertyNames;
5 private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
6 private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
7 private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
8 private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
9 private Constructor<?> defaultConstructor;
10
11 private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();
12
13 public Reflector(Class<?> clazz) {
14 type = clazz;
15
16 // 解析目标类的默认构造方法,并赋值给 defaultConstructor 变量
17 addDefaultConstructor(clazz);
18
19 // 解析 getter 方法,并将解析结果放入 getMethods 中
20 addGetMethods(clazz);
21
22 // 解析 setter 方法,并将解析结果放入 setMethods 中
23 addSetMethods(clazz);
24
25 // 解析属性字段,并将解析结果添加到 setMethods 或 getMethods 中
26 addFields(clazz);
27
28 // 从 getMethods 映射中获取可读属性名数组
29 readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
30
31 // 从 setMethods 映射中获取可写属性名数组
32 writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
33
34 // 将所有属性名的大写形式作为键,属性名作为值,存入到 caseInsensitivePropertyMap 中
35 for (String propName : readablePropertyNames) {
36 caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
37 }
38 for (String propName : writeablePropertyNames) {
39 caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
40 }
41 }
42
43 // 省略其他方法
44}
45
Reflector 的构造方法看起来略为复杂,不过好在一些比较复杂的逻辑都封装在了相应的方法中,这样整体的逻辑就比较清晰了。Reflector 构造方法所做的事情均已进行了注释,大家对照着注释先看一下。相关方法的细节待会会进行分析。看完构造方法,下面我来通过表格的形式,列举一下 Reflector 部分成员变量的用途。如下:
变量名 | 类型 | 用途 |
---|---|---|
readablePropertyNames | String[] | 可读属性名称数组,用于保存 getter 方法对应的属性名称 |
writeablePropertyNames | String[] | 可写属性名称数组,用于保存 setter 方法对应的属性名称 |
setMethods | Map<String, Invoker> | 用于保存属性名称到 Invoke 的映射。setter 方法会被封装到 MethodInvoker 对象中,Invoke 实现类比较简单,大家自行分析 |
getMethods | Map<String, Invoker> | 用于保存属性名称到 Invoke 的映射。同上,getter 方法也会被封装到 MethodInvoker 对象中 |
setTypes | Map<String, Class<?>> | 用于保存 setter 对应的属性名与参数类型的映射 |
getTypes | Map<String, Class<?>> | 用于保存 getter 对应的属性名与返回值类型的映射 |
caseInsensitivePropertyMap | Map<String, String> | 用于保存大写属性名与属性名之间的映射,比如 <NAME, name> |
上面列举了一些集合变量,这些变量用于缓存各种元信息。关于这些变量,这里描述的不太好懂,主要是不太好解释。要想了解这些变量更多的细节,还是要深入到源码中。所以我们成热打铁,继续往下分析。
getter 方法解析的逻辑被封装在了addGetMethods方法中,这个方法除了会解析形如getXXX的方法,同时也会解析isXXX方法。
351private void addGetMethods(Class<?> cls) {
2 Map<String, List<Method>> conflictingGetters = new HashMap<String, List<Method>>();
3 // 获取当前类,接口,以及父类中的方法。该方法逻辑不是很复杂,这里就不展开了
4 Method[] methods = getClassMethods(cls);
5 for (Method method : methods) {
6 // getter 方法不应该有参数,若存在参数,则忽略当前方法
7 if (method.getParameterTypes().length > 0) {
8 continue;
9 }
10 String name = method.getName();
11 // 过滤出以 get 或 is 开头的方法
12 if ((name.startsWith("get") && name.length() > 3)
13 || (name.startsWith("is") && name.length() > 2)) {
14 // 将 getXXX 或 isXXX 等方法名转成相应的属性,比如 getName -> name
15 name = PropertyNamer.methodToProperty(name);
16 /*
17 * 将冲突的方法添加到 conflictingGetters 中。考虑这样一种情况:
18 *
19 * getTitle 和 isTitle 两个方法经过 methodToProperty 处理,
20 * 均得到 name = title,这会导致冲突。
21 *
22 * 对于冲突的方法,这里先统一起存起来,后续再解决冲突
23 */
24 addMethodConflict(conflictingGetters, name, method);
25 }
26 }
27
28 // 解决 getter 冲突
29 resolveGetterConflicts(conflictingGetters);
30}
31
32
33 ============= 优化 ==============
34 Arrays.stream(methods).filter(m -> m.getParameterTypes().length == 0 && PropertyNamer.isGetter(m.getName()))
35 .forEach(m -> addMethodConflict(conflictingGetters, PropertyNamer.methodToProperty(m.getName()), m));
如上,addGetMethods 方法的执行流程如下:
获取当前类,接口,以及父类中的方法
遍历上一步获取的方法数组,并过滤出以get和is开头的方法
将方法名转换成相应的属性名
将属性名和方法对象添加到冲突集合中
解决冲突
在上面的执行流程中,前三步比较简单,大家自行分析吧。第4步也不复杂,下面我会把源码贴出来,大家看一下就能懂。在这几步中,第5步逻辑比较复杂,这一步逻辑我们重点关注一下。
781/** 添加属性名和方法对象到冲突集合中 */
2private void addMethodConflict(Map<String, List<Method>> conflictingMethods, String name, Method method) {
3 List<Method> list = conflictingMethods.get(name);
4 if (list == null) {
5 list = new ArrayList<Method>();
6 conflictingMethods.put(name, list);
7 }
8 list.add(method);
9}
10
11/** 解决冲突 */
12private void resolveGetterConflicts(Map<String, List<Method>> conflictingGetters) {
13 for (Entry<String, List<Method>> entry : conflictingGetters.entrySet()) {
14 Method winner = null;
15 String propName = entry.getKey();
16 for (Method candidate : entry.getValue()) {
17 if (winner == null) {
18 winner = candidate;
19 continue;
20 }
21 // 获取返回值类型
22 Class<?> winnerType = winner.getReturnType();
23 Class<?> candidateType = candidate.getReturnType();
24
25 /*
26 * 两个方法的返回值类型一致,若两个方法返回值类型均为 boolean,则选取 isXXX 方法
27 * 为 winner。否则无法决定哪个方法更为合适,只能抛出异常
28 */
29 if (candidateType.equals(winnerType)) {
30 if (!boolean.class.equals(candidateType)) {
31 throw new ReflectionException(
32 "Illegal overloaded getter method with ambiguous type for property "
33 + propName + " in class " + winner.getDeclaringClass()
34 + ". This breaks the JavaBeans specification and can cause unpredictable results.");
35
36 /*
37 * 如果方法返回值类型为 boolean,且方法名以 "is" 开头,
38 * 则认为候选方法 candidate 更为合适
39 */
40 } else if (candidate.getName().startsWith("is")) {
41 winner = candidate;
42 }
43
44 /*
45 * winnerType 是 candidateType 的子类,类型上更为具体,
46 * 则认为当前的 winner 仍是合适的,无需做什么事情
47 */
48 } else if (candidateType.isAssignableFrom(winnerType)) {
49
50 /*
51 * candidateType 是 winnerType 的子类,此时认为 candidate 方法更为合适,
52 * 故将 winner 更新为 candidate
53 */
54 } else if (winnerType.isAssignableFrom(candidateType)) {
55 winner = candidate;
56 } else {
57 throw new ReflectionException(
58 "Illegal overloaded getter method with ambiguous type for property "
59 + propName + " in class " + winner.getDeclaringClass()
60 + ". This breaks the JavaBeans specification and can cause unpredictable results.");
61 }
62 }
63
64 // 将筛选出的方法添加到 getMethods 中,并将方法返回值添加到 getTypes 中
65 addGetMethod(propName, winner);
66 }
67}
68
69private void addGetMethod(String name, Method method) {
70 if (isValidPropertyName(name)) {
71 getMethods.put(name, new MethodInvoker(method));
72 // 解析返回值类型
73 Type returnType = TypeParameterResolver.resolveReturnType(method, type);
74 // 将返回值类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes 中
75 getTypes.put(name, typeToClass(returnType));
76 }
77}
78
以上就是解除冲突的过程,代码有点长,不太容易看懂。这里大家只要记住解决冲突的规则即可理解上面代码的逻辑。相关规则如下:
冲突方法的返回值类型具有继承关系,子类返回值对应的方法被认为是更合适的选择。
冲突方法的返回值类型相同,如果返回值类型为boolean,那么以is开头的方法则是更合适的方法。
冲突方法的返回值类型相同,但返回值类型非boolean,此时出现歧义,抛出异常。
冲突方法的返回值类型不相关,无法确定哪个是更好的选择,此时直接抛异常。
分析完 getter 方法的解析过程,下面继续分析 setter 方法的解析过程。
与 getter 方法解析过程相比,setter 方法的解析过程与此有一定的区别。主要体现在冲突出现的原因,以及冲突的解决方法上。那下面,我们深入源码来找出两者之间的区别。
231private void addSetMethods(Class<?> cls) {
2 Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
3 // 获取当前类,接口,以及父类中的方法。该方法逻辑不是很复杂,这里就不展开了
4 Method[] methods = getClassMethods(cls);
5 for (Method method : methods) {
6 String name = method.getName();
7 // 过滤出 setter 方法,且方法仅有一个参数
8 if (name.startsWith("set") && name.length() > 3) {
9 if (method.getParameterTypes().length == 1) {
10 name = PropertyNamer.methodToProperty(name);
11 /*
12 * setter 方法发生冲突原因是:可能存在重载情况,比如:
13 * void setSex(int sex);
14 * void setSex(SexEnum sex);
15 */
16 addMethodConflict(conflictingSetters, name, method);
17 }
18 }
19 }
20 // 解决 setter 冲突
21 resolveSetterConflicts(conflictingSetters);
22}
23
从上面的代码和注释中,我们可知道 setter 方法之间出现冲突的原因。即方法存在重载,方法重载导致methodToProperty方法解析出的属性名完全一致。而 getter 方法之间出现冲突的原因是getXXX和isXXX对应的属性名一致。既然冲突发生了,要进行调停,那接下来继续来看看调停冲突的逻辑。
701private void resolveSetterConflicts(Map<String, List<Method>> conflictingSetters) {
2 for (String propName : conflictingSetters.keySet()) {
3 List<Method> setters = conflictingSetters.get(propName);
4 /*
5 * 获取 getter 方法的返回值类型,由于 getter 方法不存在重载的情况,
6 * 所以可以用它的返回值类型反推哪个 setter 的更为合适
7 */
8 Class<?> getterType = getTypes.get(propName);
9 Method match = null;
10 ReflectionException exception = null;
11 for (Method setter : setters) {
12 // 获取参数类型
13 Class<?> paramType = setter.getParameterTypes()[0];
14 if (paramType.equals(getterType)) {
15 // 参数类型和返回类型一致,则认为是最好的选择,并结束循环
16 match = setter;
17 break;
18 }
19 if (exception == null) {
20 try {
21 // 选择一个更为合适的方法
22 match = pickBetterSetter(match, setter, propName);
23 } catch (ReflectionException e) {
24 match = null;
25 exception = e;
26 }
27 }
28 }
29 // 若 match 为空,表示没找到更为合适的方法,此时抛出异常
30 if (match == null) {
31 throw exception;
32 } else {
33 // 将筛选出的方法放入 setMethods 中,并将方法参数值添加到 setTypes 中
34 addSetMethod(propName, match);
35 }
36 }
37}
38
39/** 从两个 setter 方法中选择一个更为合适方法 */
40private Method pickBetterSetter(Method setter1, Method setter2, String property) {
41 if (setter1 == null) {
42 return setter2;
43 }
44 Class<?> paramType1 = setter1.getParameterTypes()[0];
45 Class<?> paramType2 = setter2.getParameterTypes()[0];
46
47 // 如果参数2可赋值给参数1,即参数2是参数1的子类,则认为参数2对应的 setter 方法更为合适
48 if (paramType1.isAssignableFrom(paramType2)) {
49 return setter2;
50
51 // 这里和上面情况相反
52 } else if (paramType2.isAssignableFrom(paramType1)) {
53 return setter1;
54 }
55
56 // 两种参数类型不相关,这里抛出异常
57 throw new ReflectionException("Ambiguous setters defined for property '" + property + "' in class '"
58 + setter2.getDeclaringClass() + "' with types '" + paramType1.getName() + "' and '"
59 + paramType2.getName() + "'.");
60}
61
62private void addSetMethod(String name, Method method) {
63 if (isValidPropertyName(name)) {
64 setMethods.put(name, new MethodInvoker(method));
65 // 解析参数类型列表
66 Type[] paramTypes = TypeParameterResolver.resolveParamTypes(method, type);
67 // 将参数类型由 Type 转为 Class,并将转换后的结果缓存到 setTypes
68 setTypes.put(name, typeToClass(paramTypes[0]));
69 }
70}
关于 setter 方法冲突的解析规则,这里也总结一下吧。
冲突方法的参数类型与 getter 的返回类型一致,则认为是最好的选择。
冲突方法的参数类型具有继承关系,子类参数对应的方法被认为是更合适的选择。
冲突方法的参数类型不相关,无法确定哪个是更好的选择,此时直接抛异常
到此关于 setter 方法的解析过程就说完了。我在前面说过 MetaClass 的hasSetter最终调用了 Refactor 的hasSetter方法,那么现在是时候分析 Refactor 的hasSetter方法了。代码如下如下:
31public boolean hasSetter(String propertyName) {
2 return setMethods.keySet().contains(propertyName);
3}
代码如上,就两行,很简单,就不多说了。
对于较为复杂的属性,需要进行进一步解析才能使用。那什么样的属性是复杂属性呢?来看个测试代码就知道了。
461public class MetaClassTest {
2
3 private class Author {
4 private Integer id;
5 private String name;
6 private Integer age;
7 /** 一个作者对应多篇文章 */
8 private Article[] articles;
9
10 // 省略 getter/setter
11 }
12
13 private class Article {
14 private Integer id;
15 private String title;
16 private String content;
17 /** 一篇文章对应一个作者 */
18 private Author author;
19
20 // 省略 getter/setter
21 }
22
23
24 public void testHasSetter() {
25 // 为 Author 创建元信息对象
26 MetaClass authorMeta = MetaClass.forClass(Author.class, new DefaultReflectorFactory());
27 System.out.println("------------☆ Author ☆------------");
28 System.out.println("id -> " + authorMeta.hasSetter("id"));
29 System.out.println("name -> " + authorMeta.hasSetter("name"));
30 System.out.println("age -> " + authorMeta.hasSetter("age"));
31 // 检测 Author 中是否包含 Article[] 的 setter
32 System.out.println("articles -> " + authorMeta.hasSetter("articles"));
33 System.out.println("articles[] -> " + authorMeta.hasSetter("articles[]"));
34 System.out.println("title -> " + authorMeta.hasSetter("title"));
35
36 // 为 Article 创建元信息对象
37 MetaClass articleMeta = MetaClass.forClass(Article.class, new DefaultReflectorFactory());
38 System.out.println("\n------------☆ Article ☆------------");
39 System.out.println("id -> " + articleMeta.hasSetter("id"));
40 System.out.println("title -> " + articleMeta.hasSetter("title"));
41 System.out.println("content -> " + articleMeta.hasSetter("content"));
42 // 下面两个均为复杂属性,分别检测 Article 类中的 Author 类是否包含 id 和 name 的 setter 方法
43 System.out.println("author.id -> " + articleMeta.hasSetter("author.id"));
44 System.out.println("author.name -> " + articleMeta.hasSetter("author.name"));
45 }
46}
如上,Article
类中包含了一个Author
引用。然后我们调用 articleMeta 的 hasSetter 检测author.id
和author.name
属性是否存在,我们的期望结果为 true。测试结果如下:
如上,标记⑤处的输出均为 true,我们的预期达到了。标记②处检测 Article 数组的是否存在 setter 方法,结果也均为 true。这说明 PropertyTokenizer 对数组和复合属性均进行了处理。那它是如何处理的呢?答案如下:
611JAAVpublic class PropertyTokenizer implements Iterator<PropertyTokenizer> {
2
3 private String name;
4 private final String indexedName;
5 private String index;
6 private final String children;
7
8 public PropertyTokenizer(String fullname) {
9 // 检测传入的参数中是否包含字符 '.'
10 int delim = fullname.indexOf('.');
11 if (delim > -1) {
12 /*
13 * 以点位为界,进行分割。比如:
14 * fullname = www.coolblog.xyz
15 *
16 * 以第一个点为分界符:
17 * name = www
18 * children = coolblog.xyz
19 */
20 name = fullname.substring(0, delim);
21 children = fullname.substring(delim + 1);
22 } else {
23 // fullname 中不存在字符 '.'
24 name = fullname;
25 children = null;
26 }
27 indexedName = name;
28 // 检测传入的参数中是否包含字符 '['
29 delim = name.indexOf('[');
30 if (delim > -1) {
31 /*
32 * 获取中括号里的内容,比如:
33 * 1. 对于数组或List集合:[] 中的内容为数组下标,
34 * 比如 fullname = articles[1],index = 1
35 * 2. 对于Map:[] 中的内容为键,
36 * 比如 fullname = xxxMap[keyName],index = keyName
37 *
38 * 关于 index 属性的用法,可以参考 BaseWrapper 的 getCollectionValue 方法
39 */
40 index = name.substring(delim + 1, name.length() - 1);
41
42 // 获取分解符前面的内容,比如 fullname = articles[1],name = articles
43 name = name.substring(0, delim);
44 }
45 }
46
47 // 省略 getter
48
49
50 public boolean hasNext() {
51 return children != null;
52 }
53
54
55 public PropertyTokenizer next() {
56 // 对 children 进行再次切分,用于解析多重复合属性
57 return new PropertyTokenizer(children);
58 }
59
60 // 省略部分方法
61}
以上是 PropertyTokenizer 的源码分析,注释的比较多,应该分析清楚了。大家如果看懂了上面的分析,那么可以自行举例进行测试,以加深理解。
对象导航图语言(OGNL)是一种开源的JAVA表达式语言,可以方便的存取对象属性和调用方法。下面是一个OGNL表达式的案例:
421
2public void ognlTest() {
3 ExpressionEvaluator evaluator = new ExpressionEvaluator();
4
5 Comment comment = new Comment();
6 comment.setId(10);
7 comment.setBlog(new Blog());
8 comment.setContent("这文章写的也太好了!");
9
10 ArrayList<Comment> comments = new ArrayList<>();
11 comments.add(comment);
12
13 Blog blog = new Blog();
14 blog.setId(1);
15 blog.setAuthor(new User());
16 blog.setComments(comments);
17
18 // 1. 访问属性
19 boolean b1 = evaluator.evaluateBoolean("id != null && author.username == null", blog);
20 System.out.println(b1); //true
21
22 // 2. 访问集合属性
23 boolean b2 = evaluator.evaluateBoolean("comments[0].id > 0", blog);
24 System.out.println(b2);
25
26 // 3. 调用无参方法
27 boolean b3 = evaluator.evaluateBoolean("isHasComment == true && isHasComment() == true", blog);
28 System.out.println(b3);
29
30 // 4. 调用带参方法
31 boolean b4 = evaluator.evaluateBoolean("findCommentContent(0).equals(\"这文章也的也太好了!\")", blog);
32 System.out.println(b4);
33
34 // 5. 遍历集合
35 Iterable<?> iterable = evaluator.evaluateIterable("comments", blog);
36 for (Object obj : iterable) {
37 System.out.println(obj);
38 }
39
40 // 6. 注意:防止出现空指针异常!
41 evaluator.evaluateBoolean("body.length() > 0", blog); // java.lang.NullPointerException: target is null for method length
42}