当前位置:首页 >综合 >Mybatis占位符#和$的区别?源码解读 位符点个star一. 启动时

Mybatis占位符#和$的区别?源码解读 位符点个star一. 启动时

2024-06-28 19:25:32 [百科] 来源:避面尹邢网

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

作者:waynaqua 数据库 其他数据库 Mybatis 作为国内开发中常用到的半自动 orm 框架,相信大家都很熟悉,区别它提供了简单灵活的源码xml映射配置,方便开发人员编写简单、解读复杂SQL,位符在国内互联网公司使用众多。区别

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

Mybatis占位符#和$的区别?源码解读 位符点个star一. 启动时

  • • 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占位符#和$的区别?源码解读 位符点个star一. 启动时

Mybatis占位符#和$的区别?源码解读 位符点个star一. 启动时

流程图

Mybatis在buildSqlSessionFactory()会遍历所有mapperLocations(xml文件)调用xmlMapperBuilder.parse()解析,位符源码如下:

在 parse() 方法中,区别Mybatis通过configurationElement(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(parser.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")      .conflictMessageProducer((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()方法最后。

原来 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个实现类如下:

可以看出我们的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_status = #{ orderStatus}        </if>        <if test="isDeleted!=null and isDeleted!=''">            and is_deleted = #{ isDeleted}        </if>        <if test="startTime != null and startTime.trim() != ''">            and create_time > #{ startTime}        </if>        <if test="endTime != null and endTime.trim() != ''">            and create_time < #{ endTime}        </if>    </where>    <if test="sortField!=null and order!=null">        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;  }

1、${ }占位符处理

在rootSqlNode.apply(context) -> MixedSqlNode.apply(DynamicContext context)中会将 SqlNode 集合拼接成实际要执行的 sql 语句 保存在 DynamicContext 对象中。这里给出 SqlNode 集合的调试截图。

可以看出我们的${ }占位符文本的 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、#{ }占位符处理

#{ } 占位符文本的 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_orderorder by create_time desclimit #{ start},#{ limit}

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

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 会直接将我们的传入参数转换成问号(就是 jdbc 规范中的问号),也就是说我们的 sql 语句是预处理的。能够避免 sql 注入问题

三. 总结

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

责任编辑:姜华 来源: 今日头条 Mybatis占位符

(责任编辑:焦点)

    推荐文章
    热点阅读