目录
#{}和${}
#{}和${}的使用
针对Interger类型
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where id = #{id}")
UserInfo getUserInfo1(@Param("id") Integer id);
}
Test代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo1() {
System.out.println(userInfoMapper.getUserInfo1(3));
}
}
运行结果:
我们输⼊的参数并没有在后⾯拼接,id的值是使⽤ ? 进⾏占位. 这种SQL 我们称之为"预编译SQL"。
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where id = ${id}")
UserInfo getUserInfo1(@Param("id") Integer id);
}
Test代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo1() {
System.out.println(userInfoMapper.getUserInfo1(3));
}
}
运行结果:
可以看到, 这次的参数是直接拼接在SQL语句中了.
针对使用#{}和${}的对比
针对String类型
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username = #{username}")
UserInfo getUserInfo2(@Param("username") String username);
}
Test代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo2() {
System.out.println(userInfoMapper.getUserInfo2("xiaoming"));
}
}
运行结果:
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username = ${username}")
UserInfo getUserInfo2(@Param("username") String username);
}
Test代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo2() {
System.out.println(userInfoMapper.getUserInfo2("xiaoming"));
}
}
运行结果:
可以看到, 这次的参数依然是直接拼接在SQL语句中了, 但是字符串作为参数时, 需要添加引号 ' ' , 使⽤ ${} 不会拼接引号 ' ' , 导致程序报错.
改正代码
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username = '${username}'")
UserInfo getUserInfo2(@Param("username") String username);
}
运行结果:
小结
从上⾯两个例⼦可以看出:
#{} 使⽤的是预编译SQL, 通过 ? 占位的⽅式, 提前对SQL进⾏编译, 然后把参数填充到SQL语句中. #{} 会根据参数类型, ⾃动拼接引号 '' .
${} 会直接进⾏字符替换, ⼀起对SQL进⾏编译. 如果参数为字符串, 需要加上引号 '' .
参数为数字类型时, 也可以加上, 查询结果不变, 但是可能会导致索引失效, 性能下降.
mapper接口
@Mapper
public 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接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username = '${username}'")
UserInfo getUserInfo2(@Param("username") String username);
}
xml实现
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo2() {
System.out.println(userInfoMapper.getUserInfo2("xiaoming"));
}
}
运行结果:
SQL注入代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo2() {
System.out.println(userInfoMapper.getUserInfo2("' or 1='1"));
}
}
运行结果:
使用#{}
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username = #{username}")
UserInfo getUserInfo2(@Param("username") String username);
}
xml实现
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo2() {
System.out.println(userInfoMapper.getUserInfo2("xiaoming"));
}
}
运行结果:
xml实现
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void getUserInfo2() {
System.out.println(userInfoMapper.getUserInfo2("' or 1='1"));
}
}
运行结果:
SQL注入场景
SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀.
如果发⽣在⽤⼾登录的场景中, 密码输⼊为 ' or 1='1 , 就可能完成登录(不是⼀定会发⽣的场景,需要看登录代码怎么写)。
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username= '${username}' and password = '${password}'")
List<UserInfo> queryUserByNameAndPassword(String username, String password);
}
UserService
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public List<UserInfo> queryUserByNameAndPassword(String name, String password) {
return userInfoMapper.queryUserByNameAndPassword(name, password);
}
}
UserController
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Boolean login(String name, String password){
//根据账号和密码查询数据库
List<UserInfo> userInfo = userService.queryUserByNameAndPassword(name, password);
if(userInfo==null){
return false;
}
return true;
}
}
数据库信息
正常登录
SQL注入
${}的应用场景示例
排序功能
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo order by id ${order}")
List<UserInfo> queryUserListByOrder(String order);
}
Test代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void queryUserListByOrder() {
System.out.println(userInfoMapper.queryUserListByOrder("desc"));
}
}
运行结果:
将${}替换成#{}
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo order by id #{order}")
List<UserInfo> queryUserListByOrder(String order);
}
运行结果:
不难发现,#{}是给 desc 添加了单引号,但是这句SQL语句的 desc并不应该加单引号。
除此之外, 还有表名作为参数时, 也只能使⽤ ${}.
like查询
mapper接口
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username like '%#{username}%'")
List<UserInfo> queryUserListByLike(String username);
}
Test代码
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void queryUserListByLike() {
System.out.println(userInfoMapper.queryUserListByLike("zh"));
}
}
运行结果:
将#{}改为${}
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username like '%${username}%'")
List<UserInfo> queryUserListByLike(String username);
}
运行结果:
将#{}改为${}后,虽然能正确查出来,但是存在SQL注入问题,因此不能直接用${}。
解决方法:使用MySQL的内置函数concat来处理:
mapper代码
@Mapper
public interface UserInfoMapper {
@Select("select * from userinfo where username like CONCAT('%',#{username},'%')")
List<UserInfo> 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 会将其转换为字符串形式进行替换。