> 技术文档 > 详解MyBatis篇四

详解MyBatis篇四

目录

#{}和${}

#{}和${}的使用

针对Interger类型

针对String类型

改正代码

小结

#{} 和 ${} 的区别

#{}的优点

性能更高

更安全(防止SQL注入)

SQL注入场景

正常登录 

SQL注入

${}的应用场景示例 

排序功能

like查询

总结


#{}和${}

#{}和${}的使用
针对Interger类型

mapper接口

@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where id = #{id}\") UserInfo getUserInfo1(@Param(\"id\") Integer id);}

Test代码

@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo1() { System.out.println(userInfoMapper.getUserInfo1(3)); }}

运行结果:

我们输⼊的参数并没有在后⾯拼接,id的值是使⽤ ? 进⾏占位. 这种SQL 我们称之为\"预编译SQL\"。 

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where id = ${id}\") UserInfo getUserInfo1(@Param(\"id\") Integer id);}​

Test代码

​@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo1() { System.out.println(userInfoMapper.getUserInfo1(3)); }}

运行结果:

可以看到, 这次的参数是直接拼接在SQL语句中了. 

针对使用#{}和${}的对比

针对String类型

mapper接口

@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = #{username}\") UserInfo getUserInfo2(@Param(\"username\") String username);}

 Test代码

@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}

运行结果:

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = ${username}\") UserInfo getUserInfo2(@Param(\"username\") String username);}

Test代码

​@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}

运行结果:

可以看到, 这次的参数依然是直接拼接在SQL语句中了, 但是字符串作为参数时, 需要添加引号 \' \' , 使⽤ ${} 不会拼接引号 \' \' , 导致程序报错. 

改正代码

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = \'${username}\'\") UserInfo getUserInfo2(@Param(\"username\") String username);}

运行结果:

小结

从上⾯两个例⼦可以看出:
#{} 使⽤的是预编译SQL, 通过 ? 占位的⽅式, 提前对SQL进⾏编译, 然后把参数填充到SQL语句中. #{} 会根据参数类型, ⾃动拼接引号 \'\' .
${} 会直接进⾏字符替换, ⼀起对SQL进⾏编译. 如果参数为字符串, 需要加上引号 \'\' .
参数为数字类型时, 也可以加上, 查询结果不变, 但是可能会导致索引失效, 性能下降.

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where id = \'${id}\'\") UserInfo getUserInfo1(@Param(\"id\") Integer id);}

运行结果:

#{} 和 ${} 的区别

#{} 和 ${} 的区别就是预编译SQL和即时SQL 的区别.

简单回顾:
当客⼾发送⼀条SQL语句给服务器后, ⼤致流程如下:
1. 解析语法和语义, 校验SQL语句是否正确
2. 优化SQL语句, 制定执⾏计划
3. 执⾏并返回结果

⼀条 SQL如果⾛上述流程处理, 我们称之为 Immediate Statements(即时 SQL)。  

#{}的优点
性能更高

绝⼤多数情况下, 某⼀条 SQL 语句可能会被反复调⽤执⾏, 或者每次执⾏的时候只有个别的值不同(⽐如 select 的 where ⼦句值不同, update 的 set ⼦句值不同, insert 的 values 值不同). 如果每次都需要经过上⾯的语法解析, SQL优化、SQL编译等,则效率就明显不⾏了.

预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译(只是输⼊的参数不同), 省去了解析优化等过程, 以此来提⾼效率。

更安全(防止SQL注入)

SQL注⼊:是通过操作输⼊的数据来修改事先定义好的SQL语句,以达到执⾏代码对服务器进⾏攻击的⽅法。

由于没有对⽤⼾输⼊进⾏充分检查,⽽SQL⼜是拼接⽽成,在⽤⼾输⼊参数时,在参数中添加⼀些
SQL关键字,达到改变SQL运⾏结果的⽬的,也可以完成恶意攻击。

SQL注入演示

使用${}

正常代码

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = \'${username}\'\") UserInfo getUserInfo2(@Param(\"username\") String username);}

xml实现

​@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}

运行结果:

SQL注入代码

​​@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"\' or 1=\'1\")); }}

运行结果:

使用#{}

mapper接口

@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = #{username}\") UserInfo getUserInfo2(@Param(\"username\") String username);}

xml实现

@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}

 运行结果:

 xml实现

​@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"\' or 1=\'1\")); }}

运行结果:

SQL注入场景

SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀.
如果发⽣在⽤⼾登录的场景中, 密码输⼊为 \' or 1=\'1 , 就可能完成登录(不是⼀定会发⽣的场景,需要看登录代码怎么写)。

mapper接口

@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username= \'${username}\' and password = \'${password}\'\") List queryUserByNameAndPassword(String username, String password);}

UserService

@Servicepublic class UserService { @Autowired private UserInfoMapper userInfoMapper; public List queryUserByNameAndPassword(String name, String password) { return userInfoMapper.queryUserByNameAndPassword(name, password); }}

UserController

@RequestMapping(\"/user\")@RestControllerpublic class UserController { @Autowired private UserService userService; @RequestMapping(\"/login\") public Boolean login(String name, String password){ //根据账号和密码查询数据库 List userInfo = userService.queryUserByNameAndPassword(name, password); if(userInfo==null){ return false; } return true; }}

 数据库信息

正常登录 

SQL注入

${}的应用场景示例 
排序功能

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo order by id ${order}\") List queryUserListByOrder(String order);}

Test代码

@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void queryUserListByOrder() { System.out.println(userInfoMapper.queryUserListByOrder(\"desc\")); }}

 运行结果:

将${}替换成#{} 

mapper接口

​​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo order by id #{order}\") List queryUserListByOrder(String order);}

运行结果:

不难发现,#{}是给 desc 添加了单引号,但是这句SQL语句的 desc并不应该加单引号。

除此之外, 还有表名作为参数时, 也只能使⽤ ${}.

like查询

mapper接口

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username like \'%#{username}%\'\") List queryUserListByLike(String username);}

Test代码

@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void queryUserListByLike() { System.out.println(userInfoMapper.queryUserListByLike(\"zh\")); }}

运行结果:

将#{}改为${}

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username like \'%${username}%\'\") List queryUserListByLike(String username);}

 运行结果:

将#{}改为${}后,虽然能正确查出来,但是存在SQL注入问题,因此不能直接用${}。

解决方法:使用MySQL的内置函数concat来处理:

mapper代码

​@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username like CONCAT(\'%\',#{username},\'%\')\") List queryUserListByLike(String username);}

运行结果:

总结

1.预编译处理:
        #{} 是预编译(PreparedStatement)的占位符。当使用#{}时,MyBatis 会将 SQL 中的#{}替换为?,然后通过 PreparedStatement 的 setXXX 方法来设置参数值。这样做的好处是可以防止 SQL 注入,因为 PreparedStatement 会将传入的参数值当做字符串处理,并对其进行转义。
        ${} 是字符串替换的占位符。MyBatis 在处理 SQL 时,会直接将${}中的变量替换为变量的值,然后再进行 SQL 的编译。这意味着如果变量来自于用户输入,并且没有进行适当的处理,那么就有可能出现 SQL 注入的风险。
2.使用场景:
        #{} 主要用于 SQL 语句中的值替换,特别是当需要防止 SQL 注入时。它是 MyBatis 推荐的参数替换方式。
        ${} 主要用于动态 SQL 的构建,比如表名、列名、动态 SQL 片段等。因为这些情况下,#{}无法做到字符串的直接替换,而${}则可以实现。但是,由于${}存在 SQL 注入的风险,所以在使用时需要格外小心,确保替换的变量是安全的。
3.参数类型:
        #{} 可以接收简单类型、POJO(Plain Old Java Object)类型、Map 类型等,并且 MyBatis 会自动处理这些类型的参数,将其设置到 SQL 语句中相应的位置。
        ${} 主要用于字符串替换,所以它接收的主要是字符串类型的参数。当然,你也可以传递其他类型的参数,但 MyBatis 会将其转换为字符串形式进行替换。