MyBatis源码之:SqlSource
1. GenericTokenParser
MyBatis底层通过GenericTokenParser
组件来解析SQL文本中的#{}
和${}
占位符;该组件的使用方式如下所示:
public class PlaceholderDemo {
public static void main(String[] args) {
// 创建一个属性源,里面存放了占位符名称及其对应的真实值
Map<String, String> dataMap = new HashMap<>();
dataMap.put("name", "NightDW");
dataMap.put("age", "27");
// TokenHandler代表占位符解析器,是GenericTokenParser的回调接口
// GenericTokenParser每解析到一个占位符名称,就会通过TokenHandler来获取到该占位符名称对应的真实值
TokenHandler callback = content -> {
System.out.println("解析到占位符名称:" + content);
String value = dataMap.get(content);
System.out.println("该占位符对应的实际值为:" + value);
return value;
};
// 创建一个GenericTokenParser组件,该组件负责解析${xxx}之类的占位符,并且会通过callback来获取占位符的真实值
GenericTokenParser parser = new GenericTokenParser("${", "}", callback);
// 通过GenericTokenParser组件来解析带占位符的字符串
System.out.println("开始解析");
String parse = parser.parse("Hello, my name is ${name}, and i am ${age} years old.");
// 打印解析结果
System.out.println("解析结果:" + parse);
}
}
上面的程序的运行结果如下所示:
开始解析
解析到占位符名称:name
该占位符对应的实际值为:NightDW
解析到占位符名称:age
该占位符对应的实际值为:27
解析结果:Hello, my name is NightDW, and i am 27 years old.
占位符的解析过程如下所示:
public class GenericTokenParser {
private final String openToken; // 占位符前缀
private final String closeToken; // 占位符后缀
private final TokenHandler handler; // 占位符解析器
// 省略全参构造器
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// 搜索字符串中的占位符前缀;如果没有,则说明没有占位符,因此直接返回原字符串
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
// 否则,先将字符串转成char数组,方便操作
char[] src = text.toCharArray();
// offset代表char数组中未被处理的部分的起始下标
int offset = 0;
// builder用于存放最终的解析结果
final StringBuilder builder = new StringBuilder();
// expression用于存放占位符名称
StringBuilder expression = null;
// 只要发现了占位符前缀,就不断循环
while (start > -1) {
// 如果该占位符前缀的前一个字符是反斜杠,说明该占位符前缀被转义了
// 这种情况下,需要把该占位符前缀及其前面的未被处理的部分都添加到builder中(去掉反斜杠),并更新offset指针
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
// 否则,说明确实找到了一个占位符前缀
} else {
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
// 先将占位符前缀前面的未被处理的部分添加到builder中,并更新offset指针
builder.append(src, offset, start - offset);
offset = start + openToken.length();
// 获取到占位符后缀的下标
int end = text.indexOf(closeToken, offset);
// 同样,只要发现了占位符后缀,就不断循环
while (end > -1) {
// 如果该占位符后缀被转义了,则将该占位符后缀及其前面的未被处理的部分都添加到expression中(去掉反斜杠)
// 然后更新offset指针,并继续查找占位符后缀
if (end > offset && src[end - 1] == '\\') {
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
// 否则,将该占位符后缀前面的未被处理的部分添加到expression中,并更新offset指针,然后退出循环
} else {
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
// 如果没有发现未被转义的占位符后缀,则直接将该占位符前缀及其后面的所有字符都添加到builder中,并更新offset指针
if (end == -1) {
builder.append(src, start, src.length - start);
offset = src.length;
// 否则,此时的expression就是完整的占位符名称,因此解析占位符名称,并将解析结果存放到builder中,并更新offset指针
} else {
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
// 继续查找下一个占位符前缀
start = text.indexOf(openToken, offset);
}
// 执行到这里,说明找不到占位符前缀了,因此将剩余的未被处理的部分都添加到builder中
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
2. SqlSource
我们先来了解一下动态SQL和静态SQL:
- 动态SQL:含有
${}
占位符或者if
等动态标签的SQL文本 - 静态SQL:只含有
#{}
占位符或者不包含任何MyBatis特殊语法的SQL文本
-- 动态SQL,包含${}占位符
SELECT * FROM sys_user ORDER BY ${orderByClause}
-- 动态SQL,包含if等动态标签
SELECT * FROM sys_user WHERE 1 = 1
<if text="userType != null">
AND user_type = #{userType}
</if>
-- 静态SQL:只含有#{}占位符
SELECT * FROM sys_user WHERE user_id = #{userId}
-- 静态SQL:不包含任何MyBatis特殊语法
SELECT COUNT(*) FROM sys_user
静态SQL的特点:
- 我们知道,
BoundSql
就相当于SQL模板 + 占位符信息 + 占位符的属性源(即用户的实际传参) - 而很显然,静态SQL的SQL模板和占位符信息是固定的
- 因此,在通过静态SQL获取
BoundSql
时,可以直接将静态SQL的SQL模板、占位符信息和用户的传参打包成BoundSql
实例
动态SQL的特点:
- 动态SQL的SQL模板是不确定的,因为其中可能包含
${}
占位符 - 动态SQL的占位符信息也是不确定的,因为
if
等动态标签可以根据实际情况来引入若干个#{}
占位符 - 因此,在通过动态SQL获取
BoundSql
时,必须先解析掉其中的动态内容、获取到静态SQL,再通过静态SQL来获取BoundSql
/**
* 一个SqlSource实例就代表一条映射语句的具体内容
* 比如,mapper.xml文件中的select等标签的内容就会被封装成相应的SqlSource实例(@Select等注解同理)
* 可以这么理解,一个SqlSource实例就相当于一条动态SQL或静态SQL
*/
public interface SqlSource {
/**
* 根据用户的实际传参来生成对应的BoundSql实例
*/
BoundSql getBoundSql(Object parameterObject);
}
3. StaticSqlSource
/**
* 静态的SqlSource,用于保存静态SQL的解析结果
* 我们可以将本类的实例看作是经过解析后的静态SQL;而之前提到的静态SQL,都是指未被解析过的静态SQL
*
* MyBatis在解析静态SQL时,会将解析结果封装到StaticSqlSource实例中
* 这样一来,在通过静态SQL获取BoundSql实例时,就可以直接读取StaticSqlSource实例中的解析结果,然后创建相应的BoundSql实例了
*/
public class StaticSqlSource implements SqlSource {
/** 静态SQL对应的SQL模板 */
private final String sql;
/** 静态SQL对应的占位符信息 */
private final List<ParameterMapping> parameterMappings;
/** MyBatis配置 */
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
/**
* 根据用户的传参来构造BoundSql实例
* 这里会直接利用静态SQL的解析结果和用户的传参来构造BoundSql实例
*/
@Override
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
}
4. SqlSourceBuilder
/**
* 本类主要负责解析静态SQL,并生成相应的StaticSqlSource实例
* 本类继承自BaseBuilder类,该父类主要提供了一些工具方法,没什么好讲的,因此不用管
*/
public class SqlSourceBuilder extends BaseBuilder {
/**
* 先来看这个静态内部类
* 本类实现了TokenHandler接口,负责解析#{}占位符,并将其转成相应的ParameterMapping实例
*/
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
/** 用于存放静态SQL的所有占位符信息 */
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
/** 用户传的参数列表的类型 */
private Class<?> parameterType;
/** 额外参数的MetaObject形式 */
private MetaObject metaParameters;
/**
* 构造器;需指定MyBatis配置、用户参数列表的类型和额外参数
*/
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;
}
/**
* 解析占位符名称(即#{}占位符里面的内容),这里的占位符名称有两种格式:
* 1. property, javaType=xxx, jdbcType=xxx, ...
* 2. (expression), javaType=xxx, jdbcType=xxx, ...
*/
@Override
public String handleToken(String content) {
// 将占位符名称解析成ParameterMapping实例(具体的解析过程不用去管)
// 然后将解析结果存放到parameterMappings集合中
parameterMappings.add(buildParameterMapping(content));
// 用问号来替换#{}占位符
return "?";
}
}
public SqlSourceBuilder(Configuration configuration) {
super(configuration);
}
/**
* 解析静态SQL文本,并返回相应的StaticSqlSource实例
*
* @param originalSql 静态SQL的文本
* @param parameterType 用户传的参数列表的类型
* @param additionalParameters 额外参数
*/
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 先创建一个ParameterMappingTokenHandler实例,负责处理#{}占位符
ParameterMappingTokenHandler handler =
new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 创建一个解析#{}占位符的解析器,并对静态SQL文本进行解析,获取到解析后的SQL模板
// 这里底层会将静态SQL文本中的#{}占位符替换成问号,同时将该占位符转成相应的ParameterMapping实例
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
// 根据SQL模板和占位符信息来构造StaticSqlSource实例并返回
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
}
5. RawSqlSource
/**
* RawSqlSource代表未被解析的静态SQL
* RawSqlSource实例会在构造阶段对静态SQL进行解析
*/
public class RawSqlSource implements SqlSource {
/** 解析后的静态SQL;该字段的值固定是StaticSqlSource类型 */
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
/**
* 主要来看这个构造器;该构造器会对静态SQL文本进行解析,然后将解析结果赋给this.sqlSource字段
*/
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
}
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
/**
* 获取BoundSql实例;这里是通过底层的StaticSqlSource实例来获取BoundSql实例的
*/
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
}
6. DynamicContext
我们知道,在通过动态SQL来获取
BoundSql
时,需要先将动态SQL转成静态SQL 而在将动态SQL转成静态SQL时,${}
占位符和if
等动态标签会根据用户的传参来决定是否需要拼接SQL语句、拼接什么SQL语句 而DynamicContext
就是SQL语句拼接的上下文,其底层持有一个StringBuilder
,用于拼接SQL语句;并且还会持有用户的传参
6.1. 内部类
/**
* 本类是DynamicContext的静态内部类,继承自HashMap
* 本类在HashMap的基础上,会额外持有一个MetaObject对象
* 本类的get()方法会优先从HashMap中获取数据;如果HashMap中没有,则从MetaObject对象中获取数据
*/
static class ContextMap extends HashMap<String, Object> {
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}
if (parameterMetaObject != null) {
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
/**
* 本类也是DynamicContext的静态内部类,实现了OGNL中的PropertyAccessor接口
*
* 我们知道,if标签底层是通过OGNL表达式执行器来判断其test属性是否为true的(其它标签同理)
* 而在执行OGNL表达式时,MyBatis会将ContextMap作为属性源传给OGNL表达式执行器
* 而OGNL表达式执行器会通过PropertyAccessor组件来操作属性源
* 因此,PropertyAccessor组件其实就是个属性源适配器
*
* 本组件主要负责从ContextMap中读取数据,以及往ContextMap中写入数据
*/
static class ContextAccessor implements PropertyAccessor {
/**
* 从属性源(target)中读取数据
*/
@Override
public Object getProperty(Map context, Object target, Object name) throws OgnlException {
// 由于我们能够确定属性源一定是ContextMap类型,因此进行强转
Map map = (Map) target;
// 调用ContextMap的get()方法来读取属性;如果读取到了,则直接返回读取结果
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
// 否则,从属性源中获取到用户传的参数列表(Map形式),并返回参数列表中的相应数据
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map) parameterObject).get(name);
}
return null;
}
/**
* 往属性源(target)中写入数据
*/
@Override
public void setProperty(Map context, Object target, Object name, Object value) throws OgnlException {
Map<Object, Object> map = (Map<Object, Object>) target;
map.put(name, value);
}
@Override
public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
@Override
public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
}
6.2. DynamicContext
public class DynamicContext {
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
static {
// 将ContextAccessor组件注册为ContextMap类型的属性源的编辑器
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}
/**
* bindings字段代表用户的实际传参,同时负责保存一些额外参数(比如,foreach标签就会往这里添加额外参数)
* bindings的HashMap本体用于存放额外参数,而其底层的MetaObject对象就代表用户的传参
* 因此,在读取参数时,额外参数的优先级大于用户的实际传参
*/
private final ContextMap bindings;
/**
* sqlBuilder字段用于拼接SQL语句
*/
private final StringBuilder sqlBuilder = new StringBuilder();
/**
* 计数器;主要用于生成唯一的数字
*/
private int uniqueNumber = 0;
/**
* 构造器;需指定用户的传参;本构造器主要是在初始化this.bindings字段
*/
public DynamicContext(Configuration configuration, Object parameterObject) {
// 如果用户的传参不是null,且不是Map类型,则将其封装成MetaObject形式,并创建相应的ContextMap实例
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
// 否则,创建一个底层的MetaObject对象为null的ContextMap实例;这段代码和ContextAccessor类有点关联
} else {
bindings = new ContextMap(null);
}
// 将用户的传参作为额外参数添加到ContextMap实例中,其key为"_parameter"
// 这样一来,如果用户的传参是Map形式,那么ContextAccessor组件就可以通过该属性来获取到用户参数,并读取用户参数中的数据
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
// 将数据库id也作为额外参数添加到ContextMap实例中
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
/**
* 获取到所有的额外参数
*/
public Map<String, Object> getBindings() {
return bindings;
}
/**
* 添加额外参数
*/
public void bind(String name, Object value) {
bindings.put(name, value);
}
/**
* 拼接SQL片段
*/
public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}
/**
* 获取最终的SQL语句(模板)
*/
public String getSql() {
return sqlBuilder.toString().trim();
}
/**
* 生成一个唯一的数字
*/
public int getUniqueNumber() {
return uniqueNumber++;
}
}
7. SqlNode
/**
* 每个动态标签或SQL文本片段都会被封装成相应的SqlNode实例
*
* 假设SQL文本为:SELECT * FROM sys_user ORDER BY ${orderByClause}
* 那么,该SQL文本会被封装成一个TextSqlNode实例,其底层会持有该SQL文本
*
* 假设SQL文本为:SELECT * FROM sys_user WHERE 1 = 1 <if test="name != null">AND name = #{name}</if>
* 那么,前半部分会被封装成StaticTextSqlNode实例,而if标签会被封装成IfSqlNode实例
*/
public interface SqlNode {
/**
* 判断本SqlNode是否生效;如果是,则对DynamicContext对象进行操作(比如,追加SQL片段,或者新增额外参数)
* 返回值代表本SqlNode是否生效了(存疑)
*/
boolean apply(DynamicContext context);
}
7.1. 文本相关
/**
* 静态文本(纯文本)节点,不包含${}占位符
*/
public class StaticTextSqlNode implements SqlNode {
/**
* 本节点对应的文本;需要在构造时指定
*/
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
/**
* 直接将本节点对应的文本拼接到最终的SQL语句中
*/
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
}
}
/**
* 文本节点,其中可能会包含${}占位符
*/
public class TextSqlNode implements SqlNode {
/** SQL文本,其中可能会包含${}占位符 */
private final String text;
/** 在解析${}占位符时,如果解析到的值不能被该正则表达式匹配到,则认为有SQL注入的风险,因此此时会报错 */
private final Pattern injectionFilter;
public TextSqlNode(String text) {
this(text, null);
}
public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
/**
* 创建解析${}占位符的解析器
*/
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
/**
* 判断该节点是否含有动态内容(即是否有${}占位符)
* 这里我们只需要知道DynamicCheckerTokenParser的handleToken()方法会将其isDynamic字段置为true即可
*/
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
/**
* 解析SQL文本中的${}占位符,并将解析后的文本拼接到最终的SQL语句中
*/
@Override
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
/**
* 本组件是解析${}占位符的核心组件
*/
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
/**
* 获取到占位符对应的真实值
*/
@Override
public String handleToken(String content) {
// 如果用户指定的参数列表为null或是简单类型的,则新增一个名称为"value"的额外参数,其值即为用户指定的参数列表
// 从这我们可以看出,value属性代表用户传的单个简单参数;因此,我们可以通过${value}来获取到这个单一的简单参数
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
// 通过OGNL表达式执行器来从ContextMap中读取该占位符名称对应的真实值
Object value = OgnlCache.getValue(content, context.getBindings());
// 将占位符的值转成字符串形式,然后检查SQL注入,并返回该字符串
String srtValue = (value == null ? "" : String.valueOf(value));
checkInjection(srtValue);
return srtValue;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
}
7.2. mixed/if/bind/choose
/**
* 我们知道,一条SQL文本往往会对应多个同级的SqlNode实例
* 但是,为了方便使用,一条SQL文本只会被解析成一个SqlNode实例
* 因此就有了本类;本类主要用于封装多个同级的SqlNode实例
*/
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
for (SqlNode sqlNode : contents) {
sqlNode.apply(context);
}
return true; // 个人觉得这里不应该直接返回true;应该对所有节点的apply结果进行或运算,并返回运算结果
}
}
/**
* if标签对应的SqlNode实现;when标签最终也会被解析成IfSqlNode实例
*/
public class IfSqlNode implements SqlNode {
/** OGNL表达式执行器 */
private final ExpressionEvaluator evaluator;
/** if标签的test属性 */
private final String test;
/** if标签中的内容(可能会有多个同级的SqlNode实例;注意,纯文本也会被封装成对应的SqlNode实例) */
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}
/**
* 判断test表达式是否为true;如果是,则调用if标签内部的节点来对DynamicContext实例进行操作
*/
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
}
/**
* bind标签对应的SqlNode实现
*/
public class VarDeclSqlNode implements SqlNode {
/** bind标签的name属性值 */
private final String name;
/** bind标签的value属性值 */
private final String expression;
public VarDeclSqlNode(String var, String exp) {
name = var;
expression = exp;
}
/**
* 获取到表达式对应的真实值,并将该值作为额外参数存到DynamicContext中(参数名称为name)
*/
@Override
public boolean apply(DynamicContext context) {
final Object value = OgnlCache.getValue(expression, context.getBindings());
context.bind(name, value);
return true;
}
}
/**
* choose标签对应的SqlNode实现
*/
public class ChooseSqlNode implements SqlNode {
/** otherwise子标签的内容 */
private final SqlNode defaultSqlNode;
/** when子标签的内容;多个when子标签会根据声明的顺序进行排序 */
private final List<SqlNode> ifSqlNodes;
public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
this.ifSqlNodes = ifSqlNodes;
this.defaultSqlNode = defaultSqlNode;
}
@Override
public boolean apply(DynamicContext context) {
// 先依次使用when标签来处理该DynamicContext实例;如果其中一个处理成功,则立刻返回true
for (SqlNode sqlNode : ifSqlNodes) {
if (sqlNode.apply(context)) {
return true;
}
}
// 否则,如果有otherwise标签,则用该标签来处理该DynamicContext实例,并返回true;否则返回false
if (defaultSqlNode != null) {
defaultSqlNode.apply(context);
return true;
}
return false;
}
}
7.3. trim/where/set
/**
* trim标签对应的SqlNode实现
*/
public class TrimSqlNode implements SqlNode {
private final SqlNode contents; // trim标签的内容
private final String prefix; // trim标签的prefix属性
private final String suffix; // trim标签的suffix属性
private final List<String> prefixesToOverride; // trim标签的prefixOverrides属性(多个值用'|'隔开,并且会自动转成大写)
private final List<String> suffixesToOverride; // trim标签的suffixOverrides属性(多个值用'|'隔开,并且会自动转成大写)
private final Configuration configuration; // MyBatis配置
// 省略构造器和parseOverrides()方法
@Override
public boolean apply(DynamicContext context) {
// 创建一个新的FilteredDynamicContext实例,并将trim标签中的内容应用到该实例中
// 这样一来,trim标签的内容对应的SQL语句就会临时存放在FilteredDynamicContext实例中
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
boolean result = contents.apply(filteredDynamicContext);
// 对FilteredDynamicContext实例中的SQL语句进行裁剪和拼接,并将处理后的SQL语句追加到context中
filteredDynamicContext.applyAll();
// 返回处理结果
return result;
}
/**
* 本类是DynamicContext的装饰器,也是处理trim标签的核心组件
*/
private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate; // 被装饰的DynamicContext实例
private boolean prefixApplied; // 是否已经完成了与前缀相关的操作
private boolean suffixApplied; // 是否已经完成了与后缀相关的操作
private StringBuilder sqlBuffer; // 临时的StringBuilder,用于存放trim标签中的内容对应的SQL语句
public FilteredDynamicContext(DynamicContext delegate) {
super(configuration, null);
this.delegate = delegate;
this.prefixApplied = false;
this.suffixApplied = false;
this.sqlBuffer = new StringBuilder();
}
// getBindings()/bind()/getUniqueNumber()/getSql()方法底层都是在调用delegate的相应方法,因此省略
/**
* 追加SQL片段
* 这里是追加到临时的SQL缓冲区中,而不是直接调用delegate的appendSql()方法
* 另外,这里没有再追加一个空格,个人觉得这样是会有问题的
*/
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
/**
* 对临时的SQL语句进行裁剪和拼接,然后将处理后的SQL语句追加到delegate中
*/
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());
}
/**
* 去掉SQL缓冲区中的指定前缀(如果有的话),并拼接上新的前缀(如果有的话)
* 注意,applySuffix()方法和本方法类似,因此这里省略了applySuffix()方法
*
* @param sql SQL缓冲区
* @param trimmedUppercaseSql SQL缓冲区中的字符串的大写形式
*/
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) {
prefixApplied = true;
// 如果有指定要删除的前缀
if (prefixesToOverride != null) {
// 依次判断该SQL是否以这些前缀为开头;如果是,则删除该前缀,并结束循环
// 从这我们可以看出,这里只会删除第一个匹配的前缀,并且只会删除一次
for (String toRemove : prefixesToOverride) {
if (trimmedUppercaseSql.startsWith(toRemove)) {
sql.delete(0, toRemove.trim().length());
break;
}
}
}
// 如果有指定新的前缀
if (prefix != null) {
// 往SQL缓冲区的开头插入一个空格,再往开头插入该前缀
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
}
}
/**
* where标签对应的SqlNode实现;本类继承自TrimSqlNode
*/
public class WhereSqlNode extends TrimSqlNode {
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n","OR\n","AND\r","OR\r","AND\t","OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}
}
/**
* set标签对应的SqlNode实现;本类继承自TrimSqlNode
*/
public class SetSqlNode extends TrimSqlNode {
private static List<String> suffixList = Arrays.asList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", null, null, suffixList);
}
}
7.4. foreach
/**
* foreach标签对应的SqlNode实现
*/
public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_";
/**
* 本方法负责生成占位符名称;生成规则是:ITEM_PREFIX + item属性值 + "_" + 唯一数字(一般是下标值)
*
* 假设foreach标签如下所示,并假设userIds集合中有3个元素
* <foreach collection="userIds" open="(" close=")" separator="," item="userId">#{userId}</foreach>
* 那么,这个foreach标签对应的SQL片段为:(#{__frch_userId_0}, #{__frch_userId_1}, #{__frch_userId_2})
*/
private static String itemizeItem(String item, int i) {
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
private final String collectionExpression; // foreach标签的collection属性值
private final SqlNode contents; // foreach标签的内容
private final String open; // foreach标签的open属性值
private final String close; // foreach标签的close属性值
private final String separator; // foreach标签的separator属性值
private final String item; // foreach标签的item属性值
private final String index; // foreach标签的index属性值
private final Configuration configuration; // MyBatis配置
// 省略全参构造器
/**
* 每获取到集合中的一个元素,就会调用本方法,将该元素标记为当前元素,并将该元素绑定到额外参数中
*
* @param o 当前元素;如果集合是Map类型,则会将Map的value值看作是集合中的元素
* @param i 当前元素对应的唯一数字(一般是下标值)
*/
private void applyItem(DynamicContext context, Object o, int i) {
// 如果确实指定了item属性值
if (item != null) {
// 以item属性值作为key、当前元素作为value,将该键值对绑定到额外参数中
// 在获取到下一个元素之前,我们都可以通过item属性值直接获取到当前的这个元素
context.bind(item, o);
// 为当前元素生成一个唯一的占位符名称,并以该名称作为key、当前元素作为value,将该键值对绑定到额外参数中
// 稍后会在SQL语句中拼接一个该名称的#{}占位符,因此,我们要将当前元素绑定到额外参数中,让该占位符与当前元素对应起来
context.bind(itemizeItem(item, i), o);
}
}
/**
* 每获取到集合中的一个元素,就会调用本方法,将该元素下标标记为当前元素下标,并将该元素下标绑定到额外参数中
*
* @param o 当前元素的下标;如果集合是Map类型,则会将Map的key值看作是元素的下标值
* @param i 当前元素对应的唯一数字(一般是下标值)
*/
private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
context.bind(index, o);
context.bind(itemizeItem(index, i), o);
}
}
@Override
public boolean apply(DynamicContext context) {
Map<String, Object> bindings = context.getBindings();
// 获取到目标集合的迭代器;如果该迭代器中没有数据,则直接返回true(为什么不是返回false?)
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
// first代表当前元素是否有可能是第一个有效元素
// 如果是,则当前元素前面不需要拼接上分隔符;否则,需要拼接分隔符
boolean first = true;
// 先将open字符串追加到context中(如果有的话)
applyOpen(context);
// 当前元素的下标
int i = 0;
// 遍历迭代器中的所有元素
for (Object o : iterable) {
// 先将当前的DynamicContext保存起来,并将其包装成一个新的PrefixedContext实例
// PrefixedContext是一个装饰器,会在第一次往delegate中追加有效SQL时,先额外添加一个前缀
// 如果当前元素有可能是第一个有效元素,或者未指定分隔符,则不需要添加前缀,否则需要添加分隔符作为前缀
DynamicContext oldContext = context;
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
// 生成一个唯一数字,该数字将会用于生成唯一的占位符名称
int uniqueNumber = context.getUniqueNumber();
// 绑定一些额外参数
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);
}
// 创建一个FilteredDynamicContext装饰器,对PrefixedContext实例进行包装
// 该装饰器在appendSql()方法中,会将SQL片段中的#{item}和#{index}占位符替换成真正有效的占位符
// 假设item为"userId",uniqueNumber为1,那么该装饰器会将SQL片段中的"#{userId}"子串替换成"#{__frch_userId_1}"字符串
// 假设index为"idx",uniqueNumber为2,那么该装饰器会将SQL片段中的"#{idx}"子串替换成"#{__frch_idx_2}"字符串
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
// 如果当前元素有可能是第一个有效元素,则判断当前元素是否真的生效了
// 如果是,则将first置为false;否则,first依旧保持为true(也就是说,下一个元素有可能是第一个有效元素)
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
// 重新将context指向原先的DynamicContext,并且下标加一,为下次循环做准备
context = oldContext;
i++;
}
// 将close字符串追加到context中(如果有的话)
applyClose(context);
// 删除掉额外参数中的当前元素和当前元素下标,然后返回true
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
}
8. DynamicSqlSource
/**
* DynamicSqlSource代表动态SQL
*/
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
/** 该动态SQL对应的SQL节点 */
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);
rootSqlNode.apply(context);
// 对解析到的静态SQL进行解析,获取到对应的StaticSqlSource实例
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 获取BoundSql实例,并将context中的额外参数复制到BoundSql实例中,然后返回这个BoundSql实例
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
}
转载自:https://juejin.cn/post/7248183510233677884