• Mybatis的mapper接口实现原理



    1 概述

    在这里插入图片描述

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

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

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

    一个简单的例子

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

    先来搞个简单的查询:

    UserMapper一个接口:

    User findById(Long id);
    
    • 1

    userMapper.xml的sql语句

    <mapper namespace="com.oldlu.UserMapper">
     
    <select id="findById" resultType="com.oldlu.User">
        select *  from user where id = #{id}
    select>
     
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

    - 被引用的对象是谁呢?
    - 接口被调用时候发生了什么?

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

    代理有两种方式:

    • 静态代理
    • 动态代理

    设计模式之代理模式的懂静态代理和动态代理:https://blog.csdn.net/ZGL_cyy/article/details/132910893

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

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

    • JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
    • CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

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

    综合上面的猜想:

    1. 第一步:通过反射机制给接口生成对象
    2. 第二步:动态代理反射对象,这样接口被调用,就会触发动态代理

    2 动态代理和反射对象

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

    • 新建一个接口
    • 创建代理类,实现java.lang.reflect.InvocationHandler接口
    • 接口测试

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

    public class Test {
     
            // 接口
            static interface Subject {
                void sayHi();
     
                void sayHello();
     
            }
     
            // 默认实现类(我们可以反射生成)
            static class SubjectImpl implements Subject {
                @Override
                public void sayHi() {
                    System.out.println("hi");
                }
     
                @Override
                public void sayHello() {
                    System.out.println("hello");
                }
            }
     
     
            // jkd动态代理
            static class ProxyInvocationHandler implements InvocationHandler {
     
                private Subject target;
     
                public ProxyInvocationHandler(Subject target) {
                    this.target = target;
                }
     
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.print("say:");
                    return method.invoke(target, args);
                }
            }
     
            public static void main(String[] args) {
                Subject subject = new SubjectImpl();
                Subject subjectProxy = (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), new ProxyInvocationHandler(subject));
                subjectProxy.sayHi();
                subjectProxy.sayHello();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

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

    Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
    
    • 1
    • loader: 用哪个类加载器去加载代理对象
    • interfaces:动态代理类需要实现的接口
    • h:动态代理方法在执行时,会调用h里面的invoke方法去执行

    3 源码分析

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

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

    SqlSession sqlSession = sqlSessionFactory.openSession();
     
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    • 1
    • 2
    • 3

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

    • org.apache.ibatis.session.Configuration#getMapper
    @Override
    public <T> T getMapper(Class<T> type) {
    return getConfiguration().getMapper(type, this);
    }
    
    • 1
    • 2
    • 3
    • 4

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

    • org.apache.ibatis.session.Configuration#getMapper
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
    }
    
    • 1
    • 2
    • 3

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

    • org.apache.ibatis.binding.MapperRegistry#getMapper
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
     
            //获取mapper代理工厂
            final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
     
            if (mapperProxyFactory == null) {
                throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
            }
     
            try {
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception e) {
                throw new BindingException("Error getting mapper instance. Cause: " + e, e);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    ok,这里分为了两步:

    • knownMappers.get(type);
    • 获取已知的加载过的mapper中获取出mapper代理工厂
    • mapperProxyFactory.newInstance(sqlSession);
    • 代理工厂生成动态代理返回

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

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

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

    • org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace
    private void bindMapperForNamespace() {
            String namespace = builderAssistant.getCurrentNamespace();
            if (namespace != null) {
                Class<?> boundType = null;
                try {
                    //反射生成namespace的对象
                    boundType = Resources.classForName(namespace);
                } catch (ClassNotFoundException e) {
                    // ignore, bound type is not required
                }
     
                if (boundType != null) {
                    if (!configuration.hasMapper(boundType)) {
                        // Spring may not know the real resource name so we set a flag
                        // to prevent loading again this resource from the mapper interface
                        // look at MapperAnnotationBuilder#loadXmlResource
                        configuration.addLoadedResource("namespace:" + namespace);// namespace:com.lfq.UserMapper
                        configuration.addMapper(boundType);
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    <mapper namespace="com.lfq.UserMapper">
    
    • 1

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

    • org.apache.ibatis.session.Configuration#addMapper
    public <T> void addMapper(Class<T> type) {
            mapperRegistry.addMapper(type);
        }
    
    • 1
    • 2
    • 3
    • org.apache.ibatis.binding.MapperRegistry#addMapper
    public <T> void addMapper(Class<T> type) {
            if (type.isInterface()) {
                if (hasMapper(type)) {
                    throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
                }
                boolean loadCompleted = false;
                try {
    /**
     * TODO 将mapper接口包装成mapper代理
     * 原创:公众号:java思维导图
     */
                    knownMappers.put(type, new MapperProxyFactory<>(type));
    //解析接口上的注解或者加载mapper配置文件生成mappedStatement(com/lfq/UserMapper.java)
                    MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
    // 开始解析
                    parser.parse();
    // 加载完成标记
                    loadCompleted = true;
                } finally {
                    if (!loadCompleted) {
                        knownMappers.remove(type);
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

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

    • org.apache.ibatis.binding.MapperProxyFactory#newInstance(MapperProxy)
    public T newInstance(SqlSession sqlSession) {
            //MapperProxy为InvocationHandler的实现类
            final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
            //真实生成代理
            return newInstance(mapperProxy);
        }
     
        protected T newInstance(MapperProxy<T> mapperProxy) {
            //采用JDK自带的Proxy代理模式生成
            return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

    4 总结

    1. 通过 @MapperScan(“com.xxx.dao”) 并将它们的beanClass设置为 MapperFactoryBean
    2. MapperFactoryBean 实现了 spring 的 FactoryBean 接口,俗称工厂Bean
    3. 当我们通过 @Autowired 注入Dao接口时,返回的对象就是 MapperFactoryBean 这个工厂Bean中的 getObject() 方法对象
    4. getObject 是通过JDK动态代理,返回了一个Dao接口的代理对象 MapperProxy

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

  • 相关阅读:
    java遇到的一些集合相关知识
    利用拦截器加缓存完成接口防刷操作
    ClickHouse—函数汇总
    访谈:联合开发网站长,罚款50万之后的问答与建议
    【附安装包】3ds Max2023安装教程
    avalanche 少量tcp长连接持续构建HTTP请求测试
    MLX90640 开发 微型红外成像仪
    代码随想录训练营day49, 买卖股票的最佳时机I/II
    广东二级造价工程师《造价管理》真题解析
    GRU 关键词提取实例
  • 原文地址:https://blog.csdn.net/ZGL_cyy/article/details/132910959