乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

参数传递与占位符

导学

本节将深入掌握MyBatis中参数传递的核心机制。你将理解#{}与${}的本质区别,掌握#{}的内部结构参数,明确二者的适用场景与安全边界,并通过SQL注入攻击演示建立牢固的安全意识。

定义

#{}与${}是MyBatis SQL映射文件中用于参数占位的两种语法。在JDBC原始写法中,参数传递只有一种方式:通过PreparedStatement.setXxx()进行预编译绑定。MyBatis在此基础上提供了两种占位符:#{}将参数以预编译方式安全绑定,${}将参数以字符串替换方式直接拼接到SQL中。二者看似都是"占位",但底层实现、安全特性和适用场景截然不同,用错一步就可能导致严重的SQL注入漏洞。

适用位置与核心属性

#{}与${}书写在select、insert、update、delete元素的SQL语句内部,用于标记需要替换为实际参数值的位置。

#{} 内部结构参数

属性是否必填说明
property是参数对象中的属性名,或@Param注解指定的名称
javaType否Java类型全限定名,通常自动推断
jdbcType否JDBC类型,如VARCHAR、INTEGER,用于处理null值时指定类型
typeHandler否自定义类型处理器全限定名
mode否存储过程参数模式:IN、OUT、INOUT
numericScale否数值类型的精度
resultMap否用于嵌套结果映射
jdbcTypeName否数据库特定的类型名称

日常开发中,绝大多数场景只需写#{propertyName},MyBatis会自动推断其余属性。

${} 特性

特性说明
替换方式字符串直接替换,不进行预编译
安全性存在SQL注入风险,不可用于用户输入
适用场景动态表名、列名、ORDER BY字段、数据库函数名等非用户输入场景

核心原理

#{}与${}在MyBatis内部的执行路径完全不同,这也是二者安全差异的根源。

#{} 参数绑定流程

  1. SQL解析:MyBatis解析映射文件中的SQL,将#{name}识别为参数占位符。
  2. SQL重写:将#{name}替换为JDBC的?占位符,生成预编译SQL模板。
  3. 参数绑定:ParameterHandler根据参数类型调用PreparedStatement.setXxx(index, value)方法。
  4. 预编译执行:数据库接收带?的SQL模板和参数值,分开处理,杜绝注入。

${} 字符串替换流程

  1. SQL解析:MyBatis解析映射文件中的SQL,将${tableName}识别为文本替换点。
  2. 字符串拼接:直接将参数值的字符串形式拼接到SQL中,生成完整的SQL文本。
  3. 直接执行:通过Statement(非PreparedStatement)将拼接后的SQL发送给数据库。
  4. 无保护:若参数值包含恶意SQL片段,将直接参与执行,导致注入攻击。

完整示例

场景说明

乐途公司技术部的学员管理系统需要支持按姓名模糊查询和按分数排序。管理员在界面上输入查询条件,后端接收参数后拼接SQL。本节演示#{}的安全查询与${}的危险用法,并通过模拟攻击展示SQL注入后果。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
);

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

当前数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

完整的映射文件片段与Java代码

Mapper接口

package com.flywing.mapper;

import com.flywing.entity.Student;
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface StudentMapper {
    // 安全查询:使用#{}进行模糊查询
    List<Student> findByNameSafe(@Param("keyword") String keyword);

    // 危险查询:使用${}拼接用户输入(仅用于演示,生产环境禁止)
    List<Student> findByNameUnsafe(@Param("keyword") String keyword);

    // 合法使用${}:动态排序字段(非用户输入)
    List<Student> findAllOrderBy(@Param("orderColumn") String orderColumn);
}

映射文件 StudentMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.flywing.mapper.StudentMapper">

    <!-- 场景一:安全查询,#{}预编译绑定 -->
    <select id="findByNameSafe" resultType="com.flywing.entity.Student">
        SELECT id, name, age, major, score
        FROM student
        WHERE name LIKE CONCAT('%', #{keyword}, '%')
    </select>

    <!--
        场景二:危险查询,${}直接拼接
        此写法仅用于教学演示SQL注入,生产环境绝对禁止将用户输入传入${}
    -->
    <select id="findByNameUnsafe" resultType="com.flywing.entity.Student">
        SELECT id, name, age, major, score
        FROM student
        WHERE name LIKE '%${keyword}%'
    </select>

    <!--
        场景三:合法使用${},排序字段由后端代码控制,非用户直接输入
    -->
    <select id="findAllOrderBy" resultType="com.flywing.entity.Student">
        SELECT id, name, age, major, score
        FROM student
        ORDER BY ${orderColumn}
    </select>

</mapper>

测试代码

package com.flywing.test;

import com.flywing.entity.Student;
import com.flywing.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;
import java.util.List;

public class ParameterTest {
    public static void main(String[] args) throws Exception {
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
        SqlSession session = factory.openSession();
        StudentMapper mapper = session.getMapper(StudentMapper.class);

        // 场景一:安全查询
        System.out.println("=== #{} 安全查询 ===");
        List<Student> safeList = mapper.findByNameSafe("小");
        System.out.println("查询'小',结果数:" + safeList.size());
        for (Student s : safeList) {
            System.out.println("  " + s.getName());
        }

        // 场景二:模拟SQL注入攻击
        System.out.println("\n=== ${} SQL注入演示 ===");
        String attackInput = "' OR '1'='1";
        try {
            List<Student> unsafeList = mapper.findByNameUnsafe(attackInput);
            System.out.println("注入攻击后结果数:" + unsafeList.size() + "(本应是0,却返回了全表数据!)");
            for (Student s : unsafeList) {
                System.out.println("  " + s.getName() + " | " + s.getMajor());
            }
        } catch (Exception e) {
            System.out.println("注入攻击触发异常:" + e.getMessage());
        }

        // 场景三:合法使用${}排序
        System.out.println("\n=== ${} 合法使用:动态排序 ===");
        List<Student> orderList = mapper.findAllOrderBy("score DESC");
        System.out.println("按分数降序:");
        for (Student s : orderList) {
            System.out.println("  " + s.getName() + " | " + s.getScore());
        }

        session.close();
    }
}

实际执行结果

控制台SQL输出

=== #{} 安全查询 ===
[DEBUG] com.flywing.mapper.StudentMapper.findByNameSafe - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE name LIKE CONCAT('%', ?, '%')
[DEBUG] com.flywing.mapper.StudentMapper.findByNameSafe - ==> Parameters: 小(String)
[DEBUG] com.flywing.mapper.StudentMapper.findByNameSafe - <==      Total: 1
查询'小',结果数:1
  小崔

=== ${} SQL注入演示 ===
[DEBUG] com.flywing.mapper.StudentMapper.findByNameUnsafe - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE name LIKE '%' OR '1'='1%'
[DEBUG] com.flywing.mapper.StudentMapper.findByNameUnsafe - ==> Parameters:
注入攻击后结果数:5(本应是0,却返回了全表数据!)
  大翔 | 计算机科学
  白歌 | 软件工程
  小崔 | 计算机科学
  黄俪 | 信息安全
  李眉 | 软件工程

=== ${} 合法使用:动态排序 ===
[DEBUG] com.flywing.mapper.StudentMapper.findAllOrderBy - ==>  Preparing: SELECT id, name, age, major, score FROM student ORDER BY score DESC
[DEBUG] com.flywing.mapper.StudentMapper.findAllOrderBy - ==> Parameters:
按分数降序:
  大翔 | 95.5
  小崔 | 92.0
  黄俪 | 90.5
  白歌 | 88.0
  李眉 | 87.0

注入攻击结果对比

攻击输入预期行为实际行为(${})
' OR '1'='1查询不到任何记录返回全表5条记录

分析

  1. #{}的安全机制:findByNameSafe生成的SQL是... LIKE CONCAT('%', ?, '%'),参数小被setString()绑定到?位置。即使传入恶意字符串,也只会被当作普通文本处理,不会改变SQL结构。
  2. ${}的注入风险:findByNameUnsafe将攻击输入直接拼接到SQL中,生成的语句变成了... LIKE '%' OR '1'='1%',WHERE条件被恶意改写为恒真,导致全表泄露。
  3. ${}的合法场景:findAllOrderBy中的score DESC由后端代码硬编码或从受控枚举中选择,不直接暴露给用户输入,因此使用${}是安全的。ORDER BY列名、GROUP BY列名、表名等无法使用?占位符的场景,是${}的唯一合法用途。

易错场景/常见误区

误区正解
为了省事,所有参数都用${}用户输入的参数必须用#{};${}仅限非用户输入的元数据(表名、列名、排序字段)
认为${}性能比#{}好预编译SQL在数据库端有执行计划缓存,重复执行时#{}性能更优;${}每次拼接不同SQL,无法利用缓存
在#{}中写jdbcType是画蛇添足当参数可能为null时,必须指定jdbcType,否则MyBatis无法确定调用哪个setNull()重载
模糊查询时写'%#{keyword}%'正确写法是CONCAT('%', #{keyword}, '%')或#{keyword}在Java端拼接好通配符后传入
认为加了#{}就绝对安全#{}防SQL注入,但不防业务逻辑漏洞;如keyword传%会返回全表,这是业务层需控制的

面试考点

Q1:#{}和${}有什么区别?

#{}是预编译参数占位符,MyBatis将其替换为?,通过PreparedStatement.setXxx()绑定参数,安全且防SQL注入。${}是字符串替换,直接将参数值拼接到SQL文本中,通过Statement执行,存在SQL注入风险,仅适用于表名、列名、ORDER BY字段等非用户输入场景。

Q2:什么时候必须使用${}?

当参数需要作为SQL的"结构部分"而非"数据部分"时。例如动态表名SELECT * FROM ${tableName}、动态列名ORDER BY ${column}、数据库函数名等。这些位置数据库不支持?占位符,必须用字符串替换。但务必确保参数来源受控,不来自用户输入。

Q3:#{}中jdbcType的作用是什么?

当传入的参数为null时,JDBC的setNull()需要知道目标列的JDBC类型。若不指定jdbcType,MyBatis可能抛出TypeException。例如#{name, jdbcType=VARCHAR}明确告诉MyBatis,即使name为null,也按VARCHAR类型处理。

Q4:如何防止${}在合法场景下被滥用?

建立白名单校验机制:在将参数传入${}前,检查参数值是否在预定义的合法集合中(如允许的排序字段列表["id", "name", "score"])。拒绝任何不在白名单中的输入,即使该参数理论上由前端传入,也要在服务端做最终校验。

小结

#{}与${}是MyBatis参数传递的双刃剑。#{}通过预编译绑定实现安全、高效的参数传递,是日常开发的绝对首选;${}通过字符串替换满足动态SQL结构的特殊需求,但携带SQL注入风险,必须严格限制使用场景。记住一条铁律:凡是来自用户输入的参数,一律使用#{};只有后端受控的元数据(表名、列名、排序字段)才考虑${},且必须配合白名单校验。

下一章引子

数据插入后,如何获取数据库生成的主键?MySQL的自增主键可以通过useGeneratedKeys自动回填,但Oracle等数据库使用序列生成主键,无法通过JDBC的getGeneratedKeys获取。selectKey元素为此而生,它能在插入前后执行查询语句获取主键值。下一节将对比useGeneratedKeys与selectKey两种策略,演示MySQL自增主键和模拟Oracle序列的完整示例。

上一页
delete
下一页
主键生成策略