Mapper 接口为什么没有实现类,却能执行 SQL?
如果你刚接触 MyBatis。
可能不会觉得这有什么问题。
但如果你认真想一下,其实这件事非常离谱。
我们平时写 Java:
public interface UserService {
User getById(Long id);
}
接口是不能直接执行的。
因为接口没有实现。
所以必须写:
@Service
public class UserServiceImpl implements UserService {
@Override
public User getById(Long id) {
return null;
}
}
然后才能调用:
userService.getById(1L);
这是所有 Java 开发都知道的事情。
但到了 MyBatis。
事情突然变了。
我们只写一个接口:
@Mapper
public interface UserMapper {
User selectById(Long id);
}
再写一条 SQL:
<select id="selectById">
select * from user where id = #{id}
</select>
然后直接调用:
userMapper.selectById(1L);
竟然成功执行了。
问题来了。
接口没有实现类。
那到底是谁执行了这个方法?
先思考一个问题
假设让你设计 MyBatis。
用户写:
userMapper.selectById(1L);
你会怎么实现?
最简单的方法当然是:
public class UserMapperImpl implements UserMapper {
@Override
public User selectById(Long id) {
return jdbcTemplate.queryForObject(...);
}
}
但是很快你会发现。
项目里可能有:
100个Mapper
1000个Mapper
甚至5000个Mapper
难道每个 Mapper 都生成一个实现类?
显然不现实。
MyBatis 的答案:动态代理
MyBatis 发现。
所有 Mapper 干的事情其实都一样。
无非就是:
接收参数
找到对应SQL
执行SQL
返回结果
既然逻辑一样。
那根本没必要生成大量实现类。
只需要生成一个代理对象即可。
代理对象到底是什么?
假设你调用:
userMapper.selectById(1L);
实际上执行的并不是:
UserMapper
而是:
com.sun.proxy.$ProxyXX
类似这样:
UserMapper mapper = proxy;
当方法被调用时:
mapper.selectById(1L);
最终进入:
invoke()
方法。
public Object invoke(
Object proxy,
Method method,
Object[] args) {
}
这里才是真正的入口。
MyBatis 怎么知道执行哪条 SQL?
假设调用:
userMapper.selectById(1L);
MyBatis 会拿到:
method.getName()
结果:
selectById
然后结合:
UserMapper.class
拼出唯一标识:
com.demo.mapper.UserMapper.selectById
这个字符串非常重要。
因为在启动时。
MyBatis 已经把 XML 中的 SQL 全部解析好了。
例如:
<select id="selectById">
select * from user where id = #{id}
</select>
最终会保存成:
com.demo.mapper.UserMapper.selectById
↓
SQL
这样就建立起了映射关系。
真正执行 SQL 的是谁?
很多人以为:
MapperProxy 负责执行 SQL。
其实不是。
MapperProxy 只是中间人。
它最终会调用:
SqlSession
源码链路大概如下:
MapperProxy
↓
MapperMethod
↓
SqlSession
↓
Executor
↓
JDBC
↓
Database
真正执行 SQL 的核心其实是:
Executor
后面我们会详细分析。
看一眼源码
Mapper 的代理对象创建入口:
MapperProxyFactory
核心代码:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy);
}
是不是很熟悉?
上一篇 Spring 系列讲动态代理时。
我们已经见过类似代码。
本质上都是:
JDK Dynamic Proxy
为什么这样设计?
如果不用动态代理。
假设系统有:
300个Mapper
就需要:
300个实现类
维护成本极高。
而使用动态代理以后:
一个 MapperProxy
就能代理所有 Mapper 接口。
这也是 MyBatis 设计最巧妙的地方。
总结
很多人以为:
userMapper.selectById()
是在调用接口。
实际上不是。
真正执行的是 MyBatis 创建的代理对象。
整个流程如下:
Mapper接口
↓
JDK动态代理
↓
MapperProxy
↓
MapperMethod
↓
SqlSession
↓
Executor
↓
数据库
所以:
Mapper 接口之所以没有实现类还能执行 SQL,本质上是因为 MyBatis 在运行期间为它创建了动态代理对象。
这也是整个 MyBatis 框架最核心的设计之一。
上一篇:
《Spring 系列完结》
下一篇:
《Mapper 接口为什么没有实现类,却能执行 SQL?》


