Mybatis源码学习笔记(四)之Mybatis执行增删改查方法的流程解析

news2024/11/30 2:49:29

1 Mybatis流程解析概述

Mybatis框架在执行增伤改的流程基本相同, 很简单,这个大家只要自己写个测试demo跟一下源码,基本就能明白是怎么回事,查询操作略有不同, 这里主要通过查询操作来解析一下整个框架的流程设计实现。

2 Mybatis查询操作的基本流程

2.1 Mapper代理对象MapperProxy的创建流程

在这里插入图片描述
Mapper(实际上是一个代理对象)是从SqlSession中获取的, 因为SqlSession的实现类DefaultSqlSession类中存在Configuration类对象, 而我们在创建sqlSession对象的操作完成之后, configuration对象中已经存储了所有的mapper及其对应的代理对象之间的映射关系,如下图:
在这里插入图片描述
在这里插入图片描述
看看mapperRegistry如下图:
在这里插入图片描述
MapperRegistry类中创建代理对象的核心步骤如下
在这里插入图片描述
MapperProxyFactory类中的newInstance方法的调用
在这里插入图片描述
这里通过动态代理创建出目标mapper层的代理类对象并返回。

2.2 创建Mapper代理对象MapperProxy的调用流程

总结getMapper(Class clazz)方法创建动态代理对象的调用逻辑如下图:
在这里插入图片描述

2.3 动态代理对象执行增删改查的核心流程

这里我们使用查询接口来进行流程剖析

在应用层, 我们会调用下面这行代码来实现查询, 现在我们来分析一下这行代码在Mybatis的源码中是如何执行的, 它究竟是如何实现查询我们想要的数据的功能的

List<LibBook> libBooks = mapper.selectBooksByCondition("T311.5/3-1", null, null, null);

动态代理大家肯定都熟悉, 如果你要看源码, 连这点儿基础都没有, 建议不要硬看, 把java基础夯实了再来研究源码吧, 我们在使用Proxy创建动态对象时,比然要传递一个InvocationHandler接口的实现类或者匿名内部类对象,
2.1章节我们在创建动态代理类的时候,在MapperProxyFactory类中的newInstance方法中, 我们传递的InvocationHandler得实现类就是我们的MapperProxy类对象, 所以这个MapperProxy类必然实现了InvocationHandler接口, 我们验证一下是不是:

果然MapperProxy类实现了InvocationHandler接口并实现了invoke方法
在这里插入图片描述
在这里插入图片描述
下面我们调用Mapper接口中的方法selectBooksByCondition, 实际上都是通过MapperProxy.invoke()方法去执行的。

2.3.1 MapperProxy.invoke()方法调用解析

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      /**
       *  这里给大家解释一下这个invoke判断里面加这个判断的原因:
       *      大家都知道Object对象默认已经实现了很多方法, 我们的Mapper接口在进行定义的时候, 可能定义了静态方法、 默认方法以及抽象方法
       *      因此在创建了动态代理对象的时候, 这个动态代理类肯定也包含了很多的方法, 从Object类继承的方法, 从接口继承的默认方法,
       *      以及从接口继承抽象方法需要实现取执行SQL语句的方法
       *
       *      这个if分值判断的只要目的在将无需走SQL执行流程的方法如(toString/equals/hashCode)等先过滤掉
       *      然后再抽象方法及默认方法中通过一个接口MapperMethodInvoker再进行一次判断,找到所有需要执行SQL的方法通过PlainMethodInvoker的invoke
       *      方法取执行SQL语句获取结果,能够加快获取 mapperMethod 的效率
       */

      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

在这里插入图片描述
PlainMethodInvoker().invoke()方法会调用MapperMethod类中的execute()方法
在这里插入图片描述

2.3.2 MapperMethod.execute()方法调用解析

MapperMethod类中的execute()方法具体执行逻辑如下
在这里插入图片描述

 public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      // 插入操作
      case INSERT: {
          // 如果你用过Mybatis的话, 你一定清楚, Mybatis的中参数传递的方式有以下几种
          	// 第一种: [arg0,arg1,...]
          	// 第二种: [param1,param2,...]
          	//  convertArgsToSqlCommandParam(args)方法就是实现这种映射
	       // 举例说明:我之前传递的参数是一个Book对象{"bookId": "val1", "bookIndexNo":"val2", "bookName":"val3"}
	       // 经过convertArgsToSqlCommandParam()方法处理之后得到{"bookId": "val1", "bookIndexNo":"val2", "bookName":"val3", "param1": "val1", "param2":"val2", "param3":"val3"}
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行SQL操作并对返回结果进行封装处理
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      // 更新操作
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行SQL操作并对返回结果进行封装处理
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
       // 删除操作
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        // 执行SQL操作并对返回结果进行封装处理
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
      	// 查询操作的情况比较复杂, 需要分情况逐一处理
        if (method.returnsVoid() && method.hasResultHandler()) {
          // 如果查询操作的返回为空并且方法已经指定了结果处理器时,执行以下语句
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          // 如果查询操作的返回返回结果为List集合, 执行以下语句
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          // 如果查询操作的返回返回结果为Map集合, 执行以下语句
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          // 如果查询操作的返回返回结果为Cursor, 执行以下语句
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
           // 如果查询操作的返回返回结果为单个对象并且指定返回类型为Optional, 则将返回结果封装成Optional对象返回
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

当前我们执行的查询语句, 返回的结果列表是一个List, 所以调用的是executeForMany(sqlSession, args);
在这里插入图片描述

	private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
	// 这里是判断你的mapper层接口方法是否传递了RowBounds对象,根据是否传递了RowBounds对象来调用sqlSession对象的selectList的不同传参的重载方法
	// 这个RowBounds对象主要是来进行分页功能,会将所有符合条件的数据全都查询出来加载到内存中,然后在内存中再对数据进行分页(当数据量非常大的时候, 就会发生OOM, 一般不推荐使用)
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.selectList(command.getName(), param, rowBounds);
    } else {
	  // 我们这里没有传递RowBounds对象, 所以调用的是这个方法
      result = sqlSession.selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }

2.3.3 executor.query()执行器执行查询的逻辑流程解析

2.3.3.1 创建PreparedStatement对象

我们在创建configuration类对象的时候,已经设置过默认的executor对象类型就是Simple
在这里插入图片描述

那么这里的executor应该是SimpleExecutor对象啊, 但是实际上并不是, 这里的executor对象是CachingExecutor对象 这里大家肯定会有疑问, 在debug的过程中, 我明明没有看到有创建CachingExecutor对象啊, 这个对象是啥时候创建的? 为啥不是SimpleExecutor对象对象啊?

这里解答一下, 下面这两行代码,只要你使用过Mybatis框架, 肯定不陌生

SqlSessionFactory factory = factoryBuilder.build(ins);
SqlSession sqlSession = factory.openSession(true);

factory.openSession(true)这个方法调用我们跟进去看看

在这里插入图片描述在这里插入图片描述
好了以上就是executor是什么时候创建的以及为什么明明应该是simpleExecutor,但是实际却是cachingExecutor对象的原因。

下面接着说查询方法的执行过程

sqlsession对象会调用执行器的查询方法开始查询
在这里插入图片描述
这里调用了委托者SimpleExecutor对象的query()方法,但是SimpleExecutor对象没有query方法, 就调用了父类BaseExecutor里面query()方法
在这里插入图片描述
query()方法中又调用了父类BaseExecutor里面queryFromDatabase()方法
在这里插入图片描述
queryFromDatabase()()方法中又调用了父类BaseExecutor的抽象方法doQuery()方法,父类中没有实现该方法,但是子类SimpleExecutor对该方法进行了重写, 调用子类SimpleExecutor中的doQuery()方法执行
在这里插入图片描述
子类SimpleExecutor中的doQuery()方法如下图:
在这里插入图片描述
在SimpleExecutor类中的doQuery()方法中StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);这行代码执行完成的时候,当前代理方法的ParameterHandler对象和ResultSetHandler对象也会创建并扩展完成,如果我们又在配置文件中自定义ParameterHandler、ResultSetHandler、StatementHandler在这个过程都会被通过拦截器注册并依次调用。
在这里插入图片描述

RoutingStatementHandler里面没有任何的实现,是用来创建基本的StatementHandler的。这里会根据MappedStatement里面的statementType(一共有三种:STATEMENT、PREPARED、CALLABLE))决定StatementHandler的类型,MappedStatement对象默认是PREPARED。
在这里插入图片描述

在SimpleExecutor类中的doQuery()方法中StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);Configuration类中的如下方法将被调用

/** 拦截参数处理器:通过拦截器拓展插件功能对ParameterHandler的基础功能进行增强 */
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

/** 拦截结果映射处理器:通过拦截器拓展插件功能对ResultSetHandler的基础功能进行增强 */
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

/** 拦截SQL语句处理器:通过拦截器拓展插件功能对StatementHandler的基础功能进行增强 */
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

RoutingStatementHandler对象创建完成之后会调用prepareStatement()方法创建Statement对象
在这里插入图片描述

2.3.3.2 Statement中的入参处理

在SimpleExecutor对象中再创建出最终要执行的Statement对象之后, 就是处理入库真实替换SQL语句中的占位符了,主要是通过statementHandler.parameterize(stmt)来实现的, 具体的还是调用了PreparedStatementHandler类中的.parameterize(stmt)方法来处理
在这里插入图片描述
这里就用到了我们的ParameterHandler对象了, 通过setParameters()方法完成参数替换。
具体的方法实现有兴趣可以自行研究

2.3.3.3 ResultSet结果及处理

SimpleExecutor类的handler.query(stmt, resultHandler)方法最终会调用PreparedStatementHandler的query方法执行SQL语句并返回结果
在这里插入图片描述
PreparedStatementHandler的query方法会调用PreparedStatement.execute()执行SQL语句,返回查询结果集
在这里插入图片描述
DefaultResultSetHandler.handleResultSets()方法会将查询结果集处理成Java中的List对象返回。

3 查询方法的调用逻辑图

在这里插入图片描述

4 源码阅读过程中的笔记

MapperProxy
	invoke

MapperMethod
	execute()
	executeForMany()


DefaultSqlSession
	selectList()

CachingExecutor
	query()
		1、生成二级缓存KEY
	createCacheKey()
		1、将缓存KEY作为参数传递,调用重载query()方法
	query()
		1、从MappedStatement对象中获取Cache对象
		2、如果Cache对象不是null, 说明有二级缓存
			2.1、判断MappedStatement对象的isFlushCacheRequired属性是为true, 如果为true,刷新缓存
			2.2、判断MappedStatement对象是否使用缓存(isUseCache属性是否为true),并且resultHandler属性默认为null, 如果两者都满足, 直接从Cache对象中通过缓存KEY查询结果数据list
			2.3、判断结果数据list是否为空,如果为空,从数据库中查询出结果,然后将查询结果放置到Cache对象中的缓存KEY下,最后把查询结果list返回
		3、如果Cache对象是null, 直接从数据库中查询出结果(调用BaseExecutor.query())
		
BaseExecutor
	createCacheKey()
	query()
		1、判断当前的queryStack是否为0,并且MappedStatement对象的isFlushCacheRequired属性是为true,满足条件就清空一级缓存,否则跳过
		2、判断MappedStatement对象的resultHandler属性是否为null,满足条件就直接从一级缓存中通过缓存KEY查询结果数据list
			2.1、如果结果数据list不为空,调用handleLocallyCachedOutputParameters()方法对结果进行处理(主要是因为如果调用的是存储过程,执行的结果需要接受并处理)
			2.2、如果结果数据list为空,调用queryFromDatabase()直接从数据库中进行查询结果
		3、处理嵌套查询
		    if (queryStack == 0) {
			  for (DeferredLoad deferredLoad : deferredLoads) {
				deferredLoad.load();
			  }
			  // issue #601
			  deferredLoads.clear();
			  // 嵌套查询会借助一级缓存,所以一级缓存不能关闭
			  // 嵌套查询是肯定会延迟加载的,存入DeferredLoad类,避免重复的查询执行
              // 执行嵌套查询时,当有结果值就直接存入,没有就存入一个占位符,这样相同的嵌套查询,在一级缓存中只会存在一个,当所有的都处理完成以后,然后再最终处理所有的延迟加载
		4、返回最终结果

	queryFromDatabase()
		1、首先通过缓存KEY在缓存对象中先开辟空间,因为缓存结果还没有从数据库中查询,先设置一个占位符告诉其他人这个坑已经有人占了
		2、调用doQuery()方法查询数据
		3、将第一步的缓存对象清除(因为它没有真正的保存数据对象,只是在我查询数据还没有返回数据结果的这段时间假装已经有缓存了)
		4、将真正从数据中查询出来的数据对象放入一级缓存,然后然后结果list
SimpleExecutor
	doQuery()
		1、创建StatementHandler对象
			1.1 MappedStatement对象中默认的statementType对象是"PREPARED", 这里会执行new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
			1.2 new PreparedStatementHandler的构造方法中调用了父类BaseStatementHandler的构造方法
			1.3 父类BaseStatementHandler的构造方法中做了parameterHandler和resultSetHandler的初始化操作,调用了configuration.newParameterHandler()和configuration.newResultSetHandler()
			方法进行对象创建,这里默认都是创建的DefaultParameterHandlerDefaultResultSetHandler对象
		2prepareStatement() 
			2.1 创建Connection连接对象
			2.2 执行statementHandler对象的prepare()方法,返回Statement对象
				// 2.2.1 调用RoutingStatementHandler类下的prepare()方法,返回Statement对象
				// 2.2.2 调用BaseStatementHandler类下的prepare()方法,返回Statement对象
			2.3 执行statementHandler对象的parameterize()方法
				// 2.3.1 调用RoutingStatementHandler类下的parameterize()方法
				// 2.3.2 调用PreparedStatementHandler类下的parameterize()方法
			2.4 返回Statement对象
		3、调用StatementHandlerquery()方法
			3.1 调用RoutingStatementHandler类的query()方法
			3.2 调用PreparedStatementHandler类的query()方法

BaseStatementHandler
	这个方法在创建
	prepare()
		1、调用instantiateStatement()创建Statement对象
			1.1 调用PreparedStatementHandler类下的instantiateStatement()方法创建Statement对象
		2、调用setStatementTimeout()设置执行和事务的超时时间
		3、调用setFetchSize()设置驱动的结果集获取数量(fetchSize)
		4、最后返回Statement对象

PreparedStatementHandler
	instantiateStatement()
		1、调用JDBC的connection.prepareStatement(sql)方法创建出一个PreparedStatement对象返回
	parameterize()
		1、调用parameterHandler对象的setParameters()方法将PrepareStatement对象中的SQL语句中的参数占位符都替换成传入的参数值
			1.1 调用DefaultParameterHandler类的setParameters()方法
			1.2 遍历所有的ParameterMapping对象,依次替换所有的占位符为实际的传入的参数值
	query()
		1、获取到PreparedStatement对象
		2、调用PreparedStatementexecute()执行SQL语句
		3、调用DefaultResultSetHandler类的handleResultSets()方法处理查询结果集

DefaultResultSetHandler
	handleResultSets()
			handleResultSet()
				handleRowValues()
					handleRowValuesForSimpleResultMap
						getRowValue()
							applyPropertyMappings()方法完成从ResultSet结果集中的每一个行数据和Java对象之间的映射
						storeObject()getRowValue()处理完成的Objcet数据添加到List集合





	

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363704.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python】argparse 模块的使用、Pycharm中使用argparse

目录1、简介2、使用步骤1&#xff09;导入argparse模块&#xff0c;并创建解释器2&#xff09;添加所需参数3&#xff09;解析参数3、使用 pycharm 传递参数给 argparse1、简介 argparse 模块是 Python 标准库中提供的一个命令行解析模块&#xff0c;它可以让使用者以类似 Uni…

给安全平台编写插件模块的思路分享

一、背景 最近在GitHub看到一个新的开源安全工具&#xff0c;可以把工具都集成到一个平台里&#xff0c;觉得挺有意思&#xff0c;但是平台现有的工具不是太全&#xff0c;我想把自己的工具也集成进去&#xff0c;所以研究了一番 蜻蜓安全工作台是一个安全工具集成平台&#x…

我的零分周赛:CSDN周赛第30期,成绩“0”分,天然气定单、小艺读书、买苹果、圆桌

CSDN周赛第30期&#xff0c;成绩“0”分&#xff0c;天然气定单、小艺读书、买苹果&#x1f34e;、圆桌。 (本文获得CSDN质量评分【91】)【学习的细节是欢悦的历程】Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教…

steam搬砖项目,小投入高回报,可放大操作,(内附教学资料)

我必须要说&#xff0c;steam搬砖项目就是全网门槛最低的副业&#xff0c;有手就行&#xff01; 本人90后底层员工一枚&#xff0c;新入csgo搬砖项目&#xff0c;轻松翻身 什么做抖音、海外问卷、直播卖货&#xff0c;电商等等对比我这个都是小钱。我这个方法是利用了大部分人…

C++线程/阻塞/同步异步----2

本章节内容为记录改写RTK代码时&#xff0c;学习的知识 同步和异步区别 1.定义不同&#xff1a;同步需要将通信双方的时钟统一到一个频率上&#xff0c;异步通信发送的字符间隔时间可以是任意的; 2.准确性不同&#xff1a;同步通信需要比较高精度的精确度&#xff0c;异步则不…

【算法基础】栈与队列

一、栈1.1 模拟栈实现一个栈&#xff0c;栈初始为空&#xff0c;支持四种操作&#xff1a;push x – 向栈顶插入一个数 x&#xff1b;pop – 从栈顶弹出一个数&#xff1b;empty – 判断栈是否为空&#xff1b;query – 查询栈顶元素。现在要对栈进行 M 个操作&#xff0c;其中…

【2023最火教程】Python性能测试框架Locust实战教程(建议收藏)

01、认识Locust Locust是一个比较容易上手的分布式用户负载测试工具。它旨在对网站&#xff08;或其他系统&#xff09;进行负载测试&#xff0c;并确定系统可以处理多少个并发用户&#xff0c;Locust 在英文中是 蝗虫 的意思&#xff1a;作者的想法是在测试期间&#xff0c;放…

图解 paxos 论文《The Part-Time Parliament》

本文以图文并茂的方式重新演绎 Paxos 开山之作 《The Part-Time Parliament》[1]&#xff0c;并尝试解释原论文中语焉不详的地方。 背景 在 Paxos 小岛上&#xff0c;施行着一种 Parliament(议会) 政治。小岛上执行的所有 decree(法令) 都需要先由 Parliament 在 Chamber 内表…

leetcode 21~30 学习经历

leetcode 21~30 学习经历21. 合并两个有序链表22. 括号生成23. 合并K个升序链表24. 两两交换链表中的节点25. K 个一组翻转链表26. 删除有序数组中的重复项27. 移除元素28. 找出字符串中第一个匹配项的下标29. 两数相除30. 串联所有单词的子串小结21. 合并两个有序链表 将两个升…

15. Qt中OPenGL的参数传递问题

1. 说明 在OPenGL中&#xff0c;需要使用GLSL语言来编写着色器的函数&#xff0c;在顶点着色器和片段着色器之间需要参数值的传递&#xff0c;且在CPU中的数据也需要传递到顶点着色器中进行使用。本文简单介绍几种参数传递的方式&#xff1a; &#xff08;本文内容仅个人理解&…

学校节能减排实施方案-浅析高校能耗及节能管理

摘要:高校能源管理是高校治理体系和高校后勤保障的重要组成部分。利用数据统计等手段对蚌埠医学院近年来的能源使用情况进行统计分析&#xff0c;通过横向及纵向对比&#xff0c;结合国家相关政策法规及同类高校能耗情况&#xff0c;对该校能源消耗现状进行了综合分析&#xff…

Web3中文|2023年zk赛道爆发,即将推出的Polygon zkEVM有多重要?

Crypto行业被广泛关注的零知识证明&#xff08;Zero—Knowledge Proof&#xff09;技术&#xff0c;在1980年就被数学家S.Goldwasser、S.Micali及C.Rackoff提出。 零知识证明涉及一系列步骤&#xff0c;可以实现密码学中的「可用而不可知」。 而区块链有着公开透明、不可篡改…

Nginx之反向代理、负载均衡、动静分离。

Nginx之反向代理、负载均衡、动静分离。 1、Nginx是啥&#xff1f; 轻量级Web服务器、反向代理服务器、电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器 在 BSD-like 协议下发行、占内存少、并发高&#xff08;同时处理请求能力&#xff09;。 2、安装 官网&#xf…

【Python】序列与列表(列表元素的增删改查,求之,列表推导式、列表的拷贝)

一、序列序列的概念&#xff1a;按照某种顺序排列的数据类型就叫做序列&#xff0c;比如字符串&#xff0c;列表&#xff0c;元组&#xff0c;集合序列的共同点是都有下标&#xff0c;支持index()方法和count()&#xff0c;也支持切片处理(等同于字符串序列的切片处理)l1 [0, …

Leetcode(每日一题)——1140. 石子游戏 II

摘要 ​​​​​​1140. 石子游戏 II 877. 石子游戏 1406. 石子游戏 III 375. 猜数字大小 II 464. 我能赢吗 486. 预测赢家 1025. 除数博弈 一、动态规划解析 Alice一开始有两个选择&#xff1a;拿前一堆/前两堆石子。如果 Alice 拿前一堆&#xff0c;那么轮到 Bob 时…

Propargyl-PEG1-SS-PEG1-PFP ester,1817735-30-0,炔基应用于生物标记

【中文名称】丙炔-单乙二醇-二硫键-单乙二醇-五氟苯酚酯【英文名称】 Propargyl-PEG1-SS-PEG1-PFP ester【结 构 式】【CAS号】1817735-30-0【分子式】C16H15F5O4S2【分子量】430.4【基团部分】炔基基团【纯度标准】95%【包装规格】1g&#xff0c;5g&#xff0c;10g&#xff0c…

互联网行业中,哪些岗位越老越吃香?

你是不是也想转行IT行业&#xff0c;找一门适合自己学习&#xff0c;能拿高薪的技术&#xff0c;最好还越老越吃香&#xff1f;或许先应该看看对应岗位&#xff0c;老资格的同行们可以拿到的薪资数再做判断。整体上看&#xff0c;大部分岗位的起薪水平相差不大&#xff0c;但随…

matplotlib绘制三维图

目录线状堆积图 PolygonPlot三维表面图 SurfacePlot散点图ScatterPlot柱形图 BarPlot三维直方图螺旋曲线图 LinePlotContourPlot轮廓图网状图 WireframePlot箭头图二维三维合并文本图Text三维多个子图线状堆积图 PolygonPlot Axes3D.add_collection3d(col, zs0, zdir‘z’)  …

(考研湖科大教书匠计算机网络)第六章应用层-第一、二节:应用层概述和C/S及P2P

获取pdf&#xff1a;密码7281专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;应用层概述二&#xff1a;客户/服务器&#xff08;C/S&#xff09;和对等&#xff08;P2P&#xff09;方式&#xff08;1&#xff09;客户/服务器&…

Vue页面组成及常用属性

一、Vue页面组成 目前的项目中&#xff0c;Vue页面都是采用组件套娃的形式&#xff0c;由一个一个的组件拼接而成整个页面。一个组件就是一个.vue文件。组件通常由template和script两部分组成&#xff1a; template部分&#xff1a;页面展示的具体元素内容&#xff0c;比如文字…