mybatis代理机制讲解
问题描述
在使用Mybatis开发中,或者和Spring整合中,在Dao层中的Mapper接口与xml中的sql对应着,在service中直接调用Dao中的方法就可以直接访问sql。如下所示:
/**
* interface 层的代码
*/
public interface ArticleMapper {
Article selectByPrimaryKey(Integer id);
}
在xml中,我们的sql语句这样定义:
<?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="cn.com.gkmeteor.article.provider.mapper.ArticleMapper"> <!-- interface 与 sql文件 关联语句 -->
... <!-- xml中对应的sql -->
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from tb_article
where id = #{id,jdbcType=INTEGER}
</select> ...
</mapper>
我们在业务代码中,如下调用的方式就可以从数据库中获取到数据:
@Resource
private ArticleMapper articleMapper; ... public Article getArticleById(Integer id) {
return articleMapper.selectByPrimaryKey(id);
}
如果仔细研究就会发现,ArticleMapper 接口并没有实现类,我们看到xml代码中的关联语句,知道知道mapper和sql是有关联的,但是也只知道这么多了。问题是,执行mapper中的方法后为什么就可以执行xml中定义的sql呢?本文对这个问题进行回答。
mybatis执行sql的完整流程
在MyBatis框架下中调用一个sql时候,会经历下面这个完整的流程:
// 解析配置文件,生成配置
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource); // 根据配置,构建一个SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 得到一个真正可用的SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(); // 从SqlSession获取interface的代理
ArticleMapper articleMapperProxy = sqlSession.getMapper(ArticleMapper.class); // 执行代理类中的方法
Article article = articleMapperProxy.selectByPrimaryKey(123); // 以下省略对 article 的操作
我们这里不讨论mybatis解析配置文件和加载configuration的过程,也就是上面的 build 方法。这里面同样有些学问,在这里我们简单认为通过 build,我们得到了 sqlSessionFactory 对象,里面包含 configuration 对象。
在这篇文章中,我们只关注mybatis初始化完成之后,mybatis框架到底做了哪些事情来让我们自如地执行sql。我们先来了解上面代码中重要的三个概念。
SqlSessionFactoryBuilder
这是SqlSessionFactory的构造器,其中最重要的是build方法,通过build方法可以构建出 SqlSessionFactory的实例。
SqlSessionFactory
SqlSessionFactory实例被 build 方法创建出来以后,在应用的运行期间一直存在。它包含了很多重要信息,这些信息在之后的调用过程中会反复使用。让我们来看一下:
# sqlSessionFactory 中的重要信息 sqlSessionFactory
configuration
environment # 里面有 dataSource 信息
mapperRegistry
config # 里面有配置信息
knownMappers # 里面有所有的 mapper,让我们记住它,它将是后面篇章中的主角
mappedStatements # 里面有所有 mapper 的所有方法
resultMaps # 里面有所有 xml 中的所有 resultMap
sqlFragments # 里面有所有的 sql 片段
sqlSessionFactory最重要的是 openSession 方法,返回了一个 SqlSession 实例。
SqlSession
一个请求线程会有一个SqlSession实例。该实例贯穿了数据库连接的生命周期,是访问数据库的唯一渠道。sqlSession 中有 selectList,selectOne 等所有的sql增删改查数据库的方法,最终对数据库的操作都落在 sqlSession 上。
sqlSession的getMapper流程
有了上面三个重点类的基本知识以后,我们知道对数据库的操作都落在 sqlSession 上。可以先把结论告诉你,sqlSession.getMapper之后,获取到的其实是 configuration 对象中的 mapper的代理类。那么,具体的过程是怎样的?
详细流程
我们跟踪进去看sqlSession.getMapper以后到底发生了什么。
public class DefaultSqlSession implements SqlSession { private final Configuration configuration;
private final Executor executor; private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList; public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
this.configuration = configuration;
this.executor = executor;
this.dirty = false;
this.autoCommit = autoCommit;
} public DefaultSqlSession(Configuration configuration, Executor executor) {
this(configuration, executor, false);
} ... // 重点方法
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
} }
通过调用DefaultSqlSession的getMapper方法并且传入一个类型对象获取,底层调用的是配置对象configuration的getMapper方法,configuration对象是我们在加载DefaultSqlSessionFactory时传入的。
然后我们再来看下这个配置对象的getMapper,传入的是【类型对象】和【自身 (DefaultSqlSession) 】。(这里说的类型对象,就是我们平时写的DAO层接口,里面是一些数据库操作的接口方法。)
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
我们看到这个configuration的getMapper方法里调用的是mapperRegistry的getMapper方法,参数依然是类型对象和sqlSession。我们要先来看下这个 MapperRegistry 是什么。
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap(); public MapperRegistry(Configuration config) {
this.config = config;
}
....
}
从这里我们可以知道其实这个MapperRegistry就是保持了一个Configuration对象和一个HashMap,而这个HashMap的key是类型对象,value是MapperProxyFactory。我们这里先不管MapperProxyFactory是什么东西,我们现在只需要知道MapperRegistry是这么一个东西就可以了。这里有人会问MapperRegistry对象是怎么来的,这里是在初始化Configuration对象时初始化了这个MapperRegistry对象的,具体代码可以看configuration对象的初始化。
接下来我们继续看下这个MapperRegistry的getMapper方法。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
这里我们可以看到从knownMappers中获取key为类型对象的MapperProxyFactory对象。然后调用MapperProxyFactory对象的newInstance方法返回,newInstance方法传入sqlSession对象。到这里我们可能看不出什么端倪,那我们就继续往下看这个newInstance方法做的什么事情吧。
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap(); public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
} public Class<T> getMapperInterface() {
return this.mapperInterface;
} public Map<Method, MapperMethod> getMethodCache() {
return this.methodCache;
} protected T newInstance(MapperProxy<T> mapperProxy) {
// 新建一个mapperProxy的代理类
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
} public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
}
这里我们可以看到MapperProxyFactory直接new了一个MapperProxy对象,然后调用另外一重载的newInstance方法传入MapperProxy对象。这里我们可以看出一些东西了,通过调用Proxy.newProxyInstance动态代理了我们的mapperProxy对象!这里的mapperInterface即我们的dao层(持久层)接口的类型对象。
所以得出总结:我们通过sqlSesssion.getMapper(clazz)得到的Mapper对象是一个mapperProxy的代理类!这点印证了结论中说的getMapper其实得到的是mapper的方法的代理。
由于代理,我每次在自己的程序里执行 mapper 中的方法,都会走 MapperProxy中的invoke方法。我们来看看 MapperProxy 的 invoke方法中。
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
....
}
MapperProxy必然实现了InvocationHandler接口。所以当我们调用我们的持久层接口的方法时,必然就会调用到这个MapperProxy对象的invoke方法。这属于JDK的代理的知识。所以接下来我们进入这个方法看看具体mybatis为我们做了什么。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
// 如果是toString, equals之类的方法,直接执行
try {
return method.invoke(this, args);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
} else {
// 从缓存的 MapperMethod 中拿,如果没有,就新建一个,并且放入缓存对象
MapperMethod mapperMethod = this.cachedMapperMethod(method);
// mapperMethod 执行
return mapperMethod.execute(this.sqlSession, args);
}
}
看看 cachedMapperMethod 方法:
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;
}
从代码中我们可以看到前面做了一个判断,这个判断主要是遇到像toString方法或者equals方法时,可以正常调用。然后我们可以看到它调用cachedMapperMethod返回MapperMethod对象,接着就执行这个MapperMethod对象的execute方法。这个cachedMapperMethod方法主要是能缓存我们使用过的一些mapperMethod对象,方便下次使用。
MapperMethod
mybatis的动态代理机制的重点来了!像其他所有的类一样,MapperMethod也有实例化的过程和重要的execute方法。
MapperMethod实例化过程
MapperMethod对象主要是获取mapper中的某个方法对应的sql命令和执行相应SQL操作等的处理,非常重要。通过 mapperMethod,mapper中的方法就与 xml 中定义的sql语句结合起来了。
public class MapperMethod {
private final MapperMethod.SqlCommand command;
private final MapperMethod.MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 获取 SqlCommand 类
// 其中主要记录了mapper中的method方法的名称与sql执行类型(INSERT还是SELECT)的映射
// 方便之后在 execute 的时候根据sql执行类型来执行对应的 switch case 逻辑
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method); // 获取 MethodSignature 类,其中记录了method方法的返回类型,以及其他一些必要的、和sql执行相关的信息
// 这些信息有很多,大部分是从configuration中处理得到的
// 如何处理得到这些信息,以及如何使用这些信息,属于另一个知识范畴了
// 在这里我们可以简单地理解为获取了method方法的返回类型等信息,在之后 execute的时候要使用的
this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
}
....
}
MapperMethod execute方法
来看一下 mapperMethod 对象的 execute方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object param;
Object result;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case 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 if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
} 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;
}
}
我们可以清晰的看到,这里针对数据库的增删改查做了对应的操作,这里我们可以看下查询操作。我们可以看到这里针对方法的不同返回值作了不同的处理,我们以其中一种情况来举例:
// SELECT语句,当所有情况都不满足时,执行下面的逻辑
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
这里我们可以看到它将方法参数类型转换成数据库层面上的参数类型,最后调用sqlSession对象的selectOne方法执行。所以我们看到最后还是回到sqlSession对象上来,符合前面所说的sqlSession是mybatis提供的与数据库交互的唯一对象。
接下来我们看下这个selectOne方法做了什么事,这里我们看的是defaultSqlSession的selectOne方法。
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
我们看到它调用selectList方法,通过取返回值的第一个值作为结果返回。那么我们来看下这个selectList方法。
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
} public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
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;
}
我们可以看到这里调用了executor的query方法,我们再进入到query里看看。这里我们看的是BaseExecutor的query方法,先不看缓存的Executor中的query方法。注意看getBoundSql这个方法:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// getBoundSql方法非常重要
// 作用是根据传入的sql参数,加上configuration和mappedStatement中的信息,组装一个 BoundSql 给你
// 具体怎么组装的,又有一段复杂的逻辑,其中也牵涉到了缓存技术,在这里不展开研究
// 在这里只要简单的认为我们得到了 mappedStatement 对应的 BoundSql
// 里面有sql语句和sql参数,是一个完整的sql了,终于马上可以去数据库执行一把了
BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
} ... 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 (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;
}
}
boundSql中到底有些什么,我们来看看:
-- boundSql 中的sql语句如下
SELECT id, job_name, job_group_name, trigger_name, trigger_group_name, cron, status, create_time, update_time, operate_id, operate_name FROM cron_quartz_record WHERE job_group_name = ?
回到代码跟踪,进入queryFromDatabase这个重点方法:
private <E> List<E> 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 {
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;
}
我们看到有个一个方法doQuery,进入方法看看做了什么。点进去后我们发现是抽象方法,我们选择simpleExecutor子类查看实现。
public <E> List<E> doQuery( MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null; List var9;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
} return var9;
} private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Connection connection = this.getConnection(statementLog);
Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
我们看到了熟悉的 statement 和 prepareStatement 方法,这很像 JDBC 中的 preparedStatement。我们也看到了 query 方法,这很像 JDBC 中的 executeQuery。我们知道,代码跟踪的旅程快要结束了。
我们可以看到通过configuration对象的newStatementHandler方法构建了一个StatementHandler,然后在调用prepareStatement方法中获取连接对象,通过StatementHandler得到Statement对象。另外我们注意到在获取了Statement对象后调用了parameterize方法。如果继续跟踪下去,我们可以发现会调用ParameterHandler对象的setParameters方法去处理我们的参数。所以这里的prepareStatement方法主要使用了StatementHandler和ParameterHandler对象帮助我们处理语句集和参数的处理。最后还调用了StatementHandler的query方法,我们继续跟踪下去。
这里我们进入到PreparedStatementHandler这个handler查看代码。
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
c ps = (PreparedStatement)statement;
ps.execute(); // 操作数据库
return this.resultSetHandler.handleResultSets(ps);
}
看到这里,我们终于找到了操作数据库的地方了,就是ps.execute()这句代码。底层我们可以发现就是原始的JDBC!然后将这个执行后的PreparedStatement交给resultSetHandler处理结果集,最后返回我们需要的结果集。
总结
我们总结一下mybatis的动态代理机制:
sqlSession是唯一访问数据库的渠道,通过sqlSession.getMapper方法,得到到是mapper的代理类,已经不是我们在预先写好的mapper接口了。当我们执行mapper中的方法,在代理类中的执行逻辑如下:
- if 判断
- else if 判断
- cachedMapperMethod 重点方法
- mapperMethod.execute 重点方法,实际执行的方法
在mapperMethod对象中,通过getBoundSql方法,早早的为你准备好了mapper中的方法实际对应的sql语句,而且是可以被执行的preparedStatement。这样一来,我们在业务代码中执行mapper中的方法,实际上就是去数据库中执行对应的sql语句了。
参考资料
- https://segmentfault.com/a/1190000015117926
- http://www.mybatis.org/mybatis-3/zh/getting-started.html
创作时间:05/22/2019 21:23
mybatis代理机制讲解的更多相关文章
- 从头捋了一遍 Java 代理机制,收获颇丰
尽人事,听天命.博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 「CS-Wiki」Gitee ...
- Java 动态代理机制详解
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
- java的动态代理机制详解
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
- java Proxy(代理机制)
我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的我们的功能,我们更需要学习 ...
- mybatis源代码分析:mybatis延迟加载机制改进
在上一篇博客<mybatis源代码分析:深入了解mybatis延迟加载机制>讲诉了mybatis延迟加载的具体机制及实现原理. 可以看出,如果查询结果对象中有一个属性是需要延迟加载的,那整 ...
- Java的动态代理机制详解(转)
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
- 详解Java动态代理机制
之前介绍的反射和注解都是Java中的动态特性,还有即将介绍的动态代理也是Java中的一个动态特性.这些动态特性使得我们的程序很灵活.动态代理是面向AOP编程的基础.通过动态代理,我们可以在运行时动态创 ...
- (转)java的动态代理机制详解
原文出自:http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一 ...
- [转载] java的动态代理机制详解
转载自http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 代理模式 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代 ...
随机推荐
- mui中判断是点击还是滑动
判断和滑动是两种触发方式 滑动分为四种,上下左右(swipeup,swipedown,swipeleft,swiperight) 点击分为两种,点击和双击,一般用单机(tap) 根据自己不同的需求进行 ...
- 【Java】后台将文件上传至远程服务器
问题:由于系统在局域网(能访问外网)内,但外网无法请求局域网内服务器文件和进行处理文件. 解决:建立文件服务器,用于存储文件及外网调用. 客户端(文件上传): package cn.hkwl.lm.u ...
- 用到的Dos命令总结 持续更新
1.xcopy命令:复制的扩展命令 常用参数:/s:复制空文件夹 不使用可能会造成文件混乱 /y忽略覆盖提示 使用/y会直接覆盖全部 例子:xcopy lark-UI\dist C:\U ...
- [ZJOI2006]物流运输trans
Description 物流公司要把一批货物从码头A运到码头B.由于货物量比较大,需要n天才能运完.货物运输过程中一般要转停好几个码头.物流公司通常会设计一条固定的运输路线,以便对整个运输过程实施严格 ...
- Java 面试-即时编译( JIT )
当我们在写代码时,一个方法内部的行数自然是越少越好,这样逻辑清晰.方便阅读,其实好处远不止如此,通过即时编译,甚至可以提高执行时的性能,今天就让我们好好来了解一下其中的原理. 简介 当 JVM 的初始 ...
- 【Cocos2d-x】学习笔记目录
从2019年7月开始学习游戏引擎Cocos2dx,版本3.17. 学习笔记尽量以白话的形式表达自己对源码的理解,而不是大篇幅复制粘贴源码. 本人水平有限,欢迎批评指正! Cocos2d-x 学习笔记 ...
- Api版本管理
关于SpringMVC中如何添加,这一篇说的很详细了. http://www.cnblogs.com/jcli/p/springmvc_restful_version.html 版本管理可以通过路径进 ...
- Python开发【第七篇】列表
问题:当我们要用一系列数字的时候,我们需要将数字进行存储,我们就需要找个容器把数字装起来,我们需要用的时候再拿出来.如何将计算机运算的数据存储在一个地方,同时方便 对数据进行 增.删.改.查 列表 列 ...
- comparator接口实现时,只需要实现 int compare(T o1, T o2)方法?
从Comparator接口的源码,可以看到Comparator接口中的方法有三类: 1 普通接口方法 2 default方法 3 static方法 其中default方法和static方法 是java ...
- opencv::基本阈值操作
图像阈值(threshold) 阈值 是什么?简单点说是把图像分割的标尺,这个标尺是根据什么产生的,阈值产生算法?阈值类型.(Binary segmentation) 阈值类型一阈值二值化(thres ...