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

1. #{name}和${name}的区别。

#{name}:表示这是一个参数(ParameterMapping)占位符,值来自于运行时传递给sql的参数,也就是XXXMapper.xml里的parameterType。其值通过PreparedStatement的setObject()等方法赋值。

动态sql中的标签绑定的值,也是使用#{name}来使用的。

#{name}用在sql文本中。

${name}:表示这是一个属性配置占位符,值来自于属性配置文件,比如jdbc.properties,其值通过类似replace方法进行静态替换。比如${driver},将被静态替换为com.mysql.jdbc.Driver。

${name}则可以用在xml的Attribute属性,还可以用在sql文本当中。

select count(1) from (

select

stud_id as studId

, name, email

, dob

, phone

from students #{offset}, ${driver}

) tmp

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

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在解析标签时,就已经静态替换${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 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());

}

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)。

public SqlSource parse(String originalSql, Class> parameterType, Map 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 parameterMappings = new ArrayList();

private Class> parameterType;

private MetaObject metaParameters;

public ParameterMappingTokenHandler(Configuration configuration, Class> parameterType, Map additionalParameters) {

super(configuration);

this.parameterType = parameterType;

this.metaParameters = configuration.newMetaObject(additionalParameters);

}

public List 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 entry : context.getBindings().entrySet()) {

boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());

}

return boundSql;

}

}

BoundSql不仅保存了最终的可执行的sql,还保存了sql中?号占位符的参数列表。

public class BoundSql {

private String sql;

private List parameterMappings;

// ...

}

最后,在执行sql时,通过org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(PreparedStatement)方法,遍历List 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 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 mapEntry = (Map.Entry) 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。感谢大家看到最后,我是不务正业的程序汪,文章每天持续更新!欢迎大家指出我的文章的不足之处,也欢迎大家关注、点赞+转发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值