Mybatis底层源码分析(最详细的版本)

2023-10-27

Mybatis底层源码分析(最详细的版本)

1.概要介绍

  MyBatis 是一款优秀的持久层框架,也是当前最流行的java持久层框架之一,它内部封装了jdbc,使开发
者只需要关注sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。采
用ORM思想解决了实体和数据库映射的问题,对jdbc进行了封装,屏蔽了jdbc api底层访问细节,使我们不用
与jdbc api打交道,就可以完成对数据库的持久化操作。
  Mybatis通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中SQL
的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。

2.总体执行流程

  • 通过getResourceAsReader(String resource)方法读取Resources下的mybatis.xml配置文件转换成Reader
  • 通过构建者的设计模式去生成SqlSessionFactory对象:new SqlSessionFactoryBuilder().build()方法去生成
  • 有了SqlSessionFactory的工厂对象后,就可以通过openSession()去获取DefaultSqlSession对象
  • 然后就可以对数据库进行insert、update、delete、select操作
  • 进行了数据库的相关操作之后,通过sqlSession.commit()进行事务的提交
  • 最后通过sqlSession.close()关闭连接,释放资源

代码如下:

//第一步:读取配置文件转换成Reader
resourceAsReader = Resources.getResourceAsReader("mybatis.xml");
//第二步:通过流找到我们的SqlsessionFactory对象
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsReader);
//第三步:获取SqlSesion对象(会话对象)
SqlSession sqlSession = sqlSessionFactory.openSession();
//第四步:提交事务
sqlSession.commit();
//第五步:关闭连接,释放资源
sqlSession.close();

3. 对各个流程的细分详解

3.1 读取配置文件转换成Reader

public static Reader getResourceAsReader(String resource) throws IOException {
	InputStreamReader reader;
	if (charset == null) {
    reader = new InputStreamReader(getResourceAsStream(resource));
	} else {
	reader = new InputStreamReader(getResourceAsStream(resource), charset);
	}
	return reader;
    }

3.2 使用build()方法生成SQLSessionFactory对象的流程

    public SqlSessionFactory build(Reader reader) {
        return this.build((Reader)reader, (String)null, (Properties)null);
    }

看上去是用的build(Reader reader)方法来构建的对象,但是内部实际上调用的是build(Reader reader,String environment,Properties properties)方法,然后声明了一个XML配置器parser,返回了一个builde(parser.parse());

public SqlSessionFactory build(Reader reader, String environment, Properties
properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, 
            properties);
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            ……;} finally {……}
        return var5;
}
3.2.1 分析build(parser.parse())

这个方法是用来分析配置文件是否被解析过

public Configuration parse() {
	if (this.parsed) {
	   throw new BuilderException("Each XMLConfigBuilder can only be used once.");
	} else {
	this.parsed = true;
	//这个地方表示要解析configuration中所有内容
	this.parseConfiguration(this.parser.evalNode("/configuration"));
	return this.configuration;
	}
}
3.2.2 分析parseConfiguration(…)方法
private void parseConfiguration(XNode root) {
	try {
		//解析配置文件中的properties节点
	   this.propertiesElement(root.evalNode("properties"));
	   //解析settings节点里的所有内容
       Properties settings = this.settingsAsProperties(root.evalNode("settings"));
       /*
		* 这两个方法用于远程加载settings
		*/
       this.loadCustomVfs(settings);
       this.loadCustomLogImpl(settings);
       //解析取别名的配置
	   this.typeAliasesElement(root.evalNode("typeAliases"));
	   //解析插件的地方
	   this.pluginElement(root.evalNode("plugins"));
	   this.objectFactoryElement(root.evalNode("objectFactory"));
	   this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
	   this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
	   //这里是将所有的properties设置到Configuration中并加载一些默认配置
	   this.settingsElement(settings);
	   //解析environments的节点
	   this.environmentsElement(root.evalNode("environments"));
	   //此处是多数据源的配置 但一般不配置
	   this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
	   //这里是用来处理Mybatis的数据类型和java的数据类型的映射关系的 一般也不配置
	   this.typeHandlerElement(root.evalNode("typeHandlers"));
	   //这里最重要! 用来获取mappers节点的内容
	   this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. 
            Cause: " + var3, var3);
        }
    }
3.2.2.1 解析propertiesElement()的过程
 private void propertiesElement(XNode context) throws 
 Exception {
	if (context != null) {
	//获取properties内容下的所有值
	Properties defaults = context.getChildrenAsProperties();
	String resource = context.getStringAttribute("resource");
	String url = context.getStringAttribute("url");
	//判断resource和url的值,两者不能同时存在
	if (resource != null && url != null) {
	……
	}
	if (resource != null) {
		defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
		defaults.putAll(Resources.getUrlAsProperties(url));
	}
	//获取configuration的值
	Properties vars = this.configuration.getVariables();
	if (vars != null) {
	//将configuration中的properties和vars中properties的属性融合到一起
	defaults.putAll(vars);
	}
	this.parser.setVariables(defaults);
	//将所有解析到的properties放到configuration中
	this.configuration.setVariables(defaults);
	}
}
3.2.2.2 解析取别名的过程typeAliasesElement()

最终的结论:是将取好的别名和class放到了configuration中的TypeAliasRegistry这个对象中的typeAliases中

private void typeAliasesElement(XNode parent) {
	if (parent != null) {
		Iterator var2 = parent.getChildren().iterator();
        while(var2.hasNext()) {
        XNode child = (XNode)var2.next();
        String alias;
        if ("package".equals(child.getName())) {
        alias = child.getStringAttribute("name");
        //实际调用的方法是registerAliases(String packageName, Class<?> superType)
        //获取所有需要取别名的类
        this.configuration.getTypeAliasRegistry().registerAliases(alias);
	} else {
		alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
		try {
			Class<?> clazz = Resources.classForName(type);
	        if (alias == null) {
	        	//判断是否有Alias注解
				this.typeAliasRegistry.registerAlias(clazz);
			} else {
				//注册别名,并且此方法可以看到为什么可以区分大小写
				this.typeAliasRegistry.registerAlias(alias, clazz);
			}
		} catch (ClassNotFoundException var7) {
			throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + var7, var7);
			}
		}}}}
3.2.2.3 解析插件的过程pluginElement()
private void pluginElement(XNode parent) throws Exception {
	if (parent != null) {
		Iterator var2 = parent.getChildren().iterator();
        while(var2.hasNext()) {
			XNode child = (XNode)var2.next();
			//获取interceptor属性
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            //通过多态,获取interceptor这个接口的实现类的对象,并设置属性
            Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).
            getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            //将interceptor对象设置到configuration中去
            this.configuration.addInterceptor(interceptorInstance);
		}
	}
}
3.2.2.4 解析environmentsElement()

获取要使用的数据库环境,可以是MySQL、Oracle等,通过比较environments和比较environment的id来判断使用哪种数据库。同时,这里还判断了transactionManager这个节点,通过dataSource获取了数据源,最后采用构建者的设计模式将environment对象放到configuration中。

private void environmentsElement(XNode context) throws Exception {
	if (context != null) {
		if (this.environment == null) {
			this.environment = context.getStringAttribute("default");
		}
		Iterator var2 = context.getChildren().iterator();
        while(var2.hasNext()) {
			XNode child = (XNode)var2.next();
            String id = child.getStringAttribute("id");
            //比较environment的id来判断使用哪种数据库
            if (this.isSpecifiedEnvironment(id)) {
            //这里是判断transactionManager这个节点的
            TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
            DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
            //通过dataSource来获取数据源
            DataSource dataSource = dsFactory.getDataSource();
            Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
            //使用构建者的设计模式来把environment对象放到configuration中
            this.configuration.setEnvironment(environmentBuilder.build());
			}
		}
	}
}
3.2.2.5 解析mapperElement()

这里通过判断配置方式,第一种就是通过包的路径进行配置,第二种就是通过resource中的resource、url、class(三者属性之一)进行配置。

  1. 假设配置的是resource属性,首先通过路径去获取到resource的流,然后通过构建者的设计模式初始化一个专门解析Mapper.xml的解析器,最后通过mapperParser.parse()来解析整个mapper.xml文件
  2. 假设配置的是url属性,首先同样是将url转化为流对象,然后通过构建者的设计模式去初始化一个专门解析Mapper.xml的解析器,最后通过mapperParser.parse()来解析整个mapper.xml文件
  3. 假设配置的是mapperClass属性,首先通过类名去获取到class对象,最后直接将这个class对象配置到configuration中去
  4. 这里需要进一步解析mapperParser.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);
            	//获取Mapper.xml的流 字节流
            	InputStream inputStream = Resources.getResourceAsStream(resource);
            	//初始化一个专门解析Mapper.xml的解析器
            	XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            	//解析整个mapper.xml文件
            	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.");
          }
        }
      }
    }
  }
3.2.2.5.1 mapperParser.parse()方法探讨

首先判断configuration是否已经加载过了,如果加载过了就执行parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();这三个方法,如果没有加载过,则先解析mapper节点,然后将resource配置到configuration中去,然后通过bindMapperForNamespace()方法将mapper接口和命名空间绑定到一起。

mapper节点的解析底层:
解析这个节点时,首先拿到命名空间,然后将其放进builderAssistant对象中去,这里相当于是设置了一个全局的名称,后面这些内容解析的时候会用到这个名称,接着解析cache-ref和cache、parameterMap、resultMap和Sql片段,最后是解析增删改查的地方,通过上下文构建statement对象,真正解析增删改查的方法是buildStatementFromContext(list, null),通过构建者的设计模式生成了一个statementParser对象专门用来解析增删改查标签,parseStatementNode()通过增删改查各个标签的属性进行解析与设置,最终通过构造者模式添加到builderAssistant对象中,这是用来构建statement对象的,最重要的是addMappedStatement()这个方法,首先判断是不是select的方法,因为select方法涉及到缓存,判断完了之后,通过构建者模式构建statement对象,并把它添加到MappedStatement类型的statement变量中,最终将statement对象放到configuration这个对象中,就是将所有的增删改查的标签都解析成了一个statement对象,放到了configuration这个对象中)

bindMapperForNamespace()方法的底层解析:
首先通过builderAssistant.getCurrentNamespace()获取到命名空间(由于是接口的实现,所以这里的命名空间可以是包路径,用于后面反射获取当前接口),然后获取命名空间的class对象,然后将这个Mapper接口的class对象添加进configuration中。

  /**
   * 解析Mapper.xml文件的位置
   */
  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      //解析mapper节点下的所有东西
      configurationElement(parser.evalNode("/mapper"));

      //这里将resource配置到configuration
      configuration.addLoadedResource(resource);

      //下面这句话的意思很重要就是 我们绑定Mapper接口和命名空间之间这个纽带
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
  /**
   * 解析增删改查的节点
   * @param context
   */
  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");
      }
      //将命名空间放到builderAssistant这个对象中去
      builderAssistant.setCurrentNamespace(namespace);

      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));

      //这句话才是解析增删改查的地方1
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
 /**
   * 解析增删改查的地方2
   * @param list
   * @param requiredDatabaseId
   */
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {

      //初始化了一个解析 增删改查标签的这个解析器
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        //这里就是解析数据的地方2
        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;
    }

    String nodeName = context.getNode().getNodeName();
    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);

    // 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");

    //这里才是主角
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

  /**
   * 将mapper和命名空间绑定到一起的地方
   */
private void bindMapperForNamespace() {
    //获取咋们的命名空间
    String namespace = builderAssistant.getCurrentNamespace();

    if (namespace != null) {
      Class<?> boundType = null;
      try {
        //这句话是啥意思? Class.forName("")
        //获取命名空间的class对象
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //如果上面出现了异常 说明我们的命名空间是随便写的一个值
        //如果是随便写的 那么这里就不执行
        //ignore, bound type is not required
      }

      //说明我们的命名空间设置的是 Mapper接口的全路径 随意这里就需要进行处理
      if (boundType != null) {

        //下面判断这个Configuration中是否已经绑定了这个Mapper对象
        //如果是没有绑定过这个对象
        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);

          //添加我们的Mapper对象
          // boundType :这个是Mapper接口的class对象
          configuration.addMapper(boundType);
        }
      }
    }
  }

3.3 使用getSqlSession()获取到sqlSession对象

3.4 通过sqlSession对象的getMapper(UserMapper.class) 这里使用User来举例

3.4.1 getMapper(UserMapper.class) 方法分析:

configuration.getMapper(type, this)根据传入的接口class对象来获取mapper,上面的方法是由mapperRegistry.getMapper(type, sqlSession)返回的mapper代理类对象来的。

如何生成的代理类对象?
首先knownMappers.get(type)根据对应的class类型,来找对应的MapperProxyFactory,然后通过mapperProxyFactory.new instance(sqlSession)来返回接口的实现代理类对象。

 /**
 1. 获取Mapper接口的对象 ?
 2. @param type
 3. @param sqlSession
 4. @param <T>
 5. @return
   */
  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {

    //获取MapperProxyFactory对象
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    //如果没有获取到 那么说明 这个对象是有问题的
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      //最终返回我们的接口的对象的地方在这里
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

3.5 通过sqlSession.insert(“UserMapper.addUser”,user)开始数据库操作(这里举例insert)delete update select以此类推

sqlSession.insert(“UserMapper.addUser”,user)源码分析:
sqlSession.insert(“UserMapper.addUser”,user)最终是调用的update(String statement, Object parameter)方法,执行sql,statement:namespace.id,parameter:执行sql所需要的参数

  /**
   * 执行到了这里
   * @param statement Unique identifier matching the statement to execute.
   * @param parameter A parameter object to pass to the statement.
   * @return
   */
  @Override
  public int insert(String statement, Object parameter) {
    //最终调用的是 update方法
    return update(statement, parameter);
  }

⬇️ 先设置dirty,然后通过configuration.getMappedStatement()获取所有增删改查标签封装的mapperedStatement对象,最后通过执行器去执行executor.update方法

@Override
public int update(String statement, Object parameter) {
  try {
    dirty = true;

    //  statement:命名空间.id
    // MappedStatement:解析Mapper.xml文件之后 将增删改差的每一组标签弄成了MappedStatement对象
    //最终以 命名空间.id为key  MappedStatement为值放到了 Configuration中的
    //拿出来的目的是干嘛? 找到SQL语句 找到参数....  执行SQL语句
    MappedStatement ms = configuration.getMappedStatement(statement);

    //执行代码
    // 返回的Executor是 SimpleExecutor
    //  wrapCollection(parameter)  //就是将你传输的参数 弄成SQL中的集合  处理请求的参数
    return executor.update(ms, wrapCollection(parameter));
  } catch (Exception e) {……} finally {……}
}

⬇️ 这里调用doUpdate方法,先获取configuration这个对象,然后通过configuration.newStatementHandler获取处理器,再获取prepareStatement对象
(获取的prepareStatement对象是在SimpleExecutor类中的prepareStatement调用父类BaseExecutor来生成的),最后通过handler.update(stmt)去执行sql语句

 @Override
 public int update(MappedStatement ms, Object parameter) throws SQLException {
   ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
   if (closed) {
     throw new ExecutorException("Executor was closed.");
   }
   clearLocalCache();
   return doUpdate(ms, parameter);
 }

/**
  * 所以最终执行到这里来了
  * @param ms 这个里面包含了SQL语句
  * @param parameter:这个里面是SQL语句执行需要的参数
  * @return
  * @throws SQLException
  */
 @Override
 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {

   //JDBC中的那个东西
   Statement stmt = null;

   try {
     //从statement中获取到整个解析的后存放数据的类
     Configuration configuration = ms.getConfiguration();

     //handler:xxx处理器
     // statement:这里玩的是 statement的处理器
     // this:SimpleExecutor
     // ms :MappedStatement
     // parameter:SQL需要的数据

     //返回的对象 RoutingStatementHandler
     StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);


     //获取到了 Statement对象
     // stmt :statement?  PrepareStatment?  CallableStatement ?
     // 这里返回的是PrepareStatment对象
     stmt = prepareStatement(handler, ms.getStatementLog());

     //执行SQL语句
     return handler.update(stmt);
   } finally {
     closeStatement(stmt);
   }
 }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Mybatis底层源码分析(最详细的版本) 的相关文章

随机推荐

  • jquery html方法xss,jQuery DOM方法中的XSS漏洞演示

    HTML 导入代码模板 XSS vulnerabilities in jQuery DOM methods Input Output native innerHTML The HTML5 spec states that script ta
  • 3.荔枝派 zero(全志V3S)-制作linux烧录镜像

    上面是我的微信和QQ群 欢迎新朋友的加入 目录 1 安装工具 2 生成新的img文件 3 分割虚拟磁盘 4 挂载虚拟磁盘并格式化 5 开始备份 6 卸载虚拟磁盘 7 烧录测试 最近学习linux 发现烧录镜像都有点麻烦 例如荔枝派 需要先用
  • 文件上传 相关知识

    文件上传 参考文章 平井缘 要点 1 示例一个 FormData 对象 要点 2 将上传时获取到的 file 文件 append 到 formdata 对象中 要点 3 配置上传接口的 请求头 方式一 表单提交文件 原生
  • JS Es6中判断b数组对象是否有跟a数组对象相同的数值(例如:id),有的话就过滤掉

    如下 数组 对象a和b let a id 1 value this id 2 value is let b id 1 value hello id 3 value world filter 方法创建一个新的数组 新数组中的元素是通过检查指定
  • 【数据分析】初识 AB 测试

    初识 AB 测试 1 简述 AB 测试 AB 测试是指为了评估模型 项目的效果 在 APP PC 端同时设计多个版本 在同一时间维度下 分别让组成成分相同 相似 的访客群组随机访问这些版本 收集各群组的用户体验数据和业务数据 最后分析评估出
  • Maven解决静态资源过滤问题

    前言 在我们使用Maven构建项目的时候 会默认过滤掉静态资源 所以 需要手动来配置 一 认识静态资源与动态资源 静态资源 包含HTMl 图片 CSS JS等不需要与数据库交互的一类文件 动态资源 需要与数据库交互 可以根据需要显示不同的数
  • 选择文件后自动上传文件' aria-label='选择文件后自动上传文件'> 选择文件后自动上传文件

    想要一个选择了文件就自动上传的效果 但之前的
  • 计算机毕业设计Node.js+Vue基于Java网络游戏后台管理系统(程序+源码+LW+部署)

    该项目含有源码 文档 程序 数据库 配套开发软件 软件安装教程 欢迎交流 项目运行 环境配置 Node js Vscode Mysql5 7 HBuilderX Navicat11 Vue Express 项目技术 Express框架 No
  • steam"无法连接到更新服务器"的问题

    问题现象如下图所示 在打开steam游戏时出现了上述问题 无法正常游戏 不光如此steam官网也无法正常显示 只有部分文字和图片 样式缺失 打开chrome的F12开发者工具 进入network模块监视可发现大量的请求并没有被响应 甚至没有
  • SQL语句执行顺序

    首先了解一下sql语句的执行步骤 1 语法分析 分析语句的语法是否符合规范 衡量语句中各表达式的意义 2 语义分析 检查语句中涉及的所有数据库对象是否存在 且用户有相应的权限 3 视图转换 将涉及视图的查询语句转换为相应的对基表查询语句 4
  • 软件测试必看!5分钟掌握sql查询的聚合函数

    数据查询操作之排序 语法格式 select from 表名 order by 字段名 asc desc 重点 1 字段名可以有多个 如果字段名1 相同 再按照字段名2排序 2 默认情况下按照从小到大去排列 3 asc 就是从小到大排列 de
  • 【图论】—— 有向图的强连通分量

    给定有向图 若存在 满足从 出发能到达 中所有的点 则称 是一个 流图 Flow Graph 记为 其中 称为流图的源点 在一个流图 上从 进行深度优先遍历 每个点只访问一次 所有发生递归的边 换言之 从 到 是对 的第一次访问 构成一棵以
  • Java中Juc并发编程基础

    1 什么是JUC 就是java util concurrent并发包下面使用的工具包 1 1 线程和进程 进程 是一个程序 QQ exe 网易云音乐 大数据领域的NameNode其实就是程序的集合 一个进程往往可以包含多个线程 至少包含一个
  • 同步和异步

    同步和异步通常用来形容方法的调用方式 同步方法表明调用一旦开始 调用者必须等待方法执行完成 才能继续执行后续方法 异步方法表明 方法一旦开始 立即返回 调用者无需等待其中方法执行完成 就可以继续执行后续方法 通常我们写的方法都是同步方法 方
  • 使用Nginx解决跨域问题

    目录 使用Nginx解决跨域问题 1 修改浏览器 客户端访问地址 2 在nginx conf配置文件需配置server 3 在Nginx中配置客户端访问的接口 按照规则或通配 并设置被代理的服务器 4 在Nginx中统一配置客户端访问的头部
  • java实时获取汇率

    1 分享三个觉得挺不错的汇率api 1 每小时免费50次查询配额 NOWapi 2 0 1元2000次 年 阿里云 汇率api 3 每天免费100次查询配额 需要实名认证 聚合科技 如果只是针对很少外币获取汇率的话 个人推荐去阿里云购买 毕
  • Java中使用Jar包时读取当前jar文件所在的目录工具

    在实际使用中 jar包所放的位置是不一定的所以要动态获取当前目录 package com gj5u publics util import java io File 获取打包后jar的路径信息 author Rex public class
  • angularJs摸态框实例加详细注解

  • mos 多路模拟电子开关_同步四开关 BuckBoost 180W 模块电源

    点击上方 21Dianyuan 关注我们 本文是 21Dianyuan 社区 原创 技术文章 作者 xueyiranpiao 感谢作者的辛苦付出 本电源主要应用于电池充电 电池为8串磷酸铁锂 10000mAH 0 6C 充电 也可以用于其它
  • Mybatis底层源码分析(最详细的版本)

    Mybatis底层源码分析 最详细的版本 1 概要介绍 MyBatis 是一款优秀的持久层框架 也是当前最流行的java持久层框架之一 它内部封装了jdbc 使开发 者只需要关注sql语句本身 而不需要花费精力去处理加载驱动 创建连接 创建