MyBatis源码解析——动态SQL实现原理_mybatis注解动态sql provider

本文深入解析MyBatis动态SQL的实现原理,重点介绍了TrimSqlNode和IfSqlNode的作用。TrimSqlNode用于删除SQL中的特定前缀和后缀,实现动态SQL构建。IfSqlNode则根据条件决定是否插入相应的SQL片段,实现条件判断。文章通过代码示例详细阐述了这两个节点的内部逻辑和工作流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
img

正文

private final String prefix; // SQL语句的前缀
private final String suffix; // SQL语句的后缀
private final List prefixesToOverride; // 待重写的前缀
private final List suffixesToOverride; // 待重写的后缀
private final Configuration configuration;

public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}

protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List prefixesToOverride, String suffix, List suffixesToOverride) {
this.contents = contents;
this.prefix = prefix;
this.prefixesToOverride = prefixesToOverride;
this.suffix = suffix;
this.suffixesToOverride = suffixesToOverride;
this.configuration = configuration;
}

@Override
public boolean apply(DynamicContext context) {
// 创建过滤动态上下文
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);

// 调用委托的SqlNode的apply()方法来解析FilteredDynamicContext
boolean result = contents.apply(filteredDynamicContext);

// 过滤掉脚本中的prefixesToOverride和suffixesToOverride部分
filteredDynamicContext.applyAll();
return result;
}

private static List parseOverrides(String overrides) {
if (overrides != null) {
final StringTokenizer parser = new StringTokenizer(overrides, “|”, false);
final List list = new ArrayList<>(parser.countTokens());
while (parser.hasMoreTokens()) {
list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();
}

private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;

public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}

public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}

@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}

@Override
public void bind(String name, Object value) {
delegate.bind(name, value);
}

@Override
public int getUniqueNumber() {
return delegate.getUniqueNumber();
}

@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}

@Override
public String getSql() {
return delegate.getSql();
}

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
if (prefixesToOverride != null) {
// 将sql中开头部分的prefixesToOverride删掉
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}

private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
// 将sql中结尾部分的suffixesToOverride删掉
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}

}

}

然后来看WhereSqlNode的实现,代码如下:

public class WhereSqlNode extends TrimSqlNode {

private static List prefixList = Arrays.asList("AND ","OR ",“AND\n”, “OR\n”, “AND\r”, “OR\r”, “AND\t”, “OR\t”);

public WhereSqlNode(Configuration configuration, SqlNode contents) {
// SQL脚本中的前缀是WHERE,需要
super(configuration, contents, “WHERE”, prefixList, null, null);
}

}

在WhereSqlNode对象的apply()函数中,先调用委托SqlNode对象的apply()函数,然后执行FilteredDynamicContext的函数applyAll()时,会将SQL脚本中开头的AND和OR删掉。

public class SetSqlNode extends TrimSqlNode {

private static final List COMMA = Collections.singletonList(“,”);

public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, “SET”, COMMA, null, COMMA);
}

}

在SetSqlNode对象的apply()函数中,先调用委托SqlNode对象的apply()函数,然后执行FilteredDynamicContext的函数applyAll()时,会将SQL脚本中开头和结尾的","符号删掉。

最后我们了解一下实现动态SQL比较关键的SqlNode实现类之一——IfSqlNode的实现,代码如下:

public class IfSqlNode implements SqlNode {
// evaluator属性用于解析OGNL表达式
private final ExpressionEvaluator evaluator;

// 保存标签test属性内容
private final String test;

// 标签内的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,则调用标签内容对应的SqlNode的apply()方法
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}

}

如上面的代码所示,IfSqlNode中维护了一个ExpressionEvaluator类的实例,该实例用于根据当前参数对象解析OGNL表达式。另外,IfSqlNode维护了标签test属性指定的表达式内容和标签中的SQL内容对应的SqlNode对象。在IfSqlNode类的apply()方法中,首先解析test属性指定的OGNL表达式,只有当表达式值为true的情况下,才会执行标签中SQL内容对应的SqlNode的apply()方法。这样就实现了只有当标签test属性表达式值为true的情况下,才会追加标签中配置的SQL信息。

动态SQL解析过程

SqlSource用于描述通过XML文件或者Java注解配置的SQL资源信息;SqlNode用于描述动态SQL中、等标签信息;LanguageDriver用于对Mapper SQL配置进行解析,将SQL配置转换为SqlSource对象。要了解MyBatis动态SQL的解析过程,我们可以从XMLLanguageDriver类的createSqlSource()方法出发进行分析,该方法代码如下:

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}

如上面的代码所示,在XMLLanguageDriver类createSqlSource()方法中,Mapper SQL配置的解析实际上是委托给XMLScriptBuilder类来完成的,该方法中首先创建了一个XMLScriptBuilder对象,然后调用XMLScriptBuilder对象的parseScriptNode()方法完成解析工作。XMLScriptBuilder类的构造函数如下:

public class XMLScriptBuilder extends BaseBuilder {

private final XNode context;
private boolean isDynamic;
private final Class<?> parameterType;
private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

public XMLScriptBuilder(Configuration configuration, XNode context) {
this(configuration, context, null);
}

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}

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());
}

XMLScriptBuilder类的parseScriptNode()方法代码如下:

public SqlSource parseScriptNode() {
// 调用parseDynamicTags()方法将SQL配置转换为SqlNode对象
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 判断Mapper SQL配置中是否包含动态SQL元素,如果是,就创建DynamicSqlSource对象,否则创建RawSqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

如上面的代码所示,在XMLScriptBuilder类的parseScriptNode()方法中,调用parseDynamicTags()方法将SQL配置转换为SqlNode对象,然后判断SQL配置是否为动态SQL,如果为动态SQL,则创建DynamicSqlSource对象,否则创建RawSqlSource对象。需要注意的是,MyBatis中判断SQL配置是否属于动态SQL的标准是SQL配置是否包含、、等元素或者${}参数占位符。

接下来,我们再来看一下XMLScriptBuilder类的parseDynamicTags()方法的实现,代码如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
List contents = new ArrayList<>();
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脚本中包含KaTeX parse error: Expected '}', got 'EOF' at end of input: … { // 如果SQL中不包含{}参数占位符,则不是动态SQL
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// 如果子元素为、等标签,则使用对应的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文本,则使用TextSqlNode对象描述SQL节点信息,若SQL节点中存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;如果子元素为、等标签,则使用对应的NodeHandler处理。

XMLScriptBuilder类中定义了一个私有的NodeHandler接口,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,通过实现类处理对应的动态SQL标签,把动态SQL标签转换为对应的SqlNode对象。

XMLScriptBuilder类中为NodeHandler接口提供了8个实现类,每个实现类用于处理对应的动态SQL标签,例如IfHandler用于处理动态SQL配置中的标签,将标签内容转换为IfSqlNode对象。

接下来我们来看一下NodeHandler接口的定义,代码如下:

private interface NodeHandler {
void handleNode(XNode nodeToHandle, List targetContents);
}

如上面的代码所示,NodeHandler接口中只有一个handleNode()方法,该方法接收一个动态SQL标签对应的XNode对象和一个存放SqlNode对象的List对象,handleNode()方法中对XML标签进行解析后,把生成的SqlNode对象添加到List对象中。我们可以参考一下IfHandler类的实现,代码如下:

private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}

@Override
public void handleNode(XNode nodeToHandle, List targetContents) {
// 继续调用parseDynamicTags方法解析标签中的子节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);

// 获取标签test属性
String test = nodeToHandle.getStringAttribute(“test”);
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);

// 将IfSqlNode对象添加到targetContents集合中
targetContents.add(ifSqlNode);
}
}

在IfHandler类的handleNode()方法中会继续调用XMLScriptBuilder类的parseDynamicTags()方法完成标签子节点的解析,将子节点转换为MixedSqlNode对象,然后获取标签test属性对应的OGNL表达式,接着创建IfSqlNode对象并添加到List对象中。parseDynamicTags()方法的内容前面我们已经分析过了,该方法中会获取当前节点的所有子节点,如果子节点内容为动态SQL标签,继续调用动态SQL标签对应的NodeHandler进行处理,这样就“递归”地完成了所有动态SQL标签的解析。

其他SqlNode实现类的处理逻辑与之类似。例如,下面是ForEachHandler类的实现代码:

private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}

@Override
public void handleNode(XNode nodeToHandle, List targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String collection = nodeToHandle.getStringAttribute(“collection”);
Boolean nullable = nodeToHandle.getBooleanAttribute(“nullable”);
String item = nodeToHandle.getStringAttribute(“item”);
String index = nodeToHandle.getStringAttribute(“index”);
String open = nodeToHandle.getStringAttribute(“open”);
String close = nodeToHandle.getStringAttribute(“close”);
String separator = nodeToHandle.getStringAttribute(“separator”);
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
targetContents.add(forEachSqlNode);
}
}

如上面的代码所示,ForEachHandler类的handleNode()方法中也会调用XMLScriptBuilder类的parseDynamicTags()解析标签所有子元素,如果子元素中包含标签或标签,则继续调用IfHandler或者ForEachHandler对象的handleNode()方法进行处理,直到所有的动态SQL元素全部被转换成SqlNode对象。

需要注意的是,XMLScriptBuilder类的构造方法中,会调用initNodeHandlerMap()方法将所有NodeHandler的实例注册到Map中,代码如下:

public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}

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());
}

需要解析动态SQL标签时,只需要根据标签名称获取对应的NodeHander对象进行处理即可,而不用每次都创建对应的NodeHandler实例,这也是享元思想的应用。上面是动态SQL配置转换为SqlNode对象的过程,那么SqlNode对象是如何根据调用Mapper时传入的参数动态生成SQL语句的呢?接下来我们回顾一下XMLScriptBuilder类的parseScriptNode()方法,代码如下:

public SqlSource parseScriptNode() {
// 调用parseDynamicTags()方法将SQL配置转换为SqlNode对象
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 判断Mapper SQL配置中是否包含动态SQL元素,如果是,就创建DynamicSqlSource对象,否则创建RawSqlSource对象
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

动态SQL标签解析完成后,将解析后生成的SqlNode对象封装在SqlSource对象中。通过前面的学习我们知道,MyBatis中的MappedStatement用于描述Mapper中的SQL配置,SqlSource创建完毕后,最终会存放在MappedStatement对象的sqlSource属性中,Executor组件操作数据库时,会调用MappedStatement对象的getBoundSql()方法获取BoundSql对象,代码如下:

public final class MappedStatement {
private SqlSource sqlSource; // 解析SQL语句生成的SqlSource实例

// 调用SqlSource类的getBoundSql()函数获取BoundSql对象实例
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List 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;
}

如上面的代码所示,MappedStatement对象的getBoundSql()方法会调用SqlSource对象的getBoundSql()方法,这个过程就完成了SqlNode对象解析成SQL语句的过程。我们可以了解一下DynamicSqlSource类的getBoundSql()方法的实现,代码如下:

public class DynamicSqlSource implements SqlSource {

private final Configuration configuration;
private final SqlNode rootSqlNode;

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}

@Override
public BoundSql getBoundSql(Object parameterObject) {
// 通过参数对象创建动态SQL上下文对象
DynamicContext context = new DynamicContext(configuration, parameterObject);

// 以动态SQL上下文对象作为参数调用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);

// 将标签绑定的参数添加到BoundSql对象中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}

}

如上面的代码所示,在DynamicSqlSource类的getBoundSql()方法中,首先根据参数对象创建DynamicContext对象,然后调用SqlNode对象的apply()方法对动态SQL进行解析。动态SQL解析完成后,调用DynamicContext对象的getSql()方法获取动态SQL解析后的结果。接着调用SqlSourceBuilder对象的parse()方法对动态SQL解析后的结果进一步解析处理,该方法返回一个StaticSqlSource对象,StaticSqlSource用于描述动态SQL解析后的静态SQL资源。

接下来,我们再来了解一下SqlSourceBuilder类的parse()方法对动态SQL解析后的结果到底做了什么操作。该方法的代码如下:

public class SqlSourceBuilder extends BaseBuilder {

private static final String PARAMETER_PROPERTIES = “javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName”;

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);
String sql;

// 调用GenericTokenParser对象的parse()方法将#{}参数占位符替换为?
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

如上面的代码所示,在SqlSourceBuilder类的parse()方法中,首先创建了一个ParameterMappingTokenHandler对象,ParameterMappingTokenHandler为MyBatis参数映射处理器,用于处理SQL中的#{}参数占位符。接着创建了一个GenericTokenParser对象,GenericTokenParser用于对SQL中的#{}参数占位符进行解析,获取#{}参数占位符中的内容。

针对上述函数,如果输入的原始SQL脚本是

INSERT INTO BLOG (ID, NAME, NOTE, COMMENT)
VALUES ( #{uuu.u}, #{__frch_u_0.id}, #{__frch_u_0,typeHandler=org.apache.ibatis.type.StringTypeHandler}, #{__frch_u_0:VARCHAR,typeHandler=org.apache.ibatis.type.StringTypeHandler} )

则GenericTokenParser对象调用函数parse()后解析结果如下:

INSERT INTO BLOG (ID, NAME, NOTE, COMMENT) VALUES ( ?, ?, ?, ? )

SqlSourceBuilder类的parse()方法调试过程中,局部变量和参数的值如下:

我们首先来看GenericTokenParser类解析#{}参数占位符的过程,代码如下:

public class GenericTokenParser {

private final String openToken; // 上述样例中为#{
private final String closeToken; // 上述样例中为}
private final TokenHandler handler; // TokenHandler对象实例

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 “”;
}
// 获取第一个#{在SQL中的位置
int start = text.indexOf(openToken);

start为-1说明SQL中不存在任何#{}参数占位符
if (start == -1) {
return text;
}

// 将SQL转换为char数组
char[] src = text.toCharArray();
int offset = 0; // 用offset记录已解析的#{或者}的偏移量,避免重复解析
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null; // expression为#{}中的参数内容

// 遍历获取所有#{}参数占位符的内容,然后调用TokenHandler对象的handleToken()方法替换参数占位符
do {
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] != ‘\’)) {
expression.append(src, offset, end - offset);
break;
}
// 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);
}
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);
} while (start > -1);
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}

从上面的代码可以看出,SQL配置中的所有#{}参数占位符内容都被替换成了“?”字符,为什么要替换成一个“?”字符呢?读者可能会联想到JDBC中的PreparedStatement,MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。除此之外,ParameterMappingTokenHandler的handleToken()方法中还做了另一件事情,就是调用buildParameterMapping()方法对占位符内容进行解析,将占位符内容转换为ParameterMapping对象。

private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

private final List parameterMappings = new ArrayList<>();
private final Class<?> parameterType;
private final MetaObject metaParameters;

public ParameterMappingTokenHandler(Configuration configuration
, Class<?> parameterType,Map<String, Object> additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}

@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return “?”;
}

ParameterMapping对象用于描述MyBatis参数映射信息,便于后续根据参数映射信息获取对应的TypeHandler为PreparedStatement对象设置值。buildParameterMapping()方法解析参数占位符生成ParameterMapping对象的过程如下:

private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get(“property”);
Class<?> propertyType; if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params propertyType = metaParameters.getGetterType(property); } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { propertyType = java.sql.ResultSet.class; } else if (property == null || Map.class.isAssignableFrom(parameterType)) { propertyType = Object.class; } else { MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); if (metaClass.hasGetter(property)) { propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } 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();
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 " + PARAMETER_PROPERTIES);
    }
    }
    if (typeHandlerAlias != null) {
    builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
    }
    return builder.build();
    }

private Map<String, String> parseParameterMapping(String content) {
try {
return new ParameterExpression(content);
} catch (BuilderException ex) {
throw ex;
} catch (Exception ex) {
throw new BuilderException(“Parsing error was found in mapping #{” + content

  • "}. Check syntax #{property|(expression), var1=value1, var2=value2, …} ", ex);
    }
    }
    }

如上面的代码所示,在ParameterMappingTokenHandler类的buildParameterMapping()方法中首先将参数占位符内容转换为Map对象,例如参数占位符内容如下:#{userId,javaType=long,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

将会转换成如下Map对象:

然后通过一系列的逻辑判断参数的类型(javaType属性值),具体逻辑读者可参考上面代码中的注释内容。最后通过建造者模式构建ParameterMapping对象。到此为止,动态SQL的解析已经全部完成。

我们首先来看参数占位符的解析过程。当动态SQL配置中存在{}参数占位符的解析过程。当动态SQL配置中存在参数占位符的解析过程。当动态SQL配置中存在{}参数占位符时,MyBatis会使用TextSqlNode对象描述对应的SQL节点,在调用TextSqlNode对象的apply()方法时会完成动态SQL的解析。也就是说,${}参数占位符的解析是在TextSqlNode类的apply()方法中完成的,下面是该方法的实现:

public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;

@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}

最后

面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。

96道前端面试题:

常用算法面试题:

前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}

最后

面试一面会问很多基础问题,而这些基础问题基本上在网上搜索,面试题都会很多很多。最好把准备一下常见的面试问题,毕竟面试也相当与一次考试,所以找工作面试的准备千万别偷懒。面试就跟考试一样的,时间长了不复习,现场表现肯定不会太好。表现的不好面试官不可能说,我猜他没发挥好,我录用他吧。

96道前端面试题:

  • [外链图片转存中…(img-Tz2sbdrY-1713704373371)]

常用算法面试题:

  • [外链图片转存中…(img-WqpDv6Dd-1713704373371)]

前端基础面试题:
内容主要包括HTML,CSS,JavaScript,浏览器,性能优化

  • [外链图片转存中…(img-wg2TQTpo-1713704373371)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-xBePqJQE-1713704373372)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

<think>好的,用户想了解MyBatis动态SQL注解用法,特别是示例。我需要先回忆MyBatis动态SQL的常用注解,比如@SelectProvider、@UpdateProvider等。记得这些注解是用来引用生成动态SQL的类的。 首先,用户可能已经知道MyBatis的基本用法,但不太清楚如何在注解中使用动态SQL。需要提供一个具体的例子,展示如何通过注解实现条件查询。比如,根据不同的参数动态生成WHERE子句。 然后,我需要确认示例的正确性。通常,@SelectProvider需要指定type和method,type对应一个类,method对应该类中的方法名。在方法里,使用SQL构建工具如SQL类来拼接SQL语句。 接下来,可能需要提到Provider类中的方法如何生成SQL。例如,使用条件判断来添加不同的条件。同时,要注意参数传递,比如使用@Param注解来绑定参数,确保在Provider方法中可以正确获取。 用户可能还关心如何调用这个注解方法,比如在Mapper接口中定义方法,并传入参数。这时,需要说明当name或age为null时,对应的条件不会被添加,这样动态SQL就能生效了。 另外,需要注意MyBatis版本是否支持这些注解,可能需要提到版本兼容性。同时,可以引用站内的引用资料,比如引用[3]提到的注解使用数据库列名的情况,可能和动态SQL的参数绑定有关联。 最后,生成相关问题时,要考虑用户可能的后续疑问,比如动态SQL的其他注解、性能优化、XML与注解的对比等。这些相关问题可以帮助用户深入理解。</think>MyBatis 动态 SQL注解用法主要通过 `@SelectProvider`、`@UpdateProvider` 等注解实现,结合 SQL 构建工具类动态生成 SQL 语句。以下是具体实现步骤和示例: ### 一、定义动态 SQL 提供类 ```java public class UserSqlProvider { public String selectByCondition(Map<String, Object> params) { return new SQL() {{ SELECT("id, name, age"); FROM("users"); if (params.get("name") != null) { WHERE("name = #{name}"); } if (params.get("age") != null) { WHERE("age = #{age}"); } ORDER_BY("id DESC"); }}.toString(); } } ``` 该方法会根据参数动态生成包含 `WHERE` 条件的 SQL[^3]。 ### 二、Mapper 接口注解配置 ```java public interface UserMapper { @SelectProvider(type = UserSqlProvider.class, method = "selectByCondition") List<User> selectByCondition( @Param("name") String name, @Param("age") Integer age ); } ``` 这里通过 `@SelectProvider` 注解绑定动态 SQL 生成类[^1]。 ### 三、执行动态查询 ```java // 当仅传递 name 参数时 List<User> users = userMapper.selectByCondition("Alice", null); // 生成的 SQL: // SELECT id, name, age FROM users WHERE name = ? ORDER BY id DESC ``` ### 四、动态 SQL 注解类型对比 | 注解类型 | 对应操作 | 使用场景 | |------------------|-----------|--------------------------| | @SelectProvider | 查询 | 动态条件查询 | | @InsertProvider | 插入 | 动态字段插入 | | @UpdateProvider | 更新 | 动态字段更新 | | @DeleteProvider | 删除 | 动态条件删除 | ### 五、注意事项 1. 参数必须使用 `@Param` 注解声明,否则无法正确映射到 SQL 参数占位符 2. Provider 类方法必须返回 String 类型的 SQL 语句 3. 建议配合 MyBatisSQL 构建工具类使用(如 `org.apache.ibatis.jdbc.SQL`) 该方法通过动态构建 SQL 语句避免了 SQL 注入风险,同时保持了代码的可维护性[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值