青苗 青苗 | 827 | 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}
           </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>
    

    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源码深度解析之#{}参数 点赞 收藏 评论