查看原文
其他

为啥mybatis的mapper只有接口没有实现类,但它却能工作?

吕一明 MarkerHub 2022-11-04

说起mybatis,大伙应该都用过,有些人甚至底层源码都看过了。在mybatis中,mapper接口是没有实现类的,取而代之的是一个xml文件。也就是说我们调用mapper接口,其实是使用了mapper.xml中定义sql完成数据操作。

大家有没想过,为什么mapper没有实现类,它是如何和xml关联起来的?

一个简单的例子

ok,别急,现在我们已经抛出问题,现在我们从demo开始,再结合我们所拥有的知识点出发,一一剖析整个过程。

先来搞个简单的查询:

UserMapper一个接口:

  1. User findById(Long id);

userMapper.xml的sql语句

  1. <mappernamespace="com.lfq.UserMapper">

  2. <selectid="findById"resultType="com.lfq.User">

  3. select * * from user where id = #{id}

  4. </select>

  5. </mapper>

猜想

我们知道,接口是不直接被初始化的,但是可以被实现,所以new对象的时候是初始化实现类,然后接口再引用该对象。那么调用接口的方法实际上就是调用被引用对象的方法,也就是实现类的方法。

那么,UserMapper.findById被调用时候,不禁有这两个疑问?

  • 被引用的对象是谁呢?

  • 接口被调用时候发生了什么?

我们先来回答第二个问题,既然找不到实现类,UserMapper有没可能被代理起来呢,findById方法调用时候,我们找到代理对象来执行就行了。

代理有两种方式:

  • 静态代理

  • 动态代理

而静态代理基本是不可能的了,静态代理需要对UserMapper所有的方法进行重写。那么只能是动态代理,动态代理接口的所有方法,每次接口被调用,就会进入动态代理对象的invoke方法,然后加载xml中的sql完成操作数据库,再返回结果。

再然后说到动态代理,常见的方式有以下2种方式:

  • JDK动态代理:

  • 利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

  • CGlib动态代理:

  • 利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

所以,动态代理代理还是对象类,那么我们只有接口,不能new,哪来的对象呢?别忘了,我们还有反射机制,我们是不是可以通过反射给接口生成对象,还记得Class.*forName*吗。

综合上面的猜想:

第一步:通过反射机制给接口生成对象

第二步:动态代理反射对象,这样接口被调用,就会触发动态代理

嗯,好像有点道理,我果然是个天才!

知识点:动态代理

动态代理有几种实现方式,这里我们就先讲JDK动态代理,使用步骤如下:

  • 新建一个接口

  • 创建代理类,实现java.lang.reflect.InvocationHandler接口

  • 接口测试

接口我们就用UserMapper,我们来写个代理对象。

  1. publicclassTest{

  2. // 接口

  3. staticinterfaceSubject{

  4. void sayHi();

  5. void sayHello();

  6. }


  7. // 默认实现类(我们可以反射生成)

  8. staticclassSubjectImplimplementsSubject{

  9. @Override

  10. publicvoid sayHi() {

  11. System.out.println("hi");

  12. }

  13. @Override

  14. publicvoid sayHello() {

  15. System.out.println("hello");

  16. }

  17. }


  18. // jkd动态代理

  19. // 原创:公众号:java思维导图

  20. staticclassProxyInvocationHandlerimplementsInvocationHandler{

  21. privateSubject target;

  22. publicProxyInvocationHandler(Subject target) {

  23. this.target=target;

  24. }


  25. @Override

  26. publicObject invoke(Object proxy, Method method, Object[] args) throwsThrowable{

  27. System.out.print("say:");

  28. return method.invoke(target, args);

  29. }

  30. }


  31. publicstaticvoid main(String[] args) {

  32. Subject subject=newSubjectImpl();


  33. Subject subjectProxy=(Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), newProxyInvocationHandler(subject));


  34. subjectProxy.sayHi();

  35. subjectProxy.sayHello();

  36. }

  37. }

ok,一个简单的动态代理例子送给你们,上面代码中关键生成动态代理对象的关键代码是:

  1. Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);

  • loader: 用哪个类加载器去加载代理对象

  • interfaces:动态代理类需要实现的接口

  • h:动态代理方法在执行时,会调用h里面的invoke方法去执行

源码分析

好啦,上面该做的准备已经都准备好了,我们对mybatis的这个mapper接口大概都有些思路了,下面我们去正式验证一下,那么肯定就要去看源码了。我们只是去验证上面的mapper接口问题,所以不需要去看全部的代码,当然如果你看整个流程下来的话,会更加清晰。

论证猜想,我们可以采用结果导向的方式去看源码,从获取mapper那里开始看,也就是

  1. SqlSession sqlSession = sqlSessionFactory.openSession();

  2. UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

主要从sqlSession.getMapper(UserMapper.class);这里开始,先看整个UserMapper是不是被动态代理的。ok,我们进入代码中:

  • org.apache.ibatis.session.SqlSessionManager#getMapper

  1. @Override

  2. public<T> T getMapper(Class<T> type) {

  3. return getConfiguration().getMapper(type, this);

  4. }

继续走到Configuration方法里,Configuration是mybatis所有配置相关的地方,mybatis-cfg.xml、UserMapper.xml等文件都会被预先加载到Configuration里。

  • org.apache.ibatis.session.Configuration#getMapper

  1. public<T> T getMapper(Class<T> type, SqlSession sqlSession) {

  2. return mapperRegistry.getMapper(type, sqlSession);

  3. }

这时候,我们发现Configuration里面出现了一个mapperRegistry,翻译过来可以理解为mapper的注册器,其实在加载UserMapper.xml的时候,我们就需要在mapperRegistry里面进行注册,所有,我们可以从这里面进行获取。继续走~

  • org.apache.ibatis.binding.MapperRegistry#getMapper

  1. public<T> T getMapper(Class<T> type, SqlSession sqlSession) {


  2. //获取mapper代理工厂

  3. // 原创:公众号:java思维导图

  4. finalMapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);


  5. if(mapperProxyFactory == null) {

  6. thrownewBindingException("Type "+ type + " is not known to the MapperRegistry.");

  7. }

  8. try{

  9. return mapperProxyFactory.newInstance(sqlSession);

  10. } catch(Exception e) {

  11. thrownewBindingException("Error getting mapper instance. Cause: "+ e, e);

  12. }

  13. }

ok,这里分为了两步:

  • knownMappers.get(type);

  • 获取已知的加载过的mapper中获取出mapper代理工厂

  • mapperProxyFactory.newInstance(sqlSession);

  • 代理工厂生成动态代理返回

我们一步步分析,别急,knownMappers其实是个map,根据userMapper.class获取MapperProxyFactory:

  1. Map<Class<?>, MapperProxyFactory<?>> knownMappers

所以knownMappers必然是源码前面的步骤中set进去的。我们先找找,到底是哪里set进去的。找呀找,找到这里:

  • org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace

  1. privatevoid bindMapperForNamespace() {

  2. Stringnamespace= builderAssistant.getCurrentNamespace();

  3. if(namespace!= null) {

  4. Class<?> boundType = null;

  5. try{

  6. //反射生成namespace的对象

  7. // 原创:公众号:java思维导图

  8. boundType = Resources.classForName(namespace);

  9. } catch(ClassNotFoundException e) {

  10. //ignore, bound type is not required

  11. }

  12. if(boundType != null) {

  13. if(!configuration.hasMapper(boundType)) {

  14. // Spring may not know the real resource name so we set a flag

  15. // to prevent loading again this resource from the mapper interface

  16. // look at MapperAnnotationBuilder#loadXmlResource

  17. configuration.addLoadedResource("namespace:"+ namespace);// namespace:com.lfq.UserMapper

  18. configuration.addMapper(boundType);

  19. }

  20. }

  21. }

  22. }

我们看这个boundType,是通过Resources.classForName(namespace);生成的class,Resources.classForName底层其实就是调用Class.forName生成的反射对象,而参数是namespace,namespacne不正是com.lfq.UserMapper嘛:

  1. <mappernamespace="com.lfq.UserMapper">

完美!!Class.forName(com.lfq.UserMapper)生成反射对象。论证了我们第一点猜想。生成的boundType在被configuration.addMapper(boundType);所以就有了:

  • org.apache.ibatis.session.Configuration#addMapper

  1. public<T> void addMapper(Class<T> type) {

  2. mapperRegistry.addMapper(type);

  3. }

  • org.apache.ibatis.binding.MapperRegistry#addMapper

  1. public<T> void addMapper(Class<T> type) {

  2. if(type.isInterface()) {

  3. if(hasMapper(type)) {

  4. thrownewBindingException("Type "+ type + " is already known to the MapperRegistry.");

  5. }

  6. boolean loadCompleted = false;

  7. try{


  8. /**

  9. * TODO 将mapper接口包装成mapper代理

  10. * 原创:公众号:java思维导图

  11. */

  12. knownMappers.put(type, newMapperProxyFactory<>(type));


  13. //解析接口上的注解或者加载mapper配置文件生成mappedStatement(com/lfq/UserMapper.java)

  14. MapperAnnotationBuilder parser = newMapperAnnotationBuilder(config, type);

  15. // 开始解析

  16. parser.parse();


  17. // 加载完成标记

  18. loadCompleted = true;

  19. } finally{

  20. if(!loadCompleted) {

  21. knownMappers.remove(type);

  22. }

  23. }

  24. }

  25. }

上面的代码,就给我论证了这个MapperProxyFactory是哪里来的,MapperProxyFactory里面其实就一个参数mapperInterface,就是反射生成的这个对象。ok,第一个猜想已经论证完毕,接着我们看刚才说到的第二点:动态代理。

回到mapperProxyFactory.newInstance(sqlSession); 这个MapperProxyFactory就是我们刚刚new出来的,我们打开newInstance方法看看:

  • org.apache.ibatis.binding.MapperProxyFactory#newInstance(MapperProxy)

  1. public T newInstance(SqlSession sqlSession) {


  2. //MapperProxy为InvocationHandler的实现类

  3. // 原创:公众号:java思维导图

  4. finalMapperProxy<T> mapperProxy = newMapperProxy<>(sqlSession, mapperInterface, methodCache);


  5. //真实生成代理

  6. return newInstance(mapperProxy);

  7. }

  8. protected T newInstance(MapperProxy<T> mapperProxy) {

  9. //采用JDK自带的Proxy代理模式生成

  10. return(T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), newClass[] { mapperInterface }, mapperProxy);

  11. }

终于在里面看到Proxy.newProxyInstance了,好激动呀。又论证了第二点的动态代理猜想 上面代码中,首先把sqlSession, mapperInterface, methodCache三个参数封装到MapperProxy中,而MapperProxy是实现了InvocationHandler接口的方法,因此动态代理被调用的时候,会进入到MapperProxy的invoke方法中。

sqlSession是必须的,因为操作数据库需要用到sqlsession。具体invoke里面的内容,我们不做多分析啦,刚兴趣的同学自己去看下源码哈。可以猜想:找到对应的sql,然后执行sql操作,哈哈哈。

总结

好啦,今天的内容就到这里啦~

如果你喜欢我的文章,欢迎关注我的公众号:MarkerHub,给我点个在看或者转发一下,万分感谢哈!

(完)


【推荐阅读】

终于有人把 Docker 讲清楚了,万字详解!

将20M文件从30秒压缩到1秒,我是如何做到的?

Spring MVC 到 Spring BOOT的简化之路


好文!必须点赞

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存