【Mybatis源码分析 7】mapper.xml获取参数的两种方式源码分析:${}和#{},两种传参方式的差别

【Mybatis源码分析 6】XMLConfigBuilder将Mybatis全局配置文件.xml中设置的节点提取赋给Configuration对象中讲到SqlSessionFactoryBuilder创建SqlSessionFactory对象时,

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resource);

将mybatis的全局配置文件中的11个节点提取并处理后赋给Configuration对象,其中就有mappers映射器的提取和处理

org.apache.ibatis.builder.xml.XMLConfigBuilder调用parseConfiguration方法提取配置文件中的11个节点

    private void parseConfiguration(XNode root) {
        try {
            this.propertiesElement(root.evalNode("properties"));
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.loadCustomLogImpl(settings);
            this.typeAliasesElement(root.evalNode("typeAliases"));
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
            this.settingsElement(settings);
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));

            // 提取和处理映射器
            this.mapperElement(root.evalNode("mappers"));

        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }

提取和处理映射器,mappers下可以定义多个mapper子标签,引用多个.xml或接口

注意:不能同时使用resource和url,否则抛出BuilderException异常

     private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            // 获取所有mapper子节点的迭代器
            Iterator var2 = parent.getChildren().iterator();
            while(true) {
                // 遍历所有mapper子节点,提取sql字符串和参数,并对sql字符串格式化(处理${},#{})
                while(var2.hasNext()) {
                    XNode child = (XNode)var2.next();
                    String resource;
                    // package子标签表示扫描该包下面所有的mapper接口
                    if ("package".equals(child.getName())) {
                        // 获取包名
                        resource = child.getStringAttribute("name");
                        // 将该包下面所有的mapper接口注册到configuration对象的映射器注册器中
                        this.configuration.addMappers(resource);
                    } else {
                        // 单个mapper文件(.xml或接口)
                        resource = child.getStringAttribute("resource");
                        String url = child.getStringAttribute("url");
                        String mapperClass = child.getStringAttribute("class");
                        XMLMapperBuilder mapperParser;
                        InputStream inputStream;
                        // 如果是resource引用,则规定该.xml在资源文件夹下,否则扫描不到
                        if (resource != null && url == null && mapperClass == null) {
                            ErrorContext.instance().resource(resource);
                            // 加载.xml文件的二进制数据流
                            inputStream = Resources.getResourceAsStream(resource);
                            // 创建xml映射文件解析器
                            mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
                            //重点方法:解析处理sql语句和statementid节点传入的参数
                            mapperParser.parse();
                        } 
                        // url引用本地或网络资源,但是要保证resource和url只能使用一个,否则抛出BuilderException异常
                        else if (resource == null && url != null && mapperClass == null) {
                            ErrorContext.instance().resource(url);
                            inputStream = Resources.getUrlAsStream(url);
                            mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
                            mapperParser.parse();
                        } else {
                            if (resource != null || url != null || mapperClass == null) {
                                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                            }

                            // 引用mapper接口
                            Class<?> mapperInterface = Resources.classForName(mapperClass);
                            //重点方法:扫描mapper接口,解析处理注解描述的sql语句和方法传入的参数
                            this.configuration.addMapper(mapperInterface);
                        }
                    }
                }

                return;
            }
        }
    }

org.apache.ibatis.builder.xml.XMLMapperBuilder::parse解析处理sql语句和statementid节点传入的参数

public void parse() {
        if (!this.configuration.isResourceLoaded(this.resource)) {
            // 解析statementid
            this.configurationElement(this.parser.evalNode("/mapper"));
            this.configuration.addLoadedResource(this.resource);
            this.bindMapperForNamespace();
        }

        this.parsePendingResultMaps();
        this.parsePendingCacheRefs();
        this.parsePendingStatements();
    }

解析statementid

 private void configurationElement(XNode context) {
        try {
            String namespace = context.getStringAttribute("namespace");
            if (namespace != null && !namespace.equals("")) {
                this.builderAssistant.setCurrentNamespace(namespace);
                this.cacheRefElement(context.evalNode("cache-ref"));
                this.cacheElement(context.evalNode("cache"));
                // java传入的参数包装为key-value的map
                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                // 查询结果集,按照某种关系映射到java对象
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                // 提取sql字符串
                this.sqlElement(context.evalNodes("/mapper/sql"));
                // 重点方法:根据上下文解析select|insert|update|delete的sql语句              
              this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            } else {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
        } catch (Exception var3) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
        }
    }

根据上下文解析select|insert|update|delete的sql语句 

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        Iterator var3 = list.iterator();

        while(var3.hasNext()) {
            XNode context = (XNode)var3.next();
            XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);

            try {
                // 解析statementid
                statementParser.parseStatementNode();
            } catch (IncompleteElementException var7) {
                this.configuration.addIncompleteStatement(statementParser);
            }
        }

    }

org.apache.ibatis.builder.xml.XMLStatementBuilder::parseStatementNode解析mapper.xml中的statementid

public void parseStatementNode() {
        // 获取statementid
        String id = this.context.getStringAttribute("id");
        // 获取databaseId数据库标识
        String databaseId = this.context.getStringAttribute("databaseId");
        if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            String nodeName = this.context.getNode().getNodeName();
            SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
            // 如果是select语句,设置查询结果存入缓存
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
            boolean flushCache = this.context.getBooleanAttribute("flushCache", !isSelect);
            boolean useCache = this.context.getBooleanAttribute("useCache", isSelect);
            boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", false);
            // 解析参数等
            XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);
            includeParser.applyIncludes(this.context.getNode());
            String parameterType = this.context.getStringAttribute("parameterType");
            Class<?> parameterTypeClass = this.resolveClass(parameterType);
            String lang = this.context.getStringAttribute("lang");
            LanguageDriver langDriver = this.getLanguageDriver(lang);
            this.processSelectKeyNodes(id, parameterTypeClass, langDriver);
            String keyStatementId = id + "!selectKey";
            keyStatementId = this.builderAssistant.applyCurrentNamespace(keyStatementId, true);
            Object keyGenerator;
            if (this.configuration.hasKeyGenerator(keyStatementId)) {
                keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
            } else {
                keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
            }

            // 根据上下文和参数处理sql字符串(处理${}和#{})
            SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);

             //最终根据namesppace+id为key封装成MappedStatement保存到Configuration
            StatementType statementType = StatementType.valueOf(this.context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
            Integer fetchSize = this.context.getIntAttribute("fetchSize");
            Integer timeout = this.context.getIntAttribute("timeout");
            String parameterMap = this.context.getStringAttribute("parameterMap");
            String resultType = this.context.getStringAttribute("resultType");
            Class<?> resultTypeClass = this.resolveClass(resultType);
            String resultMap = this.context.getStringAttribute("resultMap");
            String resultSetType = this.context.getStringAttribute("resultSetType");
            ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType);
            if (resultSetTypeEnum == null) {
                resultSetTypeEnum = this.configuration.getDefaultResultSetType();
            }

            String keyProperty = this.context.getStringAttribute("keyProperty");
            String keyColumn = this.context.getStringAttribute("keyColumn");
            String resultSets = this.context.getStringAttribute("resultSets");
            this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
        }
    }

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver::createSqlSource
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder::parseScriptNode 根据上下文和参数处理sql字符串(处理${}和#{})

public SqlSource parseScriptNode() {
        //先判断是否是${}传值,并解析sql
        MixedSqlNode rootSqlNode = this.parseDynamicTags(this.context);
        Object sqlSource;
        if (this.isDynamic) {
            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
        } else {
            // 如果是#{}传值
            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
        }

        return (SqlSource)sqlSource;
    }

先判断是否是${}传值,并解析sql

    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList();
        NodeList children = node.getNode().getChildNodes();

        for(int i = 0; i < children.getLength(); ++i) {
            XNode child = node.newXNode(children.item(i));
            String nodeName;
            if (child.getNode().getNodeType() != 4 && child.getNode().getNodeType() != 3) {
                if (child.getNode().getNodeType() == 1) {
                    nodeName = child.getNode().getNodeName();
                    XMLScriptBuilder.NodeHandler handler = (XMLScriptBuilder.NodeHandler)this.nodeHandlerMap.get(nodeName);
                    if (handler == null) {
                        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                    }

                    handler.handleNode(child, contents);
                    this.isDynamic = true;
                }
            } else {
                nodeName = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(nodeName);
                // 调用isDynamic()方法
                if (textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    this.isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(nodeName));
                }
            }
        }

        return new MixedSqlNode(contents);
    }

org.apache.ibatis.scripting.xmltags.TextSqlNode::isDynamic()先判断是否为${}传参,并执行将${}替换为参数值的任务

    public boolean isDynamic() {
        // 创建DynamicCheckerTokenParser,并设置${}和参数处理器,对sql语句解析
        TextSqlNode.DynamicCheckerTokenParser checker = new TextSqlNode.DynamicCheckerTokenParser();
        GenericTokenParser parser = this.createParser(checker);
        parser.parse(this.text);
        return checker.isDynamic();
    }

设置token和参数处理器

    private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
    }

org.apache.ibatis.parsing.GenericTokenParser::parse核心方法,真正替换完成${}或#{}解析的动作

    public String parse(String text) {
        if (text != null && !text.isEmpty()) {
            // 定位"${"的索引 -- 初步判断是否存在${}
            int start = text.indexOf(this.openToken);
            if (start == -1) {
                return text;
            } else {
                // 转字符串数组
                char[] src = text.toCharArray();
                // 偏移量,用以indexof寻找下一个"${"或者"}"
                int offset = 0;
                // 创建容器保存字符串
                StringBuilder builder = new StringBuilder();
                // 遍历所有的"${"
                for(StringBuilder expression = null; start > -1; start = text.indexOf(this.openToken, offset)) {
                    if (start > 0 && src[start - 1] == '\\') {
                        builder.append(src, offset, start - offset - 1).append(this.openToken);
                        offset = start + this.openToken.length();
                    } else {
                        if (expression == null) {
                            expression = new StringBuilder();
                        } else {
                            expression.setLength(0);
                        }

                        // 保留offset起到"${"左边的内容
                        builder.append(src, offset, start - offset);
                        offset = start + this.openToken.length();

                        int end;
                        // 寻找匹配的"}"
                        for(end = text.indexOf(this.closeToken, offset); end > -1; end = text.indexOf(this.closeToken, offset)) {
                            if (end <= offset || src[end - 1] != '\\') {
                                // expression记录"${"和"}"之间的参数名
                                expression.append(src, offset, end - offset);
                                break;
                            }

                            expression.append(src, offset, end - offset - 1).append(this.closeToken);
                            offset = end + this.closeToken.length();
                        }

                        if (end == -1) {
                            builder.append(src, start, src.length - start);
                            offset = src.length;
                        } else {
                            // 调用handler的handleToken方法,获取参数值
                             builder.append(this.handler.handleToken(expression.toString()));
                            offset = end + this.closeToken.length();
                        }
                    }
                }

                if (offset < src.length) {
                    builder.append(src, offset, src.length - offset);
                }

                return builder.toString();
            }
        } else {
            return "";
        }
    }

org.apache.ibatis.scripting.xmltags.TextSqlNode.DynamicCheckerTokenParser::handleToken获取参数值,并将isDynamic置true.

 一开始我看着这里觉得很奇怪,${}是直接将参数值拼接到sql字符串中的,这里怎么返回null呢, 后来明白, 现在还只是加载mapper映射器的阶段,还没有运行具体的sql语句,没有参数值,怎么赋值呢,所以在加载配置文件解析映射器阶段直接返回null了。

        public String handleToken(String content) {
            this.isDynamic = true;
            return null;
        }

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder::parseDynamicTags方法中判断textSqlNode.isDynamic()

调用org.apache.ibatis.parsing.GenericToken::parse方法中如果找到${}则返回true,  org.apache.ibatis.scripting.xmltags.XMLScriptBuilder::isDynamic则置true

如果是#{}传参,org.apache.ibatis.scripting.xmltags.XMLScriptBuilder::isDynamic为默认值false,则创建org.apache.ibatis.scripting.defaults.RawSqlSource对象

public SqlSource parseScriptNode() {

        // 如果是${}传值,在这里通过isDynamic()方法已经解析完成
        MixedSqlNode rootSqlNode = this.parseDynamicTags(this.context);
        Object sqlSource;

        if (this.isDynamic) {
            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
        } else {
            // 如果是#{}传值
            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
        }

        return (SqlSource)sqlSource;
    }

org.apache.ibatis.scripting.defaults.RawSqlSource的构造方法中创建SqlSourceBuilder对象,解析sql

    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> clazz = parameterType == null ? Object.class : parameterType;
        this.sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
    }

org.apache.ibatis.builder.SqlSourceBuilder::parse方法中创建GenericTokenParser对象,设置token为#{}

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        SqlSourceBuilder.ParameterMappingTokenHandler handler = new SqlSourceBuilder.ParameterMappingTokenHandler(this.configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        return new StaticSqlSource(this.configuration, sql, handler.getParameterMappings());
    }

org.apache.ibatis.parsing.GenericTokenParser::parse详见上面的解析

    public String parse(String text) {
        //。。。
        if (end == -1) {
               builder.append(src, start, src.length - start);
               offset = src.length;
        } else {
               // ParameterMappingTokenHandler对象handler调用handleToken方法,
               // 将参数名添加到参数集中,并返回"?"占位符拼接到sql语句中
               builder.append(this.handler.handleToken(expression.toString()));
               // 然后offset继续往后移动一个"}"的长度,进入下一个循环,匹配下一个"${}"
               offset = end + this.closeToken.length();
        }
        //。。。
    }

org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler::handleToken将参数名添加到参数集中,并返回"?"占位符拼接到sql语句中

    public String handleToken(String content) {
          this.parameterMappings.add(this.buildParameterMapping(content));
          return "?";
    }

${}和#{}传参差别

1. ${prama}:获取参数的值,拼接到SQL中。存在SQL注入风险。

    #{prama}:获取参数的值,预编译到SQL中。安全。

2. ${}使用statement执行sql语句

    #{}使用PreparedStatement预编译sql语句,使用占位符注入参数

3. ${}采用拼接的方式,需要注意单引号问题

    #{}采用通配符占位设置参数的方式,自动加上单引号

4. 当存在多个参数时(当传入多个参数时,mybatis默认将这些参数放在集合中)

    ${}可以通过${参数名}, ${param1}、${param2}、、、${paramN}两种方式给sql语句注入参数

    #{}可以通过#{参数名}, #{param1}、#{param2}、、、#{paramN-1},#{0}、#{1}、、、#{N-1}三种方式给sql语句注入参数

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值