likes
comments
collection
share

MyBatis源码之:SqlSource

作者站长头像
站长
· 阅读数 11

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

  1. 动态SQL:含有${}占位符或者if等动态标签的SQL文本
  2. 静态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的特点:

  1. 我们知道,BoundSql就相当于SQL模板 + 占位符信息 + 占位符的属性源(即用户的实际传参)
  2. 而很显然,静态SQL的SQL模板和占位符信息是固定的
  3. 因此,在通过静态SQL获取BoundSql时,可以直接将静态SQL的SQL模板、占位符信息和用户的传参打包成BoundSql实例

动态SQL的特点:

  1. 动态SQL的SQL模板是不确定的,因为其中可能包含${}占位符
  2. 动态SQL的占位符信息也是不确定的,因为if等动态标签可以根据实际情况来引入若干个#{}占位符
  3. 因此,在通过动态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;
    }
}