#{} 说:“我帮你防 SQL 注入!” ${} 说:“我让你自由飞翔!” (然后就被注入了)

398 阅读2分钟

这篇文章是用来详细讲解MyBatis中#{}${}的作用、区别以及使用场景。

1. 作用

  • #{} (PreparedStatement): 用于参数占位符,MyBatis会将#{}解析为?,然后使用PreparedStatement来设置参数值。这是一种预编译的方式。
  • ${} (Statement): 用于字符串替换,MyBatis会将${}直接替换为变量的值。这是一种字符串拼接的方式。

2. 区别

特性#{} (PreparedStatement)${} (Statement)
安全性高,防止SQL注入低,易受SQL注入影响
预编译是,预编译SQL语句否,直接拼接SQL语句
数据类型自动处理数据类型字符串类型,需要手动处理
适用场景大部分场景,参数传递动态表名、排序字段等
性能较高,可重用预编译语句较低,每次都需要编译

详细解释:

  • 安全性: #{}使用预编译,会将参数值进行转义,防止恶意SQL注入。${}直接将变量值拼接到SQL语句中,如果变量值包含恶意代码,就可能导致SQL注入。
  • 预编译: #{}使用PreparedStatement,SQL语句在执行前会被预编译,数据库会缓存预编译后的语句,下次执行相同的语句时,可以直接使用缓存,提高性能。${}每次都需要重新编译SQL语句。
  • 数据类型: #{}会自动处理数据类型,例如将Java的Integer类型转换为数据库的INT类型。${}会将所有变量值都视为字符串类型,如果需要传递其他类型的数据,需要手动进行类型转换。
  • 适用场景: #{}适用于大部分场景,特别是需要传递参数值的情况。${}适用于动态表名、排序字段等不需要进行预编译的场景。
  • 性能: #{}由于预编译和语句缓存,性能通常比${}更高。

3. 使用场景

接下来我们用MyBatis-Plus来写代码示例,展示#{}${}的使用场景。

注意: MyBatis-Plus 简化了 MyBatis 的很多操作,但底层仍然是 MyBatis,所以 #{}${} 的基本原理和区别仍然适用。

1. #{} 的使用场景 (MyBatis-Plus 风格)

  • 传递参数值: 根据用户ID查询用户信息。

    // 假设 UserMapper 继承了 BaseMapper<User>
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        @Select("SELECT * FROM users WHERE id = #{id}")
        User getUserById(@Param("id") int id);
    }
    
    // 使用
    @Autowired
    private UserMapper userMapper;
    
    public void testGetUserById() {
        int userId = 123;
        User user = userMapper.getUserById(userId);
        System.out.println(user);
    }
    

    解释:

    • @Select 注解直接定义了 SQL 语句。
    • #{id} 仍然是参数占位符,MyBatis-Plus 会自动处理。
    • @Param("id") 指定了参数名称,与 #{id} 对应。
  • 插入数据:

    // UserMapper 接口 (继承 BaseMapper<User>)
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        // 不需要额外注解,BaseMapper 提供了 insert 方法
    }
    
    // 使用
    @Autowired
    private UserMapper userMapper;
    
    public void testInsertUser() {
        User newUser = new User();
        newUser.setUsername("testuser");
        newUser.setEmail("test@example.com");
        userMapper.insert(newUser); // MyBatis-Plus 提供的 insert 方法
    }
    

    解释:

    • MyBatis-Plus 的 BaseMapper 提供了通用的 insert 方法,无需手动编写 SQL。
    • #{}User 对象的属性中自动匹配,将 usernameemail 的值插入到 SQL 语句中。
  • 更新数据:

    // UserMapper 接口 (继承 BaseMapper<User>)
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        // 不需要额外注解,BaseMapper 提供了 updateById 方法
    }
    
    // 使用
    @Autowired
    private UserMapper userMapper;
    
    public void testUpdateUser() {
        User updatedUser = new User();
        updatedUser.setId(123); // 必须设置 ID,否则无法更新
        updatedUser.setUsername("newusername");
        updatedUser.setEmail("newemail@example.com");
        userMapper.updateById(updatedUser); // MyBatis-Plus 提供的 updateById 方法
    }
    

    解释:

    • BaseMapper 提供了 updateById 方法,根据 ID 更新数据。
    • #{}User 对象的属性中自动匹配,将 usernameemail 的值更新到 SQL 语句中。

2. ${} 的使用场景 (MyBatis-Plus 风格)

  • 动态表名: 根据不同的条件查询不同的表。

    // UserMapper 接口
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        @Select("SELECT * FROM ${tableName} WHERE id = #{id}")
        User getDataFromTable(@Param("tableName") String tableName, @Param("id") int id);
    }
    
    // 使用
    @Autowired
    private UserMapper userMapper;
    
    public void testGetDataFromTable() {
        String tableName = "users"; // 确保 tableName 是安全的
        int id = 123;
        User user = userMapper.getDataFromTable(tableName, id);
        System.out.println(user);
    }
    

    解释:

    • ${tableName} 直接将 tableName 的值拼接到 SQL 语句中。
    • 务必确保 tableName 是安全的,防止 SQL 注入。
  • 动态排序字段: 根据不同的字段进行排序。

    // UserMapper 接口
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        @Select("SELECT * FROM users ORDER BY ${sortField}")
        List<User> getAllUsers(@Param("sortField") String sortField);
    }
    
    // 使用
    @Autowired
    private UserMapper userMapper;
    
    public void testGetAllUsers() {
        String sortField = "username"; // 确保 sortField 是安全的
        List<User> users = userMapper.getAllUsers(sortField);
        System.out.println(users);
    }
    

    解释:

    • ${sortField} 直接将 sortField 的值拼接到 SQL 语句中。
    • 务必确保 sortField 是安全的,防止 SQL 注入。

重要提示:

  • 永远优先使用#{},除非你明确知道${}是安全的并且是必要的。
  • 在使用${}时,务必对变量值进行严格的校验和过滤,防止SQL注入。 可以使用白名单机制,只允许特定的值通过。
  • 避免将用户输入直接传递给${},这几乎肯定会导致SQL注入。

总结:

#{}是安全的、预编译的参数占位符,适用于大部分场景。${}是字符串替换,适用于动态表名、排序字段等特殊场景,但需要非常小心SQL注入风险。 在实际开发中,应该尽量避免使用${},如果必须使用,一定要做好安全措施。