Mybatis源码深度解析之#{}参数

青苗 青苗 | 1308 | 2022-09-03

mybatis中的#{}参数我们最常用的特性,在mybatis中#{}参数最终会作为编译参数来处理,也就是会被替换为‘?’,然后使用PreparedStatement的setXXX方法设置参数值,所以使用#{}参数没有sql注入的风险。

我们先简单回顾一下JDBC预编译语句的使用:

// 加载驱动
Class.forName(driver);
// 获取db连接
Connection connection = DriverManager.getConnection(url, user, password);
// 创建预编译语句
PreparedStatement ps = connection.prepareStatement("select name from user where id = ?");
// 设置预编译语句参数值
ps.setInt(1, 1);
// 执行
ResultSet resultSet = ps.executeQuery();
// 获取结果
while (resultSet.next()) {
    System.out.println("name = " + resultSet.getString("name"));
}

在使用jdbc的预编译时,我们先将语句中的参数指定为‘?’,在执行前通过setXXX方法为参数指定值。mybatis也是同样的做法,先将#{}替换为?号,然后#{}指定的类型信息来选择对应的set方法进行参数设定。

  • 再简单回顾一下mybatis中#{}的使用:
    下面例子为从一个简单的user表中根据user的id查询user的所有信息。
    Mapper配置:
<mapper namespace="cn.**.UserMapper">
    <select id="queryUserById" resultType="Map">
       select * from user
       <where>
           <if test="id != null">
               id = #{value, javaType=int, jdbcType=NUMERIC}
           </if>
       </where>
        limit 1
    </select>
</mapper

执行查询:
List<?> datas = sqlSession.selectList("queryUserById", 1);

在我们执行queryUserById语句时,mybatis会先将#{value}替换为?,再通过setInt将1给到id参数。

一、解析#{}参数

前面几篇介绍了mybatis如何生成可执行sql和如何进行${}字符串替换,在完成了这两步后就得到了一个只剩余#{}参数需要解析的sql。#{}参数由SqlSourceBuilder进行解析,SqlSourceBuilder在解析时会将#{}替换为‘?’号并将#{}中的内容解析为ParameterMapping的封装,ParameterMapping包含了参数的各个属性。此外SqlSourceBuilder会为语句生成StaticSqlSource,StaticSqlSource代表不包含任何内容的动态内sql,也就是可执行sql。

// src/main/java/cn/ly/ibatis/builder/SqlSourceBuilder.java
public class SqlSourceBuilder extends BaseBuilder {
    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        // ParameterMappingTokenHandler负责解析每一个#{}中的内容
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        // 生成语句的StaticSqlSource
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }
}

1.1 参数说明

SqlSourceBuilder在解析时需要三个参数:

originalSql
要解析的sql,该sql已经解析过${}字符串引用

parameterType
语句的参数的类型,分为两种情况:
(1) 若语句是一个静态语句(不包含${}和动态标签的语句),该参数值为parameterType配置项的指定的类型:

<select id="queryUserById" resultType="Map" parameterType="cn.***.Test">
      ...
</select>

对于这个语句SqlSourceBuilder中parameterType参数的值就为‘cn.***.Test’,若未指定parameterType配置则为Object.class。
(2) 语句非静态语句,那么parameterType就为使用sqlSession执行sql时指定的参数的类型:

sqlSession.selectList("queryUserById", 1);

对于这个查询,SqlSourceBuilder中parameterType类型就为Integer.class。

additionalParameters
额外参数,这里也分为分为两种情况:
(1) 若语句是一个静态语句(不包含${}和动态标签的语句),additionalParameters的值为一个空的HashMap。
(2) 语句非静态语句,那么该参数的值就为使用sqlSession执行sql时指定的参数的ognl上下文,在这个上下文map中包含了两个固定的属性_parameter和_databaseId以及使用bind添加的属性。

public class Test {
    private int id = 1;
    private String name = "zhangsan";
}

<select id="queryUserById" resultType="Map" parameterType="cn.***.test.mybatis.Test">
    <bind name="userName" value="name" />
    select * from user where id = #{id} and name =#{userName} limit 1
</select>

d52fee21984840e9891864e5c39e7188.png

1.2 解析过程

解析是会逐个解析#{}中的内容,为每个参数生成一个ParameterMapping对象,ParameterMapping包含了#{}支持的各个配置项目:

// src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
public class ParameterMapping {
    // 属性
    private String property;
    // OUT或INOUT将会修改参数对象的属性值
    private ParameterMode mode;
    // 属性的类型
    private Class<?> javaType = Object.class;
    // 属性的jdbcType
    private JdbcType jdbcType;
    // 保留小数点的位数
    private Integer numericScale;
    // 参数类型的typeHandler,若为未指定则通过jdbcType和javaType从typeHandlerRegistry中获取
    private TypeHandler<?> typeHandler;
    // 对应的resultMap的id
    private String resultMapId;
    // jdbcType名称
    private String jdbcTypeName;
 }

解析是除了typeHandler和javaType外其它属性都直接从配置中获取到然后设定,若未指定则为null。

// src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
     private ParameterMapping buildParameterMapping(String content) {
         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);
                }
            }
     }
}

ParameterMapping中的typeHandler和javaType是必须的并且很重要,因为在后面设置参数值时,具体使用PreparedStatement哪一个set方法是由typeHandler决定的,而typeHandler是通过javaType获取到的。

解析javaType按以下方式:

additionalParameters是否有属性,若有则获取additionalParameters中属性的set方法的类型。

  • 1,不存在,则查找parameterType是否有该类型的TypeHandler,mybatis默认自带了一些基础类型的TypeHandler

  • 2,不存在,则看#{}中配置的javaType的类型是否为CURSOR,若为CURSOR则javaType为java.sql.ResultSet.class

  • 3,不存在,则看属性是否为指定或parameterType是否为Map类型,若是则类型为Object.class

  • 4,不存在,则parameterType指定的类型是否有该属性的get方法,若有则取get方法的返回类型

若以上都不存在,则取属性的java类型取Object.class
若指定了javaType属性则直接取指定的值。

若指定了typeHandler则直接获取指定的类型,若未指定则通过上面步骤获取到javaType查找该类型的typeHandler。

// src/main/java/org/apache/ibatis/mapping/ParameterMapping.java
private ParameterMapping buildParameterMapping(String content) {
    private ParameterMapping buildParameterMapping(String content) {
        Map<String, String> propertiesMap = parseParameterMapping(content);
        String property = propertiesMap.get("property");
        // 获取属性的类型
        Class<?> propertyType;
        if (metaParameters.hasGetter(property)) {
            propertyType = metaParameters.getSetterType(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;
            }
        }
        ....
        return builder.build();
    }
}

至此sql中所有的#{}参数都被解析为了ParameterMapping,并被替换为了‘?’,接下来再将替换了#{}的sql和所有的ParameterMapping封装到StaticSqlSource中,由StaticSqlSource提供后续的支持。

// src/main/java/org/apache/ibatis/builder/StaticSqlSource.java
public class StaticSqlSource implements SqlSource {
  // 解析后的sql
  private final String sql;
  // sql中所有的#{}参数
  private final List<ParameterMapping> parameterMappings;
  private final Configuration configuration;
}

二、什么时候进行#{}解析

上面介绍了解析的过程,那什么触发#{}的解析那?

#{}的解析时机分为两种情况,静态语句和动态态语句。在SqlSessionFactoryBuilder().build()初始化mybatis时会解析所有配置的语句,对于动态语句会被解析为DynamicSqlSource而静态语句解析为RawSqlSource,这两者都是SqlSource代表了语句的sql,最终都是通过SqlSourceBuilder解析#{}参数并生成StaticSqlSource。

对于RawSqlSource由于不包含动态内容,所以在解析阶段就可以得到最终的sql,故可以在初始时解析#{}参数并生成StaticSqlSource,所以比DynamicSqlSource执行会快一些。
而对于DynamicSqlSource由于包含动态内容所以要在执行是才能知道要执行的sql,也就是到执行时才能知道需哪些#{}会被使用到,所以DynamicSqlSource在语句执行时#{}参数才会被解析。

三、设定参数值

mybatis中语句有STATEMENT,PREPARED和CALLABLE三种类型,分别对应JDBC的Statement,PreparedStatement或 CallableStatement,这三种类型在mybatis中有三种类型的有对应的StatementHandler进行特殊处理,分别为SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler, StatementHandler中定义了处理与编译参数菜单接口parameterize,在执行语句之前会调用该接口进行设置参数值。

public interface StatementHandler {
    // 设置预编译参数
    void parameterize(Statement statement)
      throws SQLException;
}

SimpleStatementHandler对的实现为空方法,所以若语句中包含#{}参数就需要将语句类型设定为PREPARED或CALLABLE,在StatementHandler中使用ParameterHandler来设置参数,ParameterHandler接口中setParameters方法设置。对于xml配置的语句其为DefaultParameterHandler。
DefaultParameterHandler在设置参数时,先获取到参数的值,然后使用ParameterMapping中之前解析到对应的typeHandler来设置值,最终如何设置取决与typeHandler的setParameter设置,例如String的StringTypeHandler使用setString方法:

// src/main/java/org/apache/ibatis/type/StringTypeHandler.java
public class StringTypeHandler extends BaseTypeHandler<String> {
    @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }
}

DefaultParameterHandler会按顺序遍历所有的ParameterMapping,逐个进行设置。在设置时先获取属性的值,获取参数值时先从额外参数中获取,也就是bind值得参数,若额外参数中没有则看传递的参数是否有对应TypeHandler,若用则直接取传递的参数值,若也没有则任务当前属性为传递的参数中的一个属性调用get方法从传递的参数中获取。

public class DefaultParameterHandler implements ParameterHandler {
  public void setParameters(PreparedStatement ps) {
    ...
    for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() == ParameterMode.OUT) {
            continue;
        }
        Object value;

        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
            // 先从额外指定的参数中获取属性值
            value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
            value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            // 有ypeHandler则该属性直接为parameterObject
            value = parameterObject;
        } else {
            // 从get方法中获取
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
        }
        /// 获取属性的typeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
    }
  }
}

至此语句中的#{}参数就解析并设置完成,也得到了一个可执行的语句,后面就执行该语句并解析结果了。

作者:蛋不炒饭
链接:https://juejin.cn/post/6915016292433051655

关联知识库

MybatisPlus 杂谈
文章标签: MybatisPlus
推荐指数:

真诚点赞 诚不我欺~

Mybatis源码深度解析之#{}参数

点赞 收藏 评论

关于作者

青苗
青苗

青苗幼儿园园长

等级 LV5

粉丝 20

获赞 47

经验 1182

关联知识库

MybatisPlus 杂谈