开发者

源码解读Mybatis占位符#和$的区别

目录
  • 一. 启动时,myBATis-spring 解析XML文件流程图
  • 二. 运行中,sql语句占位符 #{} 和 ${} 的处理
    • 2.1 ${} 占位符处理
    • 2.2 #{} 占位符处理
  • 三. 总结

    Mybatis 作为国内开发中常用到的半自动 orm 框架,相信大家都很熟悉,它提供了简单灵活的xml映射配置,方便开发人员编写简单、复杂SQL,在国内互联网公司使用众多。

    本文针对笔者日常开发中对 Mybatis 占位符 #{}${} 使用时机结合源码,思考总结而来

    • Mybatis 版本 3.5.11
    • Spring boot 版本 3.0.2
    • mybatis-spring 版本 3.0.1
    • github地址:https://github.com/wayn111 欢迎大家关注,点个star

    一. 启动时,mybatis-spring 解析xml文件流程图

    Spring 项目启动时,mybatis-spring 自动初始化解析xml文件核心流程

    源码解读Mybatis占位符#和$的区别

    MybatisbuildSqlSessionFactory() 会遍历所有 mapperlocations(xml文件) 调用 xmlMapperBuilder.parse()解析,源码如下

    源码解读Mybatis占位符#和$的区别

    在 parse() 方法中, Mybatis 通过 configphpurationElement(parser.evalNode("/mapper")) 方法解析xml文件中的各个标签

    public class XMLMapperBuilder extends BaseBuilder {
      ...
      private final MapperBuilderAssistant builderAssistant;
      private final Map<String, XNode> sqlFragments;
      ...
      
        public void parse() {
          if (!configuration.isResourceLoaded(resource)) {
            // xml文件解析逻辑
            configurationElement(parse编程客栈r.evalNode("/mapper"));
            configuration.addLoadedResource(resource);
            bindMapperForNamespace();
          }
    
          parsePendingResultMaps();
          parsePendingCacheRefs();
          parsePendingStatements();
        }
    
        private void configurationElement(XNode context) {
          try {
            // 解析xml文件内的namespace、cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete等各种标签
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.isEmpty()) {
              throw new BuilderException("Mapper's namespace cannot be empty");
            }
            builderAssistant.setCurrentNamespace(namespace);
            cacheRefElement(context.evalNode("cache-ref"));
            cacheElement(context.evalNode("cache"));
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            sqlElement(context.evalNodes("/mapper/sql"));
            buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
          } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
          }
        }
    }
    

    最后会把 namespace、cache-ref、cache、parameterMap、resultMap、select、insert、update、delete等标签内容解析结果放到 builderAssistant 对象中,将sql标签解析结果放到sqlFragments对象中,其中 由于 builderAssistant 对象会保存select、insert、update、delete标签内容解析结果我们对 builderAssistant 对象进行深入了解

    public class MapperBuilderAssistant extends BaseBuilder {
    ...
    }
    
    public abstract class BaseBuilder {
      protected final Configuration configuration;
      ...
    }  
    
    public class Configuration {
      ...
      protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
          .con开发者_JS培训flictMessageProducer((savedValue, targetValue) ->
              ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
      protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
      protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
      protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
      protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");
      protected final Set<String> loadedResources = new HashSet<>();
      protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");
      ...
    }

    builderAssistant 对象继承至 BaseBuilder,BaseBuilder 类中包含一个 configuration 对象属性, configuration 对象中会保存xml文件标签解析结果至自身对应属性mappedStatements、caches、resultMaps、sqlFragments

    这里有个问题上面提到的sql标签结果会放到 XMLMapperBuilder 类的 sqlFragments 对象中,为什么 Configuration 类中也有个 sqlFragments 属性?

    这里回看上文 buildSqlSessionFactory() 方法最后

    源码解读Mybatis占位符#和$的区别

    原来 XMLMapperBuilder 类中的 sqlFragments 属性就来自Configuration类

    回到主题,在 buildStatementFromContext(context.evalNodes("select|insert|update|delete")) 方法中会通过如下调用

    buildStatementFromContext(List<XNode> list, String requiredDatabaseId) 
    -> parseStatementNode()
    -> createSqlSource(Configuration configuration, XNode script, Class<?> parameterType)
    -> parseScriptNode()
    -> parseDynamicTags(context)

    最后通过parseDynamicTags(context) 方法解析 select、insert、update、delete 标签内容将结果保存在 MixedSqlNode 对象中的 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) {
        contents.forEach(node -> node.apply(context));
        return true;
      }
    }

    SqlNode 是一个接口,有10个实现类如下

    源码解读Mybatis占位符#和$的区别

    可以看出我们的 select、insert、update、delete 标签中包含的各个文本(包含占位符 #{} 和 ${})、子标签都有对应的 SqlNode 实现类,后续运行中, Mybatis 对于 select、insert、update、delete 标签的 sql 语句处理都与这里的 SqlNode 各个实现类相关。自此我们 mybatis-spring 初始化流程中相关的重要代码都过了一遍。

    二. 运行中,sql语句占位符 #{} 和 ${} 的处理

    这里直接给出xml文件查询方法标签内容

    <select id="findNewBeeMallOrderList" parameterType="Map" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from tb_newbee_mall_order
        <where>
            <if test="orderNo!=null and orderNo!=''">
                and order_no = #{orderNo}
            </if>
            <if test="userId!=null and userId!=''">
                and user_id = #{userId}
            </if>
            <if test="payType!=null and payType!=''">
                and pay_type = #{payType}
            </if>
            <if test="orderStatus!=null and orderStatus!=''">
                and order_jsstatus = #{orderStatus}
            </if>
            <if test="isDeleted!=null and isDeleted!=''">
                and is_deleted = #{isDeleted}
            </if>
            <if test="startTime != null and startTime.trim() != ''">
                and create_time &gt; #{startTime}
            </if>
            <if test="endTime != null and endTime.trim() != ''">
                and create_time &lt; #{endTime}
            </if>
        </where>
        <if test="sortField!=null and order!=null">
     php       order by ${sortField} ${order}
        </if>
        <if test="start!=null and limit!=null">
            limit #{start},#{limit}
        </if>
    </select>
    

    运行时 Mybatis 动态代理 MapperProxy 对象的调用流程,如下:

    -> newBeeMallOrderMapper.findNewBeeMallOrderList(pageUtil);
    -> MapperProxy.invoke(Object proxy, Method method, Object[] args)
    -> MapperProxy.invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession)
    -> MapperMethod.execute(SqlSession sqlSession, Object[] args)
    -> MapperMethod.executeForMany(SqlSession sqlSession, Object[] args)
    -> SqlSessionTemplate.selectList(String statement, Object parameter)
    -> SqlSessionInterceptor.invoke(Object proxy, Method method, Object[] args)
    -> DefaultSqlSession.selectList(String statement, Object parameter)
    -> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds)
    -> DefaultSqlSession.selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
    -> CachingExecutor.query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
    -> MappedStatement.getBoundSql(Object parameterObject)
    -> DynamicSqlSource.getBoundSql(Object parameterObject)
    -> MixedSqlNode.apply(DynamicContext context) // ${} 占位符处理
    -> SqlSourceBuilder.parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) // #{} 占位符处理

    Mybatis 通过 DynamicSqlSource.getBoundSql(Object parameterObject) 方法对 select、insert、update、delete 标签内容做 sql 转换处理,代码如下:

      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        rootSqlNode.apply(context);
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
      }

    2.1 ${} 占位符处理

    rootSqlNode.apply(context) -> MixedSqlNode.apply(DynamicContext context) 中会将 SqlNode 集合拼接成实际要执行的 sql 语句

    保存在 DynamicContext 对象中。这里给出 SqlNode 集合的调试截图

    源码解读Mybatis占位符#和$的区别

    可以看出我们的 ${} 占位符文本的 SqlNode 实现类为 TextSqlNode,apply方法相关操作如下

    public class TextSqlNode implements SqlNode {
        ...
        @Override
        public boolean apply(DynamicContext context) {
          GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
          context.appendSql(parser.parse(text));
          return true;
        }
        private GenericTokenParser createParser(TokenHandler handler) {
            return new GenericTokenParser("${", "}", handler);
        }
    
        // 划重点,${}占位符替换逻辑在就handleToken(String content)方法中
        @Override
        public String handleToken(String content) {
              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);
              }
              Object value = OgnlCache.getValue(content, context.getBindings());
              String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
              checkInjection(srtValue);
              return srtValue;
        }
    }
    
    public class GenericTokenParser {
        public String parse(String text) {
            ...
            do {
                ...
                if (end == -1) {
                  ...
                } else {
                  builder.append(handler.handleToken(expression.toString()));
                  offset = end + closeToken.length();
                }
              }
              ...
            } while (start > -1);
            ...
            return builder.toString();
        }
    }   

    划重点,${} 占位符处理如下

    handleToken(String content) 方法中, Mybatis 会通过 ognl 表达式将 ${} 的结果直接拼接在 sql 语句中,由此我们得知 ${} 占位符拼接的字段就是我们传入的原样字段,有着 Sql 注入风险

    2.2 #{} 占位符处理

    #{} 占位符文本的 SqlNode 实现类为 StaticTextSqlNode,查看源码

    public class StaticTextSqlNode implements SqlNode {
      private final String text;
    
      public StaticTextSqlNode(String text) {
        this.text = text;
      }
    
      @Override
      public boolean apply(DynamicContext context) {
        context.appendSql(text);
        return true;
      }
    
    }

    StaticTextSqlNode 会直接将节点内容拼接在 sql 语句中,也就是说在 rootSqlNode.apply(context) 方法执行完毕后,此时的 sql 语句如下

    select order_id, order_no, user_id, total_price, 
    pay_status, pay_type, pay_time, order_status, 
    extra_info, user_name, user_phone, user_address, 
    is_deleted, create_time, update_time 
    from tb_newbee_mall_order
    order by create_time desc
    limit #{start},#{limit}

    Mybatis 会通过上面提到 getBoundSql(Object parameterObject) 方法中的

    源码解读Mybatis占位符#和$的区别

    sqlSourceParser.parse() 方法完成 #{} 占位符的处理,代码如下:

    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;
      if (configuration.isShrinkWhitespacesInSql()) {
        sql = parser.parse(removeExtraWhitespaces(originalSql));
      } else {
        sql = parser.parse(originalSql);
      }
      return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

    看到了熟悉的 #{ 占位符没有,哈哈, Mybatis 对于 #{} 占位符的处理就在 GenericTokenParser类的 parse() 方法中,代码如下:

    public class GenericTokenParser {
        public String parse(String text) {
            ...
            do {
                ...
                if (end == -1) {
                  ...
                } else {
                  builder.append(handler.handleToken(expression.toString()));
                  offset = end + closeToken.length();
                }
              }
              ...
            } while (start > -1);
            ...
            return builder.toString();
        }
    }
    
    public class SqlSourceBuilder extends BaseBuilder {
        ... 
        // 划重点,#{}占位符替换逻辑在就SqlSourceBuilder.handleToken(String content)方法中
        @Override
        public String handleToken(String content) {
          parameterMappings.add(buildParameterMapping(content));
          return "?";
        }
    }

    划重点,#{} 占位符处理如下

    handleToken(String content) 方法中, Mybatis 会直接将我们的传入参数转换成问号http://www.devze.com(就是 jdbc 规范中的问号),也就是说我们的 sql 语句是预处理的。能够避免 sql 注入问题

    三. 总结

    由上经过源码分析,我们知道 Mybatis#{} 占位符是直接转换成问号,拼接预处理 sql。 ${} 占位符是原样拼接处理,有sql注入风险,最好避免由客户端传入此参数。

    到此这篇关于Mybatis占位符#和$的区别 源码解读的文章就介绍到这了,更多相关Mybatis占位符#和$的区别内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜