mybatis学习之路----#{}, ${}两种传参数方式的区别--附源码解读

来源:互联网 发布:mac pro忘记登录密码 编辑:程序博客网 时间:2024/06/03 18:50

点滴记载,点滴进步,愿自己更上一层楼。


首先下个结论,

${} 会将传入的参数完全拼接到sql语句中,也就是相当于一个拼接符号。

也就是,最后的处理方式就相当于 

String sql = select * from user where id=${value}....

mybatis会将 ${value} 完全替换为参数 value 的值  相当于replace("${value}", value)的过程。

实际上mybatis 是先将sql转成char数组

然后截取 "${"前头的部分放入到容器,替换  以"${"开头 以 "}"结尾的内容。所以说它的作用相当于拼接符号。拼接后直接作为sql语句的一部分,所以如果参数是可执行代码,sql是会直接执行的。这就是为什么它会导致sql注入。

        #{} 是一个占位符, mybatis最后会将这个占位符,替换成?,

最后才进行prepareStatement的相应位置的?的替换,也就是  state.setString(序号,值),setInt(序号,值)....

熟悉jdbc的人对这段应该不陌生。


下面来进行源码级别的论证。

首先写一个带有${} #{} 两种参数方式的sql

    <!--模糊查询 demo2 直接用concat拼字符串 此种可以有效防止sql注入 -->    <select id="findUserByName4" parameterType="com.soft.mybatis.model.User" resultMap="userMap">        select * from t_user where username like concat('%', #{username} ,'%') and password=#{password}    </select>
接口

    /**     * 验证${} #{} 两种传参处理方式     * @return     */    List<User> findUserByName04(User user);
实现
   /**     * 模糊查询第三种方式  直接接收拼好的字符串     * @return     */    public List<User> findUserByName04(User user) {        String statementId = "test.findUserByName4";        return findUserList(statementId,user);    }    /**     * 由于都需要三种方式查询除了两个地方不一样其他的处理都相同,此处抽取相同部分     * @param statementId     * @param param     * @return     */    private List<User> findUserList(String statementId, Object param){        SqlSession sqlSession = null;        try {            sqlSession = SqlsessionUtil.getSqlSession();            return sqlSession.selectList(statementId,param);        } catch (Exception e) {            e.printStackTrace();        } finally {            SqlsessionUtil.closeSession(sqlSession);        }        return new ArrayList<User>();    }
测试代码:
    @Test    public void findUserByName04() throws Exception {        User user = new User();        user.setUsername("小黄");        user.setPassword("123456");        List<User> users = dao.findUserByName04(user);        for(User userss:users){            System.out.println("findUserByName04:" + userss);        }    }

首先说明 mybatis处理 "${}" 和 "#{}" 的主要处理逻辑的源码位于

GenericTokenParser.java 的parse方法中。

源码奉上:

    public String parse(String text) {        StringBuilder builder = new StringBuilder();        if(text != null && text.length() > 0) {            char[] src = text.toCharArray();            int offset = 0;            for(int start = text.indexOf(this.openToken, offset); start > -1; start = text.indexOf(this.openToken, offset)) {                if(start > 0 && src[start - 1] == 92) {                    builder.append(src, offset, start - offset - 1).append(this.openToken);                    offset = start + this.openToken.length();                } else {                    int end = text.indexOf(this.closeToken, start);                    if(end == -1) {                        builder.append(src, offset, src.length - offset);                        offset = src.length;                    } else {                        builder.append(src, offset, start - offset);                        offset = start + this.openToken.length();                        String content = new String(src, offset, end - offset);                        builder.append(this.handler.handleToken(content));                        offset = end + this.closeToken.length();                    }                }            }            if(offset < src.length) {                builder.append(src, offset, src.length - offset);            }        }        return builder.toString();    }

mybatis对这两种参数的处理分为两个阶段,

首先为构建sqlsessionFactory的时候,这个时候的处理为,如果sql中含有${}则该条sql不做处理,如果sql中全是#{}则替换为 ? 。

然后是sqlSession执行sql阶段,该阶段首先会将${value} 原封不动的替换为 value传过来的值,然后在将sql中的#{} 替换为 ? 

最后才是preparestatement 将sql中的?替换为参数值,最后执行sql。

大概的处理逻辑就是这个,下面是源码跟踪与说明。让人更明了。

首先第一阶段,构建sqlsessionFactory阶段。

入口    SqlSessionFactoryBuilder  的 方法

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

 var5 = this.build(e.parse());  

然后会进入 XMLConfigBuilder 的

    private void parseConfiguration(XNode root) {        try {            this.propertiesElement(root.evalNode("properties"));            this.typeAliasesElement(root.evalNode("typeAliases"));            this.pluginElement(root.evalNode("plugins"));            this.objectFactoryElement(root.evalNode("objectFactory"));            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));            this.settingsElement(root.evalNode("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);        }    }
这个方法处理各种节点,看到this.mapperElement(root.evalNode("mappers"));是不是很熟悉,这里就是处理mapper文件的地方,

继续深入会到XMLMapperBuilder的

    private void configurationElement(XNode context) {        try {            String e = context.getStringAttribute("namespace");            if(e.equals("")) {                throw new BuilderException("Mapper\'s namespace cannot be empty");            } else {                this.builderAssistant.setCurrentNamespace(e);                this.cacheRefElement(context.evalNode("cache-ref"));                this.cacheElement(context.evalNode("cache"));                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));                this.resultMapElements(context.evalNodes("/mapper/resultMap"));                this.sqlElement(context.evalNodes("/mapper/sql"));                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));            }        } catch (Exception var3) {            throw new BuilderException("Error parsing Mapper XML. Cause: " + var3, var3);        }    }
看到  this.buildStatementFromContext(context.evalNodes("select|insert|update|delete")); 又眼熟,这里就是处理sql的入口。

继续深入会进入到 XMLStatementBuilder的

    public void parseStatementNode() {        String id = this.context.getStringAttribute("id");        String databaseId = this.context.getStringAttribute("databaseId");        if(this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {            Integer fetchSize = this.context.getIntAttribute("fetchSize");            Integer timeout = this.context.getIntAttribute("timeout");            String parameterMap = this.context.getStringAttribute("parameterMap");            String parameterType = this.context.getStringAttribute("parameterType");            Class parameterTypeClass = this.resolveClass(parameterType);            String resultMap = this.context.getStringAttribute("resultMap");            String resultType = this.context.getStringAttribute("resultType");            String lang = this.context.getStringAttribute("lang");            LanguageDriver langDriver = this.getLanguageDriver(lang);            Class resultTypeClass = this.resolveClass(resultType);            String resultSetType = this.context.getStringAttribute("resultSetType");            StatementType statementType = StatementType.valueOf(this.context.getStringAttribute("statementType", StatementType.PREPARED.toString()));            ResultSetType resultSetTypeEnum = this.resolveResultSetType(resultSetType);            String nodeName = this.context.getNode().getNodeName();            SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;            boolean flushCache = this.context.getBooleanAttribute("flushCache", Boolean.valueOf(!isSelect)).booleanValue();            boolean useCache = this.context.getBooleanAttribute("useCache", Boolean.valueOf(isSelect)).booleanValue();            boolean resultOrdered = this.context.getBooleanAttribute("resultOrdered", Boolean.valueOf(false)).booleanValue();            XMLIncludeTransformer includeParser = new XMLIncludeTransformer(this.configuration, this.builderAssistant);            includeParser.applyIncludes(this.context.getNode());            this.processSelectKeyNodes(id, parameterTypeClass, langDriver);            SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);            String resultSets = this.context.getStringAttribute("resultSets");            String keyProperty = this.context.getStringAttribute("keyProperty");            String keyColumn = this.context.getStringAttribute("keyColumn");            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", Boolean.valueOf(this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))).booleanValue()?new Jdbc3KeyGenerator():new NoKeyGenerator();            }            this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);        }    }
一看一大段,都是干啥的呀,其实大眼一扫其实也不吓人,就是在处理一些参数类型,statementId等等。我们把眼光放到
SqlSource sqlSource = langDriver.createSqlSource(this.configuration, this.context, parameterTypeClass);

继续深入,XMLScriptBuilder

   private List<SqlNode> parseDynamicTags(XNode node) {        ArrayList 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 var8 = (XMLScriptBuilder.NodeHandler)this.nodeHandlers.get(nodeName);                    if(var8 == null) {                        throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");                    }                    var8.handleNode(child, contents);                    this.isDynamic = true;                }            } else {                nodeName = child.getStringBody("");                TextSqlNode handler = new TextSqlNode(nodeName);                if(handler.isDynamic()) {                    contents.add(handler);                    this.isDynamic = true;                } else {                    contents.add(new StaticTextSqlNode(nodeName));                }            }        }        return contents;    }
到此很接近目标了,通过 if(handler.isDynamic()) { 进入到 TextSqlNode 的
    public boolean isDynamic() {        TextSqlNode.DynamicCheckerTokenParser checker = new TextSqlNode.DynamicCheckerTokenParser();        GenericTokenParser parser = this.createParser(checker);        parser.parse(this.text);        return checker.isDynamic();    }
它的createParser

    private GenericTokenParser createParser(TokenHandler handler) {        return new GenericTokenParser("${", "}", handler);    }
原来他首先校验的是${}  

然后 parser.parse(this.text); 就到了我门的目的地了。
终于到了最上面的GenericTokenParser 的解析部分了。

可以看到,

1 首先弄了个容器,Stringbuilder用来装替换后的sql。

2 然后是判断sql非空等等。然后将sql转成 char[] src 数组

3 开始循环,拿到 this.openToken 的位置,这里估计会问,this.openToken是什么鬼,通过debug发现这个就是 “${”.也就是如果sql语句里面包含有${} 的话 会进入循环

4 来看看循环体干了什么,

if(start > 0 && src[start - 1] == 92) { 首先判断  this.openToken开始位置 然后看其前以为是不是 "\"   92 对应的char就是反斜杠。显然不进,

int end = text.indexOf(this.closeToken, start);

if(end == -1) { 然后拿到 this.closeToken 的位置  显然也不会进这个,那就只能进另外的else了。

// 将 ${ 前面的字符串放入到builder
builder.append(src, offset, start - offset);

// 记录this.openToken的位置坐标
offset = start + this.openToken.length();

// 拿到this.openTokenthis.closeToken中间的字符串
String content = new String(src, offset, end - offset);

// 替换该字符串
builder.append(this.handler.handleToken(content));

// 记录this.closeToken 所在位置
offset = end + this.closeToken.length();

如果还有${}内容,继续循环,

注意上面代码中的this.openToken 可能为${ 也可能为 #{

走完了这步之后如果发现sql中确实有${},就将isDynamic标识为true,这个标识很有用,具体可以看XMLScriptBuilder

    public SqlSource parseScriptNode() {        List contents = this.parseDynamicTags(this.context);        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);        Object sqlSource = null;        if(this.isDynamic) {            sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);        } else {            sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);        }        return (SqlSource)sqlSource;    }
该标识导致两种结果,如果为true就直接返回了一个DynamicSqlSource实例,它的构造函数仅仅做了简单的事情

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {        this.configuration = configuration;        this.rootSqlNode = rootSqlNode;    }
但是如果sql中没有${}的话就会返回,RawSqlSource的实例看它的构造函数

    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());    }
它会调用 SqlSourceBuilder 的parse方法,最后又会进入GenericTokenParser的parse方法 进行#{}的替换工作,具体自己debug吧,不然没完了。
    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());    }
上面的过程仅仅是

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resource);的执行过程。

sqlsession的执行sql过程,还要进入到那个parse方法中进行替换操作,

这个时候要分为两种情况,如果构建SqlSessionFactory 的时候,

SqlSource 的实例为DynamicSqlSource 的话,因为sql还是xml中的形态,所以它会做两件事,

第一件就是将${}替换为对应的value值,

第二件事就是将#{}替换为?

也就是 DynamicSqlSource 的

    public BoundSql getBoundSql(Object parameterObject) {        DynamicContext context = new DynamicContext(this.configuration, parameterObject);        this.rootSqlNode.apply(context);//第一次替换${}        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);        Class parameterType = parameterObject == null?Object.class:parameterObject.getClass();        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// 替换#{}->?        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);        Iterator i$ = context.getBindings().entrySet().iterator();        while(i$.hasNext()) {            Entry entry = (Entry)i$.next();            boundSql.setAdditionalParameter((String)entry.getKey(), entry.getValue());        }        return boundSql;    }


最后sql的样子就是
select * from t_user where username like '%黄%' and password=?

最后才是statement执行sql,返回查询结果。注意上面的替换后的sql,并不是将${}替换为?,而是替换为传过来的参数值。


本来想简单弄弄算了,结果弄得这么繁琐,都有点mybatis源码解读的意思了。

上面对#{} ${}的区别,在源码层次做了讲解,希望有所帮助,反正我现在是印象深刻了。

-_-       -_-        -_-       -_-       -_-    -_-         -_-                    -_-

阅读全文
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 中奖彩票被洗了怎么办 牙龈下面长米粒肉疙瘩怎么办 书画印章盖反了怎么办 金龙鱼一个月不吃东西怎么办 罗汉鱼头撞扁了怎么办 房顶开槽埋线白色不一样怎么办 顶上灯挪位置线怎么办 马蜂窝弄掉又来怎么办 蜂窝弄掉又有怎么办 2018年小龙虾底板脏怎么办 一本分数线擦边过怎么办 玩具塑料球扁了怎么办 胶皮与海绵开了怎么办 安卓不支持flash了怎么办 看视频要加载flash怎么办 下水道管子铁皮破了怎么办 炸金花牌一样大怎么办 玩棋牌游戏输了怎么办 苹果7插耳机外放怎么办 出国种菠菜抓了怎么办 在菲做菠菜抓到怎么办 3串1中两个怎么办 微博账号封停怎么办 阴阳师账号被永久封停怎么办 寒刃2账号被禁用怎么办 输了好多钱我该怎么办 亲朋打鱼别处在玩怎么办 做糯米蛋的蛋清怎么办 水田地没耙地平怎么办 宝宝拉鸡蛋花样大便怎么办 电子琴伴奏区无旋律音怎么办 手机触摸屏摔坏了怎么办 手机充着电玩游戏卡怎么办? 4个月宝宝拉肚子怎么办 6个月宝宝上火怎么办 1月婴儿大便干燥怎么办 椰子鞋350线开了怎么办 打完篮球小腿肌肉酸痛怎么办 衣服穿少了感冒怎么办 侧手翻翻不过去怎么办 生完孩子胯宽了怎么办