Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?
Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?
如果拿一次数据库请求操作做比喻,那么前面3篇文章就是在做请求准备,真正执行操作的是本篇文章要讲述的内容。正如标题一样,本篇文章最最核心的要点就是 SqlSession实现数据库操作的源码解析。但按照惯例,我这边依然列出如下的问题:
1、 SqlSession 是如何被创建的? 每次的数据库操作都会创建一个新的SqlSession么?(也许有很多同学会说SqlSession是通过 SqlSessionFactory.openSession() 创建,但这个答案按照10分制顶多给5分)
2、 SqlSession与事务(Transaction)之间的关系? 在同一个方法中,Mybatis多次请求数据库,是否要创建多个SqlSession?
3、 SqlSession是如何实现数据库操作的?
本章内容就是围绕着上面三个问题进行解析,那么带着问题去看源码吧!
一、SqlSession 的创建
在学习Mybatis时,我们常常看到的 SqlSession 创建方式是 SqlSessionFactory.openSession() ,那么我们就拿此作为切入点,先来看看 SqlSessionFactory.openSession() 的方法源码(注意 是 DefaultSqlSessionFactory ),其内部是调用 openSessionFromDataSource() 方法,那么我们来下这个方法内部源码:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
// 创建了一个 TransactionFactory 事务工厂( 如果有仔细看过 SqlSessionFactoryBean.buildSqlSessionFactory() 过程的同学,应该能够看到是 SpringManagedTransactionFactory )
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 通过 TransactionFactory 获取了一个 事务 Transaction
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据 execType(默认是 SIMPLE ) 获取了一个 Executor (真正执行数据库操作的对象)
final Executor executor = configuration.newExecutor(tx, execType);
// 返回了一个 DefaultSqlSession 对象
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();
}
}
整个SqlSession 的创建分 3个步骤:
1、 获取到 TransactionFactory 事务工厂对象 ( 如果有仔细看过 SqlSessionFactoryBean.buildSqlSessionFactory() 过程的同学,应该能够看到是 SpringManagedTransactionFactory )
2、 通过 TransactionFactory 获取了一个 事务 Transaction
3、 根据 execType(默认是 SIMPLE ) 获取了一个Executor (真正执行数据库操作的对象)
4、 创建并返回 DefaultSqlSession 对象
通过源码我们知道每次 SqlSession(准确地说是 DefaultSqlSession )的创建都会 有一个 Transaction(在Mybatis-Spring 中 是 SpringManagedTransaction ) 事务对象 的生成。也就是说:
1、 一个事务 Transaction 对象与一个 SqlSession 对象 是一一对应的关系。
2、 同一个SqlSession 不管执行多少次数据库操作。只要没有执行close,那么整个操作都是在同一个 Transaction 中执行的。
看过之前的文章的同学应该有疑问,之前不管是创建MapperProxy 的 SqlSession 还是 MapperMethod中调用的SqlSession其实都是 SqlSessionTemplate ,与这里的 DefaultSqlSession 不是同一个SqlSession对象。那么我们就来简单分析下这2者之间的区别与职责吧!
SqlSessionTemplate 与 DefaultSqlSession 之间不可告人的秘密
在之前的文章中,我们讲到过 每创建一个 MapperFactoryBean 就会创建一个 SqlSessionTemplate 对象,而 MapperFactoryBean 在获取 MapperProxy 时会将 SqlSessionTemplate 传递到 MapperProxy中。 也就是说 SqlSessionTemplate 的生命周期是与 MapperProxy 的生命周期是一致的。( 注意: MapperProxy 是被注入到Spring容器中的,所以结果不难想象)
在之前的文章中,我们简单的描述过 SqlSessionTemplate 内部维护了一个 sqlSessionProxy ,而 sqlSessionProxy 是通过动态代理创建的一个 SqlSession 对象, SqlSessionTemplate 的 数据库操作方法 insert/update 等等都是委托 sqlSessionProxy 来执行的。那么我们想看下这个 sqlSessionProxy 的创建:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 通过 Proxy.newProxyInstance() 动态代理创建的
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
在创建 sqlSessionProxy 的代码中,我们可以发现其指定的代理对象是 SqlSessionInterceptor(SqlSession拦截器?),那么关键代码肯定在这个 SqlSessionInterceptor 中,查看 SqlSessionInterceptor发现其是一个内部类,其源码如下:
private class SqlSessionInterceptor implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 根据条件获取一个SqlSession(注意此时的SqlSession 是 DefaultSqlSession ),此时的SqlSession 可能是新创建的,也可能是上一次的请求的SqlSession。
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 反射执行 SqlSession 方法
Object result = method.invoke(sqlSession, args);
// 判断当前的 SqlSession 是有事务,如果有则不commit
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
// 判断如果是PersistenceExceptionTranslator且不为空,那么就关闭当前会话,并且将sqlSession置为空防止finally重复关闭
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
//只要当前会话不为空, 那么就会关闭当前会话操作,关闭当前会话操作又会根据当前会话是否有事务来决定会话是释放还是直接关闭。
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
整个 invoke 分5个步骤:
1、 根据条件获取一个SqlSession(注意此时的SqlSession 是 DefaultSqlSession ),此时的SqlSession 可能是新创建的,也可能是上一次的请求的SqlSession。
2、 反射执行 SqlSession 方法
3、 判断当前的 SqlSession 是否由事务所管控,如果是则不commit
4、 判断如果是PersistenceExceptionTranslator且不为空,那么就关闭当前会话,并且将sqlSession置为空防止finally重复关闭
5、 只要当前会话不为空, 那么就会关闭当前会话操作,关闭当前会话操作又会根据当前会话是否有事务来决定会话是释放还是直接关闭。
其中步骤1 是本次讨论的核心,其内部流程如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, "No SqlSessionFactory specified");
notNull(executorType, "No ExecutorType specified");
// 通过 TransactionSynchronizationManager (Spring 的一个事务同步管理器) 获取到一个 SqlSessionHolder (从字面意思就应该明白其内部维护有 SqlSession)
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 判断SqlSessionHolder是否为空、是否与事务同步
if (holder != null && holder.isSynchronizedWithTransaction()) {
if (holder.getExecutorType() != executorType) {
throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
}
// 将引用计数增加1
holder.requested();
// 返回持有的 SqLSession
return holder.getSqlSession();
}
// 如果从 事务同步管理器 没能获取到 一个 SqlSessionHolder 则 调用 sessionFactory.openSession() 新建一个SqlSession
SqlSession session = sessionFactory.openSession(executorType);
// 判断当前是否有事务,有则 根据 SqlSession 创建一个 SqlSessionHolder 并将其注册进入到 TransactionSynchronizationManager 中,以供当前事务中的下次使用
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
} else {
if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
if (logger.isDebugEnabled()) {
logger.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
}
} else {
throw new TransientDataAccessResourceException(
"SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
}
}
return session;
}
整个流程步骤分一下几步:
1、 通过 TransactionSynchronizationManager (Spring 的一个事务同步管理器) 获取到一个 SqlSessionHolder (从字面意思就应该明白其内部维护有 SqlSession)
2、 判断SqlSessionHolder是否为空、是否与事务同步,是则返回持有的 SqLSession
3、 如果从 事务同步管理器 没能获取到 一个 SqlSessionHolder 则 调用 sessionFactory.openSession() 新建一个SqlSession
4、 判断当前是否有事务,有则 根据 SqlSession 创建一个 SqlSessionHolder 并将其注册进入到 TransactionSynchronizationManager 中,以供当前事务中的下次使用
从上面的步骤中,我们发现整个获取SqlSession都与 事务 有极大的关联关系,并且从上面的流程中,我们能够得到几个关键点信息:
1、 同一事务中 不管调用多少次 mapper里的方法 ,最终都是用得同一个 sqlSession,即 一个事务中使用的是同一个sqlSession。
2、 如果没有开启事务,调用一次mapper里的方法将会新建一个 sqlSession 来执行方法。
如果细心的同学会发现这个 TransactionSynchronizationManager 事务同步管理器 是由Spring 持有的,也就是说这里完美的应证了Mybatis-Spring 中对该子项目的描述:
MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean中。
DEBUG测试发现的问题
作者在进行测试DEBUG时发现,只要是开启了事务,最终都会通过 TransactionSynchronizationManager.getResource(sessionFactory) 获取到 SqlSessionHolder ,即便是重启程序后第一次请求也是一样会获取到,并且作者 在 SqlSessionHolder 和 DefaultSqlSession 的 构造函数中打上断点 也并未走到这里,因此作者实在百思不得其解,猜测 TransactionSynchronizationManager.getResource(sessionFactory) 该方法 内部有通过反射动态的创建了SqlSession,但看源码确实没找到,目前实力不被允许!!
成功获取到 SqlSession(准确的说是 DefaultSqlSession ) 后 通过 method.invoke() 反射调用到具体的 DefaultSqlSession 方法。方法调用完成后,判断当前SqlSession是否被事务所管控,如果是则不commit,最后再调用 closeSqlSession() 方法进行SqlSession “关闭”。这里为什么要打引号呢?原因是该方法内部处理时判断了当前SqlSession是否被事务所管控,是的话仅仅将引用计数器减一,并未真正将SqlSession 关闭(这也是为了下次能够使用同一个SqlSession),如果不被事务管控则执行正在的 session.close() 操作。
至此,我们明白了 一个 SqlSession(DefaultSqlSession) 的创建不仅仅是调用一下 sessionFactory.openSession() 那么简单,这其中关联到了 SqlSessionTemplate 、 DefaultSqlSession、 SqlSessionInterceptor 以及 Spring 事务(Transaction)(TransactionSynchronizationManager) 。 所以一定要明白这几个对象之间的联系和作用。
二、SqlSession实现数据库操作
从上面的分析我们知道,任何的Mapper接口方法请求最终都会请求到 DefaultSqlSession ,即 DefaultSqlSession 内部封装了数据库操作,其他 SqlSession 子类最终都得依靠它来操作数据库。那么我们就拿 DefaultSqlSession 内部的 selectList() 方法开始讲述其如何封装了数据库操作。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 从 configuration 中获取到 指定方法的 MappedStatement (注意:statement 是由 MapperMethod 中的 SqlCommand 的 name 字段传下来的,而name 本身就来源于 MappedStatement 的 id ,所以最终 statement 会是 com.xxx.findUserByName 这种形式)
MappedStatement ms = configuration.getMappedStatement(statement);
// 通过 委托 Executor 的 query() 执行真正的数据库操作
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我们可以发现 DefaultSqlSession 的所有方法 基本上都有以下2个步骤:
1、 从 configuration 中获取到 指定方法的 MappedStatement
2、 通过 委托 Executor 来 执行真正的数据库操作
我们知道 MappedStatement 内部保存了所要执行的方法的 SqlSource (保存有从Mapper.xml中解析出来的Sql片段信息),然后通过 Executor 的 query() 方法来执行数据库操作。我们先来看下 Executor 继承关系图:

我们从继承关系图中可以看到 BaseExecutor 、CachingExecutor ,它们2个分别代表2中缓存机制, BaseExecutor 内部维护了 名为 localCache (PerpetualCache) 的 对象,该对象就是 一级缓存 的实际控制者, CachingExecutor 在二级缓存时使用,其内部实现时通过委托 BaseExecutor 来实现一级缓存的。关于Mybatis缓存机制的内容,大家可以去看看这篇文章——聊聊MyBatis缓存机制
不管是一级缓存还是二级缓存机制,其最终还是会调用到 BaseExecutor,而Mybatis默认的 BaseExecutor 实现是 SimpleExecutor,所以重点关注这2个类的实现,下面是 BaseExecutor 的 query()源码:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 从 MappedStatement 中获取到 BoundSql(实际上是通过 调用 MappedStatement 中的 SqlSource 的 getBoundSql() 获取)
BoundSql boundSql = ms.getBoundSql(parameter);
// 通过参数 解析出 cacheKey ,这个是一级缓存的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
我们可以看到整个方法内部最核心的一点就是 从 MappedStatement 中获取到 BoundSql(实际上是通过 调用 MappedStatement 中的 SqlSource 的 getBoundSql() 获取) ,最后再调用重载方法 query(),这个重发方法做了一级缓存的操作,这里就不描述了,只要知道最后调到了 SimpleExecutor 的 doQuery() 方法就行,查看 doQuery() 方法源码 :
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 从 MappedStatement 中获取到 Configuration
Configuration configuration = ms.getConfiguration();
// 通过 Configuration 的 newStatementHandler() 方法创建了一个 StatementHandler 对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 调用 prepareStatement() 方法 获取到 Statement 对象 (真正执行静态SQl的接口)
stmt = prepareStatement(handler, ms.getStatementLog());
// 调用 StatementHandler.query() 方法执行
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
从这里开始就与JDBC 挂钩了,如果熟悉JDBC的同学应该一眼就知道 Statement 这个对象具体时干嘛的了,按照惯例我们还是先分析下 doQuery() 方法内部执行流程步骤:
1、 从 MappedStatement 中获取到 Configuration
2、 通过 Configuration 的 newStatementHandler() 方法创建了一个 StatementHandler 对象
3、 调用 prepareStatement() 方法 获取到 Statement 对象 (真正执行静态SQl的接口)
4、 调用 StatementHandler.query() 方法执行(其实内部委托 Statement 来执行的)
其中不走2、3、4是重点要解析的。 我们先来看下 步骤 2 ,其中涉及到一个关键对象 StatementHandler ,其继承关系图如下:

这个结构与Executor 的类似:
1、 SimpleStatementHandler ,这个对应的 就是JDBC 中常用到的 Statement 接口,用于简单SQL的处理
2、 PreparedStatementHandler , 这个对应的就是JDBC中的 PreparedStatement,用于预编译SQL的处理
3、 CallableStatementHandler , 这个对应JDBC中 **CallableStatement ** ,用于执行存储过程相关的处理
4、 RoutingStatementHandler,这个接口是以上三个接口的路由,没有实际操作,只是负责上面三个StatementHandler的创建及调用
回过头来再看 configuration.newStatementHandler() ,不用猜测,肯定创建的是 RoutingStatementHandler ,并且其内部 的 delegate 默认是 PreparedStatementHandler (MappedStatement builder方法指定了默认的 statementType = StatementType.PREPARED )。
步骤三主要的作用就是 预编译并 获取到Statement(由于是 PreparedStatementHandler 所以默认获取到的是 PreparedStatement), 其 prepareStatement() 方法源码:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 创建 Connection 链接,查看源码其是通过 transaction.getConnection() 获取到的。
Connection connection = getConnection(statementLog);
// 预编译获取到 PrepareStatement ,即 最终会调用到 connection.prepareStatement() 方法
stmt = handler.prepare(connection);
// 设置参数信息,其参数是通过 从 BoundSql 获取
handler.parameterize(stmt);
return stmt;
}
整个流程分3个步骤:
1、 创建 Connection 链接,查看源码其是通过 transaction.getConnection() 获取到的。
2、 预编译获取到 PrepareStatement ,即 最终会调用到 connection.prepareStatement() 方法
3、 设置参数信息,其参数是通过 从 BoundSql 获取
步骤4会 调用 StatementHandler.query() 方法,以 PreparedStatementHandler 为例,其源码如下:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行SQL
ps.execute();
// 通过 ResultSetHandler 的 handleResultSets() 方法解析返回数据
return resultSetHandler.<E> handleResultSets(ps);
}
这个方法内部就2个流程步骤:
1、 执行SQL
2、 通过 ResultSetHandler 的 handleResultSets() 方法解析返回数据
这里的 ResultSetHandler 就是用来处理 JDBC中的 ResultSet (相信用过JDBC 的同学对这个不陌生),关于如果解析返回数据的逻辑这里就不在详细分析了。
我们通过上面的分析不难发现从 SqlSession 委托 Executor 执行数据库开始,整个 Executor 的执行操作其实就是封装了 JDBC 的一个执行操作,如果熟悉JDBC的同学相信一眼就能看出来。
三、个人总结
SqlSession 与 事务 的关系:
1、 同一事务中 不管调用多少次 mapper里的方法 ,最终都是用得同一个 sqlSession,即 一个事务中使用的是同一个sqlSession。
2、 如果没有开启事务,调用一次mapper里的方法将会新建一个 sqlSession 来执行方法。
SqlSession 的一次执行流程图:

本文由博客一文多发平台 OpenWrite 发布!
Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?的更多相关文章
- Mybatis源码解析4——SqlSession
上一篇文章中,我们介绍了 SqlSessionFactory 的创建过程,忘记了的,可以回顾一下,或者看下下面这张图也行. 接下来,可乐讲给大家介绍 Mybatis 中另一个重量级嘉宾--SqlSes ...
- 【Mybatis源码解析】- JDBC连接数据库的原理和操作
JDBC连接数据库的原理和操作 JDBC即Java DataBase Connectivity,java数据库连接:JDBC 提供的API可以让JAVA通过API方式访问关系型数据库,执行SQL语句, ...
- mybatis源码-解析配置文件(四-1)之配置文件Mapper解析(cache)
目录 1. 简介 2. 解析 3 StrictMap 3.1 区别HashMap:键必须为String 3.2 区别HashMap:多了成员变量 name 3.3 区别HashMap:key 的处理多 ...
- Mybatis源码解析,一步一步从浅入深(四):将configuration.xml的解析到Configuration对象实例
在Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码中我们看到了XMLConfigBuilder(xml配置解析器)的实例化.而且这个实例化过程在文章:Mybatis源码解析,一步一步从浅 ...
- mybatis源码-解析配置文件(四)之配置文件Mapper解析
在 mybatis源码-解析配置文件(三)之配置文件Configuration解析 中, 讲解了 Configuration 是如何解析的. 其中, mappers作为configuration节点的 ...
- 【MyBatis源码解析】MyBatis一二级缓存
MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...
- Mybatis源码解析,一步一步从浅入深(一):创建准备工程
Spring SpringMVC Mybatis(简称ssm)是一个很流行的java web框架,而Mybatis作为ORM 持久层框架,因其灵活简单,深受青睐.而且现在的招聘职位中都要求应试者熟悉M ...
- Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码
在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ...
- Mybatis源码解析,一步一步从浅入深(六):映射代理类的获取
在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码中我们提到了两个问题: 1,为什么在以前的代码流程中从来没有addMapper,而这里却有getMapper? 2,UserDao ...
随机推荐
- Unity 渲染教程(一):矩阵
转载:http://gad.qq.com/program/translateview/7181958 创建立方体网格.· 支持缩放.位移和旋转. · 使用变换矩阵. · 创建简单的相机投影. 这是关于 ...
- 网传英特尔酷睿第十代桌面处理器(Comet Lake 14nm)规格
自从农企(AMD)2016年开始崛起时,牙膏厂(英特尔)就开始发力,陆续两代推出性价比颇高的桌面处理器, 第八代.第九代酷睿桌面处理器相当的给力,而第十代酷睿桌面处理器会很猛啊,据传从酷睿i3到酷睿i ...
- 【转】Pandas学习笔记(四)处理丢失值
Pandas学习笔记系列: Pandas学习笔记(一)基本介绍 Pandas学习笔记(二)选择数据 Pandas学习笔记(三)修改&添加值 Pandas学习笔记(四)处理丢失值 Pandas学 ...
- POJ 3694Network(Tarjan边双联通分量 + 缩点 + LCA并查集维护)
[题意]: 有N个结点M条边的图,有Q次操作,每次操作在点x, y之间加一条边,加完E(x, y)后还有几个桥(割边),每次操作会累积,影响下一次操作. [思路]: 先用Tarjan求出一开始总的桥的 ...
- Tomcat8 访问 manager App 失败
Tomcat8 访问 manager App 失败 进入 tomcat 8 的下面路径 修改 上面 的 context.xml 注释了下面的框框 保存退出.重启tomcat
- .Net反射-基础1-Assembly、Type
Assembly:封装程序集信息,可以动态加载程序集 获取Assembly的几种方式: 1.var ass1 = Assembly.Load("ClassLibrary1");// ...
- Vue 生成PDF并下载
实现原理 该功能原理是将页面转化伟canvas在把canvas转化为base64数据 最后将数据通过pdf.js生成下载,故需要和html2canvas一起使用 友情提醒这个pdf下载不能在app里直 ...
- python输出带颜色字体
方法1: (参考https://suixinblog.cn/2019/01/print-colorful.html) 使用Python中自带的print输出带有颜色或者背景的字符串 书写语法 prin ...
- wireshark-wincap安装问题
winpcap关键模块 32位系统: C:\Windows\system32\wpcap.dll C:\Windows\system32\Packet.dll C:\Windows\system32\ ...
- 并发设计模式:Immutability模式
多个线程同时读写同一共享变量存在并发问题,其中的必要条件之一就是 读写 ,如果没有写,只存在读,是不会存在并发问题的. 如果让一个共享变量只有读操作,没有写操作,如此则可以解决并发问题.该理论的具体实 ...