Mapper接口(DAO接口)的工作原理是什么?
MyBatis Mapper接口无需实现类,通过动态代理自动生成代理对象,负责SQL执行与结果映射,消除样板代码,实现关注点分离。
这是一个非常经典且核心的关于MyBatis框架的问题。我们来深入浅出地解释一下。
核心摘要
简单来说,Mapper接口(DAO接口)之所以不需要我们编写实现类,是因为 MyBatis框架在运行时通过动态代理(Dynamic Proxy)技术,自动为我们创建了一个代理对象。这个代理对象实现了接口中的方法,并帮助我们完成了所有与数据库的交互。
我们只需要定义好“契约”(接口),以及这个“契约”该如何执行(通过XML或注解提供SQL),MyBatis就会帮我们把剩下的脏活累活全部干完。
一、Mapper接口的工作原理:动态代理的魔力
下面是当你调用一个Mapper接口方法时,背后发生的一系列事情,我们一步步来看:
1. 加载与解析(程序启动时)
当你的应用程序(尤其是整合了Spring的)启动时,MyBatis会:
- 读取配置文件(如
mybatis-config.xml)和Mapper映射文件(XML)。 - 扫描所有被
@Mapper注解标记的接口,或者XML中指定的Mapper接口。 - 将每个SQL语句(无论是XML里的
<select>、<insert>标签,还是注解里的@Select、@Insert)解析成一个MappedStatement对象。 - 这些
MappedStatement对象被存放在一个全局的Configuration对象里,以Key-Value的形式存储。这个Key通常是 “接口的全限定名 + 方法名”,例如com.example.mapper.UserMapper.selectById。
这个阶段,MyBatis已经知道了每个接口方法应该对应哪一条SQL语句以及如何处理参数和返回结果。
2. 生成代理对象(获取Mapper时)
当你从Spring容器中注入(@Autowired)一个Mapper接口,或者手动调用sqlSession.getMapper(UserMapper.class)时,神奇的事情发生了:
- MyBatis并不会去寻找一个叫
UserMapperImpl的实现类,因为它根本不存在。 - 它会使用JDK动态代理(或者CGLIB,但对于接口通常是JDK动态代理)技术,在内存中动态地创建一个代理对象。
- 这个代理对象实现了
UserMapper接口,所以它可以被正常地赋值给你声明的UserMapper变量。 - 这个代理对象内部包含了一个重要的处理器,叫做
MapperProxy。
所以,你拿到的userMapper变量,实际上是MapperProxy这个代理类的实例。
3. 方法调用与拦截(执行代码时)
当你调用接口的方法时,例如 userMapper.selectById(1):
- 这个调用实际上是作用于上一步生成的那个代理对象上。
- 动态代理机制会将这个方法调用拦截下来,转交给
MapperProxy的invoke方法来处理。
4. 执行SQL(代理对象内部)
MapperProxy的invoke方法是真正的执行者,它会:
- 获取
SqlSession:从数据库连接池中获取一个数据库连接,并包装成SqlSession对象。 - 定位
MappedStatement:根据你调用的方法(com.example.mapper.UserMapper.selectById),去第一步加载好的Configuration对象中找到对应的MappedStatement。 - 执行数据库操作:
MappedStatement中包含了所有需要的信息(SQL语句、参数类型、返回类型等)。MapperProxy会调用SqlSession中相应的方法(如selectOne,selectList,insert等),并把方法参数(例如1)和MappedStatement传递过去。 - 结果映射:
SqlSession执行完SQL后,从数据库获取到ResultSet。MyBatis会根据MappedStatement中定义的返回类型(resultType或resultMap),自动将ResultSet中的数据映射成Java对象(例如一个User对象或List<User>)。 - 返回结果:将映射好的Java对象返回给调用方。
整个过程对我们开发者是完全透明的,我们感觉就像在调用一个普通的Java接口方法一样。
二、为什么不需要我们手动编写实现类?
理解了上面的工作原理,这个问题就迎刃而解了。我们不需要实现类,是因为:
MyBatis已经自动生成了:MyBatis通过动态代理技术,在运行时为我们动态创建了一个实现了接口所有方法的代理类。这个代理类就是“实现类”,只是它存在于内存中,而不是一个
.java文件。实现类的代码是重复且模板化的:如果让我们自己写实现类,代码会是什么样的?无非就是:
- 获取
SqlSession - 拼接
statementId - 调用
sqlSession.selectOne(...) - 处理异常
- 关闭
SqlSession - ...
这些代码对于每个DAO方法都大同小异,充满了大量的样板代码(Boilerplate Code)。MyBatis把这些重复的工作都封装在了代理对象内部,极大地解放了开发者。
- 获取
实现了关注点分离(Separation of Concerns):
- 接口(Interface):只负责定义数据访问的契约,即有哪些操作,参数是什么,返回什么。
- XML或注解:只负责定义SQL以及如何映射结果。
- MyBatis框架:负责将两者粘合起来,处理所有JDBC的底层细节、事务管理、连接池等。
开发者只需要关注业务逻辑和SQL本身,而不需要关心底层的实现细节,代码更加清晰、简洁,易于维护。
符合面向接口编程的最佳实践:
业务层(Service)依赖的是DAO接口,而不是具体的实现。这使得系统耦合度更低,也更容易进行单元测试(例如,可以用一个Mock实现来替换掉MyBatis的代理对象)。
总结与比喻
你可以把Mapper接口想象成一家餐厅的菜单(Menu):
- 你(开发者) 是顾客。
- Mapper接口 是菜单,上面写了有哪些菜(方法),需要什么原料(参数)。
- XML/注解里的SQL 是菜谱,详细说明了每道菜怎么做。
- MyBatis的动态代理 是一个神通广大的服务员。
你只需要看着菜单点菜(调用接口方法),服务员(代理对象)就会自动拿着你的点单(方法调用信息)和菜谱(SQL),去后厨(数据库)把菜做好(执行SQL并映射结果),然后端到你面前(返回Java对象)。
你完全不需要关心服务员是怎么跟后厨沟通的,也不需要自己去后厨做菜,这就是MyBatis Mapper接口设计的优雅之处。