每一位使用 MyBatis 的 Java 开发者,都曾体验过这种“魔法”:我们只定义了一个简单的 UserMapper
接口,没有写任何实现类,但只要在 Service 中注入它并调用其方法,数据库操作就奇迹般地完成了。
// 在 Service 中
@Autowired
private UserMapper userMapper;
public User getUser(Long id) {
// 只是调用了一个接口方法
return userMapper.selectById(id);
}
UserMapper
只是一个接口,它没有实现类,那 userMapper
这个注入的 Bean 到底是什么?selectById
这个方法又是如何与具体的 SQL 语句关联起来的呢?
本文将为你揭开 MyBatis 这层神秘的面纱,深入剖析其接口与 SQL 的映射原理,并总结在 Spring Boot 环境下的最佳实践和常见陷阱。
1. 核心原理:JDK 动态代理 (Dynamic Proxy)
MyBatis 的“魔法”核心,正是 Java 的 JDK 动态代理 技术。
当你试图从 Spring 容器中获取一个 UserMapper
实例时,Spring 实际上是请求 MyBatis 的 MapperFactoryBean
来创建一个 Bean。MyBatis 并不会去寻找这个接口的实现类,而是利用 JDK 的 Proxy
类,在运行时动态地为你创建一个该接口的代理对象。
这个代理对象,就是你注入到 Service 中的 userMapper
。它具备了 UserMapper
接口的所有方法,但它不是一个普通的实现。你可以把它想象成一个聪明的“中介”或“调度员”。
当你调用 userMapper.selectById(1L)
时,实际发生了以下事情:
1. 方法拦截: 这个调用首先被代理对象拦截。
2. 信息解析: 代理对象会解析出你调用的信息,包括:
• 接口名:
com.example.mapper.UserMapper
• 方法名:
selectById
• 参数:
1L
3. SQL 寻址: 这是最关键的一步。代理对象会根据**“接口名 + 方法名”**这个唯一的坐标,去 MyBatis 的全局配置中寻找与之对应的 SQL 语句。
4. SQL 执行: 找到 SQL 后,代理对象会利用底层的
SqlSession
,将参数1L
传递进去,通过 JDBC 执行这条 SQL。5. 结果映射: 将数据库返回的结果,根据配置映射成 Java 对象并返回。
所以,整个问题的关键就变成了:MyBatis 是如何完成第3步——**“SQL 寻址”**的?
2. “寻址”的艺术:两种主要的映射方式
MyBatis 提供了两种方式,来告诉代理对象“接口方法”和“SQL语句”之间的对应关系。
方式一:XML 映射文件 (最强大、最常用)
这是 MyBatis 最传统也是功能最丰富的映射方式。它遵循两条黄金法则:
法则1:namespace
必须是 Mapper 接口的全限定名。
这是连接 XML
文件和 Java
接口的“红线”。
法则2:select/insert/update/delete
等标签的 id
必须与接口中的方法名完全一致。
这是定位到具体 SQL 语句的“门牌号”。
示例:
UserMapper.java
(接口定义)
package com.example.mapper;
public interface UserMapper {
User selectById(Long id);
void insert(User user);
}
UserMapper.xml
(SQL 定义)
<?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.example.mapper.UserMapper">
<select id="selectById" resultType="com.example.model.User">
SELECT * FROM users WHERE id = #{id}
</select>
<insert id="insert">
INSERT INTO users(username, email) VALUES(#{username}, #{email})
</insert>
</mapper>
当调用 UserMapper.selectById()
时,代理对象会精确地找到 namespace
为 com.example.mapper.UserMapper
的 XML 文件中,id
为 selectById
的 <select>
标签,并执行其中的 SQL。
方式二:注解 (简单 SQL 的便捷之选)
对于简单的 SQL 语句,我们可以完全抛弃 XML,直接在接口方法上使用注解。
示例:
package com.example.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO users(username, email) VALUES(#{username}, #{email})")
void insert(User user);
}
这种方式非常直观,但当 SQL 变得复杂,特别是需要动态 SQL(if
, foreach
等)时,XML 的表现力远胜于注解。
3. 让 MyBatis 找到你的 Mapper:扫描与注册
我们已经定义好了映射关系,但还需要告诉 MyBatis 去哪里“发现”这些接口和 XML 文件。
在现代 Spring Boot 项目中,这通常通过 mybatis-spring-boot-starter
自动完成,我们只需提供少量配置。
1. @MapperScan
注解 (推荐)
在你的 Spring Boot 主启动类上,添加 @MapperScan
注解,并指定 Mapper 接口所在的包路径。
@SpringBootApplication
@MapperScan("com.example.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
这个注解会告诉 MyBatis:“请扫描 com.example.mapper
包下的所有接口,并将它们注册为 Mapper Bean。”
2. @Mapper
注解
你也可以不在启动类上使用 @MapperScan
,而是在每个 Mapper 接口上单独添加 @Mapper
注解。
@Mapper
public interface UserMapper { ... }
这样,只要 UserMapper
接口在 Spring Boot 的组件扫描路径下,它就会被自动发现。但当 Mapper 接口很多时,@MapperScan
显然更方便。
3. XML 文件的位置
默认情况下,MyBatis Spring Boot Starter 会在 classpath 中,寻找与 Mapper 接口同包同名的 XML 文件。
标准的 Maven 项目结构:
src/
main/
java/
com/
example/
mapper/
UserMapper.java
resources/
com/
example/
mapper/
UserMapper.xml <-- XML 放在 resources 目录下,但包结构与 Java 接口保持一致
如果你想把 XML 文件集中存放在另一个地方,可以在 application.yml
中通过 mybatis.mapper-locations
属性来指定:
mybatis:
mapper-locations: classpath:mappers/*.xml
4. 常见问题与陷阱
•
BindingException: Type ... is not known to the MapperRegistry.
• 含义: “未在 Mapper 注册表中找到该类型”。
• 原因: MyBatis 根本没发现你的 Mapper 接口。请检查
@MapperScan
的包路径是否正确,或者接口上是否漏了@Mapper
注解。
•
BindingException: Invalid bound statement (not found): ...
• 含义: “无效的绑定语句(未找到)”。
• 原因: 接口找到了,但接口里的方法没找到对应的 SQL。请检查:
1. XML 的
namespace
是否是接口的全限定名,一个字母都不能错。2. XML 标签的
id
是否与方法名完全一致。3. XML 文件是否被构建工具(Maven/Gradle)正确地打包进了最终的
jar
文件中。
- • XML 文件未被打包:
如果你的 XML 文件放在src/main/java
目录下,Maven 默认不会打包.xml
文件。你需要在pom.xml
中添加如下配置:<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>
总结
MyBatis 的接口映射机制,看似“神奇”,实则建立在一套清晰、严谨的规则之上:
1. 核心是 JDK 动态代理,它在运行时为你的接口创建了一个实现类。
2. 映射的“钥匙”是“全限定接口名 + 方法名”。
3. 映射关系可以通过 XML 或注解来定义。
4.
@MapperScan
是告知 MyBatis 从哪里开始寻找这些“钥匙”的入口。