面试官:mybatis中#{ }和${ }的区别

小豆丁 9月前 ⋅ 305 阅读

关注“苏三说技术”,回复:代码神器、开发手册、时间管理 有惊喜。

如果有用过mybatis的朋友,肯定对#{ }非常熟悉。

让我们先一起看看#{ }的用法。

数据库中有2条数据,如图:

我们先定义一个实体:

@Data
public class JumpLogModel {

    /**
     * 系统ID
     */
    private String id;

    /**
     * 应用编号
     */
    private String app;

    /**
     * 跳转url
     */
    private String url;

    /**
     * ip地址
     */
    private String ip;

    /**
     * 区域名称
     */
    private String areaName;

    /**
     * 操作系统
     */
    private String os;

    /**
     * 浏览器
     */
    private String browser;

    /**
     * 来源
     */
    private String refer;

    /**
     * 操作时间
     */
    private Date inDate;

}

然后定义mapper:

public interface JumpLogService {
    JumpLogModel selectById(String id);
}

再定义xml文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sue.jump.mappers.JumpLogMapper">
    
    <resultMap type="com.sue.jump.model.JumpLogModel" 
     id="JumpLogResult">

        <result property="id"         column="id"/>
        <result property="app"        column="app"/>
        <result property="url"        column="url"/>
        <result property="ip"         column="ip"/>
        <result property="areaName"   column="area_name"/>
        <result property="os"         column="os"/>
        <result property="browser"    column="browser"/>
        <result property="inDate"     column="in_date"/>
    </resultMap>

    <select id="selectById" resultMap="JumpLogResult">
        select
           id,app,url,ip,area_name,os,browser
        from jump_log
        <where>
            id = #{id}
        </where>
    </select>
</mapper>

定义service层:

@Service
public class JumpLogServiceImpl implements JumpLogService {

    @Autowired
    private JumpLogMapper jumpLogMapper;


    @Override
    public JumpLogModel selectById(String id) {
        return jumpLogMapper.selectById(id);
    }
}

定义controller层

@RequestMapping("/jump")
@RestController
public class JumpLogController {

    @Autowired
    private JumpLogServiceImpl jumpLogService;

    @GetMapping("/get/{id}")
    public JumpLogModel get(@PathVariable String id) {
        return jumpLogService.selectById(id);
    }
}

调用接口,id=123456

我们看到可以通过id查询到正确的数据,说明#{ }生效了。

那么我们把#{ },改成${ }再试试。

<select id="selectById" resultMap="JumpLogResult">
        select
          id,app,url,ip,area_name,os,browser
        from jump_log
       <where>
            id = ${id}
        </where>
</select>

再调用接口,id=123456

同样可以根据id查询出正确的数据。那么有人可能会说,#{ } 和 ${ }不是一样吗?二者有什么区别呢?

接下来,我们重点看看二者的区别。

现有#{ }接收参数

<select id="selectById" resultMap="JumpLogResult">
      select
             id,app,url,ip,area_name,os,browser
      from jump_log
      <where>
            id = #{id}
        </where>
</select>

把id的值由123456改成:123456 or 1=1,再调用接口

依然可以返回正确的数据。

再改成${ }接收参数

<select id="selectById" resultMap="JumpLogResult">
        select
          id,app,url,ip,area_name,os,browser
    from jump_log
    <where>
         id = ${id}
    </where>
</select>

报错了。。。。。。

提示了:Expected one result (or null) to be returned by selectOne(), but found: 2

通过id原本只能返回第1条数据,结果返回了2条数据。怎么回事?

原来通过${ }接收参数之后,最后拼接的sql如下:

select id,app,url,ip,area_name,os,browser from jump_log where id = 123456 or 1=1

明白了,这是典型的sql注入,后面的 or 1=1 会让前面的 id=123456条件失效,相当于整个where条件都失效了,最后sql相当于执行了:

select id,app,url,ip,area_name,os,browser from jump_log

肯定会返回2条数据。

那么问题来了,#{ }的方式为什么没有问题呢?

因为#{ }接收参数使用了sql预编译,最后拼接的sql会变成:


select id,app,url,ip,area_name,os,browser from jump_log where id = ?

执行sql时会将参数进行转义,把传入的参数:123456 or 1=1加了单引号',执行时的sql是:

select id,app,url,ip,area_name,os,browser from jump_log where id = '123456 or 1=1'

可以正确返回1条数据。

我们可以得出结论,#{ } 通过预编译可以防止sql注入。

那是不是在实际开发中都用#{ }就好了,不需要使用${ }了?

其实,不然,

比如有这样的场景:数据库的名称需要通过参数统一起来,以便下次修改数据库名时,只有修改一个地方即可。

在mybatis-config.xml文件中配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties>
        <property name="mallDbName" value="sue_mall_db"/>
    </properties>
</configuration>

xml中使用

<select id="selectById" resultMap="JumpLogResult">
        select
             id,app,url,ip,area_name,os,browser
        from ${mallDbName}.jump_log
        <where>
            id = #{id}
        </where>
</select>

接下来,我们分析一下源码,看看#{}是怎么替换成?的

先看看XMLMapperBuilder类的configurationElement方法。

重点看看buildStatementFromContext方法:

会调用XMLStatementBuilder类的parseStatementNode方法:

进入LawLanguageDriver类的createSqlSource方法:

我们一起看看XmlScriptBuilder类的parseScriptNode方法:

看看this方法,即下面的构造方法:

最终我们会发现在SqlSourceBuilder类的GenericTokenParser解析器就是把#{} 符合 替换 为 ?占位符

总结一下:

${ } 直接的 字符串 替换,在mybatis的动态 SQL 解析阶段将会进行变量替换。

#{ } 通过预编译,用占位符的方式传值可以把一些特殊的字符进行转义,这样可以防止一些sql注入。

大家喜欢这篇文章的话,请关注一下 :苏三说技术


全部评论: 0

    我有话说: