查看原文
其他

从源码的角度,来阐述#{name}和${name}的区别?面试官直呼“好”~

点击关注 👉 Java面试那些事儿 2021-09-05

来源: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}的工作原理


#{name}是ParameterMapping参数占位符,Mybatis将会把#{name}替换为?号,并通过OGNL来计算#{xxx}内部的OGNL表达式的值,作为PreparedStatement的setObject()的参数值。


举例:#{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,该公众号主要为大家分享有趣有料的开发者工具,还有老鬼给你带路,永不迷路~

扫描关注,永不迷路


点击阅读原文,获得更多精彩内容!
: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存