当sql带有参数时,Mybatis是如何处理的。使用的是User类。
//省略get set方法
public class User {
private int id;
private String name;
private String phone;
}
例1 带有全局变量的sql
//UserMapper中的dao接口
List<User> getByglobal();
<select id="getByglobal" resultType="com.entity.User">
select * from user where id = ${globalId}
</select>
<!--mybatis.xml中的部分配置-->
<properties>
<property name="globalId" value="1"/>
</properties>
注意我是用的符号为$。在这个例子中globalId是在mybatis.xml文件中的property配置的。接口不传参数。
,我们知道每一个查询语句都会被包装成一个MappedStatement,这个对象用来存放我们的sql语句,返回类型,id等等。让我们回到之前的代码。
//XMLMapperBuilder.configurationElement
private void configurationElement(XNode context) {
try {
//mapper的缓存信息,命名空间等会被临时保存到MapperBuilderAssistant中,最后把这些公用的信息在存到MappedStatement中
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
//该节点已经被废弃
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
//在解析增删改查节点时,每个节点都会生成一个mapperStatement对象并保存到配置文件类中.
//mapperStatement保存这这个节点的全部信息,如id,fetchSize,timeout
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);
}
}
这部分代码应该不陌生,现在我们从context.evalNodes("select|insert|update|delete")开始。该方法开始解析增删改查的节点
//XNode.evalNodes
public List<XNode> evalNodes(String expression) {
return xpathParser.evalNodes(node, expression);
}
public List<XNode> evalNodes(Object root, String expression) {
List<XNode> xnodes = new ArrayList<>();
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
//这里创建的新的节点并添加到结合中,解析也是在创建节点的时候开始的。
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}
XNode构造方法,在创建新节点的时候,会在构造器中先进性解析,也就是调用parseBody方法.
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
this.variables = variables;
//获取节点的属性
//例如这个select就有id和resultType两个属性
this.attributes = parseAttributes(node);
//解析节点里面的内容,也就是sql了
this.body = parseBody(node);
}
variables传入配置中的全局变量
//XNode.parseBody
private String parseBody(Node node) {
//获取当前节点的信息
//例子中这里返回空
String data = getBodyData(node);
if (data == null) {
//获取孩子节点的信息
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
//获取当前节点的信息
data = getBodyData(child);
if (data != null) {
break;
}
}
}
return data;
}
为什么select节点getBodyData会返回空呢,从它的方法体中可以看出,首先它会判断节点的类型,select这个节点是ELEMENT_NODE类型,不属于它要求的文本类型或者部分节点类型。那么就直接返回空了。而当select的孩子节点,也就是sql语句select * from user where id = ${globalId}这个节点调用getBodyData方法时,sql语句是文本类型的,满足条件,才会使用解析器开始解析。
//XNode.getBodyData
private String getBodyData(Node child) {
//判断节点的类型
if (child.getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNodeType() == Node.TEXT_NODE) {
String data = ((CharacterData) child).getData();
data = PropertyParser.parse(data, variables);
return data;
}
return null;
}
//PropertyParser.parse
public static String parse(String string, Properties variables) {
//先创建一个处理器
VariableTokenHandler handler = new VariableTokenHandler(variables);
//创建解析器
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
//进行解析
return parser.parse(string);
}
这里出现了很多陌生的类。首先是GenericTokenParser通用类型的解析器,他能根据传入的参数做出相应。如果参数满足条件,就会调用handler处理器来处理参数。每个handler都要实现handleToken方法,该方法就是用来处理参数的。
例如这里传入的是以${作为开头,}作为结尾。如果传入的字符串包含一个或者多个这样的格式,就会调用VariableTokenHandler.handleToken,该方法会试图从全局中找到该变量,并修改成具体的值。
VariableTokenHandler.handleToken 传入String变量globalId,将其替换成1并返回。
public String handleToken(String content) {
//variables里面存放全局的变量,为空直接return
if (variables != null) {
String key = content;
//是否存在默认值,默认是false
if (enableDefaultValue) {
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) {
key = content.substring(0, separatorIndex);
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
return variables.getProperty(key, defaultValue);
}
}
//variables是用来存放全局变量的容器。
//这里会从全局变量中找到我们定义的globalId,然后将对应的值返回,这样我们的sql就拼接完成了
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
return "${" + content + "}";
}
}
解析器代码,根据传入的标记开始解析,这里传入开始标记${和结束标记$}。在这之后还会用来解析#{}。代码比较长,最好打个断点进去看。
//GenericTokenParser.parse
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
//查找开始标记,如果不存在返回-1 ,存在返回偏移量
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
//这个变量用来存放中间的字符,如${id}中的id
StringBuilder expression = null;
//如果存在开始标志
while (start > -1) {
//这里将从offset开始,一直到start的字符先放入builder中
//例如select * from user where id =
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);
//取到中间字符globalId
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);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//这里根据不同的处理器会有不同的操作,刚才传入的是VariableTokenHandler
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();
}
到这里全局变量就解析完成了,那么如果在全局变量中没有找到对应的值该怎么办呢?例如我这里使用的sql是select * from user where id = ${id},而不是${globalId},那么根据VariableTokenHandler处理器,它会原封不动的进行返回,等待后文的解析。
顺便一提,这一部分的解析实在解析我们的配置文件的时候就发生了,方法入口为context.evalNodes("select|insert|update|delete"),在解析配置的时候,其他节点也大量使用了context.evalNodes()方法去解,所以只要当配置mybatis.xml文件中的properties节点解析完成之后,里面的变量就是能全局使用了,这也是为什么properties节点要放在第一个解析。
又由于这个通用解析器只解析${XXX}格式的变量,所以全局的变量不能写成#{xxx}.
需要更多教程,微信扫码即可
👆👆👆
别忘了扫码领资料哦【高清Java学习路线图】
和【全套学习视频及配套资料】