mybatis 原理_Mybatis的工作原理
1.Mybatis的架构
1.1 Mybatis的框架分层
1.2 MyBatis的实现原理
mybatis底层还是采用原生jdbc来对数据库进行操作的,只是通过 SqlSessionFactory,SqlSession Executor,StatementHandler,ParameterHandler,ResultHandler和TypeHandler等几个处理器封装了这些过程
执行器:Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 参数处理器: ParameterHandler (getParameterObject, setParameters) 结构处理器 ResultSetHandler (handleResultSets, handleOutputParameters) sql查询处理器:StatementHandler (prepare, parameterize, batch, update, query)
其中StatementHandler用通过ParameterHandler与ResultHandler分别进行参数预编译 与结果处理。而ParameterHandler与ResultHandler都使用TypeHandler进行映射。如下图:
2.Mybatis工作过程
通过读mybatis的源码进行分析mybatis的执行操作的整个过程,我们通过debug调试就可以知道Mybatis每一步做了什么事,我先把debug每一步结果 截图,然后在分析这个流程。
第一步:读取配置文件,形成InputStream
2.1 创建SqlSessionFacotry的过程
从debug调试看出 返回的 sqlSessionFactory 是DefaultSesssionFactory类型的,但是configuration此时已经被初始化了。查看源码后画如下创建DefaultSessionFactory的时序图:
2.2 创建SqlSession的过程
从debug调试 看出SqlSessinoFactory.openSession() 返回的sqlSession是 DefaultSession类型的,此SqlSession里包含一个Configuration的对象,和一个Executor对象。查看源码后画如下创建DefaultSession的时序图:
2.3 创建Mapper的过程
从debug调试可以看出,mapper是一个Mapper代理对象,而且初始化了Configuration对象,Executor的对象。查看源码后画如下创建Mapper的时序图:
2.4 执行CRUD过程
2.4.1 以select为例查看各步执行的源码
1.mapper.selectEmployeeList()其实是MapperProxy执行invoke方法,此方法显示是判断Method的方法是不是Object的toString等方法如果不是就执行MapperMethod
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 判断Method的方法是不是Object的toString等方法 if(Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); } } else { //判断private final Map methodCache;这个map里面有没有这个方法的一级缓存,如果没 MapperMethod mapperMethod = this.cachedMapperMethod(method); return mapperMethod.execute(this.sqlSession, args); } } //查询一级缓存和设置一级缓存 private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method); if(mapperMethod == null) { mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()); this.methodCache.put(method, mapperMethod); } return mapperMethod; }
经过上面的调用后进入MapperMethod里面执行
//判断sql命令类型public Object execute(SqlSession sqlSession, Object[] args) { Object param; Object result; if(SqlCommandType.INSERT == this.command.getType()) { param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.insert(this.command.getName(), param)); } else if(SqlCommandType.UPDATE == this.command.getType()) { param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.update(this.command.getName(), param)); } else if(SqlCommandType.DELETE == this.command.getType()) { param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.delete(this.command.getName(), param)); } else if(SqlCommandType.SELECT == this.command.getType()) { //我们测试的是select类型,则再判断这个方法的返回类型 if(this.method.returnsVoid() && this.method.hasResultHandler()) { this.executeWithResultHandler(sqlSession, args); result = null; } else if(this.method.returnsMany()) { //我们是查询列表,此方法执行 result = this.executeForMany(sqlSession, args); } else if(this.method.returnsMap()) { result = this.executeForMap(sqlSession, args); } else { param = this.method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(this.command.getName(), param); } } else { if(SqlCommandType.FLUSH != this.command.getType()) { throw new BindingException("Unknown execution method for: " + this.command.getName()); } result = sqlSession.flushStatements(); } if(result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) { throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ")."); } else { return result; } }private Object executeForMany(SqlSession sqlSession, Object[] args) {//将param做处理 自动处理为param1,param2.. Object param = this.method.convertArgsToSqlCommandParam(args); List result; if(this.method.hasRowBounds()) { RowBounds rowBounds = this.method.extractRowBounds(args); //调用该对象的DefaultSqlSession的selectList方法 result = sqlSession.selectList(this.command.getName(), param, rowBounds); } else { result = sqlSession.selectList(this.command.getName(), param); } return !this.method.getReturnType().isAssignableFrom(result.getClass())?(this.method.getReturnType().isArray()?this.convertToArray(result):this.convertToDeclaredCollection(sqlSession.getConfiguration(), result)):result; }//处理参数方法 public Object convertArgsToSqlCommandParam(Object[] args) { int paramCount = this.params.size(); if(args != null && paramCount != 0) { if(!this.hasNamedParameters && paramCount == 1) { return args[((Integer)this.params.keySet().iterator().next()).intValue()]; } else { Map param = new MapperMethod.ParamMap(); int i = 0; for(Iterator i$ = this.params.entrySet().iterator(); i$.hasNext(); ++i) { Entry entry = (Entry)i$.next(); param.put(entry.getValue(), args[((Integer)entry.getKey()).intValue()]); String genericParamName = "param" + String.valueOf(i + 1); if(!param.containsKey(genericParamName)) { param.put(genericParamName, args[((Integer)entry.getKey()).intValue()]); } } return param; } } else { return null; } }
调用DefaultSqlSession的selectList的方法
public List selectList(String statement, Object parameter, RowBounds rowBounds) { List var5; try { //获取MappedStatement对象 MappedStatement ms = this.configuration.getMappedStatement(statement); //调用cachingExecutor执行器的方法 var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception var9) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9); } finally { ErrorContext.instance().reset(); } return var5; }//CachingExector的query方法public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql); //调用下2代码 return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } //2代码 public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if(cache != null) { this.flushCacheIfRequired(ms); if(ms.isUseCache() && resultHandler == null) { this.ensureNoOutParams(ms, parameterObject, boundSql); List list = (List)this.tcm.getObject(cache, key); if(list == null) { //这里是调用Executor里的query方法 如果开启了缓存这掉CachingExecutor的 如果没有则是调用BaseExecutor的 list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); this.tcm.putObject(cache, key, list); } return list; } } return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
BaseExecutor的query方法
public List 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(this.closed) { throw new ExecutorException("Executor was closed."); } else { if(this.queryStack == 0 && ms.isFlushCacheRequired()) { this.clearLocalCache(); } List list; try { ++this.queryStack; list = resultHandler == null?(List)this.localCache.getObject(key):null; if(list != null) { this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //如果缓存中没有就从数据库中查询 list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { --this.queryStack; } if(this.queryStack == 0) { Iterator i$ = this.deferredLoads.iterator(); while(i$.hasNext()) { BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next(); deferredLoad.load(); } this.deferredLoads.clear(); if(this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { this.clearLocalCache(); } } return list; } }//从数据库中查询private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { //放入缓存 this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER); List list; try { //此处是调用子Executor的方法,ExecutorType默认是使用的SimpleExecutor list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { this.localCache.removeObject(key); } this.localCache.putObject(key, list); if(ms.getStatementType() == StatementType.CALLABLE) { this.localOutputParameterCache.putObject(key, parameter); } return list; }
SimpleExecutor的doQuery方法
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; List var9; try { Configuration configuration = ms.getConfiguration(); //创建StateMentHandler处理器 StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql); //调用下3的方法 stmt = this.prepareStatement(handler, ms.getStatementLog()); //调用no4的方法 var9 = handler.query(stmt, resultHandler); } finally { this.closeStatement(stmt); } return var9; } //下3方法private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Connection connection = this.getConnection(statementLog); Statement stmt = handler.prepare(connection); //SatementHanlder 采用PreparedStatementHandler来实现此方法,而PreparedStatementHandler调用的是父接口ParameterHandler的方法 handler.parameterize(stmt); return stmt; }
ParameterHandler参数处理器的方法
public interface ParameterHandler { Object getParameterObject(); //此方法是用DefaultParameterHandler实现的 void setParameters(PreparedStatement var1) throws SQLException;}
DefaultParameterHandler默认参数处理器的方法
public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); List parameterMappings = this.boundSql.getParameterMappings(); if(parameterMappings != null) { for(int i = 0; i < parameterMappings.size(); ++i) { ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if(parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; if(this.boundSql.hasAdditionalParameter(propertyName)) { value = this.boundSql.getAdditionalParameter(propertyName); } else if(this.parameterObject == null) { value = null; } else if(this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) { value = this.parameterObject; } else { MetaObject metaObject = this.configuration.newMetaObject(this.parameterObject); value = metaObject.getValue(propertyName); } //这里用调用 TypeHandler类型映射处理器来映射 TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if(value == null && jdbcType == null) { jdbcType = this.configuration.getJdbcTypeForNull(); } try { //类型处理器设置参数映射 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException var10) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var10, var10); } catch (SQLException var11) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + var11, var11); } } } } }
no4的方法
public List query(Statement statement, ResultHandler resultHandler) throws SQLException { //此处调用原生sql的处理器 PreparedStatement ps = (PreparedStatement)statement; //发出原生sql命令 ps.execute(); //采用ResultHandler结果处理器对结果集封装 return this.resultSetHandler.handleResultSets(ps); }
ResultHandler代码
public interface ResultSetHandler { //此处调用的是DefaultResultSetHandler的方法 List handleResultSets(Statement var1) throws SQLException; void handleOutputParameters(CallableStatement var1) throws SQLException;}
DefaultResultSetHandler的方法
public List handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId()); List multipleResults = new ArrayList(); int resultSetCount = 0; ResultSetWrapper rsw = this.getFirstResultSet(stmt); List resultMaps = this.mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); this.validateResultMapsCount(rsw, resultMapCount); while(rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount); this.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null); rsw = this.getNextResultSet(stmt); this.cleanUpAfterHandlingResultSet(); ++resultSetCount; } String[] resultSets = this.mappedStatement.getResulSets(); if(resultSets != null) { while(rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = (ResultMapping)this.nextResultMaps.get(resultSets[resultSetCount]); if(parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = this.configuration.getResultMap(nestedResultMapId); this.handleResultSet(rsw, resultMap, (List)null, parentMapping); } rsw = this.getNextResultSet(stmt); this.cleanUpAfterHandlingResultSet(); ++resultSetCount; } } return this.collapseSingleResultList(multipleResults); }//处理结果集 private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List multipleResults, ResultMapping parentMapping) throws SQLException { try { if(parentMapping != null) { this.handleRowValues(rsw, resultMap, (ResultHandler)null, RowBounds.DEFAULT, parentMapping); } else if(this.resultHandler == null) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory); this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null); multipleResults.add(defaultResultHandler.getResultList()); } else { this.handleRowValues(rsw, resultMap, this.resultHandler, this.rowBounds, (ResultMapping)null); } } finally { this.closeResultSet(rsw.getResultSet()); } }private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { if(resultMap.hasNestedResultMaps()) { this.ensureNoRowBounds(); this.checkResultHandler(); this.handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } else { this.handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping); } } private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext resultContext = new DefaultResultContext(); this.skipRows(rsw.getResultSet(), rowBounds); Object rowValue = null; while(this.shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, (String)null); CacheKey rowKey = this.createRowKey(discriminatedResultMap, rsw, (String)null); Object partialObject = this.nestedResultObjects.get(rowKey); if(this.mappedStatement.isResultOrdered()) { if(partialObject == null && rowValue != null) { this.nestedResultObjects.clear(); this.storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); } //获取行的值 rowValue = this.getRowValue(rsw, discriminatedResultMap, rowKey, (String)null, partialObject); } else { rowValue = this.getRowValue(rsw, discriminatedResultMap, rowKey, (String)null, partialObject); if(partialObject == null) { this.storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); } } } if(rowValue != null && this.mappedStatement.isResultOrdered() && this.shouldProcessMoreRows(resultContext, rowBounds)) { this.storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); } } String resultMapId = resultMap.getId(); Object resultObject = partialObject; if(partialObject != null) { MetaObject metaObject = this.configuration.newMetaObject(partialObject); this.putAncestor(partialObject, resultMapId, columnPrefix); this.applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false); this.ancestorObjects.remove(resultMapId); } else { ResultLoaderMap lazyLoader = new ResultLoaderMap(); resultObject = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if(resultObject != null && !this.typeHandlerRegistry.hasTypeHandler(resultMap.getType())) { MetaObject metaObject = this.configuration.newMetaObject(resultObject); boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty(); if(this.shouldApplyAutomaticMappings(resultMap, true)) { foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } foundValues = this.applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; this.putAncestor(resultObject, resultMapId, columnPrefix); foundValues = this.applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues; this.ancestorObjects.remove(resultMapId); foundValues = lazyLoader.size() > 0 || foundValues; resultObject = foundValues?resultObject:null; } if(combinedKey != CacheKey.NULL_CACHE_KEY) { this.nestedResultObjects.put(combinedKey, resultObject); } } return resultObject; }private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) { return resultMap.getAutoMapping() != null?resultMap.getAutoMapping().booleanValue():(isNested?AutoMappingBehavior.FULL == this.configuration.getAutoMappingBehavior():AutoMappingBehavior.NONE != this.configuration.getAutoMappingBehavior()); } private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { List autoMapping = this.createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if(autoMapping.size() > 0) { Iterator i$ = autoMapping.iterator(); while(true) { //这里使用了内部类对参数和结果集进行映射 DefaultResultSetHandler.UnMappedColumAutoMapping mapping; Object value; do { if(!i$.hasNext()) { return foundValues; } mapping = (DefaultResultSetHandler.UnMappedColumAutoMapping)i$.next(); value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); } while(value == null && !this.configuration.isCallSettersOnNulls()); if(value != null || !mapping.primitive) { metaObject.setValue(mapping.property, value); } foundValues = true; } } else { return foundValues; } } private static class UnMappedColumAutoMapping { private final String column; private final String property; private final TypeHandler> typeHandler; private final boolean primitive; //此处才类型器对结果进行映射 public UnMappedColumAutoMapping(String column, String property, TypeHandler> typeHandler, boolean primitive) { this.column = column; this.property = property; this.typeHandler = typeHandler; this.primitive = primitive; } }
JAVA进阶架构程序员福利:我这里还总结整理了比较全面的JAVA相关的面试资料,都已经整理成了
PDF版,这些都可以分享给大家,关注私信我:【806】,免费领取!
mybatis 原理_Mybatis的工作原理相关推荐
- 计算机输入输出设备说课稿,信息技术七年级西交大版 第三节 计算机系统的组成与工作原理计算机系统及工作原理说课稿 (共15张PPT)...
<信息技术七年级西交大版 第三节 计算机系统的组成与工作原理计算机系统及工作原理说课稿 (共15张PPT)>由会员分享,可在线阅读,更多相关<信息技术七年级西交大版 第三节 计算机系 ...
- c语言智能插座多线程原理,智能插座原理—智能插座的工作原理是什么
近年来,物联技术有了一个更高层次的发展,并且物联技术深入到智能家居领域当中,与智能化家电结合,设计制造出很多非常棒的智能电子产品.其中,很多插座厂商也朝着智能化的方向发展,智能插座产生.那么智能插座的 ...
- 现在使用计算机器工作原理,计算机显示器工作原理(17页)-原创力文档
计算机显示器工作原理 计算机显示器工作原理 显示器技术 长宽比和可视区域 多频扫描显示器 显示器连接 显示器的颜色深度 LCD显示器 LCD的功能和特性 CRT显示器 CRT的功能和特性 LCD和CR ...
- Linux运维系列总结-Linux系统启动过程、WEB工作原理、DHCP工作原理、DNS解析原理、NFS网络文件系统、FTP文件传输协议、PXE+KICKSTART自动安装系统
Linux运维系列总结-Linux系统启动过程.WEB工作原理.DHCP工作原理.DNS解析原理.NFS网络文件系统.FTP文件传输协议.PXE+KICKSTART自动安装系统 1.Linux系统的启 ...
- MyBatis 核心对象,工作原理及源码解读
相关内容: 架构师系列内容:架构师学习笔记(持续更新) Mybatis工作原理 InputStream inputStream = Resources.getResourceAsStream(reso ...
- MyBatis核心流程以及工作原理
MyBatis核心对象 根据以下这四大核心对象,我们就能理清MyBatis的工作原理. SqlSession对象,该对象中包含了执行SQL语句的所有方法.类似于JDBC里面的Connection. E ...
- 计算机光盘工作原理,CD-ROM的工作原理
CD-ROM的工作原理 笔记本外置CD-ROMCD-ROM光盘由碳酸脂做成,中心带有直径15mm的孔洞.在盘基上浇铸了一个螺旋状的物理磁道,从光盘的内部一直螺旋到最外圈,磁道内部排列着一个个蚀刻的&q ...
- 计算机生活工作原理,计算机基本工作原理是什么?
在介绍计算机的工作原理之前,首先了解一下什么是计算机指令. 简单的理解,指令指的是能被计算机识别并执行的二进制代码,它规定了计算机能够完成的某一种操作.一条指令通常由 2 部分组成,分别是操作码和操作 ...
- java jsp 原理_jsp的工作原理是什么
JSP全名为Java Server Pages,其根本是一个简化的Servlet.一种动态网页技术标准.它实现了Html语法中的java扩展(以 形式).JSP与Servlet一样,是在服务器端执行的 ...
最新文章
- Joda-Time中两个日期之间的天数
- Python列表解析式总结
- 哈尔滨工业大学计算机学院官网,哈尔滨工业大学计算机学院 唐好选 tanghx@hope.hit...
- [Tips]Torch功能点记录
- POJ 3984 迷宫问题
- instrumentation模拟很多activity的操作
- 详解深度语义匹配模型DSSM和他的兄弟姐妹
- python全栈开发_day25_面向对象的接口,多态,鸭子类型,抽象父类,析构格式化,反射和断言...
- Serv-U FTP - v15.3.1.155特别版
- 首席CEO乔布斯他是如何拉住了苹果快破产的边缘?他的运营方法是什么呢?
- 1: 上山拜师--ARM简介
- (附源码)springboot大学生就业质量调查分析系统 毕业设计161457
- 福昕阅读器给pdf创建目录方法
- 矿物质饲料补充剂的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
- 尺寸有点太大了吧,我已经受不鸟了
- 如何在ESP8266中使用深度睡眠模式降低功耗
- Unity 从零开始的2D游戏开发 —— 角色移动脚本
- 艺术品经营单位备案申报材料和艺术品经营单位备案申请表格式
- 计算机专业的学生答辩稿,计算机专业毕业论文答辩自述稿范文
- JAVA——勾股定理
热门文章
- 谁与争锋,2020腾讯广告算法大赛初赛正式启动
- python爬虫外快_利用Python爬虫轻松挣外快的几个方法(值得收藏)
- 视频号、抖音、海外Tiktok到底该选择那个平台更好
- 算法题解题方法技巧及典例汇总
- 吴恩达|机器学习作业3.1前馈神经网络
- Java并发(三)——线程池
- Linux编程(9)_进程
- 用Kubeadm安装K8s后,kube-flannel-ds一直CrashLoopBackOff
- table中添加下拉框
- dataframe drop_Pandas数据结构Series和DataFrame基础详解