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

2023-12-05

文章目录

前言

在这里插入图片描述

  • 提到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文件中的元素信息,比如 insert select 这等信息解析到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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • JAVA - 带有特殊字符的 LDAP 密码不起作用

    我试图在我的系统上创建一个登录屏幕 在 Active Directory 中进行查询 但是当用户的密码包含一些特殊字符 如 和 时 它不会验证 我需要加密密码才能工作吗 我该怎么做 我使用 getPassword 通过 JPasswordF
  • V8 如何管理它的堆?

    我知道V8的垃圾收集在工作时 会从GC的root开始追踪 这样无法到达的对象就会被标记然后被清除 我的问题是GC是如何遍历那些对象的 必须有一个数据结构来存储所有可达或不可达的对象 位图 链接表 顺便说一句 JVM 也做同样的事情吗 艾伦秀
  • 使用 Java 编程式 HTML 文档生成

    有谁知道如何在 Java 中以编程方式生成 HTMLDocument 对象 而不需要在外部生成字符串 然后使用 HTMLEditorKit read 来解析它 我问的两个原因 首先 我的 HTML 生成例程需要非常快 并且我认为将字符串解析
  • 方法不必要地被调用?

    我有一个 BaseActivity 它可以通过其他所有活动进行扩展 问题是 每当用户离开 暂停 活动时 我都会将音乐静音 我也不再接听电话 问题是 onPause每当用户在活动之间切换时就会被调用 这意味着应用程序不必要地静音和停止tele
  • 在气球内显示带有照片的多个地标的最佳做法是什么?

    我有一个项目如下 从手机上拍摄几张照片 将照片保存在网络系统中 然后将照片显示在其中的谷歌地球上 我读过很多文章 但它们都使用 fetchKml 我读过的一篇好文章是使用 php 但使用 fetchKml 我不知道是否可以使用 parseK
  • 删除 servlet 中的 cookie 时出现问题

    我尝试使用以下代码删除 servlet 中的 cookie Cookie minIdCookie null for Cookie c req getCookies if c getName equals iPlanetDirectoryPr
  • 如何在 OpenAPI 3.0 中定义字节数组

    我正在将 API 从 Swagger 2 0 迁移到 OpenAPI 3 0 在 DTO 中 我有一个指定为字节数组的字段 Swagger 对 DTO 的定义 Job type object properties body type str
  • 如何正确配置Tomcat SSLHostConfig?

    我正在按照本教程在 tomcat 中启用 ssl https medium com raupach how to install lets encrypt with tomcat 3db8a469e3d2 https medium com
  • JSP 标签+ scriptlet。如何启用脚本?

    我有一个使用标签模板的页面 我的 web xml 非常基本 我只是想在页面中运行一些代码 不 我对标签或其他替代品不感兴趣 我想使用不好的做法 scriptlet 哈哈 到目前为止 我收到了 HTTP ERROR 500 错误 Script
  • Vertx HttpClient getNow 不工作

    我的 vertx HttpClient 有问题 下面的代码显示使用 vertx 和纯 java 测试 GET Vertx vertx Vertx vertx HttpClientOptions options new HttpClientO
  • 如何使用 Java 原生接口从 Java 调用 Go 函数?

    可以通过以下方式调用 C 方法JNA https en wikipedia org wiki Java Native AccessJava 中的接口 如何使用 Go 实现相同的功能 package main import fmt impor
  • 如何在将数据发送到 Firebase 数据库之前对其进行加密?

    我正在使用 Firebase 实时数据库制作聊天应用程序 我知道 Firebase 非常安全 只要您的规则正确 但我自己可以阅读使用我的应用程序的人的所有聊天记录 我想阻止这种情况 为此我需要一种解密和加密方法 我尝试使用凯撒解密 但失败了
  • 字节码和位码有什么区别[重复]

    这个问题在这里已经有答案了 可能的重复 LLVM 和 java 字节码有什么区别 https stackoverflow com questions 454720 what are the differences between llvm
  • 哪种 Java DOM 包装器是最好或最受欢迎的? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • java中的比较器链

    正在阅读Oracle 关于接口的 Java 教程 https docs oracle com javase tutorial java IandI createinterface html其中给出了一个例子Card 打牌 我试图理解接口中的
  • Mule/码头设置

    我有一个正在运行的 Mule 应用程序 我想在其上设置 Jetty 来响应 http 请求 以下配置
  • 将字符串中的字符向左移动

    我是 Stack Overflow 的新手 有一道编程课的实验室问题一直困扰着我 该问题要求我们将字符串 s 的元素向左移动 k 次 例如 如果输入是 Hello World 和3 它将输出 lo WorldHel 对于非常大的 k 值 它
  • Scala repl 抛出错误

    当我打字时scala在终端上启动 repl 它会抛出此错误 scala gt init error error while loading AnnotatedElement class file usr lib jvm java 8 ora
  • JDK 7 的快速调试/调试构建

    我正在寻找 JDK 的调试 或者我猜他们称之为快速调试构建 以启用在运行时生成的打印程序集以及查找性能问题时所需的其他诊断 就目前情况而言 我似乎找不到可以直接使用的 现成的 快速调试构建二进制包 有人可以帮我提供下载链接 或者至少提供有关
  • 将隐藏(生物识别)数据附加到 pdf 上的数字签名

    我想知道是否可以使用 iText 我用于签名 或 Java 中的其他工具在 pdf 上添加生物识别数据 我会更好地解释一下 在手写板上签名时 我会收集签名信息 例如笔压 签名速度等 我想将这些信息 java中的变量 与pdf上的签名一起存储

随机推荐

  • 自定义注解验证数据字典选项及bean注入问题

    我们在工作中经常需要对字典选项进行定义 如果客户端传来的字典项不符合要求 那么根本无法保存 但是已有的注解并没有字典值的验证 那我们就自己实现一个 一 自定义字典值验证的注解 DictValid import javax validatio
  • 如何在Linux上搭建本地Docker Registry镜像仓库并实现公网访问

    Linux 本地 Docker Registry本地镜像仓库远程连接 文章目录 Linux 本地 Docker Registry本地镜像仓库远程连接 1 部署Docker Registry 2 本地测试推送镜像 3 Linux 安装cpol
  • Windows家庭版组策略问题解决及权限维持

    实验环境 windows10虚拟机 问题一 组策略问题解决 windows家庭版组策略未能打开问题 1 在桌面创建一个记事本文件 txt 并填入以下代码 echo off pushd dp0 dir b systemroot Windows
  • C/C++,树算法——Ukkonen的“后缀树“构造算法的源程序

    1 文本格式 A C program to implement Ukkonen s Suffix Tree Construction And then build generalized suffix tree include
  • 安全行业招聘信息汇总

    1 阿里巴巴 淘天集团 安全部 社招岗位 Java开发 招聘层级 P5 P6 工作年限 本科毕业1 3年 硕士毕业1 2年 base地点 杭州 职位描述 负责淘天安全部风控基础标签平台0到1能力建设及产品规划和落地 负责标签应用的产品沉淀和
  • webpack查找配置文件的策略

    Webpack 在执行时会按照一定的策略来查找配置文件 以下是它查找配置文件的基本流程 1 命令行指定 如果在运行 Webpack 时通过 config 或 c 参数指定了配置文件的路径 那么 Webpack 将使用这个指定的配置文件 2
  • 6-15 复制字符串

    include
  • 9-3用结构体定义学生,用函数输出学生成绩

    include
  • Android 13.0 SystemUI电池电量为0时延迟关机的解决方案

    1 简述 在13 0系统rom定制化开发中 在系统开发中可能会遇到了在电池电量为0时这时未出现立即关机的情况 产生延时关机的问题 下面就来分析这个问题所产生的原因 然后解决这个问题 2 SystemUI电池电量为0延迟关机的核心代码 fra
  • 机器学习笔记 - 什么是3D语义场景完成/补全?

    一 什么是3D语义场景补全 3D 语义场景完成 Semantic Scene Completion 是一种机器学习任务 涉及以体素化形式预测给定环境的完整3D场景 完成3D形状的同时推断场景的 3D 语义分割的任务 这是通过使用深度图和为场
  • 【go语言开发】Minio基本使用,包括环境搭建,接口封装和代码测试

    本文主要介绍go语言使用Minio对象存储 首先介绍搭建minio 创建bucket等 然后讲解封装minio客户端接口 包括但不限于 上传文件 下载 获取对象url 最后测试开发的接口 文章目录 前言 Minio docker安装mini
  • 什么是跨站脚本攻击

    跨站脚本攻击 1 定义 2 跨站脚本攻击如何工作 3 跨站脚本攻击类型 4 如何防止跨站脚本攻击 1 定义 跨站脚本攻击 Cross site Scripting 通常称为XSS 是一种典型的Web程序漏洞利用攻击 在线论坛 博客 留言板等
  • 前端分片上传

    前端分片上传是一种将大文件分成若干个小块进行上传的方式 以解决大文件上传时网络不稳定或上传速度慢的问题 下面是前端分片上传的基本步骤 使用JavaScript读取文件 将文件分成若干块 可以使用File API来实现这个功能 使用XMLHt
  • 6-3 求3*3整数矩阵对角线元素之和

    include
  • JavaScript的创建对象时的语法糖

    js中创建一个自定义对象有两种方法 一种是使用new 另一种是使用对象字面量形式 即直接构建 关于字面量详见 https blog csdn net bigcarp article details 134777091 使用对象字面量定义对象
  • 开发规范

    目录 开发规范 方法命名规范 领域模型命名规约 类名使用驼峰法 DO BO DTO VO AO PO除外 抽象类使用Abstract或Base
  • 国家数据局首次国考招聘12人

    中央机关及其直属机构2024年度考试录用公务员报名已于10月15日开始 在公布的 中央机关及其直属机构2024年度考试录用公务员招考简章 中 新组建的国家数据局公布了所属五个用人司局的7类综合管理职位 定级机关司局一级主任科员及以下 共计招
  • 6-11画图---没画出来。。。

    include
  • 面试题目总结(二)

    1 IoC 和 AOP 的区别 控制反转 Ioc 和面向切面编程 AOP 是两个不同的概念 它们在软件设计中有着不同的应用和目的 IoC 是一种基于对象组合的编程模式 通过将对象的创建 依赖关系和生命周期等管理权交给外部容器或框架来实现程序
  • 超硬核解析Mybatis动态代理原理!只有接口没实现也能跑?

    文章目录 前言 Mybatis dao层两种实现方式的对比 原始Dao开发 原始Dao开发的弊端 基于Mapper动态代理的开发方式 Mybatis动态代理实现方式的原理解析 动态