2023.11.28 MyBatis 中 #{} 和 ${} 的区别
目录
引言
主要区别
sql 注入问题
使用 ${param} 的两大条件
sql 关键字
模糊查询(like)
分析
concat 函数
阅读以下文章前建议先点击下方链接了解单元测试
单元测试详解
引言
- 首先 #{} 和 ${} 均是 MyBatis 获取动态参数的两种实现
- 为了让我们更加方便观察 这二者之间的区别
- 我们可先在配置文件 application.properties 中加入相应配置项
# 配置打印 Mybatis 执行 SQL 的日志(debug 级) mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl # 配置 日志打印级别 将默认 info 级 修改为 debug 级 logging.level.com.example.demo=debug
- 下文中的 param 代表参数
主要区别
- ${param} 是 直接替换
- #{param} 是 预编译处理
预编译处理
- 指 MyBatis 在处理 #{param} 时,将 #{param} 替换成了 ?号,代表占位符
- 再使用 PreparedStatement 的 set 方法来赋值
- 也就是说如果参数为 Integer 类型,则使用 setInt 方法
- 如果参数为 Sting 类型,则使用 setString 方法
// ? 是一个占位符,只是占个位置,后面会被替换成其他的东西 String sql = "insert into staff values(?, ?)"; PreparedStatement statement = connection.prepareStatement(sql); //1 2 数字表示占位符下标,默认从1开始,set方法是一个系列:setXXX(XXX表示类型) statement.setInt(1, ID); statement.setString(2, name);
实例理解一
- 使用 ${param}
<select id="getUserByName" resultType="com.example.demo.entity.User"> select * from user where name = ${user_name} </select>
- 利用单元测试 执行调用该 sql 语句
@SpringBootTest class UserMapperTest { // 正因为该测试单元运行在 Spring Boot 下 // 所以我们可以使用依赖注入 userMapper Bean 对象 @Autowired private UserMapper userMapper; @Test void getUserByName() { User user = userMapper.getUserByName("xiaolin"); System.out.println(user); } }
执行结果:
- 发生报错!!
- 正确的 sql 语句应该为 name = 'xiaolin'
- 此处直接替换成了 name = xiaolin,缺少 单引号!!
- 所以我们在使用 ${param} 获取字符串类型的动态参数时,应该主动加上 单引号
- 例如 name = '${param}'
- 但是该种写法存在 sql 注入问题
实例理解二
- 使用 #{param}
<select id="getUserByName" resultType="com.example.demo.entity.User"> select * from user where name = #{user_name} </select>
- 利用单元测试 执行调用该 sql 语句
@SpringBootTest class UserMapperTest { // 正因为该测试单元运行在 Spring Boot 下 // 所以我们可以使用依赖注入 userMapper Bean 对象 @Autowired private UserMapper userMapper; @Test void getUserByName() { User user = userMapper.getUserByName("xiaolin"); System.out.println(user); } }
执行结果:
- 未发生报错!
注意:
- 当传来的参数为 Integer 类型时,使用 #{} 或 ${} 都行
sql 注入问题
- ${param} 存在 sql 注入问题
- #{param} 不存在该问题
实例理解
- 此处模拟实现一个验证登录功能
初始化数据库
- 创建一个 message 数据库 和 user 表
- 其中 user 表中数据为下图所示
初始化 UserMapper 接口
- 此处我们在接口中添加一个 login 方法
import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; //添加 @Mapper 注解 代表该接口会伴随这 项目的启动而注入到容器中 @Mapper public interface UserMapper { // 登录查询 User login(@Param("user_name") String name, @Param("password") String password); }
初始化 UserMapper XML 文件
- 在与 接口相对应的 XML 文件中
- 添加上与 login 方法 相对应的 sql 语句
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati s.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <select id="login" resultType="com.example.demo.entity.User"> select * from user where name = '${user_name}' and password = '${password}' </select> </mapper>
创建 login 的测试方法
- 注意这里我们传入的 password = " ' or 1 = '1 "
- 然而 name = xiaolin 用户的正确密码为 123456,此处传入的显然不是正确密码
@Test void login() { String username = "xiaolin"; String password = "' or 1 = '1"; User user = userMapper.login(username,password); System.out.println("登录状态:" + (user == null ? "失败" : "成功") + " user = " + user); } }
执行测试方法
- 测试方法执行成功
- 上述演示出现的问题,属于典型的 sql 注入问题
分析 sql 语句
- 即该 sql 语句会将 user 表中的所有用户信息全部查询出来
使用 ${param} 的两大条件
- 保证 param 的值一定为可穷举值
- 在使用之前一定要对传递的值进行合法性校验(可在 Controller 层 通过穷举的方式验证传递值的安全性)
sql 关键字
- 当我们在 sql 语句中想实现插入 sql 关键字时
- 此时必须使用 ${param} 来实现
- 一般传入的关键字也时 字符串形式,如果还是采用 #{param} 这种预处理方式的话
- #{param} 就会将关键字加上 单引号,从而不能达成我们想要实现的效果
- 而 使用 ${param} 直接替换,正好符合我们的需求
实例理解
- 此处实现一个根据关键字 进行排序查询用户信息 的功能
初始化 UserMapper 接口
- 此处我们在接口中添加一个 getUserByKeyWord 方法
import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; //添加 @Mapper 注解 代表该接口会伴随这 项目的启动而注入到容器中 @Mapper public interface UserMapper { // 根据关键字 查询用户信息 List<User> getUserByKeyWord(@Param("key_word") String keyWord); }
初始化 UserMapper XML 文件
- 在与 接口相对应的 XML 文件中
- 添加上与 getUserByKeyWord 方法 相对应的 sql 语句
- 此处使用 ${key_word}
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati s.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <select id="getUserByKeyWord" resultType="com.example.demo.entity.User"> select * from user order by id ${key_word} </select> </mapper>
创建 getUserByKeyWord 的测试方法
- 此处我们传入 desc 关键字进行降序查询
@Test void getUserByKeyWord() { List<User> users = userMapper.getUserByKeyWord("desc"); for (User user:users) { System.out.println(user); } }
执行测试方法
- 测试方法执行成功
模糊查询(like)
实例理解
- 此处实现一个 根据用户输入的字符串进行模糊查询用户信息 的功能
分析
- 用户输入的字符串,不是一个可穷举值,是不可控的值
- 所以我们直接排除使用 ${param}
- 其次如果我们直接使用 #{param} 的话,也会存在一定问题
- 实际生成的 sql 语句与我们期望生成的 sql 语句显然是不同的
- 使用预处理时,因为用户传入的是 String 类型,进行赋值时会加上单引号
concat 函数
- concat 函数为 mysql 中内置的函数,可实现字符串拼接功能
- 由上图所示,我们可以利用 concat 函数来解决我们 #{param} 所遇到的问题
初始化 UserMapper 接口
- 此处我们在接口中添加一个 getListByName 方法
import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; //添加 @Mapper 注解 代表该接口会伴随这 项目的启动而注入到容器中 @Mapper public interface UserMapper { // 根据用户名模糊查询 List<User> getListByName(@Param("key_name") String keyName); }
初始化 UserMapper XML 文件
- 在与 接口相对应的 XML 文件中
- 添加上与 getListByName 方法 相对应的 sql 语句
- 此处使用 concat 函数 和 #{param}
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati s.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <select id="getListByName" resultType="com.example.demo.entity.User"> select * from user where name like concat('%',#{key_name},'%') </select> </mapper>
创建 getListByName 的测试方法
- 此处我们传入字符串 "xiao" 进行模糊查询
@Test void getListByName() { String keyName = "xiao"; List<User> users = userMapper.getListByName(keyName); for (User user: users) { System.out.println(user); } }
执行测试方法
- 测试方法执行成功