本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是优快云中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为优快云博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/onlinedct/article/details/107306041
MyBatis支持非常灵活的 SQL语句组建方式。我们可以在组建 SQL语句时使用 foreach、where、if等标签完成复杂的语句组装工作。
<select id="selectUsers" resultMap="userMapFull">
SELECT *
FROM `user`
WHERE `id` IN
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</select>
语句最终还是会被解析成为最基本的 SQL语句才能被数据库接收,这个解析过程主要由 scripting包完成。
1.OGNL
OGNL(Object Graph Navigation Language,对象图导航语言)是一种功能强大的表达式语言(Expression Language,EL)。通过它,能够完成从集合中选取对象、读写对象的属性、调用对象和类的方法、表达式求值与判断等操作。OGNL应用十分广泛,例如,同样是获取 Map中某个对象的属性,用 Java语言表示出来如下。
userMap.get("user2").getName();
使用OGNL表达式为:
#user2.name
除了简单、清晰以外,OGNL有着更高的环境适应性。我们可以将 OGNL表达式应用在配置文件、XML文件等处,而只在解析这些文件时使用 OGNL即可。
OGNL有 Java工具包,只要引入它即可以在 Java中使用 OGNL的功能。就可以使用 Java来解析引入了 OGNL的各种文档。在介绍 OGNL用法之前,先介绍 OGNL解析时要接触的三个重要概念。
- 表达式(expression):是一个带有语法含义的字符串,是整个 OGNL的核心内容。通过表达式来确定需要进行的 OGNL操作。
- 根对象(root):可以理解为 OGNL 的被操作对象。表达式中表示的操作就是针对这个对象展开的。
- 上下文(context):整个 OGNL处理时的上下文环境,该环境是一个 Map对象。在进行 OGNL处理之前,我们可以传入一个初始化过的上下文环境。
public static void main(String[] args) {
try {
User user01 = new User(1, "易哥", 18);
User user02 = new User(2, "莉莉", 15);
Map<String, User> userMap = new HashMap<>();
userMap.put("user1", user01);
userMap.put("user2", user02);
// Java方式读取列表中对象的属性值
String userName = userMap.get("user2").getName();
System.out.println(userName);
readAndWriteProps(userMap);
callFunction();
runAfterParse(userMap);
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void readAndWriteProps(Map<String, User> userMap) throws Exception {
// 使用表达式读写根对象中信息的示例
// 该示例中要用到的OGNL函数:
// getValue(String expression, Object root) :对root内容执行expression中的操作,并返回结果
// 读取根对象的属性值
Integer age = (Integer) Ognl.getValue("age", userMap.get("user1"));
System.out.println("读取根对象属性,得到age:" + age);
// 设置根对象的属性值
Ognl.getValue("age = 19", userMap.get("user1"));
age = (Integer) Ognl.getValue("age", userMap.get("user1"));
System.out.println("设置根对象属性后,得到age:" + age);
// 使用表达式读写环境中信息的示例
// 该示例中要用到的OGNL函数:
// getValue(String expression, Map context, Object root) :在context环境中对root内容执行expression中的操作,并返回结果
// 读取环境中的信息
String userName2 = (String) Ognl.getValue("#user2.name", userMap, new Object());
System.out.println("读取环境中的信息,得到user2的name:" + userName2);
// 读取环境中的信息,并进行判断
Boolean result = (Boolean) Ognl.getValue("#user2.name != '丽丽'", userMap, new Object());
System.out.println("读取环境中的信息,并进行判断,得到:" + result);
// 设置环境中的信息
Ognl.getValue("#user2.name = '小华'", userMap, new Object());
String newUserName = (String) Ognl.getValue("#user2.name", userMap, new Object());
System.out.println("设置环境中的信息后,得到user2的name:" + newUserName);
}
/*
莉莉
读取根对象属性,得到age:18
设置根对象属性后,得到age:19
读取环境中的信息,得到user2的name:莉莉
读取环境中的信息,并进行判断,得到:true
设置环境中的信息后,得到user2的name:小华
*/
Java使用OGNL完成了对跟对象信息及上下文信息的读写操作,并且整个操作过程中也支持逻辑操作
OGNL不仅可以读写信息,还能调用对象、类中的方法。
// 调用对象方法
Integer hashCode = (Integer) Ognl.getValue("hashCode()", "yeecode");
System.out.println("对字符串对象调用hashCode方法得到:" + hashCode);
// 调用类方法
Double result = (Double)Ognl.getValue("@java.lang.Math@random()", null);
System.out.println("调用Math类中的静态方法random,得到:" + result);
/*
对字符串对象调用hashCode方法得到:-1293325242
调用Math类中的静态方法random,得到:0.2308049275052454
*/
OGNL支持表达式的预编译,对表达式进行预编译后,避免了每次执行表达式前的编译工作,能够明显地提高 OGNL的执行效率。
String userName;
// 先对表达式解析,然后再执行可以提高效率
long time1 = new Date().getTime();
// 解析表达式
Object expressionTree = Ognl.parseExpression("#user2.name");
// 重复运行多次
for (int i = 0; i < 10000; i++) {
userName = (String) Ognl.getValue(expressionTree, userMap, new Object());
}
long time2 = new Date().getTime();
// 直接重复运行多次
for (int i = 0; i < 10000; i++) {
userName = (String) Ognl.getValue("#user2.name", userMap, new Object());
}
long time3 = new Date().getTime();
System.out.println("编译之后再执行,共花费" + (time2 - time1) + "ms");
System.out.println("不编译直接执行,共花费" + (time3 - time2) + "ms");
/*
编译之后再执行,共花费19ms
不编译直接执行,共花费306ms
*/
可见,如果要多次运行一个表达式,则先将其编译后再运行的执行效率更高。我们在 JSP、XML中常常见到 OGNL表达式,而这些表达式的解析就是通过本节中介绍的方式进行的。可见,OGNL 是一种广泛、便捷、强大的语言。
2.语言驱动接口及语言驱动注册表
LanguageDriver为语言驱动类的接口,它一共定义了三个方法。
// 脚本语言解释器
// 在接口上注解的SQL语句,就是由它进行解析的
// @Select("select * from `user` where id = #{id}")
//User queryUserById(Integer id);
public interface LanguageDriver {
/**
* 创建参数处理器。参数处理器能将实参传递给JDBC statement。
* @param mappedStatement 完整的数据库操作节点
* @param parameterObject 参数对象
* @param boundSql 数据库操作语句转化的BoundSql对象
* @return 参数处理器
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
/**
* 创建SqlSource对象(基于映射文件的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
* @param configuration 配置信息
* @param script 映射文件中的数据库操作节点
* @param parameterType 参数类型
* @return SqlSource对象
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
/**
* 创建SqlSource对象(基于注解的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
* @param configuration 配置信息
* @param script 注解中的SQL字符串
* @param parameterType 参数类型
* @return SqlSource对象,具体来说是DynamicSqlSource和RawSqlSource中的一种
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
LanguageDriver接口默认有两个实现,分别是XMLLanguageDriver和RawLanguageDriver
而其中的RawLanguageDriver又是 XMLLanguageDriver的子类。RawLanguageDriver类的所有操作都是调用父类XMLLanguageDriver完成的。并且在XMLLanguageDriver类完成操作后通过 checkIsNotDynamic 方法校验获得的 SqlSource 必须为 RawSqlSource。因此说,RawLanguageDriver类实际上是通过checkIsNotDynamic方法对XMLLanguageDriver类的功能进行了裁剪,使得自身仅仅支持 RawSqlSource类型的SqlSource。
public class RawLanguageDriver extends XMLLanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 调用父类方法完成操作
SqlSource source = super.createSqlSource(configuration, script, parameterType);
// 校验得到的SqlSource是RawSqlSource
checkIsNotDynamic(source);
return source;
}
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// 调用父类方法完成操作
SqlSource source = super.createSqlSource(configuration, script, parameterType);
// 校验得到的SqlSource是RawSqlSource
checkIsNotDynamic(source);
return source;
}
/**
* 校验输入的SqlSource是RawSqlSource,否则便抛出异常
* @param source 输入的SqlSource对象
*/
private void checkIsNotDynamic(SqlSource source) {
if (!RawSqlSource.class.equals(source.getClass())) {
throw new BuilderException("Dynamic content is not allowed when using RAW language");
}
}
}
在面向对象的设计中子类通常会在继承父类方法的基础上扩充更多的方法,因此子类功能是父类功能的超集。而RawLanguageDriver类却对其父类 XMLLanguageDriver的功能进行了裁剪,使得自身的功能是父类功能的子集,这是一种先繁再简的设计方式。当我们在开发中遇到类似的需求时,可以参考这种设计方式。
MyBatis 还允许用户自己给出 LanguageDriver 的实现类,通过配置文件中的defaultScriptingLanguage 属性将其指定为默认的脚本驱动。该功能的支持由XMLConfigBuilder 类中的源码实现。在这里 MyBatis 会尝试根据用户在defaultScriptingLanguage中的配置来设置默认的语言驱动。
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
//在Configuration类中给定了默认语言驱动
public void setDefaultScriptingLanguage(Class<? extends LanguageDriver> driver) {
if (driver == null) {
driver = XMLLanguageDriver.class;
}
getLanguageRegistry().setDefaultDriverClass(driver);
}
LanguageDriverRegistry类,它作为语言驱动的注册表管理所有的语言驱动。LanguageDriverRegistry类内主要包括向其中注册驱动、从中选取驱动的方法,实现都比较简单。
3.SQL节点树的组建
<select id="selectUsersByNameOrSchoolName" parameterMap="userParam01" resultType="User">
SELECT * FROM `user`
<where>
<if test="name != null">
`name` = #{name}
</if>
<if test="schoolName != null">
AND `schoolName` = #{schoolName}
</if>
</where>
</select>
要想解析这棵树,首先要做的是将 XML中的信息读取进来,然后在内存中将 XML树组建为 SQL 节点树。SQL 节点树的组建由 XMLScriptBuilder 类负责。属性列表:
// 当前要处理的XML节点
private final XNode context;
// 当前节点是否为动态节点
private boolean isDynamic;
// 输入参数的类型
private final Class<?> parameterType;
// 节点类型和对应的处理器组成的Map
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
在 XMLScriptBuilder 类中,定义有一个接口 NodeHandler。它有一个 handleNode方法负责将节点拼装到节点树中。
private interface NodeHandler {
/**
* 该方法将当前节点拼装到节点树中
* @param nodeToHandle 要被拼接的节点
* @param targetContents 节点树
*/
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
每一种 SQL节点都有一个 NodeHandler实现类。SQL节点和NodeHandler实现类的对应关系由nodeHandlerMap负责存储。
NodeHandler接口与其实现类的类图:
以 IfHandler为例,我们查看如何基于 XML信息组建 SQL节点树。
/**
* 该方法将当前节点拼装到节点树中
* @param nodeToHandle 要被拼接的节点
* @param targetContents 节点树
*/
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析该节点的下级节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 获取该节点的test属性
String test = nodeToHandle.getStringAttribute("test");
// 创建一个IfSqlNode
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
// 将创建的IfSqlNode放入到SQL节点树中
targetContents.add(ifSqlNode);
}
在了解了 NodeHandler 接口及其实现类之后,我们看一下如何从根节点开始组建一棵SQL节点树。入口方法是parseScriptNode方法,而主要操作在 parseDynamicTags方法中展开。
/**
* 解析节点生成SqlSource对象
* @return SqlSource对象
*/
public SqlSource parseScriptNode() {
// 解析XML节点节点,得到节点树MixedSqlNode
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 根据节点树是否为动态,创建对应的SqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
/**
* 将XNode对象解析为节点树
* @param node XNode对象,即数据库操作节点
* @return 解析后得到的节点树
*/
protected MixedSqlNode parseDynamicTags(XNode node) {
// XNode拆分出的SqlNode列表
List<SqlNode> contents = new ArrayList<>();
// 输入XNode的子XNode
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
// 循环遍历每一个子XNode
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { // CDATASection类型或者Text类型的XNode节点
// 获取XNode内的信息
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 只要有一个TextSqlNode对象是动态的,则整个MixedSqlNode是动态的
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // 子XNode仍然是Node类型
String nodeName = child.getNode().getNodeName();
// 找到对应的处理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 用处理器处理节点
handler.handleNode(child, contents);
isDynamic = true;
}
}
// 返回一个混合节点,其实就是一个SQL节点树
return new MixedSqlNode(contents);
}
parseDynamicTags 会逐级分析 XML 文件中的节点并使用对应的NodeHandler 实现来处理该节点,最终将所有的节点整合到一个 MixedSqlNode 对象中。MixedSqlNode对象就是 SQL节点树。在整合节点树的过程中,只要存在一个动态节点,则 SQL节点树就是动态的。动态的SQL节点树将用来创建DynamicSqlSource对象,否则就创建 RawSqlSource对象。
4.SQL节点树的解析
对组建好的 SQL 节点树进行解析是 MyBatis 中非常重要的工作。这部分工作主要在scripting包的 xmltags子包完成。
这些 OGNL表达式的解析就是基于 OGNL包来完成的。在MyBatis的pom.xml文件中可以看到:
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.2.10</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
为了更好地完成 OGNL 的解析工作,xmltags 子包中还设置了三个相关的类。
- DefaultClassResolver 类是 OGNL 中定义的一个类,OGNL 可以通过该类进行类的读取,即将类名转化为一个类。而 OgnlClassResolver 则继承了 DefaultClassResolver 类,并覆盖了其中的toClassForName。OGNL在工作时可以使用 MyBatis中的 Resources类来完成类的读取。
@Override
protected Class toClassForName(String className) throws ClassNotFoundException {
return Resources.classForName(className);
}
- MemberAccess接口是 OGNL提供的一个钩子接口。OGNL借助这个接口为访问对象的属性做好准备。OgnlMemberAccess类就实现了 MemberAccess接口,并基于反射提供了修改对象属性可访问性的功能。OGNL便可以基于这些功能为访问对象的属性做好准备。
/**
* 设置属性的可访问性
* @param context 环境上下文
* @param target 目标对象
* @param member 目标对象的目标成员
* @param propertyName 属性名称
* @return 属性的可访问性
*/
@Override
public Object setup(Map context, Object target, Member member, String propertyName) {
Object result = null;
if (isAccessible(context, target, member, propertyName)) { // 如果允许修改属性的可访问性
AccessibleObject accessible = (AccessibleObject) member;
if (!accessible.isAccessible()) { // 如果属性原本不可访问
result = Boolean.FALSE;
// 将属性修改为可访问
accessible.setAccessible(true);
}
}
return result;
}
- OgnlCache类为了提升 OGNL 的运行效率,MyBatis 还为 OGNL 提供了一个缓存OgnlCache类。
public final class OgnlCache {
// MyBatis提供的OgnlMemberAccess对象
private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess();
// MyBatis提供的OgnlClassResolver对象
private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver();
// 缓存解析后的OGNL表达式,用以提高效率
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();
private OgnlCache() {
// Prevent Instantiation of Static Class
}
/**
* 读取表达式的结果
* @param expression 表达式
* @param root 根环境
* @return 表达式结果
*/
public static Object getValue(String expression, Object root) {
try {
// 创建默认的上下文环境
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
// 依次传入表达式树、上下文、根,从而获得表达式的结果
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
/**
* 解析表达式,得到解析后的表达式树
* @param expression 表达式
* @return 表达式树
* @throws OgnlException
*/
private static Object parseExpression(String expression) throws OgnlException {
// 先从缓存中获取
Object node = expressionCache.get(expression);
if (node == null) {
// 缓存没有则直接解析,并放入缓存
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}
}
在 OgnlCache类中,即使用 parseExpression方法对表达式进行了预先解析,并且将表达式解析的结果放入expressionCache 属性中缓存了起来。这样,在每次进行表达式解析时,会先从 expressionCache属性中查询已经解析好的结果。这样一来避免了重复解析,提高了 OGNL操作的效率。
4.1 表达式求值器
MyBatis并没有将 OGNL工具直接暴露给各个 SQL节点使用,而是对 OGNL工具进行了进一步的易用性封装,得到了ExpressionEvaluator类,即表达式求值器。ExpressionEvaluator 类提供了两个方法,一个是evaluateBoolean 方法。该方法能够对结果为 true、false形式的表达式进行求值。例如,“<if test="name!=null">”节点中的 true、false判断便可以直接调用该方法完成。
/**
* 对结果为true/false形式的表达式进行求值
* @param expression 表达式
* @param parameterObject 参数对象
* @return 求值结果
*/
public boolean evaluateBoolean(String expression, Object parameterObject) {
// 获取表达式的值
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) { // 如果确实是Boolean形式的结果
return (Boolean) value;
}
if (value instanceof Number) { // 如果是数值形式的结果
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
另外一个是 evaluateIterable 方法。该方法能对结果为迭代形式的表达式进行求值。这样,“<foreach item=“id” collection=“array” open="(“separator=”,“close=”)">#{id} </foreach>”节点中的迭代判断便可以直接调用该方法完成。
/**
* 对结果为迭代形式的表达式进行求值
* @param expression 表达式
* @param parameterObject 参数对象
* @return 求值结果
*/
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
// 获取表达式的结果
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
if (value instanceof Iterable) { // 如果结果是Iterable
return (Iterable<?>) value;
}
if (value.getClass().isArray()) { // 结果是Array
// 原注释:得到的Array可能是原始的,因此调用Arrays.asList()可能会抛出ClassCastException。因此要手工转为ArrayList
int size = Array.getLength(value);
List<Object> answer = new ArrayList<>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
if (value instanceof Map) { // 结果是Map
return ((Map) value).entrySet();
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}
基于 OGNL 封装的表达式求值器是 SQL 节点树解析的利器,它能够根据上下文环境对表达式的值做出正确的判断,这是将复杂的数据库操作语句解析为纯粹SQL语句的十分重要的一步。
4.2 动态上下文
一方面,在进行 SQL节点树的解析时,需要不断保存已经解析完成的 SQL片段;另一方面,在进行SQL节点树的解析时也需要一些参数和环境信息作为解析的依据。以上这两个功能是由动态上下文 DynamicContext提供的。
// 上下文环境
private final ContextMap bindings;
// 用于拼装SQL语句片段
private final StringJoiner sqlBuilder = new StringJoiner(" ");
// 解析时的唯一编号,防止解析混乱
private int uniqueNumber = 0;
/**
* DynamicContext的构造方法
* @param configuration 配置信息
* @param parameterObject 用户传入的查询参数对象
*/
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
// 获得参数对象的元对象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
// 判断参数对象本身是否有对应的类型处理器
boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
// 放入上下文信息
bindings = new ContextMap(metaObject, existsTypeHandler);
} else {
// 上下文信息为空
bindings = new ContextMap(null, false);
}
// 把参数对象放入上下文信息
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
// 把数据库id放入上下文信息
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
上下文环境 bindings属性中存储了以下信息。
- 数据库 id。因此在编写 SQL语句时,我们可以直接使用DATABASE_ID_KEY变量引用数据库 id的值。
- 参数对象。在编写 SQL 语句时,我们可以直接使用PARAMETER_OBJECT_KEY变量来引用整个参数对象。
- 参数对象的元数据。基于参数对象的元数据可以方便地引用参数对象的属性值,因此在编写 SQL语句时可以直接引用参数对象的属性。
DynamicContext中还有一个 ContextMap,它是 HashMap的子类。在进行数据查询时,DynamicContext会先从 HashMap中查询,如果查询失败则会从参数对象的属性中查询。正是基于这一点,我们可以在编写 SQL 语句时直接引用参数对象的属性。DynamicContext类的数据查询操作的源码:
/**
* 根据键索引值。会尝试从HashMap中寻找,失败后会再尝试从parameterMetaObject中寻找
* @param key 键
* @return 值
*/
@Override
public Object get(Object key) {
String strKey = (String) key;
// 如果HashMap中包含对应的键,直接返回
if (super.containsKey(strKey)) {
return super.get(strKey);
}
// 如果Map中不含有对应的键,尝试从参数对象的原对象中获取
if (parameterMetaObject == null) {
return null;
}
if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
return parameterMetaObject.getOriginalObject();
} else {
return parameterMetaObject.getValue(strKey);
}
}
阅读了动态上下文环境的源码,我们就知道为什么在书写映射文件时既能够直接引用实参,又能直接引用实参的属性。
4.3 SQL节点及其解析
MyBatis 有一个重要的优点是支持动态节点。可数据库本身并不认识这些节点,因此MyBatis 会先对这些节点进行处理后再交给数据库执行。这些节点在 MyBatis 中被定义为SqlNode。
SqlNode是一个接口,接口中只定义了一个 apply方法。该方法负责完成节点自身的解析,并将解析结果合并到输入参数提供的上下文环境中。
MyBatis 的 SQL 语句中支持许多种类的节点,如 if、where、foreach 等,它们都是SqlNode的子类。
接下来以常见并且典型的 IfSqlNode、ForEachSqlNode、TextSqlNode 为例,对SqlNode接口的实现类进行介绍。
4.3.1 IfSqlNode
IfSqlNode对应着数据库操作节点中的 if节点。通过 if节点可以让 MyBatis根据参数等信息决定是否写入一段 SQL片段。
<select id="selectUsersByNameOrSchoolName" parameterMap="userParam01" resultType="User">
SELECT * FROM `user`
<where>
<if test="name != null">
`name` = #{name}
</if>
<if test="schoolName != null">
AND `schoolName` = #{schoolName}
</if>
</where>
</select>
public class IfSqlNode implements SqlNode {
// 表达式评估器
private final ExpressionEvaluator evaluator;
// if判断时的测试条件
private final String test;
// if成立时,要被拼接的SQL片段信息
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
/**
* 完成该节点自身的解析
* @param context 上下文环境,节点自身的解析结果将合并到该上下文环境中
* @return 解析是否成功
*/
@Override
public boolean apply(DynamicContext context) {
// 判断if条件是否成立
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 将contents拼接到context
contents.apply(context);
return true;
}
return false;
}
}
IfSqlNode的 apply方法非常简单:直接调用表达式求值器计算if节点中表达式的值,如果表达式的值为真,则将 if 节点中的内容添加到环境上下文的末尾。
4.3.2 ForEachSqlNode
ForEachSqlNode 节点对应了数据库操作节点中的 foreach 节点。该节点能够对集合中的各个元素进行遍历,并将各个元素组装成一个新的 SQL 片段。
<select id="selectUsers" resultMap="userMapFull">
SELECT *
FROM `user`
WHERE `id` IN
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</select>
ForEachSqlNode类的 apply方法。主要流程是解析被迭代元素获得迭代对象,然后将迭代对象的信息添加到上下文中,之后再根据上下文信息拼接字符串。最后,在字符串拼接完成后,会对此次操作产生的临时变量进行清理,以避免对上下文环境造成的影响。
public static final String ITEM_PREFIX = "__frch_";
// 表达式求值器
private final ExpressionEvaluator evaluator;
// collection属性的值
private final String collectionExpression;
// 节点内的内容
private final SqlNode contents;
// open属性的值,即元素左侧插入的字符串
private final String open;
// close属性的值,即元素右侧插入的字符串
private final String close;
// separator属性的值,即元素分隔符
private final String separator;
// item属性的值,即元素
private final String item;
// index属性的值,即元素的编号
private final String index;
// 配置信息
private final Configuration configuration;
/**
* 完成该节点自身的解析
* @param context 上下文环境,节点自身的解析结果将合并到该上下文环境中
* @return 解析是否成功
*/
@Override
public boolean apply(DynamicContext context) {
// 获取环境上下文信息
Map<String, Object> bindings = context.getBindings();
// 交给表达式求值器解析表达式,从而获得迭代器
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) { // 没有可以迭代的元素
// 不需要拼接信息,直接返回
return true;
}
boolean first = true;
// 添加open字符串
applyOpen(context);
int i = 0;
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first || separator == null) { // 第一个元素
// 添加元素
context = new PrefixedContext(context, "");
} else {
// 添加间隔符
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) { // 被迭代对象是Map.Entry
// 将被迭代对象放入上下文环境中
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
// 将被迭代对象放入上下文环境中
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
// 根据上下文环境等信息构建内容
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
// 添加close字符串
applyClose(context);
// 清理此次操作对环境的影响
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
4.3.3 TextSqlNode
TextSqlNode 类对应了字符串节点,字符串节点的应用非常广泛,在 if 节点、foreach节点中也包含了字符串节点。
<select id="selectUser_B" resultMap="userMap">
select * FROM `user` WHERE `id` = #{id}
</select>
似乎 TextSqlNode对象本身就很纯粹不需要解析,其实并不是。TextSqlNode对象的解析是必要的,因为它能够替换掉其中的“${}”占位符。
在介绍 TextSqlNode 对象的解析之前,我们先介绍它的两个内部类:BindingTokenParser类和DynamicCheckerTokenParser类。BindingTokenParser 类和 DynamicCheckerTokenParser 类都是 TokenHandler 接口的子类,TextSqlNode相关类的类图
TokenHandler 接口会和通用占位符解析器GenericTokenParser 配合使用,当GenericTokenParser 解析到匹配的占位符时,会将占位符中的内容交给 TokenHandler 对象的 handleToken 方法处理。在 TextSqlNode 对象中,占位符就是“
”符号。那么遇到“
{}”符号。那么遇到“
”符号。那么遇到“{}”符号时,BindingTokenParser对象和 DynamicCheckerTokenParser对象分别会怎么处理呢?
- BindingTokenParser:该对象的 handleToken方法会取出占位符中的变量,然后使用该变量作为键去上下文环境中寻找对应的值。之后,会用找到的值替换占位符。因此,该对象可以完成占位符的替换工作。
- DynamicCheckerTokenParser:该对象的 handleToken 方法会赋值成员属性isDynamic。因此该对象可以记录自身是否遇到过占位符。
TextSqlNode类的 apply方法:
/**
* 完成该节点自身的解析
* @param context 上下文环境,节点自身的解析结果将合并到该上下文环境中
* @return 解析是否成功
*/
@Override
public boolean apply(DynamicContext context) {
// 创建通用的占位符解析器
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 替换掉其中的${}占位符
context.appendSql(parser.parse(text));
return true;
}
/**
* 创建一个通用的占位符解析器,用来解析${}占位符
* @param handler 用来处理${}占位符的专用处理器
* @return 占位符解析器
*/
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
在对“ ”占位符进行替换时,用到了 B i n d i n g T o k e n P a r s e r 内部类,它能够从上下文中取出“ {}”占位符进行替换时,用到了 BindingTokenParser内部类,它能够从上下文中取出“ ”占位符进行替换时,用到了BindingTokenParser内部类,它能够从上下文中取出“{}”占位符中的变量名对应的变量值。而 TextSqlNode类中还有一个 isDynamic方法,该方法用来判断当前的 TextSqlNode是不是动态的。对于 TextSqlNode对象而言,如果内部含有“${}”占位符,那它就是动态的,否则就不是动态的。
/**
* 判断当前节点是不是动态的
* @return 节点是否为动态
*/
public boolean isDynamic() {
// 占位符处理器,该处理器并不会处理占位符,而是判断是不是含有占位符
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
// 使用占位符处理器。如果节点内容中含有占位符,则DynamicCheckerTokenParser对象的isDynamic属性将会被置为true
parser.parse(text);
return checker.isDynamic();
}
因此 BindingTokenParser内部类具有替换字符串的能力,会在TextSqlNode类的解析方法 apply中发挥作用;DynamicCheckerTokenParser内部类具有记录能力,会在TextSqlNode类的判断是否为动态方法 isDynamic中发挥作用。
5.SqlSource
语言驱动类完成的主要工作就是生成SqlSource,在语言驱动接口LanguageDriver的三个方法中,有两个方法是用来生成SqlSource 的。而 SqlSource 子类的转化工作也主要在scripting包中完成。
SqlSource接口的四种实现类及它们的区别:
- DynamicSqlSource:动态 SQL语句。所谓动态 SQL语句是指含有动态 SQL节点(如if节点)或者含有“${}”占位符的语句。
- RawSqlSource:原生 SQL语句。指非动态语句,语句中可能含有“#{}”占位符,但不含有动态 SQL节点,也不含有“${}”占位符。
- StaticSqlSource:静态语句。语句中可能含有“?”,可以直接提交给数据库执行。
- ProviderSqlSource:上面的几种都是通过 XML 文件获取的SQL 语句,而ProviderSqlSource是通过注解映射的形式获取的SQL语句。
5.1 SqlSource的生成
- 解析映射文件生成
LanguageDriver 中接口createSqlSource方法用来解析映射文件中的节点信息,从中获得SqlSource对象。
/**
* 创建SqlSource对象(基于映射文件的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
* @param configuration 配置信息
* @param script 映射文件中的数据库操作节点
* @param parameterType 参数类型
* @return SqlSource对象
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
在 XMLLanguageDriver类中可以看出,SqlSource对象主要由XMLScriptBuilder的parseScriptNode方法生成,而该方法生成的 SqlSource 对象是 DynamicSqlSource 对象或者RawSqlSource 对象。
/**
* 创建SqlSource对象(基于映射文件的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
* @param configuration 配置信息
* @param script 映射文件中的数据库操作节点
* @param parameterType 参数类型
* @return SqlSource对象
*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
/**
* 解析节点生成SqlSource对象
* @return SqlSource对象
*/
public SqlSource parseScriptNode() {
// 解析XML节点节点,得到节点树MixedSqlNode
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 根据节点树是否为动态,创建对应的SqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
因此,解析映射文件生成的 SqlSource对象是DynamicSqlSource对象和 RawSqlSource对象中的一种。
- 解析注解信息生成 SqlSource
ProviderSqlSource类是SqlSource接口的子类。并且,ProviderSqlSource类通过调用 LanguageDriver接口中的 createSqlSource(Configuration,String,Class<?>)方法给出了另一个 SqlSource子类。
LanguageDriver接口中的 createSqlSource(Configuration,String,Class<?>)它能根据注解中的信息生成 SqlSource。
/**
* 创建SqlSource对象(基于注解的方式)。该方法在MyBatis启动阶段,读取映射接口或映射文件时被调用
* @param configuration 配置信息
* @param script 注解中的SQL字符串
* @param parameterType 参数类型
* @return SqlSource对象,具体来说是DynamicSqlSource和RawSqlSource中的一种
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
实现在 XMLLanguageDriver 类中:
// 创建SQL源码(注解方式)
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
if (script.startsWith("<script>")) {
// 如果注解中的内容以<script>开头
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// 如果注解中的内容不以<script>开头
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
根据注解中的字符串是否以“<script>”开头将注解中的 SQL信息分成了两类,从而分别处理以下代码中的两种情况。
@Select("SELECT * FROM `user` WHERE `id` = #{id}")
User queryUserById(Integer id);
@Select("<script>" +
" SELECT *\n" +
" FROM `user`\n" +
" WHERE id IN\n" +
" <foreach item=\"id\" collection=\"array\" open=\"(\" separator=\",\" close=\")\">\n" +
" #{id}\n" +
" </foreach>\n" +
" </script>")
List<User> queryUsersByIds(int[] ids);
- 对于以“<script>”开头的 SQL语句,将使用和映射文件相同的解析方式,从而生成 DynamicSqlSource对象或者RawSqlSource对象;
- 对于不以“<script>”开头的 SQL 语句,则直接生成DynamicSqlSource 对象或者RawSqlSource对象。
首先,解析注解信息生成的 SqlSource 对象是ProviderSqlSource 对象;然后,ProviderSqlSource对象通过LanguageDriver接口中的 createSqlSource(Configuration,String,Class<?>)方法转化为了 DynamicSqlSource对象或者 RawSqlSource对象。
5.2 DynamicSqlSource的转化
DynamicSqlSource类在 scripting包的 xmltags子包中,它表示含有动态 SQL节点(如if节点)或者含有“${}”占位符的语句,即动态 SQL语句。DynamicSqlSource和 RawSqlSource都会转化为 StaticSqlSource,然后才能给出一个 BoundSql对象。
/**
* SqlSource的重要实现,用以解析动态SQL语句。
*/
public class DynamicSqlSource implements SqlSource {
/**
* 获取一个BoundSql对象
* @param parameterObject 参数对象
* @return BoundSql对象
*/
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建DynamicSqlSource的辅助类,用来记录DynamicSqlSource解析出来的
// * SQL片段信息
// * 参数信息
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 这里会逐层(对于mix的node而言)调用apply。最终不同的节点会调用到不同的apply,完成各自的解析
// 解析完成的东西拼接到DynamicContext中,里面含有#{}
// 在这里,动态节点和${}都被替换掉了。
rootSqlNode.apply(context);
// 处理占位符、汇总参数信息
// RawSqlSource也会焦勇这一步
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 使用SqlSourceBuilder处理#{},将其转化为?
// 相关参数放进了context.bindings
// *** 最终生成了StaticSqlSource对象,然后由它生成BoundSql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 把context.getBindings()的参数放到boundSql的metaParameters中进行保存
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
DynamicSqlSource类的 getBoundSql方法有两步非常关键的操作:
- 在这里会从根节点开始对各个节点逐层调用 apply 方法。经过这一步后,所有的动态节点和“${}”都会被替换。这样DynamicSqlSource便不再是动态的,而是静态的。
rootSqlNode.apply(context);
- 这里会完成“#{}”符号的替换,并且返回一个StaticSqlSource对象
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
/**
* 将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource
* @param originalSql sqlNode.apply()拼接之后的sql语句。已经不包含<if> <where>等节点,也不含有${}符号
* @param parameterType 实参类型
* @param additionalParameters 附加参数
* @return 解析结束的StaticSqlSource
*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 用来完成#{}处理的处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 通用的占位符解析器,用来进行占位符替换
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 将#{}替换为?的SQL语句
String sql = parser.parse(originalSql);
// 生成新的StaticSqlSource对象
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
5.3 RawSqlSource的转化
相比于DynamicSqlSource类,RawSqlSource类要更为简单,因为它不包含动态节点和“${}”占位符,只包含“#{}”占位符。RawSqlSource类在构造方法中就完成了到 StaticSqlSource的转化。
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 处理RawSqlSource中的“#{}”占位符,得到StaticSqlSource
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
可见 RawSqlSource在构造方法中完成了“#{}”占位符的处理,得到 StaticSqlSource对象并放入自身的 sqlSource属性中。而之后的 getBoundSql操作中,BoundSql对象就直接由sqlSource属性中持有的 StaticSqlSource对象返回。
5.4 SqlSource接口实现类总结
SqlSource 接口有四个实现类,其中三个实现类的对象都通过层层转化变成了StaticSqlSource 对象。然后,SqlSource 接口中定义的 getBoundSql 抽象方法实际都是由StaticSqlSource对象完成的。
SqlSource接口的实现类之间的转化过程其实就是数据库操作语句的解析过程。在这个转化过程中,注解中的 SQL语句被分类处理,动态语句被展开,“${}”占位符被赋值,“#{}”占位符被替换,最终得到了可以交给数据库驱动执行的仅包含参数占位符“?”的SQL语句。
通过将类的转化过程梳理出来并总结成一张图,能让我们对整个数据库操作语句的转化过程有一个清晰且直观的认知。在进行源码阅读的过程中,将类的转化、状态的转化、信息的传递等过程总结成一张图片,是避免在杂乱的逻辑中迷失的一种良好手段。