程序员的资源宝库

网站首页 > gitee 正文

精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下)

sanyeah 2024-04-01 11:17:23 gitee 7 ℃ 0 评论

摘自:https://www.cnblogs.com/lifullmoon/p/14015075.html

该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址、Mybatis-Spring 源码分析 GitHub 地址、Spring-Boot-Starter 源码分析 GitHub 地址)进行阅读

MyBatis 版本:3.5.2

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

MyBatis的初始化

在MyBatis初始化过程中,大致会有以下几个步骤:

  1. 创建Configuration全局配置对象,会往TypeAliasRegistry别名注册中心添加Mybatis需要用到的相关类,并设置默认的语言驱动类为XMLLanguageDriver

  2. 加载mybatis-config.xml配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息会形成相应的对象并保存到Configuration全局配置对象中

  3. 构建DefaultSqlSessionFactory对象,通过它可以创建DefaultSqlSession对象,MyBatis中SqlSession的默认实现类

因为整个初始化过程涉及到的代码比较多,所以拆分成了四个模块依次对MyBatis的初始化进行分析:

  • 《MyBatis初始化(一)之加载mybatis-config.xml》
  • 《MyBatis初始化(二)之加载Mapper接口与XML映射文件》
  • 《MyBatis初始化(三)之SQL初始化(上)》
  • 《MyBatis初始化(四)之SQL初始化(下)》

由于在MyBatis的初始化过程中去解析Mapper接口与XML映射文件涉及到的篇幅比较多,XML映射文件的解析过程也比较复杂,所以才分成了后面三个模块,逐步分析,这样便于理解

初始化(四)之SQL初始化(下)

在上一篇文档中详细地讲述了MyBatis在解析<select /> <insert /> <update /> <delete /> 节点的过程中,是如何解析SQL语句的,如何实现动态SQL语句的,最终会生成一个org.apache.ibatis.mapping.SqlSource对象的,那么接下来我们来看看SqlSource到底是什么

主要包路径:org.apache.ibatis.mapping、org.apache.ibatis.builder

主要涉及到的类:

  • org.apache.ibatis.builder.SqlSourceBuilder:继承了BaseBuilder抽象类,SqlSource构建器,负责将SQL语句中的#{}替换成相应的?占位符,并获取该?占位符对应的 ParameterMapping对象
  • org.apache.ibatis.builder.ParameterExpression:继承了HashMap<String, String>,参数表达式处理器,在SqlSourceBuilder处理#{}的内容时,需要通过其解析成key-value键值对
  • org.apache.ibatis.mapping.ParameterMapping:保存#{}中配置的属性参数信息
  • org.apache.ibatis.mapping.SqlSource:SQL 资源接口,用于创建BoundSql对象(包含可执行的SQL语句与参数信息)
  • org.apache.ibatis.mapping.BoundSql:用于数据库可执行的SQL语句的最终封装对象
  • org.apache.ibatis.scripting.defaults.DefaultParameterHandler:实现了ParameterHandler接口,用于将入参设置到java.sql.PreparedStatement预编译对象中

用于将入参设置到java.sql.PreparedStatement预编译对象中

我们先来回顾一下org.apache.ibatis.scripting.xmltags.XMLScriptBuilderparseScriptNode()方法,将 SQL 脚本(XML或者注解中定义的 SQL )解析成 SqlSource 对象

代码如下:

public SqlSource parseScriptNode() { // 解析 XML 或者注解中定义的 SQL MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { // 动态语句,使用了 ${} 也算 sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }
  1. 如果是动态 SQL 语句,使用了 MyBatis 的自定义标签(<if /> <foreach />等)或者使用了 ${} 都是动态 SQL 语句,则会创建DynamicSqlSource对象
  2. 否则就是静态 SQL 语句,创建 RawSqlSource 对象

SqlSource接口的实现类如下图所示:

SqlSourceBuilder

org.apache.ibatis.builder.SqlSourceBuilder:继承了BaseBuilder抽象类,SqlSource构建器,负责将SQL语句中的#{}替换成相应的?占位符,并获取该?占位符对应的 org.apache.ibatis.mapping.ParameterMapping 对象

构造方法

public class SqlSourceBuilder extends BaseBuilder { private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; public SqlSourceBuilder(Configuration configuration) { super(configuration); } }

其中PARAMETER_PROPERTIES字符串定义了#{}中支持定义哪些属性,在抛异常的时候用到

parse方法

解析原始的SQL(仅包含#{}定义的参数),转换成StaticSqlSource对象

因为在DynamicSqlSource调用该方法前会将MixedSqlNode进行处理,调用其apply方法进行应用,根据DynamicContext上下文对MyBatis的自定义标签或者包含${}的SQL生成的SqlNode进行逻辑处理或者注入值,生成一个SQL(仅包含#{}定义的参数)

代码如下:

/** * 执行解析原始 SQL ,成为 SqlSource 对象 * * @param originalSql 原始 SQL * @param parameterType 参数类型 * @param additionalParameters 上下文的参数集合,包含附加参数集合(通过 <bind /> 标签生成的,或者`<foreach />`标签中的集合的元素) * RawSqlSource传入空集合 * DynamicSqlSource传入 {@link org.apache.ibatis.scripting.xmltags.DynamicContext#bindings} 集合 * @return SqlSource 对象 */ public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { // <1> 创建 ParameterMappingTokenHandler 对象 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); // <2> 创建 GenericTokenParser 对象 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); /* * <3> 执行解析 * 将我们在 SQL 定义的所有占位符 #{content} 都替换成 ? * 并生成对应的 ParameterMapping 对象保存在 ParameterMappingTokenHandler 中 */ String sql = parser.parse(originalSql); // <4> 创建 StaticSqlSource 对象 return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }

该方法的入参originalSql为原始的SQL,也就是其所有的SqlNode节点已经应用了,也就是都调用了apply方法

包含的${}也已经注入了对应的值,所以这里只剩#{}定义的入参了

  1. 创建ParameterMappingTokenHandler处理器对象handler
  2. 创建GenericTokenParser对象,用于处理#{}中的内容,通过handler将其转换成?占位符,并创建对应的ParameterMapping对象
  3. 执行解析,获取最终的 SQL 语句
  4. 创建StaticSqlSource对象

ParameterMappingTokenHandler

org.apache.ibatis.builder.SqlSourceBuilder的内部类,用于解析#{}的内容,创建ParameterMapping对象,并将其替换成?占位符

代码如下:

private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { /** * 我们在 SQL 语句中定义的占位符对应的 ParameterMapping 数组,根据顺序来的 */ private List<ParameterMapping> parameterMappings = new ArrayList<>(); /** * 参数类型 */ private Class<?> parameterType; /** * additionalParameters 参数的对应的 MetaObject 对象 */ private MetaObject metaParameters; public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) { super(configuration); this.parameterType = parameterType; // 创建 additionalParameters 参数的对应的 MetaObject 对象 this.metaParameters = configuration.newMetaObject(additionalParameters); } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } @Override public String handleToken(String content) { // <1> 构建 ParameterMapping 对象,并添加到 parameterMappings 中 parameterMappings.add(buildParameterMapping(content)); // <2> 返回 ? 占位符 return "?"; } /** * 根据内容构建一个 ParameterMapping 对象 * * @param content 我们在 SQL 语句中定义的占位符 * @return ParameterMapping 对象 */ private ParameterMapping buildParameterMapping(String content) { // <1> 将字符串解析成 key-value 键值对保存 // 其中有一个key为"property",value就是对应的属性名称 Map<String, String> propertiesMap = parseParameterMapping(content); // <2> 获得属性的名字和类型 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)) { // 有对应的类型处理器,例如java.lang.string propertyType = parameterType; } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { // 设置的 Jdbc Type 是游标 propertyType = java.sql.ResultSet.class; } else if (property == null || Map.class.isAssignableFrom(parameterType)) { // 是 Map 集合 propertyType = Object.class; } else { // 类对象 MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); if (metaClass.hasGetter(property)) { // 通过反射获取到其对应的 Java Type propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } // <3> 创建 ParameterMapping.Builder 构建者对象 ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); // <3.1> 初始化 ParameterMapping.Builder 对象的属性 Class<?> javaType = propertyType; String typeHandlerAlias = null; // 遍历 SQL 配置的占位符信息,例如这样配置:"name = #{name, jdbcType=VARCHAR}" 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); } } // <3.2> 如果 TypeHandler 类型处理器的别名非空 if (typeHandlerAlias != null) { builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); } // <3.3> 创建 ParameterMapping 对象 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); } } }

构造方法:创建additionalParameters对应的MetaObject对象,便于操作上下文的参数集合,包含附加参数集合(通过 <bind /> 标签生成的,或者<foreach />标签中的集合的元素)

handleToken(String content)方法:

  1. 调用buildParameterMapping(content)方法,解析#{}的内容创建ParameterMapping对象

  2. 直接返回?占位符

buildParameterMapping(content)方法:

  1. 将字符串解析成 key-value 键值对,通过org.apache.ibatis.builder.ParameterExpression进行解析,其中有一个key为"property",value就是对应的属性名称
  2. 获得属性的名字和类型
  3. 创建ParameterMapping.Builder构建者对象,设置参数的名称与Java Type
    1. 将上面第1步解析到key-value键值对设置到Builder中
    2. 如果TypeHandler类型处理器的别名非空,则尝试获取其对应的类型处理器并设置到Builder中
    3. 通过Builder创建ParameterMapping对象,如果没有配置TypeHandler类型处理器,则根据参数Java Type和Jdbc Type从TypeHandlerRegistry注册中心获取并赋值到该对象中

ParameterExpression

org.apache.ibatis.builder.ParameterExpression:继承了HashMap<String, String>,参数表达式处理器,在ParameterMappingTokenHandler处理#{}的内容时需要通过其解析成key-value键值对

构造方法:

public class ParameterExpression extends HashMap<String, String> { private static final long serialVersionUID = -2417552199605158680L; /** * 从类的注释中可以看出我们可以这样定义占位符 * 1. #{propertyName, javaType=string, jdbcType=VARCHAR} * 2. #{(expression), javaType=string, jdbcType=VARCHAR} * * @param expression 我们定义的占位符表达式 */ public ParameterExpression(String expression) { parse(expression); } }

在构造函数中调用其parse(String expression)方法

private void parse(String expression) { // 跳过前面的非法字符(ASCII 小于33),目的是去除空格,还有非法的字符,可以参照 ASCII 字符代码表看看 int p = skipWS(expression, 0); if (expression.charAt(p) == '(') { // 属于第二种方式,我在官方没有看到介绍,这里也不做介绍了 expression(expression, p + 1); } else { // 将整个字符串转换成 key-value 保存至 Map.Entry property(expression, p); } }

先出去前面的空格或者非法字符,然后调用property(String expression, int left)方法

// #{propertyName, javaType=string, jdbcType=VARCHAR} private void property(String expression, int left) { if (left < expression.length()) { // 获取到逗号或者冒号第一个位置,也就是分隔符 int right = skipUntil(expression, left, ",:"); // 从内容中截取第一个逗号前面的字符串,也上面第 1 种方式的 "name" put("property", trimmedStr(expression, left, right)); // 解析字符串一个逗号后面的字符串,也就是该属性的相关配置 jdbcTypeOpt(expression, right); } }

如果left开始位置小于字符串的长度,那么开始解析

  1. 调用skipUntil方法,获取从left开始,或者:第一个位置,也就是分隔符的位置

  2. 这里第一次进入的话就会先获取第一个,的位置,那么调用trimmedStr方法截取前面的字符串,也就是属性名称,然后存放一个键值对(key为property,value为属性名称)

  3. 调用jdbcTypeOpt(String expression, int p)方法,继续解析后面的字符串,也就是该属性的相关配置

private void jdbcTypeOpt(String expression, int p) { p = skipWS(expression, p); if (p < expression.length()) { if (expression.charAt(p) == ':') { // 属于上面第 2 种方式,不做分析 jdbcType(expression, p + 1); } else if (expression.charAt(p) == ',') { // 将第一个 , 后面的字符串解析成 key-value 保存 option(expression, p + 1); } else { throw new BuilderException("Parsing error in {" + expression + "} in position " + p); } } }

如果p(第一个,的位置)后面还有字符串

则调用option(String expression, int p)方法将一个,后面的字符串解析成key-value键值对保存

/** * 将字符串生成转换成key-value的形式 * 例如 expression = "name, jdbcType = VARCHAR, javaType = string" 设置 p = 6 * 这样将会往 Map 中保存两个键值对:"jdbcType"->"VARCHAR" "javaType"->"string" * * @param expression 字符串 * @param p 字符串从哪个位置转换 */ private void option(String expression, int p) { int left = skipWS(expression, p); if (left < expression.length()) { // 获取 = 的位置 int right = skipUntil(expression, left, "="); // 截取 = 前面的字符串,对应的 key String name = trimmedStr(expression, left, right); left = right + 1; // 获取 , 的位置 right = skipUntil(expression, left, ","); // 截取 = 到 , 之间的字符串,也就是对应的 value String value = trimmedStr(expression, left, right); // 将 key-value 保存 put(name, value); // 继续遍历后面的字符串 option(expression, right + 1); } }

逐步解析,将字符串解析成key-value键值对保存,这里保存的都是属性的相关配置,例如JdbcType配置

ParameterMapping

org.apache.ibatis.mapping.ParameterMapping:保存#{}中配置的属性参数信息,一个普通的实体类,代码如下:

/** * SQL 语句中 ? 占位符对应的对象 * * @author Clinton Begin */ public class ParameterMapping { /** * 全局配置对象 */ private Configuration configuration; /** * 属性名称 */ private String property; /** * 参数模式 */ private ParameterMode mode; /** * 属性的 Java Type * 一般可以直接通过入参对象知道,但是如果入参是 Map,需要显式指定,以确保使用正确的类型处理器 */ private Class<?> javaType = Object.class; /** * 属性的 Jdbc Type */ private JdbcType jdbcType; /** * 对于数值类型,指定小数点后保留的位数 */ private Integer numericScale; /** * 类型处理器 */ private TypeHandler<?> typeHandler; /** * 如果 {@link mode} 为 OUT 或者 INOUT,且{@link jdbcType} 为 CURSOR(也就是 Oracle 的 REFCURSOR) * 必须指定一个 resultMap 引用来将结果集 ResultMap 映射到参数的类型上 */ private String resultMapId; /** * Jdbc Type 名称 */ private String jdbcTypeName; private String expression; private ParameterMapping() { } }

SqlSource

org.apache.ibatis.mapping.SqlSource:SQL 资源接口,用于创建BoundSql对象(包含可执行的SQL语句与参数信息),代码如下:

/** * Represents the content of a mapped statement read from an XML file or an annotation. * It creates the SQL that will be passed to the database out of the input parameter received from the user. * * @author Clinton Begin */ public interface SqlSource { /** * 根据传入的参数对象,返回 BoundSql 对象 * * @param parameterObject 参数对象 * @return BoundSql 对象 */ BoundSql getBoundSql(Object parameterObject); }

StaticSqlSource

org.apache.ibatis.builder.StaticSqlSource:实现 SqlSource 接口,静态的 SqlSource 实现类,代码如下:

public class StaticSqlSource implements SqlSource { /** * 解析后的 SQL 语句,数据库能执行 */ private final String sql; /** * 上面 SQL 语句中占位符对应的 ParameterMapping 参数集合 */ private final List<ParameterMapping> parameterMappings; /** * 全局配置对象 */ 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; } @Override public BoundSql getBoundSql(Object parameterObject) { return new BoundSql(configuration, sql, parameterMappings, parameterObject); } }

SqlSourceBuilder构建的SqlSource类型就是StaticSqlSource,用于获取最终的静态 SQL 语句

RawSqlSource

org.apache.ibatis.scripting.defaults.RawSqlSource:实现了SqlSource接口,静态SQL语句对应的SqlSource对象,用于创建静态 SQL 资源,代码如下:

public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) { /* * 因为静态的 SQL 语句可以直接拿来解析,不需要根据入参就可以应用 * 所以调用 getSql 方法获取静态的 SQL 语句 */ 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; // 通过 SqlSourceBuilder 将这个静态的 SQL 进行转换,变量替换成 ? 占位符,并生成对应的 ParameterMapping 集合 sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); } private static String getSql(Configuration configuration, SqlNode rootSqlNode) { DynamicContext context = new DynamicContext(configuration, null); // 调用 StaticTextSqlNode 将 SQL 语句拼接起来 rootSqlNode.apply(context); return context.getSql(); } @Override public BoundSql getBoundSql(Object parameterObject) { return sqlSource.getBoundSql(parameterObject); } }

在构造函数中我们可以看到,会先调用getSql方法直接创建SqlSource

因为静态的 SQL 语句,不需要根据入参来进行逻辑上的判断处理,所以这里在构造函数中就先初始化好 SqlSource,后续需要调用Mapper接口执行SQL的时候就减少了一定的时间

getSql方法:

  1. 创建一个上下文对象DynamicContext,入参信息为null
  2. 调用StaticTextSqlNodeapply方法,将所有的SQL拼接在一起
  3. 返回拼接好的SQL语句

构造方法:

  1. 创建SqlSourceBuilder构建对象sqlSourceParser
  2. 调用sqlSourceParserparse方法对该SQL语句进行转换,#{}全部替换成?占位符,并创建对应的ParameterMapping对象
  3. 2步返回的StaticSqlSource对象设置到自己的sqlSource属性中

getBoundSql方法:直接通过StaticSqlSource创建BoundSql对象

DynamicSqlSource

org.apache.ibatis.scripting.defaults.DynamicSqlSource:实现了SqlSource接口,动态SQL语句对应的SqlSource对象,用于创建静态 SQL 资源,代码如下:

public class DynamicSqlSource implements SqlSource { private final Configuration configuration; /** * 根 SqlNode 对象 */ private final SqlNode rootSqlNode; public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { this.configuration = configuration; this.rootSqlNode = rootSqlNode; } @Override public BoundSql getBoundSql(Object parameterObject) { // <1> 创建本次解析的动态 SQL 语句的上下文 DynamicContext context = new DynamicContext(configuration, parameterObject); // <2> 根据上下文应用整个 SqlNode rootSqlNode.apply(context); // <3> 创建 SqlSourceBuilder 对象 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); // <4> 通过 SqlSourceBuilder 将应用后的 SQL 进行转换,变量替换成 ? 占位符,并生成对应的 ParameterMapping 集合 SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); // <5> 创建 BoundSql 对象 BoundSql boundSql = sqlSource.getBoundSql(parameterObject); // <6> 添加附加参数到 BoundSql 对象中,因为上一步创建的`BoundSql`对象时候传入的仅是入参信息,没有添加附加参数 context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; } }

在构造函数中仅仅是赋值,不像RawSqlSource的构造函数一样直接可创建对应的SqlSource对象,因为动态SQL语句需要根据入参信息,来解析SqlNode节点,所以这里在getBoundSql方法中每次都会创建StaticSqlSource对象

getBoundSql方法:

  1. 创建本次解析的动态 SQL 语句的上下文,设置入参信息
  2. 根据上下文应用整个 SqlNode,内部包含的所有SqlNode都会被应用,最终解析后的SQL会保存上下文中
  3. 创建 SqlSourceBuilder 构建对象sqlSourceParser
  4. 调用sqlSourceParserparse方法对第2步解析后的SQL语句进行转换,#{}全部替换成?占位符,并创建对应的ParameterMapping对象
  5. 通过第4步返回的StaticSqlSource对象创建BoundSql对象
  6. 添加附加参数到BoundSql对象中,因为上一步创建的BoundSql对象时候传入的仅是入参信息,没有添加附加参数(通过<bind />标签生成的,或者<foreach />标签中的集合的元素)

BoundSql

org.apache.ibatis.mapping.BoundSql:用于数据库可执行的SQL语句的最终封装对象,一个普通的实体类,代码如下:

public class BoundSql { /** * SQL 语句 */ private final String sql; /** * 占位符 ? 对应的入参信息 */ private final List<ParameterMapping> parameterMappings; /** * 入参对象 */ private final Object parameterObject; /** * 附加参数集合 */ private final Map<String, Object> 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); } }

DefaultParameterHandler

org.apache.ibatis.scripting.defaults.DefaultParameterHandler:实现了ParameterHandler接口,默认实现类,仅提供这个实现类,用于将入参设置到java.sql.PreparedStatement预编译对象中

回看到org.apache.ibatis.scripting.xmltags.XMLLanguageDriver语言驱动类中,实现了createParameterHandler方法,返回的参数处理器就是该对象

代码如下:

public class DefaultParameterHandler implements ParameterHandler { private final TypeHandlerRegistry typeHandlerRegistry; /** * MappedStatement 对象 */ private final MappedStatement mappedStatement; /** * 入参 */ private final Object parameterObject; /** * BoundSql 对象,实际的 SQL 语句 */ private final BoundSql boundSql; /** * 全局配置对象 */ private final Configuration configuration; public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; this.configuration = mappedStatement.getConfiguration(); this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry(); this.parameterObject = parameterObject; this.boundSql = boundSql; } @Override public Object getParameterObject() { return parameterObject; } @Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); // 获取 SQL 的参数信息 ParameterMapping 对象 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { // 遍历所有参数 for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); /* * OUT 表示参数仅作为出参,非 OUT 也就是需要作为入参 */ if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; // 获取入参的属性名 String propertyName = parameterMapping.getProperty(); /* * 获取入参的实际值 */ if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params // 在附加参数集合(<bind />标签生成的)中获取 value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { // 入参为 null 则该属性也定义为 null value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { // 有类型处理器,则直接获取入参对象 value = parameterObject; } else { // 创建入参对应的 MetaObject 对象并获取该属性的值 MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } // 获取定义的参数类型处理器 TypeHandler typeHandler = parameterMapping.getTypeHandler(); // 获取定义的 Jdbc Type JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { // 如果没有则设置成 'OTHER' jdbcType = configuration.getJdbcTypeForNull(); } try { // 通过定义的 TypeHandler 参数类型处理器将 value 设置到对应的占位符 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException( "Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } } }

PreparedStatement中设置参数的大致逻辑如下:

  1. 获取SQL的参数信息ParameterMapping对象的集合,然后对其遍历
  2. 如果参数的模式不为ParameterMode.OUT(默认为ParameterMode.IN),也就是说需要作为入参,那么开始接下来的赋值
  3. 获取该参数对应的属性名称,并通过其获取到对应的值
  4. 获取到TypeHandler类型处理器(在ParameterMapping构建的时候会创建对应的TypeHandler
  5. 获取到Jdbc Type
  6. 通过TypeHandler类型处理器,根据参数位置和Jdbc Type将属性值设置到PreparedStatement

这样就完成对PreparedStatement的赋值,然后通过它执行SQL语句

总结

在MyBatis初始化的过程中,会将XML映射文件中的<select /> <insert /> <update /> <delete />节点解析成MappedStatement对象,其中会将节点中定义的SQL语句通过XMLLanguageDriver语言驱动类创建一个SqlSource对象,本文就是对该对象进行分析

通过SqlSource这个对象根据入参可以获取到对应的BoundSql对象,BoundSql对象中包含了数据库需要执行的SQL语句、ParameterMapping参数信息、入参对象和附加的参数(通过<bind />标签生成的,或者<foreach />标签中的集合的元素等等)

好了,对于MyBatis的整个初始化过程我们已经全部分析完了,其中肯定有不对或者迷惑的地方,欢迎指正!!!感谢大家的阅读!!!??????

参考文章:芋道源码《精尽 MyBatis 源码分析》

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表