专栏首页Java技术大杂烩Mybatis 解析 SQL 源码分析一

Mybatis 解析 SQL 源码分析一

本文首发于个人公众号 Java 技术大杂烩,欢迎关注

相关文章

Mybatis Mapper 接口源码解析

Mybatis 数据库连接池源码解析

Mybatis 类型转换源码分析

Mybatis 解析配置文件的源码解析

前言

在使用 Mybatis 的时候,我们在 Mapper.xml 配置文件中书写 SQL;文件中还配置了对应的daoSQL 中还可以使用一些诸如for循环if判断之类的高级特性,当数据库列和JavaBean属性不一致时定义的 resultMap等,接下来就来看下Mybatis 是如何从配置文件中解析出 SQL 并把用户传的参数进行绑定;

Mybatis 解析 SQL的时候,可以分为两部分来看,一是从 Mapper.xml 配置文件中解析SQL,二是把 SQL 解析成为数据库能够执行的原始 SQL,把占位符替换为 ? 等。

这篇文章先来看下第一部分,Mybatis 是如何从 Mapper.xml 配置文件中解析出 SQL 的。

配置文件的解析使用了大量的建造者模式(builder)

mybatis-config.xml

Mybatis 有两个配置文件,mybaits-config.xml 配置的是 mybatis 的一些全局配置信息,而 mapper.xml 配置的是 SQL 信息,在 Mybatis 初始化的时候,会对这两个文件进行解析,mybatis-config.xml 配置文件的解析比较简单,不再细说,使用的 XMLConfigBuilder 类来对 mybatis-config.xml 文件进行解析。

 1  public Configuration parse() {
 2    // 如果已经解析过,则抛异常
 3    if (parsed) {
 4      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
 5    }
 6    parsed = true;
 7    parseConfiguration(parser.evalNode("/configuration"));
 8    return configuration;
 9  }
10  // 解析 mybatis-config.xml 文件下的所有节点
11  private void parseConfiguration(XNode root) {
12      propertiesElement(root.evalNode("properties"));
13      Properties settings = settingsAsProperties(root.evalNode("settings"));
14      // .... 其他的节点........
15      // 解析 mapper.xml 文件
16      mapperElement(root.evalNode("mappers"));
17  }
18
19 // 解析 mapper.xml 文件
20 private void mapperElement(XNode parent) throws Exception {
21    // ......
22    InputStream inputStream = Resources.getUrlAsStream(url);
23    XMLMapperBuilder mapperParser = 
24           new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
25    mapperParser.parse();
26 }

从上述代码可以看到,解析 Mapper.xml 配置文件是通过 XMLMapperBuilder 来解析的。接下来看下该类的实现:

XMLMapperBuilder

XMLMapperBuilder 类是用来解析 Mapper.xml 文件的,它继承了 BaseBuilderBaseBuilder 类一个建造者基类,其中包含了 Mybatis 全局的配置信息 Configuration ,别名处理器,类型处理器等,如下所示:

 1public abstract class BaseBuilder {
 2  protected final Configuration configuration;
 3  protected final TypeAliasRegistry typeAliasRegistry;
 4  protected final TypeHandlerRegistry typeHandlerRegistry;
 5
 6  public BaseBuilder(Configuration configuration) {
 7    this.configuration = configuration;
 8    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
 9    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
10  }
11}

关于 TypeAliasRegistry, TypeHandlerRegistry 可以参考 Mybatis 类型转换源码分析

接下来看下 XMLMapperBuilder 类的属性定义:

 1public class XMLMapperBuilder extends BaseBuilder {
 2  // xpath 包装类
 3  private XPathParser parser;
 4  // MapperBuilder 构建助手
 5  private MapperBuilderAssistant builderAssistant;
 6  // 用来存放sql片段的哈希表
 7  private Map<String, XNode> sqlFragments;
 8  // 对应的 mapper 文件
 9  private String resource;
10
11  private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
12    super(configuration);
13    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
14    this.parser = parser;
15    this.sqlFragments = sqlFragments;
16    this.resource = resource;
17  }
18  // 解析文件
19  public void parse() {
20    // 判断是否已经加载过该配置文件
21    if (!configuration.isResourceLoaded(resource)) {
22      // 解析 mapper 节点
23      configurationElement(parser.evalNode("/mapper"));
24      // 将 resource 添加到 configuration 的 addLoadedResource 集合中保存,该集合中记录了已经加载过的配置文件
25      configuration.addLoadedResource(resource);
26      // 注册 Mapper 接口
27      bindMapperForNamespace();
28    }
29    // 处理解析失败的 <resultMap> 节点
30    parsePendingResultMaps();
31    // 处理解析失败的 <cache-ref> 节点
32    parsePendingChacheRefs();
33    // 处理解析失败的 SQL 节点
34    parsePendingStatements();
35  }

从上面的代码中,使用到了 MapperBuilderAssistant 辅助类,该类中有许多的辅助方法,其中有个 currentNamespace 属性用来表示当前的 Mapper.xml 配置文件的命名空间,在解析完成 Mapper.xml 配置文件的时候,会调用 bindMapperForNamespace 进行注册Mapper接口,表示该配置文件对应的Mapper接口`,关于 Mapper 的注册可以参考 Mybatis Mapper 接口源码解析

 1  private void bindMapperForNamespace() {
 2    // 获取当前的命名空间
 3    String namespace = builderAssistant.getCurrentNamespace();
 4    if (namespace != null) {
 5      Class<?> boundType = Resources.classForName(namespace);
 6      if (boundType != null) {
 7        // 如果还没有注册过该 Mapper 接口,则注册
 8        if (!configuration.hasMapper(boundType)) {
 9          configuration.addLoadedResource("namespace:" + namespace);
10          // 注册
11          configuration.addMapper(boundType);
12        }
13     }
14  }

现在就来解析 Mapper.xml 文件的每个节点,每个节点的解析都封装成一个方法,很好理解:

 1  private void configurationElement(XNode context) {
 2      // 命名空间
 3      String namespace = context.getStringAttribute("namespace");
 4      // 设置命名空间
 5      builderAssistant.setCurrentNamespace(namespace);
 6      // 解析 <cache-ref namespace=""/> 节点
 7      cacheRefElement(context.evalNode("cache-ref"));
 8      // 解析 <cache /> 节点
 9      cacheElement(context.evalNode("cache"));
10      // 已废弃,忽略
11      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
12      // 解析 <resultMap /> 节点
13      resultMapElements(context.evalNodes("/mapper/resultMap"));
14      // 解析 <sql> 节点
15      sqlElement(context.evalNodes("/mapper/sql"));
16      // 解析 select|insert|update|delete 这几个节点
17      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
18  }

解析 <cache> 节点

Mybatis 默认情况下是没有开启二级缓存的,除了局部的 session 缓存。如果要为某个命名空间开启二级缓存,则需要在 SQL 映射文件中添加<cache>标签来告诉 Mybatis 需要开启二级缓存,先来看看 <cache> 标签的使用说明:

1<cache eviction="LRU" flushInterval="1000" size="1024" readOnly="true" type="MyCache" blocking="true"/>

<cache> 一共有 6 个属性,可以用来改变Mybatis 缓存的默认行为:

  1. eviction: 缓存的过期策略,可以取 4 个值:
    • LRU – 最近最少使用的:移除最长时间不被使用的对象。(默认)
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
    • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
  2. flushInterval: 刷新缓存的时间间隔,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
  3. size: 缓存大小
  4. readOnly: 是否是只读
  5. type : 自定义缓存的实现
  6. blocking:是否是阻塞

该类中主要使用 cacheElement 方法来解析 <cache> 节点:

 1  // 解析 <cache> 节点
 2  private void cacheElement(XNode context) throws Exception {
 3    if (context != null) {
 4      // 获取 type 属性,默认为 PERPETUAL
 5      String type = context.getStringAttribute("type", "PERPETUAL");
 6      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
 7      // 获取过期策略 eviction 属性
 8      String eviction = context.getStringAttribute("eviction", "LRU");
 9      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
10      Long flushInterval = context.getLongAttribute("flushInterval");
11      Integer size = context.getIntAttribute("size");
12      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
13      boolean blocking = context.getBooleanAttribute("blocking", false);
14      // 获取 <cache> 节点下的子节点,将用于初始化二级缓存
15      Properties props = context.getChildrenAsProperties();
16      // 创建 Cache 对象,并添加到 configuration.caches 集合中保存
17      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
18    }
19  }

接下来看下 MapperBuilderAssistant 辅助类如何创建缓存,并添加到 configuration.caches 集合中去:

 1  public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass,
 2      Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {
 3    // 创建缓存,使用构造者模式设置对应的属性
 4    Cache cache = new CacheBuilder(currentNamespace)
 5        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
 6        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
 7        .clearInterval(flushInterval)
 8        .size(size)
 9        .readWrite(readWrite)
10        .blocking(blocking)
11        .properties(props)
12        .build();
13    // 进入缓存集合
14    configuration.addCache(cache);
15    // 当前缓存
16    currentCache = cache;
17    return cache;
18  }

再来看下 CacheBuilder 是个什么东西,它是 Cache 的建造者,如下所示:

 1public class CacheBuilder {
 2  // Cache 对象的唯一标识,对应配置文件中的 namespace
 3  private String id;
 4  // Cache 的实现类
 5  private Class<? extends Cache> implementation;
 6  // 装饰器集合
 7  private List<Class<? extends Cache>> decorators;
 8  private Integer size;
 9  private Long clearInterval;
10  private boolean readWrite;
11  // 其他配置信息
12  private Properties properties;
13  // 是否阻塞
14  private boolean blocking;
15
16  // 创建 Cache 对象
17  public Cache build() {
18    // 设置 implementation 的默认值为 PerpetualCache ,decorators 的默认值为 LruCache
19    setDefaultImplementations();
20    // 创建 Cache
21    Cache cache = newBaseCacheInstance(implementation, id);
22    // 设置 <properties> 节点信息
23    setCacheProperties(cache);
24    if (PerpetualCache.class.equals(cache.getClass())) {
25      for (Class<? extends Cache> decorator : decorators) {
26        cache = newCacheDecoratorInstance(decorator, cache);
27        setCacheProperties(cache);
28      }
29      cache = setStandardDecorators(cache);
30    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
31      cache = new LoggingCache(cache);
32    }
33    return cache;
34  }
35}

解析 <cache-ref> 节点

在使用了 <cache> 配置了对应的缓存后,多个 namespace 可以引用同一个缓存,使用 <cache-ref> 进行指定

1<cache-ref namespace="com.someone.application.data.SomeMapper"/>
2
3cacheRefElement(context.evalNode("cache-ref"));

解析的源码如下,比较简单:

 1  private void cacheRefElement(XNode context) {
 2      // 当前文件的namespace
 3      String currentNamespace = builderAssistant.getCurrentNamespace();
 4      // ref 属性所指向引用的 namespace
 5      String refNamespace = context.getStringAttribute("namespace");
 6      // 会存入到 configuration 的一个 map 中, cacheRefMap.put(namespace, referencedNamespace);
 7      configuration.addCacheRef(currentNamespace , refNamespace );
 8      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, refNamespace);
 9      // 实际上调用 构建助手 builderAssistant 的 useCacheRef 方法进行解析
10      cacheRefResolver.resolveCacheRef();
11    }
12  }

构建助手 builderAssistantuseCacheRef 方法:

 1  public Cache useCacheRef(String namespace) {
 2      // 标识未成功解析的 Cache 引用
 3      unresolvedCacheRef = true;
 4      // 根据 namespace 中 configuration 的缓存集合中获取缓存
 5      Cache cache = configuration.getCache(namespace);
 6      if (cache == null) {
 7        throw new IncompleteElementException("....");
 8      }
 9      // 当前使用的缓存
10      currentCache = cache;
11      // 已成功解析 Cache 引用
12      unresolvedCacheRef = false;
13      return cache;
14  }

解析 <resultMap> 节点

resultMap 节点很强大,也很复杂,会单独另写一篇文章来介绍。

解析 <sql> 节点

<sql> 节点可以用来定义重用的SQ片段,

1    <sql id="commSQL" databaseId="" lang="">
2        id, name, job, age
3    </sql>
4
5    sqlElement(context.evalNodes("/mapper/sql"));

sqlElement 方法如下,一个 Mapper.xml 文件可以有多个 sql 节点:

 1  private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
 2    // 遍历,处理每个 sql 节点
 3    for (XNode context : list) {
 4      // 数据库ID
 5      String databaseId = context.getStringAttribute("databaseId");
 6      // 获取 id 属性
 7      String id = context.getStringAttribute("id");
 8      // 为 id 加上 namespace 前缀,如原来 id 为 commSQL,加上前缀就变为了 com.aa.bb.cc.commSQL
 9      id = builderAssistant.applyCurrentNamespace(id, false);
10      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
11        // 如果 SQL 片段匹配对应的数据库,则把该节点加入到缓存中,是一个 map
12        // Map<String, XNode> sqlFragments
13        sqlFragments.put(id, context);
14      }
15    }
16  }

ID 加上namespace前缀的方法如下:

 1  public String applyCurrentNamespace(String base, boolean isReference) {
 2    if (base == null) {
 3      return null;
 4    }
 5     // 是否已经包含 namespace 了
 6    if (isReference) {
 7      if (base.contains(".")) {
 8        return base;
 9      }
10    } else {
11      // 是否是一 namespace. 开头
12      if (base.startsWith(currentNamespace + ".")) {
13        return base;
14      }
15    }
16    // 返回 namespace.id,即 com.aa.bb.cc.commSQL
17    return currentNamespace + "." + base;
18  }

insert | update | delete | select 节点的解析

关于这些与操作数据库的SQL的解析,主要是由 XMLStatementBuilder 类来进行解析。在 Mybatis 中使用 SqlSource 来表示 SQL语句,但是这些SQL 语句还不能直接在数据库中进行执行,可能还有动态SQL语句占位符等。

接下来看下这类节点的解析:

 1buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
 2
 3private void buildStatementFromContext(List<XNode> list) {
 4// 匹配对应的数据库
 5if (configuration.getDatabaseId() != null) {
 6  buildStatementFromContext(list, configuration.getDatabaseId());
 7}
 8buildStatementFromContext(list, null);
 9}
10
11private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
12for (XNode context : list) {
13  // 为 XMLStatementBuilder 对应的属性赋值
14  final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
15  // 解析每个节点
16  statementParser.parseStatementNode();
17}

可以看到 selelct | insert | update | delete 这类节点是使用 XMLStatementBuilder 类的 parseStatementNode() 方法来解析的,接下来看下该方法的实现:

 1  public void parseStatementNode() {
 2    // id 属性和数据库标识
 3    String id = context.getStringAttribute("id");
 4    String databaseId = context.getStringAttribute("databaseId");
 5    // 如果数据库不匹配则不加载
 6    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
 7      return;
 8    }
 9    // 获取节点的属性和对应属性的类型
10    Integer fetchSize = context.getIntAttribute("fetchSize");
11    Integer timeout = context.getIntAttribute("timeout");
12    Integer fetchSize = context.getIntAttribute("fetchSize");
13    Integer timeout = context.getIntAttribute("timeout");
14    String parameterMap = context.getStringAttribute("parameterMap");
15    String parameterType = context.getStringAttribute("parameterType");
16    // 从注册的类型里面查找参数类型
17    Class<?> parameterTypeClass = resolveClass(parameterType);
18    String resultMap = context.getStringAttribute("resultMap");
19    String resultType = context.getStringAttribute("resultType");
20    String lang = context.getStringAttribute("lang");
21    LanguageDriver langDriver = getLanguageDriver(lang);
22    // 从注册的类型里面查找返回值类型
23    Class<?> resultTypeClass = resolveClass(resultType);
24    String resultSetType = context.getStringAttribute("resultSetType");
25    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
26    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
27
28    // 获取节点的名称
29    String nodeName = context.getNode().getNodeName();
30    // 根据节点的名称来获取节点的类型,枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
31    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
32    // 下面这三行代码,如果是select语句,则不会刷新缓存和需要使用缓存
33    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
34    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
35    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
36    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
37
38    // 解析 <include> 节点
39    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
40    includeParser.applyIncludes(context.getNode());
41
42    // 解析 selectKey 节点
43    processSelectKeyNodes(id, parameterTypeClass, langDriver);
44    // 创建 sqlSource 
45    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
46    // 处理 resultSets keyProperty keyColumn  属性
47    String resultSets = context.getStringAttribute("resultSets");
48    String keyProperty = context.getStringAttribute("keyProperty");
49    String keyColumn = context.getStringAttribute("keyColumn");
50    // 处理 keyGenerator
51    KeyGenerator keyGenerator;
52    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
53    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
54    if (configuration.hasKeyGenerator(keyStatementId)) {
55      keyGenerator = configuration.getKeyGenerator(keyStatementId);
56    } else {
57      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
58          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
59          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
60    }
61    // 创建 MapperedStatement 对象,添加到 configuration 中
62    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
63        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
64        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
65        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
66}

该方法主要分为几个部分:

  1. 解析属性
  2. 解析 include 节点
  3. 解析 selectKey 节点
  4. 创建MapperedStatment对象并添加到configuration对应的集合中

解析属性比较简单,接下来看看后面几个部分:

解析 include 子节点

解析include节点就是把其包含的SQL片段替换成 <sql> 节点定义的SQL片段,并将 ${xxx} 占位符替换成真实的参数:

它是使用 XMLIncludeTransformer 类的 applyIncludes 方法来解析的:

 1  public void applyIncludes(Node source) {
 2    // 获取参数
 3    Properties variablesContext = new Properties();
 4    Properties configurationVariables = configuration.getVariables();
 5    if (configurationVariables != null) {
 6      variablesContext.putAll(configurationVariables);
 7    }
 8    // 解析
 9    applyIncludes(source, variablesContext, false);
10  }
11
12  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
13    if (source.getNodeName().equals("include")) {
14      // 这里是根据 ref 属性对应的值去 <sql> 节点对应的集合查找对应的SQL片段,
15      // 在解析 <sql> 节点的时候,把它放到了一个map中,key为namespace+id,value为对应的节点,
16      // 现在要拿 ref 属性去这个集合里面获取对应的SQL片段
17      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
18      // 解析include的子节点<properties>
19      Properties toIncludeContext = getVariablesContext(source, variablesContext);
20      // 递归处理<include>节点
21      applyIncludes(toInclude, toIncludeContext, true);
22      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
23        toInclude = source.getOwnerDocument().importNode(toInclude, true);
24      }
25      // 将 include 节点替换为 sql 节点
26      source.getParentNode().replaceChild(toInclude, source);
27      while (toInclude.hasChildNodes()) {
28        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
29      }
30      toInclude.getParentNode().removeChild(toInclude);
31    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
32      // 处理当前SQL节点的子节点
33      NodeList children = source.getChildNodes();
34      for (int i = 0; i < children.getLength(); i++) {
35        applyIncludes(children.item(i), variablesContext, included);
36      }
37    } else if (included && source.getNodeType() == Node.TEXT_NODE
38        && !variablesContext.isEmpty()) {
39      // 绑定参数
40      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
41    }
42  }

selectKey 就是生成主键,可以不用看。

到这里,mapper.xml 配置文件中的节点已经解析完毕了 除了 resultMap 节点,在文章的开头部分,在解析节点的时候,有时候可能会出错,抛出异常,在解析每个解析抛出异常的时候,都会把该解析放入到对应的集合中再次进行解析,所以在解析完成后,还有如下三行代码:

1    // 处理解析失败的 <resultMap> 节点
2    parsePendingResultMaps();
3    // 处理解析失败的 <cache-ref> 节点
4    parsePendingChacheRefs();
5    // 处理解析失败的 SQL 节点
6    parsePendingStatements();

就是用来从新解析失败的那些节点的。

到这里,Mapper.xml 配置文件就解析完毕了。

本文分享自微信公众号 - Java技术大杂烩(tsmyk0715),作者:TSMYK

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-02-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 了解微信/支付宝支付的相关概念

    银行类服务商与普通类服务商的最大区别在于,普通类服务商不能清算资金,微信将会直接把资金清算给商户账户。

    Java技术大杂烩
  • Mybatis Mapper 接口源码解析

    在使用 Mybatis 的时候,我们只需要写对应的接口,即dao层的Mapper接口,不用写实现类,Mybatis 就能根据接口中对应的方法名称找到 xml 文...

    Java技术大杂烩
  • Mybatis 解析配置文件的源码解析

    使用过Mybatis 的都知道,Mybatis 有个配置文件,用来配置数据源,别名,一些全局的设置如开启缓存之类的, 在Mybatis 在初始化的时候,会加载该...

    Java技术大杂烩
  • 深入Mybatis源码——配置解析

    上一篇分析了Mybatis的基础组件,Mybatis的运行调用就是建立在这些基础组件之上的,那它的执行原理又是怎样的呢?在往下之前不妨先思考下如果是你会怎么实现...

    夜勿语
  • 5.4.3微程序控制器

    微程序控制器采用存储逻辑实现,也就是把微操作信号代码化,使每条机器指令转化成为一段微程序并存入一个专门的存储器(控制存储器)中。微操作控制信号由微指令产生。

    week
  • iGola 三年研发心得 | 如何实现高效的敏捷开发与团队管理?

    编者按 本文为码云 Gitee 的优秀客户案例,iGola 从 2015 年开始便选择码云作为其官方代码托管平台,经历了码云多个版本的迭代并成为企业版的深度用户...

    码云Gitee
  • android里面自定义View实现调用activity的方法怎么做

    在android客户端中我们经常有自己的定义视图,特别是用fragment的时候,用的还是比较多的,有时候很多响应时间都是在VIew里面完成,发现有时候调用ac...

    wust小吴
  • JAVA位运算等运算符总结

    可以将boolean的true和false看作是1和0,这样&和|的意义就是按位与和按位或。

    品茗IT
  • sublime text _注册码

    打开 Sublime Text 3 的 “Help”–“Enter Licence”,然后根据版本选择输入下面的注册码。

    shirayner
  • 学界 | 谷歌 AI 图表示学习最新成果:解决重叠区域描述难题,自动调整超参数

    AI 科技评论按,表示实体之间关系的关系数据在网络(如在线社交网络)和物理世界(如蛋白质交互网络)中随处可见。这些数据可以表示为一个带有节点(如用户、蛋白质)和...

    AI研习社

扫码关注云+社区

领取腾讯云代金券