Mybatis源码阅读(二):动态节点解析2.2 —— SqlSourceBuilder与三种SqlSource

SqlSourceBuilder

前面我们对SqlSource和SqlNode进行了介绍,在经过SqlNode.apply方法的解析之后,Sql语句会被传递到SqlSourceBuilder中进行进一步的解析。SqlSourceBuilder主要完成了两方面的操作,一方面是解析Sql中的#{}占位符定义的属性,如jdbcType、javaType(使用较少),一方面是把#{}占位符替换成?占位符

SqlSourceBuilder代码如下


/**
 * 用于进一步解析SqlSource中的${}
 * @author Clinton Begin
 */
public class SqlSourceBuilder extends BaseBuilder {

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

    public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
    }

    /**
     * 解析#{}
     * @param originalSql 被SqlNode.apply解析后的sql
     * @param parameterType 参数类型
     * @param additionalParameters DynamicContext.bindings集合
     * @return
     */
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        // 解析#{}
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        // 最终被解析成含有?的静态sql。创建StaticSqlSource,包含这个sql和参数
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

    /**
     * 内部类,是解析#{}的核心
     */
    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

        /**
         * 记录解析后得到的parameterMapping集合
         */
        private List<ParameterMapping> parameterMappings = new ArrayList<>();
        /**
         * 参数类型
         */
        private Class<?> parameterType;
        /**
         * DynamicContext.bindings对应的类
         */
        private MetaObject metaParameters;

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

        /**
         * 占位符处理器核心方法。
         * @param content
         * @return
         */
        @Override
        public String handleToken(String content) {
            parameterMappings.add(buildParameterMapping(content));
            return "?";
        }

        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();
                // 处理jdbcType、javaType等一大堆东西
                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是SqlSourceBuilder的一个内部类,该类是解析#{}的核心。

ParameterMapping中记录了#{}占位符中的参数属性,字段如下

public class ParameterMapping {

    private Configuration configuration;

    /**
     * 参数名
     */
    private String property;
    /**
     * 参数模式。输入参数还是输出参数
     */
    private ParameterMode mode;
    private Class<?> javaType = Object.class;
    private JdbcType jdbcType;
    private Integer numericScale;
    private TypeHandler<?> typeHandler;
    private String resultMapId;
    private String jdbcTypeName;
    private String expression;
}

之后,SqlSourceBuilder会将Sql语句以及parameterMap平时集合封装成StaticSqlSource对象。StaticSqlSource.getBoundSql方法直接返回BoundSql,BoundSql代码如下。


/**
 * Sql实体。包含sql和 参数集合
 *
 * @author Clinton Begin
 */
public class BoundSql {

    private final String sql;
    /**
     * SQL中参数属性集合#{item}这些
     */
    private final List<ParameterMapping> parameterMappings;
    /**
     * 执行SQL时传入的实际参数
     */
    private final Object parameterObject;
    /**
     * DynamicContext.bindings集合
     */
    private final Map<String, Object> additionalParameters;
    /**
     * additionalParameters对应的MetaObject
     */
    private final MetaObject metaParameters;

    public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.parameterObject = parameterObject;
        this.additionalParameters = new HashMap<>();
        this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public String getSql() {
        return sql;
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    public Object getParameterObject() {
        return parameterObject;
    }

    public boolean hasAdditionalParameter(String name) {
        String paramName = new PropertyTokenizer(name).getName();
        return additionalParameters.containsKey(paramName);
    }

    public void setAdditionalParameter(String name, Object value) {
        metaParameters.setValue(name, value);
    }

    public Object getAdditionalParameter(String name) {
        return metaParameters.getValue(name);
    }
}

DynamicSqlSource

DynamicSqlSource负责解析动态SQL语句,也是最常用的SqlSource实现。DynamicSqlSource使用rootSqlNode字段,记录了带解析的SqlNode根节点DynamicSqlSource的代码如下。


/**
 * 负责解析动态sql语句
 * 包含#{}占位符
 *
 * @author Clinton Begin
 */
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) {
        // 创建
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // 解析sql节点
        rootSqlNode.apply(context);
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        // 解析sql,将#{}替换成?
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
    }

}

RawSqlSource

RawSqlSource的逻辑和DynamicSqlSource类似,但是处理SQL的机制不同。RawSqlSource用于处理非动态SQL。当一个sql中只包含#{}占位符。不包含${}和动态sql节点,就不是动态SQL语句,会创建相应的StaticTextSqlNode在XmlScriptBuilder.parseScriptNode方法会判断整个SQL节点是否是动态的,如果是动态,就用DynamicSqlSource进行处理,否则用RawSqlSource进行处理。

RawSqlSource在构造方法中会调用getSql方法,该方法会调用SqlNode.apply方法完成sql语句的处不处理。SqlSourceBuilder完成占位符的替换,并返回StaticSqlSource对象。


/**
 * 处理非动态sql语句
 * 如果节点只包含“#{}”占位符,而不包含动态 SQL 点或未解析的 “${}”占位
 * 符的话, 则不是动态 SQL 语句
 *
 * @author Eduardo Macarron
 * @since 3.2.0
 */
public class RawSqlSource implements SqlSource {

    private final SqlSource sqlSource;

    public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
        this(configuration, getSql(configuration, rootSqlNode), parameterType);
    }

    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
        sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
    }

    private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
        DynamicContext context = new DynamicContext(configuration, null);
        rootSqlNode.apply(context);
        return context.getSql();
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        return sqlSource.getBoundSql(parameterObject);
    }
}

像foreach、if、where等标签,以及${}占位符,在mybatis初始化时并不知道其具体含义,因此这类sql就视为“动态sql”,交由DynamicSqlSource在程序运行时进行解析。而如果只含有#{}占位符,则会在mybatis初始化时就完成sql解析。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值