天天看点

Mybatis核心源码-通过sqlSession获取映射器代理工厂

作者:王朋code

先看一段代码,熟悉Mybatis的小伙伴都知道我们可以通过getMapper方法获取对应的接口,然后调用接口的方法便可以执行对应的MappedStatement中的sql,那么对应的原理是什么呢?我们今天来看一下!

Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
BusinessRequestMonitorLogMapper monitorLogMapper = sqlSession.getMapper(BusinessRequestMonitorLogMapper.class);
List<String> strings = monitorLogMapper.selectByIdAndNameAndMethodMap(map);
System.out.println(strings);           

mybatis-config.xml文件,我们只研究最下面的mappers

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- plugins在配置文件中的位置必须符合要求,否则会报错,顺序如下: properties?, settings?, typeAliases?,
        typeHandlers?, objectFactory?,objectWrapperFactory?, plugins?, environments?,
        databaseIdProvider?, mappers? -->
    <properties resource="dataSource.properties"/>
    <!-- 全局参数 -->
    <settings>
        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="true"/>
        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <!--
                <setting name="lazyLoadingEnabled" value="false"/>
        -->
        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true -->
        <setting name="multipleResultSetsEnabled" value="true"/>
        <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
        <setting name="useColumnLabel" value="true"/>
        <!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false -->
        <setting name="useGeneratedKeys" value="true"/>
        <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        <!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH:
            执行器可以重复执行语句和批量更新) -->
        <setting name="defaultExecutorType" value="SIMPLE"/>
        <!-- 使用驼峰命名法转换字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
        <setting name="localCacheScope" value="SESSION"/>
        <!-- 设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
        <setting name="jdbcTypeForNull" value="NULL"/>
        <!-- 开启sql打印 -->
        <!--		value取值问题查看笔记:mybatis整合日志框架-->
<!--
        <setting name="logImpl" value="STDOUT_LOGGING" />
-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <environments default="business">
    </environments>
    <mappers>
        <!--        用于扫mapper.xml的,xml和接口名字一致-->
<!--        <package name="org.apache.test"/>-->
        <!--每一个Mapper.xml都需要在Mybatis核心配置文件中注册!-->
<!--
        <mapper resource="org/apache/test/BusinessRequestMonitorLogMapper.xml"/>
-->
        <!--每一个Mapper.xml都需要在Mybatis核心配置文件中注册!-->
        <mapper class="org.apache.test.BusinessRequestMonitorLogMapper"/>
    </mappers>
</configuration>           

先看SqlSessionFactoryBuilder这个类,这个类的作用是我们今天可以简单就是初始化了所有的我们所设置的信息并放到了Configuration这个全局对象中

这里的加载非常重要,我们后续的getMapper要用到里面的信息

//它使用了一个参照了XML文档或更特定的SqlMapConfig.xml文件的Reader实例。
  //可选的参数是environment和properties。Environment决定加载哪种环境(开发环境/生产环境),包括数据源和事务管理器。
  //如果使用properties,那么就会加载那些properties(属性配置文件),那些属性可以用${propName}语法形式多次用在配置文件中。和Spring很像,一个思想?
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        //委托XMLConfigBuilder来解析mybatis-config.xml文件,并构建
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      //解析mybatis-config.xml并把对应的信息转换成Configuration这个全局对象
      //重点看parser.parse()
      return build(parser.parse());
    } catch (Exception e) {
      ...
    }
  }           
//解析配置
  public Configuration parse() {
    //如果已经解析过了,报错;对应xml配置文件,每一个XMLConfigBuilder都只能解析一次
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //根节点是configuration,然后逐层开始解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
//我们只看今天研究的
private void parseConfiguration(XNode root) {
    try {
      //分步骤解析
      ...
      //10.映射器,解析所有的mapper接口,并获取MappedStatement
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }           
//直接看29行即可
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //10.4自动扫描包下所有映射器
          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) {
            //10.1使用类路径
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            //映射器比较复杂,调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            //10.2使用绝对url路径
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比较复杂,调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            //因为我们在配置文件中只用到了class标签,所以当前方法会进入到此判断中
            //10.3使用java类名
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            //直接把这个映射加入配置,这里直接调用了MapperRegistry#addMapper方法
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }           
//MapperRegistry中的方法
public <T> void addMapper(Class<T> type) {
    //mapper必须是接口!才会添加
    if (type.isInterface()) {
      if (hasMapper(type)) {
        //如果重复添加了,报错
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        //这里非常重要!!!将对应的全限定接口名作为key,同时创建的一个代理工厂对象
        //我们在这里进行了put,后面会用过get方法拿到对应的mapper代理对象!!
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        //这个方法篇幅太长了,我在这里说一下做的事情
        //解析当前的接口全限定类名,通过反射获取接口中所有的方法名,然后拼接为全限定接口名+"."+方法名
        //然后存到mappedStatements这个map中,key=全限定接口名+"."+方法名,value=statement对象(里面有当前这个xml的所有信息)
        parser.parse();
        loadCompleted = true;
      } finally {
        //如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }           

代理工厂对象

public class MapperProxyFactory<T> {

  //这里就是全限定接口名,我们上面传进来的
  private final Class<T> mapperInterface;
  private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //用JDK自带的动态代理生成映射器
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  //后面我们使用getMapper方法的时候就是调用的这个方法!!!很重要
  public T newInstance(SqlSession sqlSession) {
    //MapperProxy是真正干活的地方!!!
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}           
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  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 {
    //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
    //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //这里优化了,去缓存中找MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //执行
    return mapperMethod.execute(sqlSession, args);
  }

  //去缓存中找MapperMethod
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      //找不到才去new,这里创建的时候会根据全限定接口名+方法名去查询mappedStatements这个map
      //这个map上面我们也说了,存的value是xml中的标签对象
      //所以这里就可以拿到对应的xml中的全部信息,当然也包括sql了
      //然后上面执行mapperMethod.execute方法,就会去查询对应的sql了
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

}           

ok,上面的准备工作完成了,我们接下来来看getMapper做了什么事!

BusinessRequestMonitorLogMapper monitorLogMapper = sqlSession.getMapper(BusinessRequestMonitorLogMapper.class);
//实际执行的是下面的代码
@Override
  public <T> T getMapper(Class<T> type) {
    //最后会去调用MapperRegistry.getMapper
    return configuration.<T>getMapper(type, this);
  }
//继续
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }
//继续
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  //这里根据全限定接口名拿到了对应工厂的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 {
      //这里调用了newInstance,实际是创建了MapperProxy代理对象并返回
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }           

List<String> strings = monitorLogMapper.selectByIdAndNameAndMethodMap(map);

当我们执行这行代码时,实际就是调用了MapperProxy中的invoke方法(PS:不熟悉代理的小伙伴先温习一下java的代理吧~),invoke方法中执行的代码在上面已经解释了,通过代理,我们就可以执行了对应的sql并获取数据了~

简单总结一下:

SqlSessionFactoryBuilder类build的时候获取到所有的mapper接口并放到了knownMappers中,key=接口名,value=MapperProxyFactory;同时解析对应的xml标签,然后放到了mappedStatements这个map中,key=全限定接口名+"."+方法名,value=statement对象(里面有当前这个xml的所有信息);数据准备完毕。

开始执行:BusinessRequestMonitorLogMapper monitorLogMapper = sqlSession.getMapper(BusinessRequestMonitorLogMapper.class);

执行getMapper接口,通过knownMappers.get()获取对应的MapperProxyFactory,然后MapperProxyFactory通过newInstance方法中的MapperProxy方法创建了代理对象并返回。

开始执行:List<BusinessRequestMonitorLog> strings = monitorLogMapper.selectById("1");

实际执行的是代理对象的MapperProxy#invoke方法,而invoke方法则会根据全限定接口名+方法名从mappedStatements这个map中get到statement对象,然后执行statement对象中的sql查询出数据并返回。