MyBatis Mapper 接口为什么没有实现类?

使用 MyBatis 时,我们通常只写一个 Mapper 接口,却能直接注入并调用它。实现类在哪里?答案是 MyBatis 在运行时为 Mapper 接口创建代理对象,并把方法调用映射到对应的 SQL 配置。

1. 从接口到代理对象

1
2
3
public interface UserMapper {
User findById(long id);
}

项目启动时,MyBatis 与 Spring 的整合配置会扫描 Mapper 接口,并向 Spring 容器注册相应的 Mapper 工厂对象。注入时,框架通过代理机制提供接口实例。调用 findById 后,代理根据 Mapper 命名空间、方法名和参数找到映射语句,创建并执行数据库操作,再把结果映射回 Java 对象。

常见流程可以概括为:

1
2
3
4
5
6
调用 Mapper 方法
→ Mapper 代理解析接口与方法
→ 找到对应的 MappedStatement
→ 创建 SQL 与参数映射
→ 通过 SqlSession / JDBC 执行
→ 把结果映射成对象并返回

注解 SQL 和 XML SQL 是两种定义映射语句的方式。简单查询用注解较直接;复杂动态 SQL、结果映射通常更适合 XML。是否扫描到接口取决于项目的 Mapper 扫描配置。

2. #{}:绑定参数的默认选择

1
2
3
<select id="findById" resultType="User">
SELECT id, name FROM user WHERE id = #{id}
</select>

#{} 会把参数作为预编译语句的值绑定,SQL 结构与参数值分开处理。这通常是传递用户输入的正确方式,也能避免把普通字符串直接拼进 SQL。

3. ${}:原样替换,必须限制输入

1
ORDER BY ${columnName}

${} 会把字符串直接替换进 SQL 文本,常用于表名、列名等不能作为普通值绑定的位置。它不会自动把用户输入变安全,因此不能把请求参数未经检查地传给 ${}。

若排序字段需要动态选择,应使用白名单映射:

1
2
3
4
5
6
7
8
Map<String, String> allowedColumns = Map.of(
"created", "created_at",
"price", "price"
);
String column = allowedColumns.get(sortKey);
if (column == null) {
throw new IllegalArgumentException("不支持的排序字段");
}

随后只把白名单中的列名交给 SQL,并继续使用绑定参数传递筛选值。

4. 动态 SQL 的常见结构

MyBatis XML 提供 <if>、<where>、<set> 和 <foreach> 等标签,用于按条件生成 SQL。比如可选筛选条件用 <if>,更新字段用 <set>,批量 ID 查询用 <foreach>。动态 SQL 仍要检查空集合、空字符串和字段权限,避免生成无效或过宽的语句。

5. 排查 Mapper 调用

  • Mapper 是否被扫描并注册到 Spring 容器?
  • XML 的 namespace、id 是否与接口全限定名和方法名对应?
  • 参数名是否能被映射到 SQL 中?多参数时是否显式使用 @Param?
  • 返回列与 Java 属性是否匹配,是否需要 resultMap 或驼峰映射?
  • 最终生成的 SQL 和绑定参数是否符合预期?

6. 小结

Mapper 不是“凭空有了实现”,而是运行时代理把接口方法与 SQL 映射连接起来。传值优先用 #{};确实需要替换 SQL 标识符时,用严格白名单控制 ${} 的输入。

参考:MyBatis Mapper XML 文档。