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

news2025/1/18 2:01:49

文章目录

  • 前言
  • Mybatis dao层两种实现方式的对比
    • 原始Dao开发
      • 原始Dao开发的弊端
    • 基于Mapper动态代理的开发方式
  • Mybatis动态代理实现方式的原理解析
    • 动态代理调用链路解析
      • 先给出链路调用结果
      • 1、调用方法的开始:session.getMapper
      • 2、DeaultSqlSession的getMapper
      • 3、Configuration的getMapper
      • 4、MapperRegistry的getMapper
      • 5、MapperProxyFactory的newIntance
      • 6、MapperProxy的invoke
      • 7、MapperMethod的execute
    • 动态代理类的接口注册/生成
      • 先给出链路调用结果
      • 1、SqlSessionFactoryBuilder().build全局配置文件解析
      • 2、XMLConfigBuilder#parse--parseConfiguration
      • 4、映射器Mapper文件的解析:XMLConfigBuilder#mapperElement
      • 5、XMLMapperBuilder#parse
      • 6、XMLMapperBuilder#configurationElement(XNode context)
      • 7、XMLMapperBuilder#bindMapperForNamespace()核心方法
      • 8、MapperRegistry#addMappper()
  • 总结

前言

在这里插入图片描述

  • 提到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

4、找到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(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析mapper映射器文件
      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 {
	String resource;
	.....
    resource = child.getStringAttribute("resource");
    String url = child.getStringAttribute("url");
    String mapperClass = child.getStringAttribute("class");
    XMLMapperBuilder mapperParser;
    InputStream inputStream;
    if (resource != null && url == null && mapperClass == null) {
        ErrorContext.instance().resource(resource);
        inputStream = Resources.getResourceAsStream(resource);
        mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
        mapperParser.parse();

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1280760.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb 带条件的分页查询

最终效果图 注意&#xff1a;没有带条件的时候 默认的是第一页数据 条件是组合的 sql->sql的动态变换 注意第二次查询的时候回显问题 就是填完条件后显示完当页数据ok 但是我点击第二页的时候条件还存在着 此时ListSerevlet不仅要拿到页码 页容量 还要拿到三个条件参数 封装…

Linux网络之连接跟踪 conntrack

一 Linux网络之连接跟踪 conntrack k8s 有关conntrack的分析 ① 什么是连接跟踪 netfilter连接跟踪 conntrack 详述 思考&#xff1a;连接跟踪模块会对哪些协议进行跟踪?TCP、UDP、ICMP、DCCP、SCTP、GRE ② 为什么需要连接跟踪 没有连接跟踪有很多问题是不好解决的&a…

掌握视频剪辑技巧:批量置入视频封面,提升视频品质

在当今数字化时代&#xff0c;视频已成为生活的重要组成部分。无论是观看电影、电视剧、综艺节目&#xff0c;还是分享个人生活、工作成果&#xff0c;视频都以其独特的魅力吸引着大众的视线。视频封面是视频内容的缩影&#xff0c;是观众对视频的第一印象。一个好的封面能吸引…

分享一个简单的基于C语言嵌入式GUI界面切换代码

目录 前言 一、数据类型 二、页面调度 三、页面显示 四、视频展示 前言 最近在用LVGL写一个简单的UI界面&#xff0c;需要进行几个页面的切换&#xff0c;所以就自己写了一个简单页面切换代码&#xff0c;方便进行页面切换&#xff0c;同时使UI代码结构更加清晰。这个结构…

基于Java SSM框架实现实现四六级英语报名系统项目【项目源码+论文说明】

基于java的SSM框架实现四六级英语报名系统演示 摘要 本论文主要论述了如何使用JAVA语言开发一个高校四六级报名管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作…

什么是跨站脚本攻击

跨站脚本攻击 1. 定义2. 跨站脚本攻击如何工作3. 跨站脚本攻击类型4. 如何防止跨站脚本攻击 1. 定义 跨站脚本攻击&#xff08;Cross-site Scripting&#xff0c;通常称为XSS&#xff09;&#xff0c;是一种典型的Web程序漏洞利用攻击&#xff0c;在线论坛、博客、留言板等共享…

leetCode 51.皇后 + 回溯算法 + 图解 + 笔记

按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。给你一个整数 n &#xff0c;返回所有不同的 n 皇后问题 的解决方案。每一种解法包…

Python读取栅格遥感影像并加以辐射校正后导出为Excel的一列数据

本文介绍基于Python语言中的gdal模块&#xff0c;读取一景.tif格式的栅格遥感影像文件&#xff0c;提取其中每一个像元的像素数值&#xff0c;对像素值加以计算&#xff08;辐射定标&#xff09;后&#xff0c;再以一列数据的形式将计算后的各像元像素数据保存在一个.csv格式文…

【力扣206】反转链表

【力扣206】反转链表 一.题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1 &#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2 &#xff1a; 输入&#xff1a;head [1,2] 输出&#x…

机器学习---pySpark代码开发

1、eclipse开发pySpark程序 在eclipse中开发pySpark程序&#xff0c;需要安装pydev插件。 1).eclipse安装python插件,安装完成后重启。 2). 在window--->preferences中找到python interpreter配置安装python的路径&#xff1a; 3).新建python项目&#xff1a; 2、pyCharm开…

Centos图形化界面封装OpenStack Centos镜像

目录 背景 环境 宿主机环境安装 创建与安装Centos7.8虚机 虚机设置 安全相关 安装ACPI服务 安装cloud-init 安装cloud-utils-growpart 停⽌虚拟机 删除个性化信息 模板化与压缩 登录与验证 背景 今天早上在Centos官网下载的CentOS-7-aarch64-GenericCloud-2003.…

3dMax拼图生成工具Puzzle2D使用教程

Puzzle2D for 3dsMax拼图生成工具使用教程 Puzzle2D简介&#xff1a; 2D拼图随机生成器&#xff08;英文&#xff1a;Puzzle2D&#xff09; &#xff0c;是一款由#沐风课堂#用MAXScript脚本语言开发的3dsMax建模小工具&#xff0c;可以随机创建2D可编辑样条线拼图图形。可批量…

解决vscode中html部分无法嵌套注释

不管是React项目还是Vue项目&#xff0c;相信你一定遇到过同样的问题&#xff0c;如果想要注释的结构内部也存在注释&#xff0c;那么编译器会报以下问题 使用 HTML-Comment 这个插件即可解决问题 选中需要注释的区域并根据系统输入快捷键&#xff0c;可以发现就算嵌套了注释…

使用Redis构建任务队列

文章目录 第1关&#xff1a;先进先出任务队列第2关&#xff1a;优先级任务队列第3关&#xff1a;定时任务队列 第1关&#xff1a;先进先出任务队列 编程要求 在Begin-End区域编写 add_task(task_name) 函数&#xff0c;实现将任务加入队列的功能&#xff0c;具体参数与要求如下…

C++基础 -35- string类

string类的格式 string a;如下图&#xff0c;使用string类比常规的字符串处理方便很多 而且需要进行的字符串处理&#xff0c;在类中都能完成 #include "iostream"using namespace std;extern "C" {#include "string.h" }int main() {//c的写…

Java基本数据类型详解

✨个人主页&#xff1a;全栈程序猿的CSDN博客 &#x1f4a8;系列专栏&#xff1a;Java从入门到精通 ✌座右铭&#xff1a;编码如诗&#xff0c;Bug似流星&#xff0c;持续追求优雅的代码&#xff0c;解决问题如同星辰般自如 Java是一种强类型语言&#xff0c;数据类型在程序中起…

Prefix-Tuning 论文概述

Prefix-Tuning 论文概述 前缀调优&#xff1a;优化生成的连续提示前言摘要论文十问实验数据集模型实验结论摘要任务泛化性能 前缀调优&#xff1a;优化生成的连续提示 前言 大规模预训练语言模型(PLM)在下游自然语言生成任务中广泛采用fine-tuning的方法进行adaptation。但是f…

Java中各种数据类型之间的转换

低类型向高类型自动进行转换&#xff0c;高类型向低类型的准换会丢失数据&#xff0c;整数到字符类型的转换将获取对应编码的字符。 进行高精度向低精度的强制类型准换时&#xff0c;需要将想要转换成的数据类型加一个括号()。 如何完成自动转换呢&#xff1f; 转换前的数据类…

正是阶段高等数学复习--函数极限的计算

之前在预备阶段中函数极限的解决方式分三步&#xff0c;第一步观察形式并确定用什么方式来解决&#xff0c;第二步化简&#xff0c;化简方式一共有7种&#xff0c;分别是最重要的三种&#xff08;等价替换、拆分极限存在的项、计算非零因子&#xff09;以及次重要的4种&#xf…

Maven的安装与配置本地仓库,镜像源,环境变量详细步骤

参考视频&#xff1a; 黑马程序员2023新版JavaWeb开发教程&#xff0c;实现javaweb企业开发全流程 【小飞非系列】最新Maven实战教程-项目实战构建利器 文章目录 一.下载Maven安装包二.配置Maven的本地仓库(本机仓库)三.配置镜像源&#xff08;加速jar包的下载)四.配置Maven的环…