美文网首页Java高开发MyBatis+SpringMVC+SpringBootspring+springmvc+mybatis(SSM)
面试官问:为什么mybatis的mapper没有实现类?底层是咋

面试官问:为什么mybatis的mapper没有实现类?底层是咋

作者: java高并发 | 来源:发表于2019-07-12 17:21 被阅读5次

    Java动态代理
    代理模式在GoF设计模式尤为突出,Spring AOP 就是代理模式的一个例子,而且它使用的也是JDK的动态代理实现。MyBatis同样在Mapper接口执行时也是使用这个,当你第一次使用Mybatis的Mapper接口时肯定和我一样非常惊讶,为什么主要定义接口,不需要实现,就可以使用了呢?

    说说JDK动态代理,主要是三点

    A:target-interface // 定义接口
    
    B:target-proxy implements InvocationHandler // 代理实现动态代理接口
    
    C:Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
    
    new Class[]{target-interface.class},
    
    new target-proxy()); // 生成代理对象
    

    只要完成以上三步,生成的代理对象调用接口的任何方法都会去调用 invoke 方法。其中 invoke 方法就是代理对象实现 InvocationHandler 的抽象方法。

    Mybatis DAO接口为什么不需要实现类的原理
    Mybatis 提供了 Mapper接口的代理对象,在执行 Mapper接口方法时,实际执行的是Mybatis的代理对象,代理对象在 invoke 方法内获取 Mapper接口类全名+方法全名 作为statement的ID,然后通过ID去Statement匹配注册的SQL,然后使用 SqlSession 执行这个 SQL。

    所以,这也解释了为什么Mybatis映射文件需要 namespace 和 id , 前者是类全名,后者是方法名。

    主要的类

    SqlSessionFactoryBean
    
    SqlSessionFactory
    
    XMLConfigBuilder
    
    Configuration
    
    MapperRegistry
    
    MapperProxyFactory
    
    MapperProxy
    
    MapperMethod
    
    SqlCommand
    
    MethodSignature
    

    从源码出发
    1)Spring 配置文件配置 SqlSessionFactoryBean

    2)SqlSessionFactoryBean 调用 buildSqlSessionFactory() 创建 SqlSessionFactory 时会调用

    3)XMLConfigBuilder.parse() ,目的就是构建 Configuration

    4)Configuration 主要存储 Mybatis 所有的配置信息,当然也会有Mapper代理对象

    5)XMLConfigBuilder.parseConfiguration 里面就注册了 Configuration 的部分配置,含 Mapper

    6)执行重点方法 XMLConfigBuilder 的 mapperElement

    private void mapperElement(XNode parent) throws Exception {
    -- 省略部分代码
    if (resource == null && url == null && mapperClass != null) {
    // mapperInterface 就是目标对象,Mybatis 将为他生成代理
    Class<?> mapperInterface = Resources.classForName(mapperClass);
    configuration.addMapper(mapperInterface);
    } else {
    throw new BuilderException("A mapper element may only specify a url,
    resource or class, but not more than one.");
    }
    -- 省略部分代码
    }
    7)继续跟踪 configuration.addMapper(mapperInterface);调用的是 MapperRegistry 的方法
    
    8)看看 MapperRegistry 的 addMapper 方法,很明显一个缓存式代码
    
    public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
    -- 省略部分代码
    try {
    // 终于看到代理的影子
    knownMappers.put(type, new MapperProxyFactory<T>(type));
    MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
    // 这里主要是把我们写的 SQL 语句加载到 Configuration 中,因为执行SQL时需要
    parser.parse();
    }
    -- 省略部分代码
    }
    9)MapperProxyFactory 就非常简单了,通过参数构建出代理对象,就如其名。
    
    // 使用JDK动态代理对象 Proxy 创建代理对象 mapperProxy
    protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
    new Class[] { mapperInterface },
    mapperProxy);
    }
    

    到这里就结束了Mapper接口是如何加载的,那么下面就正式进入到 Mapper 接口是如何被Mybatis代理的,从上面可以看出重点就是 MapperProxy 类,没错,想要被 Proxy调用,这个类必须实现 InvocationHandler接口,并且实现 invoke 方法,这样他才能在执行接口方法的时候会执行 invoke。那么既然有了 MapperProxy 对象,只要在这个对象的invoke方法里调用执行 SQL 语句就达到目的了,因此也就不需要接口实现类了。如下继续走第10步

    10)MapperProxy<T> implements InvocationHandler 同时实现 invoke 方法

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 如果不是接口,那么就调用方法本身,什么都没处理
    if (Object.class.equals(method.getDeclaringClass())) {
    try {
    return method.invoke(this, args);
    } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
    }
    }
    // 如果是接口,先缓存,后执行 SQL
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
    }
    

    11)MapperMethod 有两个重点,其一就是初始化,其二就是执行SQL,为什么说初始化很重要呢?因为初始化的时候需要通过 接口名全称+方法全称 去 Configuration 找我们之前加载的SQL,这也就是为什么接口方法定义和SQL的ID必须保持一致的原因;其二执行SQL。

    12)MapperMethod 初始化时实例 command 和 method

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 找到方法执行类型以及SQL注册的ID
    this.command = new SqlCommand(config, mapperInterface, method);
    // 方法的返回值映射
    this.method = new MethodSignature(config, mapperInterface, method);
    }
    13)MapperMethod 调用 execute 执行 SQL,代码很简单如下
    
    // command 对象包含了执行类型和执行SQL的ID,都是初始化时构建好
    switch (command.getType()) {
    case INSERT: {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.insert(command.getName(), param));
    break;
    }
    

    -- 省略部分代码

    case SELECT:
    if (method.returnsMany()) {
    result = executeForMany(sqlSession, args);
    } else if (method.returnsMap()) {
    result = executeForMap(sqlSession, args);
    } else if (method.returnsCursor()) {
    result = executeForCursor(sqlSession, args);
    } else {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
    }
    break;
    case FLUSH:
    result = sqlSession.flushStatements();
    break;
    default:
    throw new BindingException("Unknown execution method for: " + command.getName());
    }
    

    到这里就获取到运行结果 Result 了,可以直接返回,对于如何执行 SQL的,那就要跟踪 SqlSession 的实现类了,比如 DefaultSqlSession,里面会涉及到SQL参数的注入,结果集如何映射到返回实例,一级缓存和二级缓存以及分页处理等。

    欢迎工作一到五年的Java工程师朋友们加入JavaQQ群:219571750,群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

    相关文章

      网友评论

        本文标题:面试官问:为什么mybatis的mapper没有实现类?底层是咋

        本文链接:https://www.haomeiwen.com/subject/hguykctx.html