Mybatis源码-动态SQL实现
目录
动态SQL指的是事先无法预知具体的条件,需要在运行时根据具体的情况动态地生成SQL语句。假设我们有一个获取用户信息查询操作,具体的查询条件是不确定的,取决于Web前端表单提交的数据,可能根据用户的Id进行查询,也可能根据用户手机号或姓名进行查询,还有可能是这几个条件的组合。
这个时候就需要使用MyBatis的动态SQL特性了。
<select id="getUserByEntity" resultType="User">
select
<include refid="userAllField"/>
from user
<where>
<if test="id != null">
AND id = #{id}
</if>
<if test="name != null">
AND name = #{name}
</if>
<if test="phone != null">
AND phone = #{phone}
</if>
</where>
</select>
除了<if>还有<choose/when/otherwise>,<foreach>
<trimIset>:这两个标签的作用和<where>标签的作用类似,用于WHERE子句中因为不同的条件成立时导致AND或OR关键字多余,或者SET子句中出现多余的逗号问题。
SqlSource与BoundSql #
MyBatis 中和 SQL 语句有关的两个组件,即 SqlSource 和BoundSql
MyBatis中的 SqlSource用于描述SQL资源,通过前面章节的介绍,我们知道MyBatis可以通过两种方式配置SQL信息,一种是通过@Selelect、@Insert、@Delete、@Update或者@SelectProvider、@InsertProvider、@DeleteProvider、@UpdateProvider 等注解;另一种是通过XML 配置文件。
SqlSource就代表Java注解或者XML文件配置的 SQL资源。下面是SqlSource接口的定义:
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
这4种SqlSource实现类的作用如下。
- ProviderSqlSource:用于描述通过@Select、@SelectProvider等注解配置的SQL资源信息。
- DynamicSqISource:用于描述MapperXML文件中配置的 SQL资源信息,这些SQL通常包含动态SQL配置或者${}参数占位符,需要在Mapper调用时才能确定具体的SQL语句。
- RawSqISource:用于描述MapperXML文件中配置的SQL资源信息,与DynamicSqlSource不同的是,这些SQL语句在解析XML配置的时候就能确定,即不包含动态SQL相关配置。
- StaticSqISource:用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource 解析后得到的静态SQL资源。
无论是Java注解还是XML文件配置的SQL信息,在Mapper调用时都会根据用户传入的参数将Mapper配置转换为 StaticSqlSource类。我们不妨了解一下StaticSqlSource类的实现。
public class StaticSqlSource implements SqlSource {
// Mapper解析后的sql内容
private final String sql;
// 参数映射信息
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
BoundSql是对Executor组件执行SQL信息的封装,具体实现代码如下:
public class BoundSql {
// Mapper配置解析后的sql语句
private final String sql;
// Mapper参数映射信息
private final List<ParameterMapping> parameterMappings;
// Mapper参数对象
private final Object parameterObject;
// 额外参数信息,包括<bind>标签绑定的参数,内置参数
private final Map<String, Object> additionalParameters;
// 参数对象对应的MetaObject对象
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<String, Object>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
如上面的代码所示,BoundSql除了封装了Mapper解析后的SQL语句和参数映射信息外,还封装了Mapper调用时传入的参数对象。另外,MyBatis任意一个Mapper都有两个内置的参数,即**_parameter和_databaseId**。_parameter代表整个参数,包括<bind>标签绑定的参数信息,这些参数存放在BoundSql对象的additionalParameters属性中。_databaseId为Mapper配置中通过databaseld属性指定的数据库类型。
LanuageDriver #
怎么生成的BoundSql和SqlSource的?通过这个类LanuageDriver。SQL配置信息到SqlSource对象的转换是由LanguageDriver组件来完成的
public interface LanguageDriver {
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
MyBatis 中为LanguageDriver 接口提供了两个实现类,分别为XMLLanguageDriver 和RawLanguageDriver。
- XMLLanguageDriver为XML语言驱动,为MyBatis提供了通过XML标签(我们常用的<if>、<where>等标签)结合OGNL表达式语法实现动态SQL的功能。
- RawLanguageDriver表示仅支持静态SQL配置,不支持动态SQL功能。
追踪一下XMLLanguageDriver:
public class XMLLanguageDriver implements LanguageDriver {
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 该方法用于解析XML文件中配置的SQL信息
// 创建XMLScriptBuilder对象
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源
return builder.parseScriptNode();
}
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// 该方法用于解析Java注解中配置的SQL信息
// 字符串以<script>标签开头,则以XML方式解析
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// 解析SQL配置中的全局变量
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
// 如果SQL中是否仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}
}
从XMLLanguageDriver类的createSqlSource()方法的实现来看,我们除了可以通过XML配置文件结合OGNL表达式配置动态SQL外,还可以通过Java注解的方式配置,只需要注解中的内容加上<script>标签。下面是使用Java注解配置动态SQL的案例代码:
@Select("<script>" +
"select * from user\n" +
"<where>\n" +
" <if test=\"name != null\">\n" +
" AND name = #{name}\n" +
" </if>\n" +
" <if test=\"phone != null\">\n" +
" AND phone = #{phone}\n" +
" </if>\n" +
"</where>" +
"</script>")
UserEntity getUserByPhoneAndName(@Param("phone") String phone, @Param("name") String name);
自定义脚本驱动 #
要实现自定义的脚本语言驱动,只需要实现 LanguageDriver 接口,创建自定义的 SqlSource对象,然后对SqlSource对象进行解析,生成最终的BoundSql对象即可。有兴趣的读者可以参考velocity-scripting模块的源码,该模块为MyBatis的Mapper配置提供Velocity语法支持。
<dependency>
<groupId>org.mybatis.scripting</groupId>
<artifactId>mybatis-velocity</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<typeAliases>
<typeAlias alias="velocityDriver" type="org.mybatis.scripting.velocity.Driver"/>
<typeAlias alias="User" type="com.blog4java.mybatis.example.entity.UserEntity"/>
</typeAliases>
<select id="getUserByNames" lang="velocityDriver" resultType="User">
select * from user
#where()
#in( $_parameter.names $name "name" )
@{name}
#end
#end
</select>
需要注意的是,在配置Mapper时,需要通过 lang属性指定velocity-scripting模块中定义LanguageDriver的别名。
SqlNode详解 #
SqINode 接口的内容非常简单,只有一个apply()方法,该方法用于解析SQL节点,根据参数信息生成静态SQL内容。apply()方法需要接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper调用时传入的参数信息及MyBatis内置的parameter和_databaseId参数。
public interface SqlNode {
boolean apply(DynamicContext context);
}
每种标签对应不同的实现类其中:
- MixedSqINode:用于描述一组SqlNode对象,通常一个Mapper配置是由多个SqlNode对象组成的,这些SqINode对象通过MixedSqlNode进行关联,组成一个完整的动态SQL配置。
- StaticTextSqINode:用于描述动态SQL中的静态文本内容
- TextSqINode:该类与StaticTextSqINode类不同的是,当静态文本中包含${}占位符时,说明${}需要在Mapper调用时将${}替换为具体的参数值。因此,使用TextSqINode类来描述。
- VarDecISqINode:用于描述动态SQL中的<bind>标签,动态SQL解析时,会把<bind>标签配置信息转换为VarDeclSqINode对象。
SqINode与动态SQL配置之间的对应关系。
SqINode对象创建完毕后,我们就可以调用MixedSqINode的apply()方法根据参数内容动态地生成SQL内容了。该方法接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper 调用时的参数信息。
上面的代码中,我们创建了一个DynamicContext,然后调用MixedSqlNode对象的apply()方法,动态SQL的解析结果封装在DynamicContext对象中,我们只需要调用DynamicContext对象的getSql()方法即可获取动态SQL解析后的SQL语句。运行上面这段代码后,生成的SQL内容如下:select * from user where 1=l AND id= #{id}
们再来了解一下SqINode解析生成SQL语句的过程。首先来看MixedSqINode的实现,代码如下:
public class MixedSqlNode implements SqlNode {
// 通过一个List对象维护所有的SqINode对象
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
// MixedSqlNode 类的apply()方法中对所有SqINode对象进行遍历
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true;
}
}
//StaticTextSqINode的解析
public class StaticTextSqlNode implements SqlNode {
// 静态SQL文本内容
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
// 追加SQL内容
context.appendSql(text);
return true;
}
}
// IfSqlNode的解析
public class IfSqlNode implements SqlNode {
// evaluator属性用于解析OGNL表达式
private final ExpressionEvaluator evaluator;
// 保存<if>标签test属性内容
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();
}
@Override
public boolean apply(DynamicContext context) {
// 如果OGNL表达式值为true,则调用<if>标签内容对应的SqlNode的apply()方法
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
动态SQL的解析过程 #
从XML解析配置追踪:
// org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
// 解析mapper标签的配置
// org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
// 开始mapper的每个部分
private void configurationElement(XNode context) {
try {
// 获取命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 设置当前正在解析的Mapper配置的命名空间
builderAssistant.setCurrentNamespace(namespace);
// 解析<cache-ref>标签
cacheRefElement(context.evalNode("cache-ref"));
// 解析<cache>标签
cacheElement(context.evalNode("cache"));
// 解析所有的<parameterMap>标签
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析所有的<resultMap>标签
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析所有的<sql>标签
sqlElement(context.evalNodes("/mapper/sql"));
// 解析所有的<select|insert|update|delete>标签
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 通过XMLStatementBuilder对象,对<select|update|insert|delete>标签进行解析
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 调用parseStatementNode()方法解析
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
public void parseStatementNode() {
// ...
// 获取LanguageDriver对象
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 通过LanguageDriver解析SQL内容,生成SqlSource对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// ...
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
// 调用的是 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, org.apache.ibatis.parsing.XNode, java.lang.Class<?>)
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 该方法用于解析XML文件中配置的SQL信息
// 创建XMLScriptBuilder对象
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源
return builder.parseScriptNode();
}
public SqlSource parseScriptNode() {
// 调用parseDynamicTags()方法將SQL配置转换为SqlNode对象
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
// 判断Mapper SQL配置中是否包含动态SQL元素,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
// 对XML子元素进行遍历
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
// 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 判断SQL文本中包含${}参数占位符,则为动态SQL
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 如果SQL文本中不包含${}参数占位符,则不是动态SQL
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
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;
}
}
return new MixedSqlNode(contents);
}
如上面的代码所示,XMLScriptBuilder类的parseDynamicTags()方法的逻辑相当复杂,在该方法中对SQL配置的所有子元素进行遍历,如果子元素类型为SQL文本,则使用TextSqINode对象描述SQL节点信息,若SQL节点中存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;
如果子元素为
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
SqINode对象是如何根据调用Mapper时传入的参数动态生成SQL语句的呢?
动态SQL标签解析完成后,将解析后生成的SqlINode对象封装在 SqlSource对象中。
MyBatis中的MappedStatement用于描述Mapper中的 SQL配置,SqlSource创建完毕后,最终会存放在MappedStatement对象的sqlSource属性中。
Executor组件操作数据库时,会调用MappedStatement对象的getBoundSql()方法获取BoundSql对象,代码如下:
// org.apache.ibatis.mapping.MappedStatement#getBoundSql public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) { boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); } // check for nested result maps in parameter mappings (issue #30) for (ParameterMapping pm : boundSql.getParameterMappings()) { String rmId = pm.getResultMapId(); if (rmId != null) { ResultMap rm = configuration.getResultMap(rmId); if (rm != null) { hasNestedResultMaps |= rm.hasNestedResultMaps(); } } } return boundSql; } // org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql public BoundSql getBoundSql(Object parameterObject) { // 通过参数对象,创建动态SQL上下文对象 DynamicContext context = new DynamicContext(configuration, parameterObject); // 以DynamicContext对象作为参数调用SqlNode的apply()方法 rootSqlNode.apply(context); // 创建SqlSourceBuilder对象 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); // 调用DynamicContext的getSql()方法获取动态SQL解析后的SQL内容, // 然后调用SqlSourceBuilder的parse()方法对SQL内容做进一步处理,生成StaticSqlSource对象 SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); // 调用StaticSqlSource对象的getBoundSql()方法,获得BoundSql实例 BoundSql boundSql = sqlSource.getBoundSql(parameterObject); // 將<bind>标签绑定的参数添加到BoundSql对象中 for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } // 最终生成的SQL语句 return boundSql; } public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { // ParameterMappingTokenHandler为Mybatis参数映射处理器,用于处理SQL中的#{}参数占位符 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); // Token解析器,用于解析#{}参数 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); // 调用GenericTokenParser对象的parse()方法將#{}参数占位符转换为? String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); } // GenericTokenParser对象的parse() public String parse(String text) { if (text == null || text.isEmpty()) { return ""; } // 获取第一个openToken在SQL中的位置 int start = text.indexOf(openToken, 0); // start为-1说明SQL中不存在任何参数占位符 if (start == -1) { return text; } // 將SQL转换为char数组 char[] src = text.toCharArray(); // offset用于记录已解析的#{或者}的偏移量,避免重复解析 int offset = 0; final StringBuilder builder = new StringBuilder(); // expression为参数占位符中的内容 StringBuilder expression = null; // 遍历获取所有参数占位符的内容,然后调用TokenHandler的handleToken()方法替换参数占位符 while (start > -1) { if (start > 0 && src[start - 1] == '\\') { // this open token is escaped. remove the backslash and continue. builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { // found open token. let's search close token. if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) { if (end > offset && src[end - 1] == '\\') { // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else { expression.append(src, offset, end - offset); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { // 调用TokenHandler的handleToken()方法替换参数占位符 builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); }
在GenericTokenParser的parse()方法中,对SQL配中的所有#{}参数占位符进行解析,获取参数占位符的内容,然后调用ParameterMappingTokenHandler的handleToken()方法对参数占位符内容进行替换。
public String handleToken(String content) { parameterMappings.add(buildParameterMapping(content)); return "?"; }
从上面的代码可以看出,SQL配置中的所有#{}参数占位符内容都被替换成了“?”字符,MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。
除此之外,handleToken()方法中还做了另一件事情,就是调用buildParameterMapping()方法对占位符内容进行解析,将占位符内容转换为ParameterMapping对象。
ParameterMapping对象用于描述MyBatis参数映射信息,便于后续根据参数映射信息获取对应的TypeHandler为PreparedStatement 对象设置值。buildParameterMapping()方法解析参数占位符生成ParameterMapping对象的过程如下:
private ParameterMapping buildParameterMapping(String content) { // 將#{}占位符内容转换为Map对象 Map<String, String> propertiesMap = parseParameterMapping(content); // property 对应的值为参数占位符名称,例如userId String property = propertiesMap.get("property"); // 推断参数类型 Class<?> propertyType; // 如果内置参数,或<bind>标签绑定的参数包含该属性,则参数类型为Getter方法返回值类型 if (metaParameters.hasGetter(property)) { propertyType = metaParameters.getGetterType(property); // 判读该参数类型是否注册了TypeHandler,如果注册了则使用参数类型 } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; // 如果指定了jdbcType属性,并且为CURSOR类型,则使用ResultSet类型 } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { propertyType = java.sql.ResultSet.class; // 如果参数类型为Map接口的子类型,则使用Object类型 } else if (property == null || Map.class.isAssignableFrom(parameterType)) { propertyType = Object.class; } else { // 获取parameterType对应的MetaClass对象,方便获取参数类型的反射信息 MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); // 如果参数类型中包含该属性,则使用Getter方法返回类型 if (metaClass.hasGetter(property)) { propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } // 使用建造者模式构建ParameterMapping对象 ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); Class<?> javaType = propertyType; String typeHandlerAlias = null; for (Map.Entry<String, String> entry : propertiesMap.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); // 指定ParameterMapping对象的属性 if ("javaType".equals(name)) { javaType = resolveClass(value); builder.javaType(javaType); } else if ("jdbcType".equals(name)) { builder.jdbcType(resolveJdbcType(value)); } else if ("mode".equals(name)) { builder.mode(resolveParameterMode(value)); } else if ("numericScale".equals(name)) { builder.numericScale(Integer.valueOf(value)); } else if ("resultMap".equals(name)) { builder.resultMapId(value); } else if ("typeHandler".equals(name)) { typeHandlerAlias = value; } else if ("jdbcTypeName".equals(name)) { builder.jdbcTypeName(value); } else if ("property".equals(name)) { // Do Nothing } else if ("expression".equals(name)) { throw new BuilderException("Expression based parameters are not supported yet"); } else { throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties); } } if (typeHandlerAlias != null) { builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); } // 返回ParameterMapping对象 return builder.build(); }
在ParameterMappingTokenHandler类的buildParameterMapping()方法中首先将参数占位符内容转换为Map对象,然后构建者模式创建对象。
#{}和${}的区别 #
看${}参数占位符的解析过程。当动态SQL配置中存在${}参数占位符时,MyBatis会使用TextSqINode对象描述对应的SQL节点,在调用TextSqlINode对象的apply()方法时会完成动态SQL的解析。也就是说,${}参数占位符的解析是在TextSqlINode类的apply()方法中完成的,下面是该方法的实现:
@Override
public boolean apply(DynamicContext context) {
// 通过GenericTokenParser对象解析${}参数占位符,使用BindingTokenParser对象处理参数占位符内容
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
// 调用GenericTokenParser对象的parse()方法解析
context.appendSql(parser.parse(text));
return true;
}
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
public class GenericTokenParser {
private final String openToken;
private final String closeToken;
private final TokenHandler handler;
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// 获取第一个openToken在SQL中的位置
int start = text.indexOf(openToken, 0);
// start为-1说明SQL中不存在任何参数占位符
if (start == -1) {
return text;
}
// 將SQL转换为char数组
char[] src = text.toCharArray();
// offset用于记录已解析的#{或者}的偏移量,避免重复解析
int offset = 0;
final StringBuilder builder = new StringBuilder();
// expression为参数占位符中的内容
StringBuilder expression = null;
// 遍历获取所有参数占位符的内容,然后调用TokenHandler的handleToken()方法替换参数占位符
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 调用TokenHandler的handleToken()方法替换参数占位符
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
上面代码的核心内容是遍历获取所有SO参数占位符的内容,然后调用BindingTokenParser对象的handleToken()方法对参数占位符内容进行替换。BindingTokenParser类的handleToken()方法实现如下:
public String handleToken(String content) {
// 获取Mybatis内置参数_parameter,_parameter属性中保存所有参数信息
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
// 將参数对象添加到ContextMap对象中
context.getBindings().put("value", parameter);
}
// 通过OGNL表达式获取参数值
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
checkInjection(srtValue);
// 返回参数值
return srtValue;
}
上面的内容是一条不合法的SQL语句,因此会执行失败,如果希望${}参数占位符解析后生成正确的SQL语句,则可以在参数内容前后加上一个单引号,具体如下:
public void testGetUserByName() {
String userName = "'Test4'";
UserEntity userEntity = userMapper.getUserByName(userName);
System.out.println(userEntity);
}
最后我们再来总结一下#{}和${}参数占位符的区别。使用#{}参数占位符时,占位符内容会被替换成"?",然后通过PreparedStatement对象的setXXX()方法为参数占位符设置值;而${}参数占位符内容会被直接替换为参数值。使用#{}参数占位符能够有效避免SQL注入问题,所以我们可以优先考虑使用#占位符,当#{}参数占位符无法满足需求时,才考虑使用${}参数占位符。