百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Mybatis源码框架核心-宏观(mybatis 源码分析)

bigegpt 2024-08-03 11:47 1 浏览

Mybatis源码

下载地址:github.com/mybatis/myb…

我的版本:3.5.4分支,IDEA:2018.3.6

Mybatis编译:

先下载mybatis的依赖工程:github.com/mybatis/par…

然后切换到该依赖目录下执行:mvn clean install

然后到该目录下查看版本pom.xml:


然后进入mybatis目录下,修改pom.xml中的版本号,以及依赖的parent相对路径

这两个要对应,只要一样即可,无所谓谁对应谁,不过如果是改动parent的版本号记得重新编译一下,不改会不会报错就不知道了。还要注意保证同级目录

然后编译报错:

这是本身pom.xml中的pdf插件导致,注释掉它



再次编译

撒花成功。

1. 传统JDBC的弊端

  1. JDBC底层没有使用连接池,操作数据库需要频繁的创建和关联链接,在并发很大的场景中,资源开销很大;
  2. 写原生的jdbc代码在java中,一旦修改某个sql,需要重新编译java,不利于系统的维护;
  3. 使用PreparedStatement预编的方式对sql中的变量复制,通过下标设置1,2,3,一旦sql语句变量很多,这样的序号不利于维护;
  4. 返回result结果集也需要硬编码

2. ORM框架Mybatis介绍

MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use simple XML or Annotations for configuration and map primitives, Map interfaces and Java POJOs (Plain Old Java Objects) to database records.

MyBatis 是一款优秀的持久层框架(Object Relational Mapping),它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

3. Mybatis链接

  1. 官网参考:mybatis.org/mybatis-3/g… 直接用mybatis中test的测试用例

1. Mybatis连接数据库

  1. 前置环境: 数据库源: driver:驱动 url:连接ip username:数据库user password:密码 执行语句 select insert update delete 对数据库操作 connection PrepareStatement ResultSet


2. Mybatis是如何获取到数据库源

  1. 测试用例说明参见:www.cnblogs.com/liuzhiyong0…我截图如下:


创建与数据库对应的POJO(数据自己创建一个简单的做测试即可,主要目的是打断点看源码)

package org.apache.ibatis.anzhitestmybatis;

public class MyBook {
  private String username;
  private double balance;


  public MyBook() {
  }

  public MyBook(String username, double balance) {
    this.username = username;
    this.balance = balance;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public double getBalance() {
    return balance;
  }

  public void setBalance(double balance) {
    this.balance = balance;
  }

  @Override
  public String toString() {
    return "MyBook{" +
      "username='" + username + '\'' +
      ", balance=" + balance +
      '}';
  }
}
复制代码
  • 接口 package org.apache.ibatis.anzhitestmybatis; import org.apache.ibatis.annotations.Param; public interface MyBookMapper { public MyBook selectBook(double balance); } 复制代码
  • mybatis-config.xml全局配置文件 <?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> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"> </transactionManager> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/demo"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml"/> </mappers> </configuration> 复制代码
  • 映射文件配置: 注意几个点:namespace是我们映射的mapper接口的全路径 id是方法,resultType是返回的类型 <?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="org.apache.ibatis.anzhitestmybatis.MyBookMapper"> <select id="selectBook" resultType="org.apache.ibatis.anzhitestmybatis.MyBook"> select * from account where balance = #{balance} </select> </mapper> 复制代码
  • 测试类 package org.apache.ibatis.anzhitestmybatis; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.jupiter.api.Test; import java.io.InputStream; import java.io.Reader; public class MainTest { public static void main(String[] args) throws Exception{ //1、创建SqlSessionFactory String resource = "org/apache/ibatis/anzhitestmybatis/mybatis-config.xml"; InputStream stream = Resources.getResourceAsStream(resource); SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(stream); stream.close(); //2、获取sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); System.out.println(sqlSession); //3、获取mapper MyBookMapper mapper = sqlSession.getMapper(MyBookMapper.class); //4、执行数据库操作,并处理结果集 MyBook goods = mapper.selectBook(200); System.out.println(goods); } } 复制代码
  • 开始测试: //这是jdk中的IO流,与我们的mybatis框架无关 InputStream stream = Resources.getResourceAsStream(resource); //从这里开始,mybatis开动 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(stream); 复制代码 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(stream); //进入该方法中 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //创建XMLConfigBuilder对象解析xml配置文件 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. } } } 复制代码 build(parser.parse()); //开始解析 public Configuration parse() { //保证单线程访问 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //解析后封装成一个 parseConfiguration(parser.evalNode("/configuration")); return configuration; } 复制代码 parser.evalNode("/configuration")返回一个XNode类型,交给parseConfiguration进一步解析,即xml中的标签属性 public XNode evalNode(String expression) { return evalNode(document, expression); } 复制代码 parseConfiguration,可以看到xml各标签的属性,也知道为什么再xml配置时,标签的约束规则为什么是这样的 private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 复制代码 进入这里后,通过Evaluate查看root此时的属性,右键root,然后点击Evaluate,如图:


  • 再右键拷贝value值:

    1. <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"> </transactionManager> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/demo"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml"/> </mappers> </configuration> 复制代码 此时mybatis-config.xml已经被解析出来 那么数据源是如何载入的呢? environmentsElement(root.evalNode("environments")); 复制代码 private void environmentsElement(XNode context) throws Exception { ...... //获取dataSource标签的属性,解析数据源,转化为对象 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); ...... } 复制代码 dataSourceElement(child.evalNode("dataSource")); private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null) { String type = context.getStringAttribute("type"); Properties props = context.getChildrenAsProperties(); //转化为类对象,一直点resolveClass,往上翻,在TypeAliasRegistry中包含了各种类型 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory."); } 复制代码 ... registerAlias("float", Float.class); registerAlias("boolean", Boolean.class); registerAlias("byte[]", Byte[].class); registerAlias("long[]", Long[].class); ... 复制代码 至此数据源获取完毕。

    1. 小结:

    获取数据库源过程:

    org.apache.ibatis.session.SqlSessionFactoryBuilder.build(java.io.InputStream)

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.parse

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.environmentsElement

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.dataSourceElement

    》org.apache.ibatis.session.Configuration.setEnvironment#######

    4. Mybatis是如何拿到Sql语句

    上面我们知道parseConfiguration可以解析并拿到xml的标签属性,所以再次回到parseConfiguration这里,再次把它贴出来

    private void parseConfiguration(XNode root) {
        try {
    .......
            //数据源获取
            environmentsElement(root.evalNode("environments"));
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            typeHandlerElement(root.evalNode("typeHandlers"));
            //mapper映射
            mapperElement(root.evalNode("mappers"));
    .......
    }
    复制代码

    mapperElement(root.evalNode("mappers"));将MyBookMapper.xml加载出来,那么MyBookMapper加载有几种方式呢? 4种

    <mappers>
        <mapper resource="org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml"/>    
    </mappers>
    ---------------
    <!ELEMENT mappers (mapper*,package*)>
    
    <!ELEMENT mapper EMPTY>
    <!ATTLIST mapper
    resource CDATA #IMPLIED   //1
    url CDATA #IMPLIED        //2
    class CDATA #IMPLIED      //3
    >
    <!ELEMENT package EMPTY>   
    <!ATTLIST package         //4
    name CDATA #REQUIRED
    >
    复制代码

    来看一下mapperElement实现:

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                if ("package".equals(child.getName())) { //package优先级第一
                    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());
                        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.");
                    }
                }
            }
        }
    }
    复制代码

    new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());

    mapperParser.parse(); //实现mapper配置文件的解析

    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
            configurationElement(parser.evalNode("/mapper"));
            configuration.addLoadedResource(resource);
            bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    复制代码

    configurationElement(parser.evalNode("/mapper")); 此时获取结果如下

    <mapper namespace="org.apache.ibatis.anzhitestmybatis.MyBookMapper">
        <select resultType="org.apache.ibatis.anzhitestmybatis.MyBook" id="selectBook">
            select * from account where balance = #{balance}
        </select>
    </mapper>
    
    复制代码

    那么此时如何解析sql语句呢?

    看一下configurationElement实现

    private void configurationElement(XNode context) {
    .........
        	//sql语句的解析
            buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    ..........
    }
    复制代码

    看一实现

    private void buildStatementFromContext(List<XNode> list) {
      if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
      }
      buildStatementFromContext(list, null); //表明可以有多个sql语句
    }
    复制代码

    buildStatementFromContext实现,sql语句解析

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
      for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
          statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
          configuration.addIncompleteStatement(statementParser);
        }
      }
    }
    复制代码

    statementParser.parseStatementNode()

      public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
    
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        //select 属性解析
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        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;
        }
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
          resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        //最终将select属性在这里填充
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    复制代码

    addMappedStatement

    //构建者设计模式
    public MappedStatement addMappedStatement(
        String id,
        SqlSource sqlSource,
        StatementType statementType,
    
        ..........
    )
    .........
    
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    
    复制代码

    封装成一个类MappedStatement属性:可以发现,一个标签对应一个封装类

    public final class MappedStatement {
    
    ........
      private StatementType statementType;
      private ResultSetType resultSetType;
      private SqlSource sqlSource;
      private Cache cache;
      private ParameterMap parameterMap;
      private List<ResultMap> resultMaps;
      private boolean flushCacheRequired;
      private boolean useCache;
    .........
    复制代码

    来看一下getMappedStatementNames实现:最终实现sql解析完成

    public Collection<String> getMappedStatementNames() {
      buildAllStatements();
      return mappedStatements.keySet();
    }
    复制代码

    至此sql获取完成

    1. 小结

    获取执行语句源码过程:

    org.apache.ibatis.session.SqlSessionFactoryBuilder.build(java.io.InputStream)

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.parse

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration

    》org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement

    》org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement

    》org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode

    》org.apache.ibatis.session.Configuration.addMappedStatement######

    5. Mybatis是如何获取结果

    1. sqlSessionFactory.openSession(); public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } 复制代码
    2. openSessionFromDataSource() private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { //获取前面设置好的数据源 final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 复制代码 如图:



    1. configuration.newExecutor(tx, execType); public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; } -------------------- ExecutorType 执行器,类型: public enum ExecutorType { SIMPLE, REUSE, BATCH } 复制代码 最终完成创建一个执行器的过程

    1. 执行查询语句

    sqlSession.selectOne()

    public <T> T selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) { //当结果大于1的时候,抛出异常
            throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }
    复制代码

    List list = this.selectList(statement, parameter);

    public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }
    复制代码

    selectList(statement, parameter, RowBounds.DEFAULT)

    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      try {
      	//前面添加的addMappedStatement,在这里获取getMappedStatement
        MappedStatement ms = configuration.getMappedStatement(statement);
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    复制代码

    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        //创建缓存
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    
    复制代码

    query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
    
        //获取缓存,默认二级缓存
        Cache cache = ms.getCache();
        if (cache != null) {
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        
        //装饰器模式
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    复制代码

    delegate.query()

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                //第一次查询没有,所以到数据库中查询
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
    复制代码

    Executor:

    queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql)

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ......
      try {
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
      } finally {
        localCache.removeObject(key);
      }
    ......
    }
    复制代码

    doQuery(ms, parameter, rowBounds, resultHandler, boundSql)

    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            //判断sql类型
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = prepareStatement(handler, ms.getStatementLog());
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
    
    复制代码

    newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    
        switch (ms.getStatementType()) {
            case STATEMENT:
                delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case PREPARED:
                delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case CALLABLE:
                delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            default:
                throw new ExecutorException("Unknown statement type: " + ms.getStatementTyp());
        }
    
    }
    复制代码

    prepareStatement(handler, ms.getStatementLog());

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
      Statement stmt;
      //获取连接
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);
      return stmt;
    }
    复制代码

    经过一系列的querry方法处理后最终调用下面这querry,终于活成我们想要的样子,jdbc,最终底层还是由jdbc完成

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
      //jdbc,最终底层还是由jdbc完成
      PreparedStatement ps = (PreparedStatement) statement;
      ps.execute();
      return resultSetHandler.handleResultSets(ps);
    }
    复制代码

    handleResultSets()

    public List<Object> handleResultSets(Statement stmt) throws SQLException {
    .......
      int resultSetCount = 0;
      ResultSetWrapper rsw = getFirstResultSet(stmt);
    .......
    复制代码

    getFirstResultSet

    private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
        ResultSet rs = stmt.getResultSet();
        while (rs == null) {
            // move forward to get the first resultset in case the driver
            // doesn't return the resultset as the first result (HSQLDB 2.1)
            if (stmt.getMoreResults()) {
                rs = stmt.getResultSet();
            } else {
                if (stmt.getUpdateCount() == -1) {
                    // no more results. Must be no resultset
                    break;
                }
            }
        }
        return rs != null ? new ResultSetWrapper(rs, configuration) : null;
    }
    复制代码

    ResultSetWrapper(rs, configuration)

    public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
      super();
      this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
      this.resultSet = rs;
      final ResultSetMetaData metaData = rs.getMetaData();
      //获取数据库信息
      final int columnCount = metaData.getColumnCount();
      for (int i = 1; i <= columnCount; i++) {
        columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
        jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
        classNames.add(metaData.getColumnClassName(i));
      }
    }
    复制代码

    最终体现出mybatis ORM框架


    作者:YuJian
    链接:https://juejin.cn/post/6979169416747581477
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    1. 那么最终是如何拿到这些数据的呢? handleResultSet(rsw, resultMap, multipleResults, null); private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException { try { if (parentMapping != null) { handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping); } else { if (resultHandler == null) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); multipleResults.add(defaultResultHandler.getResultList()); } else { handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); } } } finally { // issue #228 (close resultsets) closeResultSet(rsw.getResultSet()); } } # ------------------------------- private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); //获取内容 Object rowValue = getRowValue(rsw, discriminatedResultMap, null); storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } } 复制代码
    2. getRowValue private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; if (shouldApplyAutomaticMappings(resultMap, false)) { //赋值 foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; foundValues = lazyLoader.size() > 0 || foundValues; rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; } return rowValue; } 复制代码 applyPropertyMappings() private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if (!autoMapping.isEmpty()) { //对象多个属性循环赋值 for (UnMappedColumnAutoMapping mapping : autoMapping) { final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); if (value != null) { foundValues = true; } if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) { // gcode issue #377, call setter on nulls (value is not 'found') metaObject.setValue(mapping.property, value); } } } return foundValues; } 复制代码 getResult(rsw.getResultSet(), mapping.column); @Override public T getResult(ResultSet rs, String columnName) throws SQLException { try { return getNullableResult(rs, columnName); } catch (Exception e) { throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e); } } --------------- @Override public String getNullableResult(ResultSet rs, String columnName){ throws SQLException { return rs.getString(columnName); //最终还是通过JDBC实现 } 复制代码

    2. 小结:


    通过一系列方法解析xml文件(磁盘中)最终生成sqlSession对象(java对象)内存中,然后再通过方法调用查询磁盘,最后将结果返回给sqlSession

    执行操作源码过程:

    org.apache.ibatis.session.defaults.DefaultSqlSessionFactory.openSession() 》org.apache.ibatis.session.Configuration.newExecutor(org.apache.ibatis.transaction.Transaction,org.apache.ibatis.session.ExecutorType) 》org.apache.ibatis.executor.SimpleExecutor 》org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(java.lang.String, java.lang.Object) 》org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(java.lang.String, java.lang.Object) 》org.apache.ibatis.executor.CachingExecutor.query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler) 》org.apache.ibatis.executor.CachingExecutor.query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql) 》org.apache.ibatis.executor.BaseExecutor.queryFromDatabase 》org.apache.ibatis.executor.SimpleExecutor.doQuery 》org.apache.ibatis.executor.statement.PreparedStatementHandler.query

    最终通过handleResultSets返回结果

    3. Annoation注解的方式

    此时不需要MyBookMapper-config.xml配置映射,直接注释

    MyBookMapper-config.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="org.apache.ibatis.anzhitestmybatis.MyBookMapper">
    
    <!-- 注释掉,否则会有两个MyBookMapper产生无法区分,报错如下
    Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for org.apache.ibatis.anzhitestmybatis.MyBookMapper.selectBook. please check org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml and org/apache/ibatis/anzhitestmybatis/MyBookMapper.java (best guess)
    -->
    <!--
      <select id="selectBook" resultType="org.apache.ibatis.anzhitestmybatis.MyBook">
          select * from account where balance = #{balance}
        </select>
    -->
    </mapper>
    复制代码

    mybatis-config.xml配置

    <mappers>
        <!--<mapper resource="org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml"/>-->
        <mapper class="org.apache.ibatis.anzhitestmybatis.MyBookMapper"></mapper>
    </mappers>
    复制代码

    MapperBook

    public interface MyBookMapper {
      @Select("SELECT * FROM account WHERE balance = #{balance}")
      MyBook selectBook(double balance);
    }
    复制代码

    两个问题:

    1. 为什就可以识别MyBookMapper
    2. 调用selectBook与调用Mybatis中的selectone效果一样,怎么实现的? 在MapperAnnotationBuilder中的parse方法中打断点:查看调用栈

    最终通过handleResultSets返回结果

    3. Annoation注解的方式

    此时不需要MyBookMapper-config.xml配置映射,直接注释

    MyBookMapper-config.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="org.apache.ibatis.anzhitestmybatis.MyBookMapper">
    
    <!-- 注释掉,否则会有两个MyBookMapper产生无法区分,报错如下
    Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for org.apache.ibatis.anzhitestmybatis.MyBookMapper.selectBook. please check org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml and org/apache/ibatis/anzhitestmybatis/MyBookMapper.java (best guess)
    -->
    <!--
      <select id="selectBook" resultType="org.apache.ibatis.anzhitestmybatis.MyBook">
          select * from account where balance = #{balance}
        </select>
    -->
    </mapper>
    复制代码

    mybatis-config.xml配置

    <mappers>
        <!--<mapper resource="org/apache/ibatis/anzhitestmybatis/MyBookMapper.xml"/>-->
        <mapper class="org.apache.ibatis.anzhitestmybatis.MyBookMapper"></mapper>
    </mappers>
    复制代码

    MapperBook

    public interface MyBookMapper {
      @Select("SELECT * FROM account WHERE balance = #{balance}")
      MyBook selectBook(double balance);
    }
    复制代码

    两个问题:

    1. 为什就可以识别MyBookMapper
    2. 调用selectBook与调用Mybatis中的selectone效果一样,怎么实现的? 在MapperAnnotationBuilder中的parse方法中打断点:查看调用栈

    经过多态调用,最终回到了前面提到过的四种级别的判断

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                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());
                        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.");
                    }
                }
            }
        }
    }

    最终得到了和调用selectOne方法一样的结果

    如何知道是MapperAnnotationBuilder中parse方法呢? 两种方式: 1. 逆推法: 因为是使用注解的方法,所以直接到我们自定义的@Select注解语句中,点击@Select

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Select {
      /**
       * Returns an SQL for retrieving record(s).
       *
       * @return an SQL for retrieving record(s)
       */
      String[] value();
    }
    复制代码

    此时然并卵,哈哈,此时再点击Select查看那些方法使用了Select

    点击MapperAnnotationBuilder查看:看一看到有对sql的类型定义

    public class MapperAnnotationBuilder {
        ......
    
            static {
            SQL_ANNOTATION_TYPES.add(Select.class);
            SQL_ANNOTATION_TYPES.add(Insert.class);
            SQL_ANNOTATION_TYPES.add(Update.class);
            SQL_ANNOTATION_TYPES.add(Delete.class);
    
            SQL_PROVIDER_ANNOTATION_TYPES.add(SelectProvider.class);
            SQL_PROVIDER_ANNOTATION_TYPES.add(InsertProvider.class);
            SQL_PROVIDER_ANNOTATION_TYPES.add(UpdateProvider.class);
            SQL_PROVIDER_ANNOTATION_TYPES.add(DeleteProvider.class);
            ......
                //调用parse方法解析,而xml的解析也是由该方法解析
                public void parse() {
                String resource = type.toString();
                if (!configuration.isResourceLoaded(resource)) {
                    loadXmlResource();
                    configuration.addLoadedResource(resource);
                    assistant.setCurrentNamespace(type.getName());
                    parseCache();
                    parseCacheRef();
                    Method[] methods = type.getMethods();
                    for (Method method : methods) {
                        try {
                            // issue #237
                            if (!method.isBridge()) {
                                parseStatement(method);
                            }
                        } catch (IncompleteElementException e) {
                            configuration.addIncompleteMethod(new MethodResolver(this, method));
                        }
                    }
                }
                parsePendingMethods();
            }
    
        }
    复制代码

    此时在parse的方法的任意一处打断点,debug后,查看调用栈:




    点击去查看,发现该方法是一个多态实现;没办法确定,再看mapperElement,发现又回到了Node解析这一步:此时我们的mybatis配置映射是以class的方式,所以执行if (resource == null && url == null && mapperClass != null)

    //四种标签判断方式
    private void mapperElement(XNode parent) throws Exception {
      if (parent != null) {
        for (XNode child : parent.getChildren()) {
          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());
              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.");
            }
          }
        }
      }
    }

    比较快捷的方法:

    在建立sqlSession之后,通过getMapper方法获取映射对象,那么在这个方法中肯定有相关操作,点进去查看:

    <T> T getMapper(Class<T> type);
    复制代码


    点击DefaultSqlSession查看,

    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);  //肯定向里面添加,然后查看那些方法调用了该方法
    }
    @Override
    public <T> T getMapper(Class<T> type) {
      return configuration.getMapper(type, this);  //在这之上还有个addMapper,点击查看
    }


    1. 又找到了XMLMapperBuilder,点击查看 最终同样到了XMLConfigBuilder下的mapperElement方法。

    4. 总结Mybatis xml和annotation优缺点

    Annotaion方式:

    1. 不适合比较复杂的sql 比如关联查询
    2. 不方便(收集)管理sql

    Xml方法:

    1. xml繁琐、麻烦
    2. 条件不确定的查询
    3. 容易出错写错,特殊字符转义 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#getSqlSourceFromAnnotations


    作者:YuJian
    链接:https://juejin.cn/post/6979169416747581477
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



    相关推荐

    得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

    一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

    warm-flow新春版:网关直连和流程图重构

    本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

    扣子空间体验报告

    在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

    spider-flow:开源的可视化方式定义爬虫方案

    spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

    solon-flow 你好世界!

    solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

    新一代开源爬虫平台:SpiderFlow

    SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

    通过 SQL 训练机器学习模型的引擎

    关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

    鼠须管输入法rime for Mac

    鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

    Go语言 1.20 版本正式发布:新版详细介绍

    Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

    iOS 10平台SpriteKit新特性之Tile Maps(上)

    简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

    程序员简历例句—范例Java、Python、C++模板

    个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

    Telerik UI for iOS Q3 2015正式发布

    近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

    ios使用ijkplayer+nginx进行视频直播

    上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

    IOS技术分享|iOS快速生成开发文档(一)

    前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

    macOS下配置VS Code C++开发环境

    本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...