MyBatis面试高频:#{}和${}的核心区别,看完直接背

6 阅读3分钟

在MyBatis开发和面试中,#{}和${}是两个高频出现的参数替换语法,很多开发者在日常使用中容易混淆,面试时也常常因为分不清二者的差异而丢分。本文将从底层实现、安全性、使用场景等维度,结合独立构思的代码示例,清晰拆解二者的核心区别,帮你快速掌握考点、规避使用误区。

一、一句话吃透核心区别

#{}会通过预编译生成PreparedStatement,自动对参数进行转义,能有效防止SQL注入,适用于大多数参数值传递场景;${}则是直接进行字符串拼接,不做任何转义处理,存在SQL注入风险,仅适用于动态表名、列名等必须拼接原生SQL片段的场景,日常开发中需优先使用#{}。

二、详细解析:5个维度分清二者差异

1. 底层处理机制(最核心差异)

#{}的底层是预编译机制,MyBatis会将#{}替换为SQL中的?占位符,然后通过PreparedStatement将参数值填充到占位符中,整个过程会对参数进行转义处理;而${}是直接将参数值作为字符串,拼接到底层SQL语句中,相当于字符串拼接操作,不做任何转义。

举个简单例子:假设参数username = "ZhangSan",分别使用两种语法编写SQL:

-- 使用#{}的SQL映射
<select id="getUserByUsername" parameterType="String" resultType="com.example.entity.User">
    SELECT id, username, age FROM t_user WHERE username = #{username}
</select>

-- 最终执行的SQL(预编译后)
SELECT id, username, age FROM t_user WHERE username = ?
-- 参数填充后(自动转义)
SELECT id, username, age FROM t_user WHERE username = 'ZhangSan'
-- 使用${}的SQL映射
<select id="getUserByUsername" parameterType="String" resultType="com.example.entity.User">
    SELECT id, username, age FROM t_user WHERE username = ${username}
</select>

-- 最终执行的SQL(直接拼接)
SELECT id, username, age FROM t_user WHERE username = ZhangSan

从示例能明显看出,${}拼接后缺少引号,会直接导致SQL语法错误;而#{}自动添加引号,避免了语法问题,这也是二者最直观的区别。

2. SQL注入风险对比

这是二者最关键的差异之一,也是面试重点考察内容。#{}因为使用预编译和参数转义,能有效抵御SQL注入攻击;而${}直接拼接字符串,若参数来自用户输入且未做校验,极易被注入恶意SQL。

场景演示:假设用户输入恶意参数username = "ZhangSan' OR 1=1 --",观察两种语法的执行结果:

-- 使用#{},参数被转义,恶意内容失效
-- 最终执行SQL
SELECT id, username, age FROM t_user WHERE username = 'ZhangSan'' OR 1=1 --'
-- 效果:仅查询username为ZhangSan' OR 1=1 --的用户(无匹配结果),无注入风险
-- 使用${},直接拼接恶意参数
-- 最终执行SQL
SELECT id, username, age FROM t_user WHERE username = ZhangSan' OR 1=1 --
-- 效果:--注释掉后续SQL,实际执行SELECT id, username, age FROM t_user WHERE username = ZhangSan OR 1=1
-- 会查询出表中所有用户,造成数据泄露,存在严重注入风险

3. 适用场景区分

二者的适用场景有明确边界,不可混用,具体如下:

#{}适用场景:动态参数值传递

适用于WHERE条件中的参数、INSERT语句中的插入值、UPDATE语句中的更新值等,也就是需要传递“具体参数值”的场景,这是日常开发中最常用的场景。

-- 1. WHERE条件参数(最常用)
<select id="getUserById" parameterType="Integer" resultType="com.example.entity.User">
    SELECT id, username, age FROM t_user WHERE id = #{id}
</select>

-- 2. INSERT插入值
<insert id="addUser" parameterType="com.example.entity.User">
    INSERT INTO t_user (username, age) VALUES (#{username}, #{age})
</insert>

-- 3. UPDATE更新值
<update id="updateUserAge" parameterType="map">
    UPDATE t_user SET age = #{age} WHERE id = #{id}
</update>

${}适用场景:动态SQL片段拼接

适用于需要动态拼接SQL片段的场景,比如动态表名、动态列名、排序字段、排序方向等,这些场景无法使用#{}(会被转义导致SQL失效),但使用时必须手动校验参数,避免注入风险。

-- 1. 动态表名(假设存在t_user_2024、t_user_2025等分表)
<select id="getUserByTable" parameterType="map" resultType="com.example.entity.User">
    SELECT id, username FROM ${tableName} WHERE age > #{age}
</select>

-- 2. 动态排序字段和方向
<select id="getUserList" parameterType="map" resultType="com.example.entity.User">
    SELECT id, username, age FROM t_user ORDER BY ${sortField} ${sortOrder}
</select>

注意:使用${}时,需提前校验参数合法性,比如sortField只能是表中存在的列名(id、username、age),sortOrder只能是ASC或DESC,避免恶意参数注入。

4. 参数类型处理差异

#{}会自动匹配参数的数据类型,根据参数类型决定是否添加引号,无需开发者手动处理;${}则会将所有参数都作为字符串直接拼接,不区分数据类型,可能导致类型错误。

-- 示例:参数age = 25(Integer类型)
-- 使用#{},自动匹配数值类型,不加引号
<select id="getUserByAge" parameterType="Integer" resultType="com.example.entity.User">
    SELECT id, username FROM t_user WHERE age = #{age}
</select>
-- 执行SQL:SELECT id, username FROM t_user WHERE age = 25(正确)

-- 使用${},直接拼接为字符串,添加引号(导致数值类型不匹配)
<select id="getUserByAge" parameterType="Integer" resultType="com.example.entity.User">
    SELECT id, username FROM t_user WHERE age = ${age}
</select>
-- 执行SQL:SELECT id, username FROM t_user WHERE age = '25'(若age字段为int类型,部分数据库会隐式转换,但不规范,可能影响性能)

5. 性能差异

#{}使用预编译机制,MyBatis会将预编译后的SQL缓存起来,后续相同SQL(仅参数不同)可直接复用预编译结果,无需重新解析SQL,性能更高;${}每次都会拼接出全新的SQL语句,MyBatis无法缓存,每次都需要重新解析SQL,频繁使用会降低性能。

三、核心区别总结表

特性#{}${}
处理机制预编译(PreparedStatement),参数转义字符串直接拼接,不转义
SQL注入风险安全,无注入风险不安全,需手动校验参数
适用场景动态参数值(WHERE/INSERT/UPDATE)动态SQL片段(表名、列名、排序等)
参数类型处理自动匹配类型,无需手动处理全部作为字符串拼接,可能导致类型错误
性能高,预编译SQL可复用低,每次生成新SQL,需重新解析

四、使用注意事项

  1. 日常开发中,优先使用#{},无论何种参数传递场景,只要能用#{},就不使用${},从根源上避免SQL注入风险;

  2. 必须使用${}时(如动态表名、排序字段),一定要对参数进行严格校验,比如限制参数可选值、过滤恶意字符,杜绝注入漏洞;

  3. 避免在${}中直接使用用户输入的参数,若必须使用,需通过后端代码进行参数校验,确保参数合法性。

以上就是MyBatis中#{}和${}的全部核心区别,结合代码示例能更直观地理解二者的差异,面试时只需抓住“预编译vs字符串拼接”“安全vs不安全”“参数值vsSQL片段”这几个关键点,就能轻松应对相关问题。