美文网首页
从源码的角度,来阐述#「name」和$「name」的区别?面试官

从源码的角度,来阐述#「name」和$「name」的区别?面试官

作者: 程序员麦冬 | 来源:发表于2020-10-29 14:03 被阅读0次

    1. #{name}和${name}的区别。

    .#{name}:表示这是一个参数(ParameterMapping)占位符,值来自于运行时传递给sql的参数,也就是XXXMapper.xml里的parameterType。其值通过PreparedStatement的setObject()等方法赋值。

    动态sql中的<bind>标签绑定的值,也是使用#{name}来使用的。

    .#{name}用在sql文本中。

    {name}:表示这是一个属性配置占位符,值来自于属性配置文件,比如jdbc.properties,其值通过类似replace方法进行静态替换。比如{driver},将被静态替换为com.mysql.jdbc.Driver。

    ${name}则可以用在xml的Attribute属性,还可以用在sql文本当中。

        <select id="countAll" resultType="${driver}">
            select count(1) from (
                select 
                stud_id as studId
                , name, email
                , dob
                , phone
            from students #{offset}, ${driver}
            ) tmp 
        </select>
    

    2. ${name}的工作原理

    org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()部分源码。

      public void parseStatementNode() {
    //...
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    // ...
    }
    

    org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)部分源码。

    private void applyIncludes(Node source, final Properties variablesContext) {
        if (source.getNodeName().equals("include")) {
          // new full context for included SQL - contains inherited context and new variables from current include node
          Properties fullContext;
    
          String refid = getStringAttribute(source, "refid");
          // replace variables in include refid value
          refid = PropertyParser.parse(refid, variablesContext);
          Node toInclude = findSqlFragment(refid);
          Properties newVariablesContext = getVariablesContext(source, variablesContext);
          if (!newVariablesContext.isEmpty()) {
            // merge contexts
            fullContext = new Properties();
            fullContext.putAll(variablesContext);
            fullContext.putAll(newVariablesContext);
          } else {
            // no new context - use inherited fully
            fullContext = variablesContext;
          }
          applyIncludes(toInclude, fullContext);
          if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
            toInclude = source.getOwnerDocument().importNode(toInclude, true);
          }
          source.getParentNode().replaceChild(toInclude, source);
          while (toInclude.hasChildNodes()) {
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
          }
          toInclude.getParentNode().removeChild(toInclude);
        } else if (source.getNodeType() == Node.ELEMENT_NODE) {
          NodeList children = source.getChildNodes();
          for (int i=0; i<children.getLength(); i++) {
            applyIncludes(children.item(i), variablesContext);
          }
        } else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
          // replace variables in all attribute values
          // 通过PropertyParser替换所有${xxx}占位符(attribute属性)
          source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
        } else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
          // replace variables ins all text nodes
          // 通过PropertyParser替换所有${xxx}占位符(文本节点)
          source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
        }
      }
    

    也就是说,Mybatis在解析<include>标签时,就已经静态替换${name}占位符了。

    public class PropertyParser {
    
      private PropertyParser() {
        // Prevent Instantiation
      }
    
      public static String parse(String string, Properties variables) {
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
      }
    
      private static class VariableTokenHandler implements TokenHandler {
        private Properties variables;
    
        public VariableTokenHandler(Properties variables) {
          this.variables = variables;
        }
    
        @Override
        public String handleToken(String content) {
          if (variables != null && variables.containsKey(content)) {
            return variables.getProperty(content);
          }
          return "${" + content + "}";
        }
      }
    }
    

    3. #{name}的工作原理

    .#{name}是ParameterMapping参数占位符,Mybatis将会把#{name}替换为?号,并通过OGNL来计算#{xxx}内部的OGNL表达式的值,作为PreparedStatement的setObject()的参数值。

    举例:#{item.name}将被替换为sql的?号占位符,item.name则是OGNL表达式,OGNL将计算item.name的值,作为sql的?号占位符的值。

    如果只有静态sql,#{name}将在解析xml文件时,完成替换为?占位符。如果有动态sql的内容,#{name}将在执行sql时,动态替换为?占位符。

    org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.parseScriptNode()。

      public SqlSource parseScriptNode() {
        List<SqlNode> contents = parseDynamicTags(context);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        SqlSource sqlSource = null;
        if (isDynamic) {
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
      }
    
    public class RawSqlSource implements SqlSource {
    
      private final SqlSource sqlSource;
    
      public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
        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;
        // 在这里完成#{xxx}替换为?号
        sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
      }
    
      private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
        DynamicContext context = new DynamicContext(configuration, null);
        // 创建RawSqlSource时,就完成sql的拼接工作,因为它没有动态sql的内容,Mybatis初始化时,就能确定最终的sql。
        rootSqlNode.apply(context);
        return context.getSql();
      }
    
      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        return sqlSource.getBoundSql(parameterObject);
      }
    
    }
    

    org.apache.ibatis.builder.SqlSourceBuilder.parse(String, Class<?>, Map<String, Object>)。

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        //  使用ParameterMappingTokenHandler策略来处理#{xxx}
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
      }
    

    GenericTokenParser.java是通用解析占位符的工具类,它可以解析{name}和#{name},那么,解析到{name}和#{name}后,要如何处理这样的占位符,则由不同的策略TokenHandler来完成。

    4. TokenHandler

    GenericTokenParser.java负责解析sql中的占位符${name}和#{name},TokenHandler则是如何处理这些占位符。

    ParameterMappingTokenHandler:处理#{xxx}占位符。

    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
    
        private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
        private Class<?> parameterType;
        private MetaObject metaParameters;
    
        public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
          super(configuration);
          this.parameterType = parameterType;
          this.metaParameters = configuration.newMetaObject(additionalParameters);
        }
    
        public List<ParameterMapping> getParameterMappings() {
          return parameterMappings;
        }
    
        @Override
        public String handleToken(String content) {
          // 创建一个ParameterMapping对象,并返回?号占位符
          parameterMappings.add(buildParameterMapping(content));
          return "?";
        }
    //..
    }
    

    VariableTokenHandler:处理${xxx}占位符。

    private static class VariableTokenHandler implements TokenHandler {
        private Properties variables;
    
        public VariableTokenHandler(Properties variables) {
          this.variables = variables;
        }
    
        @Override
        public String handleToken(String content) {
          if (variables != null && variables.containsKey(content)) {
            return variables.getProperty(content);
          }
          return "${" + content + "}";
        }
      }
    

    DynamicCheckerTokenParser:空实现,动态sql标签,都由它来标识。

    BindingTokenParser:用于在注解Annotation中处理${xxx},待研究。

    至此,${name}将直接替换为静态Properties的静态属性值,而#{name}将被替换为?号,并同时创建了ParameterMapping对象,绑定到参数列表中。

    5. DynamicSqlSource生成sql的原理

    对于RawSqlSource,由于是静态的sql,Mybatis初始化时就生成了最终可以直接使用的sql语句,即在创建RawSqlSource时,就直接生成。而DynamicSqlSource,则是执行sql时,才动态生成。

    public class DynamicSqlSource implements SqlSource {
    
      private Configuration configuration;
      private SqlNode rootSqlNode;
    
      public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
      }
    
      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // 逐一调用各种SqlNode,拼接sql
        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);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
          boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
      }
    
    }
    

    BoundSql不仅保存了最终的可执行的sql,还保存了sql中?号占位符的参数列表。

    public class BoundSql {
    
      private String sql;
      private List<ParameterMapping> parameterMappings;
    // ...
    }
    

    最后,在执行sql时,通过org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(PreparedStatement)方法,遍历List<ParameterMapping> parameterMappings = boundSql.getParameterMappings()来逐一对sql中的?号占位符进行赋值操作。

    整个sql处理变量占位符的流程就完成了。

    6. OGNL表达式运算完成动态sql拼接

    我们就举一个略微复杂一点的ForEachSqlNode的拼接sql原理。

    public class ForEachSqlNode implements SqlNode {
    // OGNL表达式计算器
    private ExpressionEvaluator evaluator;
    //...
    @Override
      public boolean apply(DynamicContext context) {
        Map<String, Object> bindings = context.getBindings();
        // 计算集合表达式
        final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
        if (!iterable.iterator().hasNext()) {
          return true;
        }
        boolean first = true;
        applyOpen(context);
        int i = 0;
        // 遍历拼接sql
        for (Object o : iterable) {
          DynamicContext oldContext = context;
          if (first) {
            context = new PrefixedContext(context, "");
          } else if (separator != null) {
            context = new PrefixedContext(context, separator);
          } else {
              context = new PrefixedContext(context, "");
          }
          int uniqueNumber = context.getUniqueNumber();
          // Issue #709 
          if (o instanceof Map.Entry) {
            @SuppressWarnings("unchecked") 
            Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
            applyIndex(context, mapEntry.getKey(), uniqueNumber);
            applyItem(context, mapEntry.getValue(), uniqueNumber);
          } else {
            applyIndex(context, i, uniqueNumber);
            applyItem(context, o, uniqueNumber);
          }
          contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
          if (first) {
            first = !((PrefixedContext) context).isPrefixApplied();
          }
          context = oldContext;
          i++;
        }
        applyClose(context);
        return true;
      }
    //...
    }
    

    Mybatis的全部动态sql内容,至此就全部介绍完了,在实际工作中,绝大多数的sql,都是动态sql。

    最后

    感谢大家看到这里,如果本文有什么不足之处,欢迎多多指教;如果你觉得对你有帮助,请给我点个赞。

    也欢迎大家关注我的公众号:程序员麦冬,每天更新行业资讯!

    相关文章

      网友评论

          本文标题:从源码的角度,来阐述#「name」和$「name」的区别?面试官

          本文链接:https://www.haomeiwen.com/subject/aevkvktx.html