前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

Mybatis

作者头像
spilledyear
修改2018-08-25 12:44:52
1.3K0
修改2018-08-25 12:44:52
举报
文章被收录于专栏:小白鼠小白鼠

Mybatis 博客链接

本文主要对Mybatis中启动流程、Mapper解析、Mapper代理、四大对象、SQL执行、缓存、插件、与Sprin整合等相关内容进行解析,文章较长,能力有限,有问题麻烦指出,谢谢。关于调试方面,可以直接从Github上下载Mybatis源码,里面包含很多测试代码,下载下来安装依赖就可以直接运行,当然也可以直接通过IDEA关联源码,也比较方便。原创文章,转载请标明出处!

网上找到的一张Mybatis架构图:

Mybatis
Mybatis

启动

代码语言:txt
复制
public class Application {
    public static void main(String[] args) throws Exception{
        // 1、加载配置文件
        InputStream is = Resources.getResourceAsStream("application.xml");

        // 2、构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

        // 3、获取SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectByUserName("admin");
        System.err.println(user);
        sqlSession.commit();
    }
}

从xml配置文件中读取配置,然后通过SqlSessionFactoryBuilder构建SqlSessionFactory实例(建造者模式)。SqlSessionFactory是Mybatis的关键对象,它是个单个数据库映射关系经过编译后的内存镜像。SqlSessionFactory是创建SqlSession的工厂,每一个Mybatis的应用程序都以一个SqlSessionFactory对象的实例为核心,同时SqlSessionFactory也是线程安全的,SqlSessionFactory一旦被创建,在应用执行期间都存在。

有关于SqlSessionFactoryBuilder的build方法

代码语言:txt
复制
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  try {

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

public XMLConfigBuilder(Reader reader, String environment, Properties props) {
  // 在创建XMLConfigBuilder对象之前,创建了一个XPathParser对象,XPathParser用来解析xml文件。即:在构建XMLConfigBuilder之后,即完成了xml文件到dcument对象的转化
  this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  // 创建Configuration对象
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}

public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
  commonConstructor(validation, variables, entityResolver);
  // 创建document对象
  this.document = createDocument(new InputSource(reader));
}

在上面的代码中,我们重点关注两个东西:一个是XPathParser对象,在创建XPathParser对象的时候会将xml文件转换成document对象;两外一个是Configuration对象,在XMLConfigBuilder对象的构造方法内创建了Configuration对象。

Configuration对象是Myatis中非常非常重要的一个概念,它的作用就相当于是servlet中的ServletContext、spring中的容器,它就是Mybatis的中的Boss,Mybatis的运行就是依赖于这个对象。我们在xml文件中的一些配置,解析之后的mappedStatement等等等等,都维护在这个对像中,非常非常重要的一个对象。所以,有必要对这个对象有个简单的了解:

可以看到,Configuration有非常多的属性,每个属性都很重要,有关于每个属性的具体用途,这里没办法具体介绍,在接下来的源码解析中会慢慢接触到。可以看出,它的构造方法主要是设置了一些别名:

代码语言:txt
复制
protected Environment environment;

protected boolean safeRowBoundsEnabled;
protected boolean safeResultHandlerEnabled = true;
protected boolean mapUnderscoreToCamelCase;
protected boolean aggressiveLazyLoading;
protected boolean multipleResultSetsEnabled = true;
protected boolean useGeneratedKeys;
protected boolean useColumnLabel = true;
protected boolean cacheEnabled = true;
protected boolean callSettersOnNulls;
protected boolean useActualParamName = true;
protected boolean returnInstanceForEmptyRow;

protected String logPrefix;
protected Class <? extends Log> logImpl;
protected Class <? extends VFS> vfsImpl;
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
protected Integer defaultStatementTimeout;
protected Integer defaultFetchSize;
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

protected Properties variables = new Properties();
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

protected boolean lazyLoadingEnabled = false;
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

protected String databaseId;
protected Class<?> configurationFactory;

// 用于注册mapper,这个对象中缓存了所有mapper接口的代理对象
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

// 拦截器链
protected final InterceptorChain interceptorChain = new InterceptorChain();

protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

// mapper.xml 中的每个方法对应一个 MappedStatement,重点关注
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");

protected final Set<String> loadedResources = new HashSet<String>();
protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");

protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();


public Configuration() {
  typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
  typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

  typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
  typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
  typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

  typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
  typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
  typeAliasRegistry.registerAlias("LRU", LruCache.class);
  typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
  typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

  typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

  typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
  typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

  typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
  typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
  typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
  typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
  typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
  typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
  typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

  typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
  typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

  languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
  languageRegistry.register(RawLanguageDriver.class);
}

Mapper解析

XMLConfigBuilder # mapperElement

代码语言:txt
复制
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 如果配置文件中的mapper是以package的形式指定的,这里主要分析这种方式
      if ("package".equals(child.getName())) {
        // 获取到包名
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        // 非package方式,说明指明了具体的mapper接口或者xml文件
        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());
          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());
          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.");
        }
      }
    }
  }
}

这里其实涉及到Mapper的配置方式,可以通过package的形式配置一个包下的所有接口,或者不以package的形式指明具体的mapper接口。我们在工作中主要是通过package的方式指明接口的位置,所以重点关注configuration.addMappers(mapperPackage)方法:

Configuration # addMappers

代码语言:txt
复制
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);

public void addMappers(String packageName) {
  mapperRegistry.addMappers(packageName);
}

可以发现,这里引出了一个新对象:MapperRegistry,这是一个挺重要的对象,以下是其源码,非核心代码我已删除

代码语言:txt
复制
public class MapperRegistry {
  private final Configuration config;

  // 缓存,每个Mapper接口对应一个 MapperProxyFactory
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }

  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    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);
    }
  }
  
  public <T> boolean hasMapper(Class<T> type) {
    return knownMappers.containsKey(type);
  }

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

  public Collection<Class<?>> getMappers() {
    return Collections.unmodifiableCollection(knownMappers.keySet());
  }

  public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }
}

在使用Mybatis这个框架的时候,我们往往只是定义了一些Mapper接口和一些XML文件,并没有写实现类,却可以直接调用方法,很明显是Mybatis为我们生成了代理。在上面的代码中,我们注意到上面的代码中MapperProxyFactory这么一个类,根据名字也可以猜测的到这是一个工厂类,用来创建Mapper接口的代理类。事实就是这样,可以看看MapperProxyFactory的源码

很明显newInstance方法中使用了JDK动态代理创建代理类,除此之外,这里还引如了一个MapperProxy对象,我们大概可以猜测到MapperProxy实现了InvocationHandler接口并重写了invoke方法,有关于这一块内容我们等下再来验证,目前可以确定的MapperProxyFactory是创建代理类的工厂,MapperProxy是创建代理类的关键,它是实现了InvocationHandler接口

代码语言:txt
复制
public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final 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;
  }

  // JDK动态代理,MapperProxy类实现了InvocationHandler接口
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    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);
  }
}

所以,综合我们对这两个类的观察,MapperRegistry类用于Mapper接口注册,当我们调用它的addMapperf方法的时候,会为该Mapper接口创建一个MapperProxyFactory对象,并缓存在一个Map中。而MapperProxyFactory主要用于创建代理对象,在创建代理对象的时候,用到了MapperProxy这个类,它是是实现了InvocationHandler接口,所以可以推断出,在调用Mapper方法时候,实际上是调用MapperProxy的invoke方法。

在了解了MapperRegistry类的大体流程之后,我们继续回到MapperRegistry的addMapper方法,该方法大概做了两件事情,一个是创建MapperProxyFactory对象并缓存起来;另一个就是执行Mapper解析。这里调用了MapperAnnotationBuilder的parse方法,根据名字应该可以猜测的到这是和注解的解析相关

代码语言:txt
复制
public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      // 添加到缓存,每个Mapper接口对应一个 MapperProxyFactory
      knownMappers.put(type, new MapperProxyFactory<T>(type));

      // 下面两行代码是对Mapper的解析
      // 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);
      parser.parse();

      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

事实上,MapperAnnotationBuilder的作用就是解析Mapper接口中定义的注解,并生成Cache、ResultMap、MappedStatement三种类型对象。在创建一个的时候,会关联一个MapperBuilderAssistant对象,这是一个构建助理,实际上,真正将接口中的方法转换成MappedStatement对象就是通过MapperBuilderAssistant对象完成的

代码语言:txt
复制
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
  String resource = type.getName().replace('.', '/') + ".java (best guess)";

  // MapperBuilderAssistant类执行真正的解析操作
  this.assistant = new MapperBuilderAssistant(configuration, resource);
  this.configuration = configuration;
  this.type = type;

  // 下面就是Mybatis中的常见注解,下面那4个可用于实现通用Mapper
  sqlAnnotationTypes.add(Select.class);
  sqlAnnotationTypes.add(Insert.class);
  sqlAnnotationTypes.add(Update.class);
  sqlAnnotationTypes.add(Delete.class);

  sqlProviderAnnotationTypes.add(SelectProvider.class);
  sqlProviderAnnotationTypes.add(InsertProvider.class);
  sqlProviderAnnotationTypes.add(UpdateProvider.class);
  sqlProviderAnnotationTypes.add(DeleteProvider.class);
}

public void parse() {
  // 例:resource = "interface com.hand.sxy.mapper.UserMapper"
  String resource = type.toString();

  // loadedResources是configuration对象中的一个属性,是一个Set集合,用于缓存已解析过的Mapper
  if (!configuration.isResourceLoaded(resource)) {

    // 先对 Mapper.xml 文件进行解析
    loadXmlResource();

    // 添加到loadedResources缓存,Key为class对应的全类名
    configuration.addLoadedResource(resource);

    // 设置MapperBuilderAssistant的命名空间 
    assistant.setCurrentNamespace(type.getName());

    // 解析缓存对象
    parseCache();

    // 解析缓存引用
    parseCacheRef();
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // 这里涉及到桥接方法,关系到Java的泛型擦除,具体参考 https://www.zhihu.com/question/54895701/answer/141623158
        if (!method.isBridge()) {

          // 解析MappedStatement和ResultMap
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }

  // 遍历configuration的IncompleteMethods集合,重新解析MethodResolver
  parsePendingMethods();
}

这里面涉及到的内容还挺多,接下里一个个解释一下吧

loadXmlResource

这个方法主要就是解析Mapper.xml文件

代码语言:txt
复制
private void loadXmlResource() {
  // Spring may not know the real resource name so we check a flag
  // to prevent loading again a resource twice
  // this flag is set at XMLMapperBuilder#bindMapperForNamespace
  // 对于已解析过的Mapper.xml文件,会缓存起来。这里为什么要通过 namespace: + 全类名的 方式来缓存呢?因为namespace指定的是一个具体的Mapper接口,而在每个Mapper.xml文件解析之后,会将该对应的
  // 内容缓存起来,如:configuration.addLoadedResource("namespace:" + namespace)、configuration.addMapper(boundType)。可以认为"namespace:" + namespace标识着一个Mapper.xml文件,即每解析完一个Mapper.xml文件,都会缓存起该Mapper.xml文件和namespace对应的接口
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    // 通过对全类名中的 . 替换成 / 得到下 Maper.xml 文件路径,所以这也就要求 使用package的方式时, Mapper.xml 文件 和 Mapper接口名必须相同且在相同目录
    // xmlResource = "com/hand/sxy/mapper/UserMapper.xml"
    String xmlResource = type.getName().replace('.', '/') + ".xml";

    InputStream inputStream = null;
    try {
      // 获取到xml文件输入流
      inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
    } catch (IOException e) {
      // ignore, resource is not required
    }
    if (inputStream != null) {
      XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());

      // 通过XMLMapperBuilder进行解析,如果是不是通过package而是直接配置Mapper.xml文件的方式,就会直接执行下面XMLMapperBuilder的parse方法
      xmlParser.parse();
    }
  }
}

public void parse() {
  // 例:resource =  "com/hand/sxy/mapper/UserMapper.xml",为什么这里又要判断一次,因为当不是通过package或者不是配置Mapper接口的方式,而是配置Mapper.xml文件的方式,会直接执行这个方法
  if (!configuration.isResourceLoaded(resource)) {
    // 真正的解析逻辑
    configurationElement(parser.evalNode("/mapper"));

    // 添加到缓存
    configuration.addLoadedResource(resource);

    // 将已经解析完的Mapper.xml文件标识和对应对应的Mapper接口标识缓存起来,Mapper.xml文件用"namespace:" + namespace表示;Mapper接口用全类名表示。
    bindMapperForNamespace();
  }

  // 残缺不全的重新解析一遍?不太懂这里是为了处理什么情况
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

private void configurationElement(XNode context) {
  try {
    // 获取命名空间, 即全类名
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // 设置当前namespace
    builderAssistant.setCurrentNamespace(namespace);

    /** 以下用于处理各个标签的解析,详情请看:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#*/

    // 用于启用本xml对应的namespace的二级缓存  <cache-ref namespace="com.someone.application.data.SomeMapper"/>
    cacheRefElement(context.evalNode("cache-ref"));

    // 共享指定namespace的二级缓存  <cache  eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>
    cacheElement(context.evalNode("cache"));

    parameterMapElement(context.evalNodes("/mapper/parameterMap"));

    // 进行数据库列和返回Java对象的属性之间的映射 
    resultMapElements(context.evalNodes("/mapper/resultMap"));

    // 可被其他语句引用的可重用sql语句块
    sqlElement(context.evalNodes("/mapper/sql"));

    // 增删改查映射语句
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

分别对应Mapper.xml文件中的不同标签,很多都是我们常见的,比如:resultMap、sql、select 等等。每个标签的解析对应不同的方法,虽然这些都是XMLStatementBuilder类中的方法,但实际上将各个标签解析为Mybatis中对应的类时,都是通过MapperBuilderAssistant这个助理解析类来完成的。这里就简单分析一下resultMapElements 和 buildStatementFromContext 这个两个方法

resultMapElements

代码语言:txt
复制
private void resultMapElements(List<XNode> list) throws Exception {
  // 每个XNode就代表一个 <resultMap>, <resultMap>可以有多个
  for (XNode resultMapNode : list) {
    try {
      resultMapElement(resultMapNode);
    } catch (IncompleteElementException e) {
      // ignore, it will be retried
    }
  }
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
  // 每次解析一个 <resultMap> 的时候,都传一个空集合,在解析的过程中再往里面填充
  return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  
  // 获取id
  String id = resultMapNode.getStringAttribute("id",
      resultMapNode.getValueBasedIdentifier());

  // 获取 dto 全类名    
  String type = resultMapNode.getStringAttribute("type",
      resultMapNode.getStringAttribute("ofType",
          resultMapNode.getStringAttribute("resultType",
              resultMapNode.getStringAttribute("javaType"))));

  // 获取extend属性值            
  String extend = resultMapNode.getStringAttribute("extends");

 // 获取autoMapping属性值  
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");

  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);

  // 获取到该<resultMap>标签下每个 数据库字段 和 dto字段对应关系
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {
      List<ResultFlag> flags = new ArrayList<ResultFlag>();

      // 代表主键
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }

      // buildResultMappingFromContext用于将<resultMap>标签下的每行对应关系维护成一个ResultMapping对象,其实该方法内最终也是调用了MapperBuilderAssistant类的buildResultMapping方法来完成这个操作
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    // 内部通过MapperBuilderAssistant类的addResultMap方法,构建一个ResultMap对象,并缓存到configuration的resultMaps属性中,以 Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.BaseResultMap
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

可以看看buildResultMappingFromContext方法的返回结果,也就是ResultMapping对象

ResultMapping
ResultMapping

至此,有关于<resultMap>这个标签的解析到此结束,接下来看看buildStatementFromContext方法,这个方法主要时解析 select|insert|update|delete 这个四种情况,最后转换成一个MappedStatement对象,然后缓存在configuration的mappedStatements属性中,以 Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.selectByUserName

buildStatementFromContext

代码语言:txt
复制
  private void buildStatementFromContext(List<XNode> list) {
    // 这里是为了处理 在Mybatis配置文件中配置了databaseId的情况
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  // 每个 select|insert|update|delet 就是一个XNode
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

public void parseStatementNode() {

  /** 各个属性 */

  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);

  // 返回类型
  String resultMap = context.getStringAttribute("resultMap");
  String resultType = context.getStringAttribute("resultType");
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

  // 获取到该标签的名称,即时 select|insert|update|delet 中的哪一种
  String nodeName = context.getNode().getNodeName();

  // SqlCommandType 是个枚举类型,包括    UNKNOWN,INSERT,UPDATE,DELETE,SELECT,FLUSH 这几个值
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // 在解析SQL之前,处理SQL中的 <include> 标签,即引用了一段SQL
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // 解析selectKey,SelectKey在Mybatis中是为了解决Insert数据时不支持主键自动生成的问题
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // 正式开始解析SQL,这时候的SQL中的<selectKey> 和 <include> 已经解析好了并且替换成正常的sql语句了。
  // SqlSource非常重要,是用来处理SQL语句的。它有一个getBoundSql(Object parameterObject)方法,就是用来返回最终的SQL的
  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;

  // query!selectKey
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;

  // com.hand.sxy.mapper.UserMapper.query!selectKey
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);

  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }

  // MapperBuilderAssistant#addMappedStatement生成一个MappedStatement对象,并缓存在configuration的mappedStatements对象中,以Mapper全类名 + id 为key,如: com.hand.sxy.mapper.UserMapper.selectByUserName 
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered, 
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

可以看看buildStatementFromContext方法的返回结果,也就是MappedStatement对象

MappedStatement
MappedStatement

解析完之后,还有一个方法需要执行,即bindMapperForNamespace,绑定命名空间,就是将已解析的内容缓存起来

代码语言:txt
复制
private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    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);
        configuration.addMapper(boundType);
      }
    }
  }
}

有关于Mapper.xml文件的解析暂时就分析到这里,可以发现一个规律,就是将一类类标签解析成各种类,然后缓存到configuration对象的各个属性中。

前面讲解的loadXmlResource方法内容有点多,这里再将主流程的代码贴一下

代码语言:txt
复制
public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    // 解析Mapper.xml
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());

    // 解析@CacheNamespace注解,也就是对应Mapper.xml中的<cache>标签,因为在loadXmlResource方法中已经解析过<cache>标签了,所以这里只要解析@CacheNamespace注解
    parseCache();

    // 解析CacheNamespaceRef注解,也就是对应Mapper.xml中的<cache-ref>标签,与上面类似
    parseCacheRef();

    Method[] methods = type.getMethods();
    for (Method method : methods) {
      try {
        // 排除桥接方法,关系到Java的泛型擦除,具体参考 https://www.zhihu.com/question/54895701/answer/141623158
        if (!method.isBridge()) {
          // 解析方法上的注解,例:@Options、@ResultMap 等注解
          parseStatement(method);
        }
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }

  // 重新解析残缺的method?不太清楚什么情况下会产生残缺的方法
  parsePendingMethods();
}

parseStatement

这个方法主要是解析Mapper接口方法上的一些注解,例:@Options、 @Inser、@Select、@Update、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 等注解,有关于我们常用这几个注解,还是很有必要解释一下。那几个Provider注解,允许我们自己在方法里面写生成SQL的逻辑,可以通过他们在开发通用Mapper组件

代码语言:txt
复制
void parseStatement(Method method) {
  Class<?> parameterTypeClass = getParameterType(method);
  LanguageDriver languageDriver = getLanguageDriver(method);

  // 获取 @Inser、@Select、@Update、@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 注解上对应的 SqlSource
  SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

  if (sqlSource != null) {
    // @Options 注解
    Options options = method.getAnnotation(Options.class);
    final String mappedStatementId = type.getName() + "." + method.getName();
    Integer fetchSize = null;
    Integer timeout = null;
    StatementType statementType = StatementType.PREPARED;
    ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
    SqlCommandType sqlCommandType = getSqlCommandType(method);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = !isSelect;
    boolean useCache = isSelect;

    KeyGenerator keyGenerator;
    String keyProperty = "id";
    String keyColumn = null;
    if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
      // first check for SelectKey annotation - that overrides everything else
      SelectKey selectKey = method.getAnnotation(SelectKey.class);
      if (selectKey != null) {
        keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
        keyProperty = selectKey.keyProperty();
      } else if (options == null) {
        keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      } else {
        keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        keyProperty = options.keyProperty();
        keyColumn = options.keyColumn();
      }
    } else {
      keyGenerator = NoKeyGenerator.INSTANCE;
    }

    if (options != null) {
      if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
        flushCache = true;
      } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
        flushCache = false;
      }
      useCache = options.useCache();
      fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
      timeout = options.timeout() > -1 ? options.timeout() : null;
      statementType = options.statementType();
      resultSetType = options.resultSetType();
    }

    String resultMapId = null;
    // @ResultMap 注解
    ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
    if (resultMapAnnotation != null) {
      String[] resultMaps = resultMapAnnotation.value();
      StringBuilder sb = new StringBuilder();
      for (String resultMap : resultMaps) {
        if (sb.length() > 0) {
          sb.append(",");
        }
        sb.append(resultMap);
      }
      resultMapId = sb.toString();
    } else if (isSelect) {
      resultMapId = parseResultMap(method);
    }

    // 可以发现,最终也是生成一个MappedStatement对象
    assistant.addMappedStatement(
        mappedStatementId,
        sqlSource,
        statementType,
        sqlCommandType,
        fetchSize,
        timeout,
        // ParameterMapID
        null,
        parameterTypeClass,
        resultMapId,
        getReturnType(method),
        resultSetType,
        flushCache,
        useCache,
        // TODO gcode issue #577
        false,
        keyGenerator,
        keyProperty,
        keyColumn,
        // DatabaseID
        null,
        languageDriver,
        // ResultSets
        options != null ? nullOrEmpty(options.resultSets()) : null);
  }
}

重点看看这部分内容

代码语言:txt
复制
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

// 先聊来SqlSource这个接口,它只有一个getBoundSql,就是根据参数计算出实际的可执行SQL,这个可执行SQL在Mybatis中用BoundSql表示,这里就时根据注解获取它对应的SqlSource
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
  try {
    //  @Inser、@Select、@Update、@Delete 其中一个
    Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);

    // @SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 其中一个
    Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);

    // 这两种注解不能同时出现,如果你了解@xxprovider的用法就知道了
    if (sqlAnnotationType != null) {
      if (sqlProviderAnnotationType != null) {
        throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
      }

      Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
      // 获取该注解上的SQL
      final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);

      // 根据SLQ 创建一个SqlSource 
      return buildSqlSourceFromStrings(strings, parameterType, languageDriver);

    // 如果是Provider类型注解,返回ProviderSqlSource。需要注意的是,@xxxProvider注解可以指定类和方法,然后自己在指定的方法里面写SQL的生成逻辑,也就是动态SQL
    } else if (sqlProviderAnnotationType != null) {
      Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
      return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
    }
    return null;
  } catch (Exception e) {
    throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
  }
}

这一小部分内容主要需要注意的是:@xxxProvider注解可以指定类和方法,然后自己在指定的方法里面写SQL的生成逻辑,也就是动态SQL。

可以看看SqlSource的继承结构图

有关去解析这部分内容,也就是XMLConfigBuilder的parse()方法,暂时就介绍到这里,主要是讲解了Mapper这部分的解析逻辑。下面再次回到SqlSessionFactoryBuilder中的build方法上来。

构建SqlSessionFactory

在构建好configuration对象后,接下来的逻辑比较简单,在XMLConfigBuilder的parse方法执行完之后,就已经构建了一个完整的configuration对象,剩下的只是新建一个DefaultSqlSessionFactory对象,并将已构建好的configuration对象作为DefaultSqlSessionFactory对象的一个熟属性

代码语言:txt
复制
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.
    }
  }
}

// 创建了一个SqlSessionFactory,默认实现类 DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

public DefaultSqlSessionFactory(Configuration configuration) {
  this.configuration = configuration;
}

对于接口中的方法,我们也简单了解一下,主要是和SqlSession相关,具体内容在接下来的章节介绍

代码语言:txt
复制
public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

至此,有关于Mybatis这个框架的初始化操作已经完成了,现在可以正常使用了,接下来需要了解的就是这个框架的执行流程了。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.08.08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 启动
  • Mapper解析
    • loadXmlResource
      • parseStatement
      • 构建SqlSessionFactory
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档