超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?

MyBatis系列相关相关文章
究竟FactoryBean是什么?深入理解Spring的工厂神器
超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?
Mybatis与Spring结合深探——MapperFactoryBean的奥秘
Mybatis-Spring整合原理:MapperFactoryBean和MapperScannerConfigurer的区别及源码剖析
源码透析MapperScannerRegistrar和MapperScannerConfigurer的区别及作用

前言

在这里插入图片描述

  • 提到MyBatis,很多人可能已经使用过,MyBatis中的mapper接口实际上并没有对应的实现类,它的功能通过一个对应的xml配置文件来实现。这意味着当我们调用一个mapper接口时,我们实际上是在执行xml文件中定义的SQL语句来操作数据。
  • 那么Mybatis的mapper为啥只有接口没有实现类,它却能工作?答案很简单,动态代理,但是要真正理解这个动态代理的整个过程,还是有点费劲的,没事,接下来我们一步步解析。

Mybatis dao层两种实现方式的对比

我们先把刚开始学习 MyBatis 的两种开发方式都回顾一下,虽然我们说回头都是用 Mapper 接口动态代理开发,但原始 Dao 开发的方式也不要忘记,这种方式在以后的开发中可能还是用得上的。另外,通过对比我们自己实现dao层接口以及mybatis动态代理可以更加直观的展现出mybatis动态代理替我们所做的工作,有利于我们理解动态代理的过程。

原始Dao开发

DepartmentDao接口:

public interface DepartmentDao {
    
    List<Department> findAll();
    
    Department findById(String id);
}

DepartmentDaoImpl:

  • 注意这里的关键代码 sqlSession.selectList("com.linkedbear.mybatis.mapper.DepartmentMapper.findAll"),需要我们自己手动调用SqlSession里面的方法,基于动态代理的方式最后的目标也是成功的调用到这里。

  • 注意:如果是添加,更新或者删除操作的话需要在方法中增加事务的提交。

public class DepartmentDaoImpl implements DepartmentDao {
    
    private SqlSessionFactory sqlSessionFactory;
    
    public DepartmentDaoImpl(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }
    
    @Override
    public List<Department> findAll() {
    	//使用了 try-with-resource 的方式,可以省略 sqlSession.close();的代码。
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            return sqlSession.selectList("com.linkedbear.mybatis.mapper.DepartmentMapper.findAll");
        }
    }
    
    @Override
    public Department findById(String id) {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            return sqlSession.selectOne("com.linkedbear.mybatis.mapper.DepartmentMapper.findById", id);
        }
    }
}

departmentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="departmentMapper">
    <select id="findAll" resultType="com.linkedbear.mybatis.entity.Department">
        select * from tbl_department
    </select>

    <select id="findById" parameterType="string" resultType="com.linkedbear.mybatis.entity.Department">
        select * from tbl_department where id = #{id}
    </select>
</mapper>

MyBatisApplication 测试运行

public class MyBatisApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        
        DepartmentDao departmentDao = new DepartmentDaoImpl(sqlSessionFactory);
        List<Department> departmentList = departmentDao.findAll();
        departmentList.forEach(System.out::println);
    }
}

原始Dao开发的弊端

@Override
public List<Department> findAll() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        return sqlSession.selectList("com.linkedbear.mybatis.mapper.DepartmentMapper.findAll");
    }
}

@Override
public Department findById(String id) {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        return sqlSession.selectOne("com.linkedbear.mybatis.mapper.DepartmentMapper.findById", id);
    }
}

从上面的编码中,我们会发现,接口中存在好多重复代码:

可以发现,两个方法的方法名不同、参数列表不同,调用的 mapper 不同,返回值不同,其余的几乎完全相同!我们也知道,更好地优化方案是使用 Mapper 动态代理的方式。所以下面我们再接下来重点讲解使用 Mapper 动态代理的方式开发 Dao 层及其原理。

基于Mapper动态代理的开发方式

使用 Mapper 动态代理的方式开发,需要满足以下几个规范:

  • mapper.xml 中的 namespace 与 Mapper 接口的全限定名完全相同
  • mapper.xml 中定义的 statement ,其 id 与 Mapper 接口的方法名一致
  • Mapper 接口方法的方法参数类型,与 mapper.xml 中定义的 statement 的 parameterType 类型一致
  • Mapper 接口方法的返回值类型,与 mapper.xml 中定义的 statement 的 resultType 类型相同

使用动态代理的话Dao层的接口声明完成以后只需要在使用的时候通过SqlSession对象的getMapper方法获取对应Dao接口的代理对象,关键代码如下:

获取到dao层的代理对象以后通过代理对象调用查询方法就可以实现查询所有部门列表的功能。

public class MyBatisApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        
        // 获取Mapper接口的代理
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println(department);
    }
}

Mybatis动态代理实现方式的原理解析

Mybatis的动态代理工作原理概括步骤如下:

  1. Mapper接口与XML的关联
    • Mybatis初始化时,会解析XML配置文件,将里面定义的SQL语句与Mapper接口的方法建立映射关系,并保存在配置对象中。
  2. 动态代理的创建
    • 当我们调用SqlSession.getMapper()方法时,Mybatis使用Java动态代理机制,为Mapper接口创建代理对象。
    • 代理对象的创建,主要通过MapperProxyFactory类来完成。
  3. 调用代理对象的方法时的内部处理
    • 当调用Mapper接口中的方法时,实际上调用的是代理对象的invoke方法。
    • invoke方法中,Mybatis使用之前的映射关系,找到与方法对应的SQL语句。
  4. SQL语句的执行
    • 找到SQL语句后,Mybatis会调用底层的执行器(Executor)来执行SQL语句,并完成参数的绑定,查询,以及结果返回。

动态代理调用链路解析

注意,这里先从我们的使用开始,讲解它是如何被调用的,后面再解析动态代理类的接口的注册

动态代理中最重要的类:Configuration、SqlSession、MapperRegistry、MapperProxyFactory、MapperProxy、MapperMethod,下面开始从入口方法到调用结束的过程分析。

先给出链路调用结果

getMapper方法的大致调用逻辑链是:

SqlSession#getMapper()-->DeaultSqlSession#getMapper——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()-->MapperProxy#invoke-->MapperMethod#execute

1、调用方法的开始:session.getMapper

UserDao mapper = session.getMapper(UserDao.class); //因为SqlSesseion为接口,所以我们通过Debug方式发现这里使用的实现类为DefaultSqlSession。

2、DeaultSqlSession的getMapper

找到DeaultSqlSession中的getMapper方法,发现这里没有做其他的动作,只是将工作继续抛到了Configuration类中,Configuration为类不是接口,可以直接进入该类的getMapper方法中

@Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

3、Configuration的getMapper

找到Configuration类的getMapper方法,这里也是将工作继续交到MapperRegistry的getMapper的方法中,所以我们继续向下进行。

MapperRegistry还有一个方法是public <T> void addMapper(Class<T> type) 后面再进行解析

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

4、MapperRegistry的getMapper

找到MapperRegistry的getMapper的方法,看到这里发现和以前不一样了,通过MapperProxyFactory的命名方式我们知道这里将通过这个工厂生成我们所关注的MapperProxy的代理类,然后我们通过mapperProxyFactory.newInstance(sqlSession);进入MapperProxyFactory的newInstance方法中

knownMappers注意这个:后面会解析这个knownMappers 是怎么来的、如何使用的。

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
	 //根据Class对象获取创建动态代理的工厂对象MapperProxyFactory
    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);
    }
  }

5、MapperProxyFactory的newIntance

找到MapperProxyFactory的newIntance方法,通过参数类型SqlSession可以得知,上面的调用先进入第二个newInstance方法中并创建我们所需要重点关注的MapperProxy对象,第二个方法中再调用第一个newInstance方法并将MapperProxy对象传入进去,根据该对象创建代理类并返回。这里已经得到需要的代理类了,但是我们的代理类所做的工作还得继续向下看MapperProxy类。

protected T newInstance(MapperProxy<T> mapperProxy) {
 	//这里使用JDK动态代理,通过Proxy.newProxyInstance生成动态代理类
    // newProxyInstance的参数:类加载器、接口类、InvocationHandler接口实现类
    // 动态代理可以将所有接口的调用重定向到调用处理器InvocationHandler,调用它的invoke方法
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

6、MapperProxy的invoke

找到MapperProxy类,发现其确实实现了JDK动态代理必须实现的接口InvocationHandler,所以我们重点关注invoke()方法,这里看到在invoke方法里先获取MapperMethod类,然后调用mapperMethod.execute(),所以我们继续查看MapperMethod类的execute方法。

public class MapperProxy<T> implements InvocationHandler, Serializable {

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    //如果调用的是Object类中定义的方法,直接通过反射调用即可
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
     //调用XxxMapper接口自定义的方法,进行代理
    //首先将当前被调用的方法Method构造成一个MapperMethod对象,然后掉用其execute方法真正的开始执行。
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

}

7、MapperMethod的execute

找到类MapperMethod类的execute方法,发现execute中通过调用本类中的其他方法获取并封装返回结果,我们来看一下MapperMethod整个类。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else 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());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

MapperMethod类是整个代理机制的核心类,对SqlSession中的操作进行了封装使用。 该类里有两个内部类SqlCommand和MethodSignature。 SqlCommand用来封装CRUD操作,也就是我们在xml中配置的操作的节点。每个节点都会生成一个MappedStatement类。MethodSignature用来封装方法的参数以及返回类型,在execute的方法中我们发现在这里又回到了SqlSession中的接口调用,和我们自己实现UerDao接口的方式中直接用SqlSession对象调用DefaultSqlSession的实现类的方法是一样的,经过一大圈的代理又回到了原地,这就是整个动态代理的实现过程了。

sqlSession.selectList("com.linkedbear.mybatis.mapper.DepartmentMapper.findAll");

回忆一下上面的解析过程是不是就是一开始给出的链路调用流程

getMapper方法的大致调用逻辑链是: SqlSession#getMapper() ——> Configuration#getMapper() ——> MapperRegistry#getMapper() ——> MapperProxyFactory#newInstance() ——> Proxy#newProxyInstance()–>MapperProxy#invoke–>MapperMethod#execute

还有一点我们需要注意:我们通过SqlSession的getMapper方法获得接口代理来进行CRUD操作,其底层还是依靠的是SqlSession的使用方法

动态代理类的接口注册/生成

刚刚我先讲解了动态代理调用链路是怎么样的,但是刚刚上面步骤3、4中涉及的两个点,我这里再进行全面讲解:

Configuration中两个重要方法getMapper()和addMapper()–>实际实现是MapperRegistry,刚刚讲解了getMapper()的链路流程,接下来讲解addMapper()

  • getMapper(): 用于创建接口的动态类
  • addMapper(): mybatis在解析配置文件时,会将需要生成动态代理类的接口注册到其中
public class MyBatisApplication {
    
    public static void main(String[] args) throws Exception {
        InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        
        // 获取Mapper接口的代理
        DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
        Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        System.out.println(department);
    }
}

在之前的实例中,我们通过调用sqlSession.getMapper()方法获得了DepartmentMapper接口的一个实例。实际上,通过这个方法我们得到的是DepartmentMapper接口的一个动态代理实现,然后我们可以借助这个动态代理实现来调用方法。在揭秘这些动态代理是如何创建出来的之前, 让我们先来审视一下SqlSessionFactory工厂的建立过程,以及它是如何处理相关的配置如mybatis-config文件,以及它是如何加载映射文件的。

先给出链路调用结果

new SqlSessionFactoryBuilder().build(xml)-->XMLConfigBuilder#parse-->>XMLConfigBuilder#parseConfiguration--->XMLConfigBuilder#mapperElement-->XMLMapperBuilder#mapperParser.parse()-->XMLMapperBuilder#configurationElement-->XMLMapperBuilder#bindMapperForNamespace-->Configuration#MapperRegistry#addMappper()

1、SqlSessionFactoryBuilder().build全局配置文件解析

private static SqlSessionFactory sqlSessionFactory;
static {
    try {
        sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream("mybatis-config.xml"));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

我们使用new SqlSessionFactoryBuilder().build()的方式创建SqlSessionFactory工厂,走进build方法

 public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

2、XMLConfigBuilder#parse–parseConfiguration

对于mybatis的全局配置文件的解析,相关解析代码位于XMLConfigBuilder的parse()方法中:

 public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //解析全局配置文件
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      //属性解析propertiesElement
      propertiesElement(root.evalNode("properties"));
      //加载settings节点settingsAsProperties
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //加载自定义VFS loadCustomVfs
      loadCustomVfs(settings);
      //解析类型别名typeAliasesElement
      typeAliasesElement(root.evalNode("typeAliases"));
      //加载插件pluginElement
      pluginElement(root.evalNode("plugins"));
      //加载对象工厂objectFactoryElement
      objectFactoryElement(root.evalNode("objectFactory"));
      //创建对象包装器工厂objectWrapperFactoryElement
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //加载反射工厂reflectorFactoryElement
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      //元素设置
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      //加载环境配置environmentsElement
      environmentsElement(root.evalNode("environments"));
      //数据库厂商标识加载databaseIdProviderElement
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      //加载类型处理器typeHandlerElement
      typeHandlerElement(root.evalNode("typeHandlers"));
      //加载mapper文件mapperElement
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

从parseConfiguration方法的源代码中很容易就可以看出它对mybatis全局配置文件中各个元素属性的解析。当然最终解析后返回一个Configuration对象,Configuration是一个很重要的类,它包含了Mybatis的所有配置信息,它是通过XMLConfigBuilder去构建的,Mybatis通过XMLConfigBuilder读取mybatis-config.xml中配置的信息,然后将这些信息保存到Configuration中

4、映射器Mapper文件的解析:XMLConfigBuilder#mapperElement

动态代理类的接口注册/生成,就是由这部分实现的

//解析mapper映射器文件
mapperElement(root.evalNode("mappers"));

该方法是对全局配置文件中mappers属性的解析,走进去:

private void mapperElement(XNode parent) throws Exception {
   if (parent != null) {
     for (XNode child : parent.getChildren()) {
       // 如果要同时使用package自动扫描和通过mapper明确指定要加载的mapper,一定要确保package自动扫描的范围不包含明确指定的mapper,否则在通过package扫描的interface的时候,尝试加载对应xml文件的loadXmlResource()的逻辑中出现判重出错,报org.apache.ibatis.binding.BindingException异常,即使xml文件中包含的内容和mapper接口中包含的语句不重复也会出错,包括加载mapper接口时自动加载的xml mapper也一样会出错。
       if ("package".equals(child.getName())) {
         String mapperPackage = child.getStringAttribute("name");
         configuration.addMappers(mapperPackage);
       } else {
         String resource = child.getStringAttribute("resource");
         String url = child.getStringAttribute("url");
         String mapperClass = child.getStringAttribute("class");
         if (resource != null && url == null && mapperClass == null) {
           ErrorContext.instance().resource(resource);
           InputStream inputStream = Resources.getResourceAsStream(resource);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            //对Mapper映射器文件进行解析
           mapperParser.parse();
         } else if (resource == null && url != null && mapperClass == null) {
           ErrorContext.instance().resource(url);
           InputStream inputStream = Resources.getUrlAsStream(url);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          //对Mapper映射器文件进行解析
           mapperParser.parse();
         } else if (resource == null && url == null && mapperClass != null) {
           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.");
         }
       }
     }
   }
}
 

XMLMapperBuilder和刚刚的XMLConfigBuilder是不是看起来很像,一个是解析configuration,一个是解析mapper

Mybatis提供了两类配置Mapper的方法,第一类是使用package自动搜索的模式,这样指定package下所有接口都会被注册为mapper,也是在Spring中比较常用的方式,例如:

<mappers>
  <package name="org.itstack.demo"/>
</mappers>

另外一类是明确指定Mapper,这又可以通过resource、url或者class进行细分,例如;

<mappers>
    <mapper resource="mapper/User_Mapper.xml"/>
    <mapper class=""/>
    <mapper url=""/>
</mappers>

5、XMLMapperBuilder#parse

这里重点关注两个方法:configurationElement和bindMapperForNamespace

mapperParser.parse()方法就是XMLMapperBuilder对Mapper映射器文件进行解析,可与XMLConfigBuilder进行类比

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper")); //解析映射文件的根节点mapper元素
      configuration.addLoadedResource(resource);  
      bindMapperForNamespace(); //重点方法,这个方法内部会根据namespace属性值,生成动态代理类
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }


6、XMLMapperBuilder#configurationElement(XNode context)

该方法主要用于将mapper文件中的元素信息,比如insertselect这等信息解析到MappedStatement对象,并保存到Configuration类中的mappedStatements属性中,以便于后续动态代理类执行CRUD操作时能够获取真正的Sql语句信息

  private void configurationElement(XNode context) {
        try {
            String namespace = context.getStringAttribute("namespace");
            if (namespace != null && !namespace.isEmpty()) {
                this.builderAssistant.setCurrentNamespace(namespace);
                this.cacheRefElement(context.evalNode("cache-ref"));
                this.cacheElement(context.evalNode("cache"));
                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                this.sqlElement(context.evalNodes("/mapper/sql"));
                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            } else {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
        } catch (Exception var3) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + this.resource + "'. Cause: " + var3, var3);
        }
    }

XMLMapperBuilder#buildStatementFromContext方法就用于解析insert、select这类元素信息,并将其封装成MappedStatement对象,具体的实现细节这里就不细说了。

7、XMLMapperBuilder#bindMapperForNamespace()核心方法

该方法是核心方法,它会根据mapper文件中的namespace属性值,为接口生成动态代理类,这就来到了我们的主题内容——动态代理类是如何生成的。

bindMapperForNamespace方法源码如下所示:

 private void bindMapperForNamespace() {
    //获取mapper元素的namespace属性值
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 获取namespace属性值对应的Class对象
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //如果没有这个类,则直接忽略,这是因为namespace属性值只需要唯一即可,并不一定对应一个XXXMapper接口
        //没有XXXMapper接口的时候,我们可以直接使用SqlSession来进行增删改查
      }
      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属性值有对应的Java类,调用Configuration的addMapper方法,将其添加到MapperRegistry中
          configuration.addMapper(boundType);
        }
      }
    }
  }

这里提到了Configuration的addMapper方法,实际上Configuration类里面通过MapperRegistry对象维护了所有要生成动态代理类的XxxMapper接口信息,可见Configuration类确实是相当重要一类

public class Configuration {
    ...
    protected MapperRegistry mapperRegistry = new MapperRegistry(this);
    ...
    public <T> void addMapper(Class<T> type) {
      mapperRegistry.addMapper(type);
    }
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      return mapperRegistry.getMapper(type, sqlSession);
    }
    ...
}

8、MapperRegistry#addMappper()

Configuration将addMapper方法委托给MapperRegistry的addMapper进行的,源码如下:

  public <T> void addMapper(Class<T> type) {
    // 这个class必须是一个接口,因为是使用JDK动态代理,所以需要是接口,否则不会针对其生成动态代理
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 生成一个MapperProxyFactory,用于之后生成动态代理类
        knownMappers.put(type, new MapperProxyFactory<>(type));
        //以下代码片段用于解析我们定义的XxxMapper接口里面使用的注解,这主要是处理不使用xml映射文件的情况
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

MapperRegistry内部维护一个映射关系,每个接口对应一个MapperProxyFactory(生成动态代理工厂类)

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

这样便于在后面调用MapperRegistry的getMapper()时,直接从Map中获取某个接口对应的动态代理工厂类,然后再利用工厂类针对其接口生成真正的动态代理类。

总结

Mybatis的动态代理工作原理概括步骤如下:

  1. Mapper接口与XML的关联也就是我们刚刚上面解析的动态代理类的接口注册/生成
    • Mybatis初始化时,会解析XML配置文件,将里面定义的SQL语句与Mapper接口的方法建立映射关系,并保存在配置对象中。
  2. 动态代理的创建2、3、4也就是我们刚刚上面解析的动态代理实际调用的链路流程
    • 当我们调用SqlSession.getMapper()方法时,Mybatis使用Java动态代理机制,为Mapper接口创建代理对象。
    • 代理对象的创建,主要通过MapperProxyFactory类来完成。
  3. 调用代理对象的方法时的内部处理
    • 当调用Mapper接口中的方法时,实际上调用的是代理对象的invoke方法。
    • invoke方法中,Mybatis使用之前的映射关系,找到与方法对应的SQL语句。
  4. SQL语句的执行
    • 找到SQL语句后,Mybatis会调用底层的执行器(Executor)来执行SQL语句,并完成参数的绑定,查询,以及结果返回。

参考文章:
https://www.cnblogs.com/hopeofthevillage/p/11384848.html

  • 29
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值