从源码的角度,来阐述#{name}和${name}的区别?面试官直呼“好”~
来源:https://my.oschina.net/zudajun/blog/735731
本篇文章重点阐述一些动态sql的技术细节,#{name}和${name}的区别,将在本篇文章中揭晓。也许读者早已了解它们之间的区别,但是,作为技术内幕,我们不仅要了解它们的区别,还要介绍它们的工作原理,是不是很开森呢?
1. #{name}和${name}的区别。
#{name}:表示这是一个参数(ParameterMapping)占位符,值来自于运行时传递给sql的参数,也就是XXXMapper.xml里的parameterType。其值通过PreparedStatement的setObject()等方法赋值。
动态sql中的<bind>标签绑定的值,也是使用#{name}来使用的。
#{name}用在sql文本中。
${name}:表示这是一个属性配置占位符,值来自于属性配置文件,比如jdbc.properties,其值通过类似replace方法进行静态替换。比如${driver},将被静态替换为com.mysql.jdbc.Driver。
${name}则可以用在xml的Attribute属性,还可以用在sql文本当中。
<select id="countAll" resultType="${driver}">
select count(1) from (
select
stud_id as studId
, name, email
, dob
, phone
from students #{offset}, ${driver}
) tmp
</select>
2. ${name}的工作原理
org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()部分源码。
public void parseStatementNode() {
//...
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// ...
}
org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)部分源码。
private void applyIncludes(Node source, final Properties variablesContext) {
if (source.getNodeName().equals("include")) {
// new full context for included SQL - contains inherited context and new variables from current include node
Properties fullContext;
String refid = getStringAttribute(source, "refid");
// replace variables in include refid value
refid = PropertyParser.parse(refid, variablesContext);
Node toInclude = findSqlFragment(refid);
Properties newVariablesContext = getVariablesContext(source, variablesContext);
if (!newVariablesContext.isEmpty()) {
// merge contexts
fullContext = new Properties();
fullContext.putAll(variablesContext);
fullContext.putAll(newVariablesContext);
} else {
// no new context - use inherited fully
fullContext = variablesContext;
}
applyIncludes(toInclude, fullContext);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
NodeList children = source.getChildNodes();
for (int i=0; i<children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext);
}
} else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
// replace variables in all attribute values
// 通过PropertyParser替换所有${xxx}占位符(attribute属性)
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
} else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
// replace variables ins all text nodes
// 通过PropertyParser替换所有${xxx}占位符(文本节点)
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
也就是说,Mybatis在解析<include>标签时,就已经静态替换${name}占位符了。
public class PropertyParser {
private PropertyParser() {
// Prevent Instantiation
}
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
private static class VariableTokenHandler implements TokenHandler {
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
@Override
public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
}
3. #{name}的工作原理
举例:#{item.name}将被替换为sql的?号占位符,item.name则是OGNL表达式,OGNL将计算item.name的值,作为sql的?号占位符的值。
如果只有静态sql,#{name}将在解析xml文件时,完成替换为?占位符。如果有动态sql的内容,#{name}将在执行sql时,动态替换为?占位符。
org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.parseScriptNode()。
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 在这里完成#{xxx}替换为?号
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
// 创建RawSqlSource时,就完成sql的拼接工作,因为它没有动态sql的内容,Mybatis初始化时,就能确定最终的sql。
rootSqlNode.apply(context);
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
org.apache.ibatis.builder.SqlSourceBuilder.parse(String, Class<?>, Map<String, Object>)。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 使用ParameterMappingTokenHandler策略来处理#{xxx}
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
GenericTokenParser.java是通用解析占位符的工具类,它可以解析${name}和#{name},那么,解析到${name}和#{name}后,要如何处理这样的占位符,则由不同的策略TokenHandler来完成。
4. TokenHandler
GenericTokenParser.java负责解析sql中的占位符${name}和#{name},TokenHandler则是如何处理这些占位符。
ParameterMappingTokenHandler:处理#{xxx}占位符。
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
private Class<?> parameterType;
private MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
@Override
public String handleToken(String content) {
// 创建一个ParameterMapping对象,并返回?号占位符
parameterMappings.add(buildParameterMapping(content));
return "?";
}
//..
}
VariableTokenHandler:处理${xxx}占位符。
private static class VariableTokenHandler implements TokenHandler {
private Properties variables;
public VariableTokenHandler(Properties variables) {
this.variables = variables;
}
@Override
public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
DynamicCheckerTokenParser:空实现,动态sql标签,都由它来标识。
BindingTokenParser:用于在注解Annotation中处理${xxx},待研究。
至此,${name}将直接替换为静态Properties的静态属性值,而#{name}将被替换为?号,并同时创建了ParameterMapping对象,绑定到参数列表中。
5. DynamicSqlSource生成sql的原理
对于RawSqlSource,由于是静态的sql,Mybatis初始化时就生成了最终可以直接使用的sql语句,即在创建RawSqlSource时,就直接生成。而DynamicSqlSource,则是执行sql时,才动态生成。
public class DynamicSqlSource implements SqlSource {
private Configuration configuration;
private SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 逐一调用各种SqlNode,拼接sql
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
BoundSql不仅保存了最终的可执行的sql,还保存了sql中?号占位符的参数列表。
public class BoundSql {
private String sql;
private List<ParameterMapping> parameterMappings;
// ...
}
最后,在执行sql时,通过org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(PreparedStatement)方法,遍历List<ParameterMapping> parameterMappings = boundSql.getParameterMappings()来逐一对sql中的?号占位符进行赋值操作。
整个sql处理变量占位符的流程就完成了。
6. OGNL表达式运算完成动态sql拼接
我们就举一个略微复杂一点的ForEachSqlNode的拼接sql原理。
public class ForEachSqlNode implements SqlNode {
// OGNL表达式计算器
private ExpressionEvaluator evaluator;
//...
@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;
applyOpen(context);
int i = 0;
// 遍历拼接sql
for (Object o : iterable) {
DynamicContext oldContext = context;
if (first) {
context = new PrefixedContext(context, "");
} else if (separator != null) {
context = new PrefixedContext(context, separator);
} else {
context = new PrefixedContext(context, "");
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
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++;
}
applyClose(context);
return true;
}
//...
}
Mybatis的全部动态sql内容,至此就全部介绍完了,在实际工作中,绝大多数的sql,都是动态sql。
最后,推荐给大家一个有趣有料的公众号:程序员Tools,该公众号主要为大家分享有趣有料的开发者工具,还有老鬼给你带路,永不迷路~
扫描关注,永不迷路