Mybatis架构解读

1. 架构图

如题,这就是MyBatis的执行架构图。解释一下:我们在使用MyBatisCRUD操作的时候,一般有两种方式,一、直接调用sqlSessioncrud方法;二、通过调用getMapper获取到接口代理的实现类,然后在代理方法中调用了crud方法。可以看到,本质相同,最终调用的都是sqlSession的方法,上图就是CRUD执行的流程

2. 执行流程图

我们先来看一下我们执行一个MyBatis的查询,需要做什么。

//加载一个配置文件
InputStream resourceAsStream = Resources.getResourceAsStream("main.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory build = sqlSessionFactoryBuilder.build(resourceAsStream);
SqlSession sqlSession = build.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);//代理模式创建了一个实现类
List<User> all = mapper.findAll(1);
all.forEach(System.out::println);

这就是一个最简单的查询过程。下面我们来分析一下他们每一步做了什么事情。

2.1 Resources.getResourceAsSteam

很简单,读取了一个配置文件。可能有的小伙伴这个样子干过,直接将通过本类的类加载器拿到资源路径,然后直接获取这个主配置文件,但提示未找到。

看一下他的源码,他直接拿了一个系统类加载器。

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);if (in == null) {throw new IOException("Could not find resource " + resource);}return in;}ClassLoaderWrapper() {try {systemClassLoader = ClassLoader.getSystemClassLoader();} catch (SecurityException ignored) {// AccessControlException on Google App Engine}}

这个时候,我们自己使用ClassLoader获取系统类加载器加载资源, 这个时候也是可以成功获取的。于是想到了一个方法我比较了一下本类类加载器和系统类加载的类别,发现都是通过ApplicationClassLoader加载的,但系统类加载器无法加载

后来了解到的原因就是由于Maven插件的原因,在插件的地方指定一个Resource的映射路径即可,不过建议直接使用MyBatis的加载方式,简单一点。

2.2 new SqlSessionFactoryBuilder.build

创建了一个SqlSessionFactoryBuilder构建者对象,构建者模式

然后通过build方法加载配置文件的资源。配置文件包括:主配置文件、Mapper文件或者注解。

来看一下我们的主配置文件

<configuration><typeAliases><package name="com.bywlstudio.bean"/></typeAliases><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="com.mysql.cj.jdbc.Driver"/><……………………></dataSource></environment></environments><mappers><mapper class="com.bywlstudio.mapper.UserMapper"></mapper></mappers>
</configuration>

XML文件,MyBatis通过XPath语法进行解析,首先拿了一个Configuration节点,然后再解析内部的节点,每一个节点对应一个方法。看一下源码

private void parseConfiguration(XNode root) {try {//issue #117 read properties firstpropertiesElement(root.evalNode("properties"));Properties settings = settingsAsProperties(root.evalNode("settings"));loadCustomVfs(settings);loadCustomLogImpl(settings);typeAliasesElement(root.evalNode("typeAliases"));pluginElement(root.evalNode("plugins"));objectFactoryElement(root.evalNode("objectFactory"));objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));reflectorFactoryElement(root.evalNode("reflectorFactory"));settingsElement(settings);// read it after objectFactory and objectWrapperFactory issue #631environmentsElement(root.evalNode("environments"));databaseIdProviderElement(root.evalNode("databaseIdProvider"));typeHandlerElement(root.evalNode("typeHandlers"));mapperElement(root.evalNode("mappers"));} catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);}}

接下来要做的事情,就比较清晰了,解析每一个XML标签中的节点、文本和属性值,为对应的对象进行封装

我们主要看一下environments解析做了什么。

要看他做了什么,得先看它有什么。

它内部有多个environment元素,还有最关键的信息,事务管理者和数据源

所以在这个方法中他封装了一个Environment对象,内部存放了一个事务工厂和一个数据源对象。看一下Environment类信息

public final class Environment {private final String id;private final TransactionFactory transactionFactory;private final DataSource dataSource;

接下来再看重头戏Mappers的解析

Mappers中可以存在四种映射方式:面试题

  • package。指定一个需要扫描的包
  • resource。指定一个本地的mapper映射文件
  • url。指定一个url可以为网络的mapper映射文件
  • class。指定一个类作为一个需要被代理的mapper

接下来我们看一下他的处理方式:

 private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {//子节点是否为packageif ("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");//属性是否为resourceif (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);InputStream inputStream = Resources.getResourceAsStream(resource);XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());//xml方式处理mapperParser.parse();//属性是否为url} 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();//属性是否为class} 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.");}}}}}

2.2.1 xml方式

先来聊一下xml的处理方式

首先拿到对应的mapper文件,之后创建了一个解析该资源的类XMLMapperBuilder。解析子标签mapper等等属性,逻辑和之前一样,最后将所有的信息添加到了Configutation类中。

2.2.2 注解方式

一个核心方法org.apache.ibatis.binding.MapperRegistry#addMapper有一个点,当你的Mapper不是一个接口的时候,他直接不处理了

public <T> void addMapper(Class<T> type) {//是否为接口if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {//将Mapper的信息,封装到一个MapperProxyFactory工厂中knownMappers.put(type, new MapperProxyFactory<>(type));// It's important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it won't try.MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);//具体解析parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}}

这个方法做的最重要的一件事情:

  • 将所有的mapper信息存放到了MapperRegistry#knownMappers集合中

具体的解析过程中,他还设置了StatementType=PREPARED;

后面解析的过程主要进行注解解析,判断是否存在某某某注解,最后将所有的信息封装到了一个Configuration中。

每一条SQL对应一个MappedStatement对象,该对象不可变

public final class MappedStatement {private String resource;private Configuration configuration;private String id;private Integer fetchSize;private Integer timeout;private StatementType statementType;private ResultSetType resultSetType;private SqlSource sqlSource;private Cache cache;private ParameterMap parameterMap;private List<ResultMap> resultMaps;private boolean flushCacheRequired;private boolean useCache;private boolean resultOrdered;private SqlCommandType sqlCommandType;private KeyGenerator keyGenerator;private String[] keyProperties;private String[] keyColumns;private boolean hasNestedResultMaps;private String databaseId;private Log statementLog;private LanguageDriver lang;private String[] resultSets;

2.3 openSession

本质上创建了一个DefalutSqlSession对象。创建了Executor

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {final Environment environment = configuration.getEnvironment();//获取之前的事务工厂final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);//创建一个事务tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//创建一个执行器final Executor executor = configuration.newExecutor(tx, execType);//创建了一个SQLSessionreturn 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();}}

2.4 getMapper

还记得在build中,Mybatismapper信息封装为一个MapperProxyFactory添加到了一个List中,而现在的GetMapper就从里面拿到对应的Mapper代理工厂信息,然后创建对应的Mapper代理对象,最后返回

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {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);}}

我们来看一下我们的代理对象

 @SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {//这个就是我们的代理对象,也就是实现了代理接口的类final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}

看一下这个里面有什么

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 Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

2.5 具体的CRUD

getMapper中,我们知道了此时返回的是一个该接口的代理对象,当我们执行具体的方法的时候,就走了其代理方法。

主要执行逻辑是:判断Sql的操作类型,然后执行对应的方法,如果是查询,则从缓存中查询,如果没有,则查询数据库,查到以后,将查询到的信息进行封装,封装以后,将这个信息添加的缓存中,然后返回。

他首先判断了该方法的类信息是不是object,然后判断是不是默认方法,如果是分别执行。最后他给本类的methodCache中添加了一个方法映射

@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (isDefaultMethod(method)) {return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}final MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}

接下来再看一下具体的执行方法。

public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT://返回值是否为nullif (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;//返回值是否为多个(List)} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {//返回值是否为键值result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {//返回值为一个Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);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());}return result;}

这个时候就回归到了SqlSession的API调用了

2.6 SqlSession具体调用

Select为例

首先他生成了一个cacheKey,拿这个key从缓存中找,如果没有查询数据库,查到以后将对应的结果放到缓存中,然后返回给用户

//调用函数,
executor.query(MappedStatement, parameter,  RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;//查询缓存list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}

  • 查询以后放置到缓存并且返回的操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;localCache.putObject(key, EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}//缓存添加localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}//返回return list;}

  • 重头戏来了,接下来将会创建架构图里的第二个内容StatementHandler
@Overridepublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();//创建了一个StatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);stmt = prepareStatement(handler, ms.getStatementLog());return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}}

在创建的时候,他将所有的StatementHandler拦截器都执行了一遍。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {//创建了一个StatementHandlerStatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);//执行所有的Statement拦截器(所有)statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}

还记得在build的时候,指定了一个StatementType.PREPARED类型吗?这个时候这个东西就开始起作用了。在创建RoutingStatementHandler这个类的时候,他根据StatementType类型创建了一个子类,而现在创建的就是PreparedStatementHandler,而在这个类的父类里创建了ParameterHandlerResultSetHandler

public class RoutingStatementHandler implements StatementHandler {private final StatementHandler delegate;public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}}

他们在创建的时候又将对应的所有拦截器执行了一遍。

到了这里,架构图里的东西已经全部出来了。

接下来就是执行SQL

2.7 总结

我们来看一下Configuration类。一家人整整齐齐,图上的东西都在这里了

整体执行逻辑就是:

  • 创建一个Executor对象,将事务和数据源放进去
  • 创建StatementHandler实现类,将其对应的拦截器执行了
  • 在创建实现类的时候又创建了ParameterHandler实现类,并且将其拦截器执行了
  • 同时也创建了ResultSetHandler,并且将其拦截器执行了
  • 之后通过这个结果集映射做了一次对象封装,将数据存到缓存里,然后返回了。

3. 面试题

面试题整理自网络,方便复习

3.1 #{}和${}的区别

  • ${}是Properties文件中的变量占位符,可以应用于标签属性值和SQL内部,属于静态替换
  • #{}是SQL参数占位符,MyBatis会将SQL中的#{}替换为?,在SQL执行前通过PreparedStatement的参数设置方法,设置具体的参数值。

3.2 XML映射文件中,除了select|insert|update|delete还有哪些标签

答:

  • <resultMap>。自定义结果集映射
  • <cache>。定义当前命名空间中的缓存配置策略
  • <cache-ref>。引用其他命名空间的缓存配置
  • <sql>。定义一个SQL语句块,可以被引用
  • 动态SQL
    • <include>引用一个SQL块
    • <foreach>
    • <if>
    • <where>
    • <trim>

3.3 最佳实践中,通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

答:在MyBaits中,每一个命名空间的方法都拥有一个唯一标识。接口全限定类名.方法名,所以参数不同不能重载。其工作原理是通过JDK动态代理实现的。真正执行的是MapperProxy

3.4 MyBatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql

3.5 简述Mybatis的插件运行原理,以及如何编写一个插件。

答:Mybatis仅可以编写针对ParameterHandlerResultSetHandlerStatementHandlerExecutor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。

实现MybatisInterceptor接口并重写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

3.6 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

答:第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。第二种是使用sql列的别名功能,将列别名书写为对象属性名,比如T_NAME AS NAME,对象属性名一般是name,小写,但是列名不区分大小写,Mybatis会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成T_NAME AS NaMe,Mybatis一样可以正常工作。

有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

3.7 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别。

答:在MybatisResultMap中可以通过Result标签或者注解指定需要映射的表。通过内部的onemany实现具体的一对一或者一对多映射关系。

比如可以通过Result中的one实现一对一映射。内部的select指定另一个查询语句,fetchType用于指定是否使用懒加载

@Results({@Result(id = true , column = "id" , property = "id"),@Result(column = "nickName" , property = "nickName"),@Result(column = "gender" , property = "gender"),@Result(column = "city" , property = "city"),@Result(column = "province" , property = "province"),@Result(column = "wid" , property = "wxuser" ,one = @One(select = "com.bywlstudio.dao.IWXUserDao.findWXUserById" ,fetchType = FetchType.EAGER)),})

3.8 懒加载实现原理

答:通过代理方法创建代理对象以后,在真正获取数据的时候到达拦截器的方法之后,拦截器方法首先判断当前值是否为null,如果为null,则通过预先的SQL查询并且set,最后get查询。

3.9 myBatis如何执行批处理

答:通过BatchExecutor完成批处理

3.10 MyBatis有哪些Executor执行,以及他们之间的区别

答:

  • SimpleExecutor,执行一次update或者select就开启一个statement,用完立刻关闭
  • ReuseExecutor,执行update或者select,以SQL作为key查找Statement对象,存在就使用,不存在就创建,用完以后,添加到Map<String,Statement>
  • BatchExecutor,执行update,将所有的Sql添加到批处理中,等待统一执行,缓存了多个Statement对象。

3.11 Mybatis中如何指定使用哪一种Executor执行器?

在Mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。

3.12 Mybatis是否可以映射Enum枚举类?

Mybatis可以映射枚举类,不单可以映射枚举类,Mybatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()接口方法。TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。

往期回顾

撩改JVM常见调优参数​mp.weixin.qq.com

入门JVM?读这一篇就够了​mp.weixin.qq.com

多线程知识点小结​mp.weixin.qq.com

Lock和Synchronized​mp.weixin.qq.com

你了解线程池吗?​mp.weixin.qq.com

mybatis源码_MyBatis架构和源码相关推荐

  1. Tensorflow源码解析1 -- 内核架构和源码结构

    1 主流深度学习框架对比 当今的软件开发基本都是分层化和模块化的,应用层开发会基于框架层.比如开发Linux Driver会基于Linux kernel,开发Android app会基于Android ...

  2. docker containerd 架构和源码简单分析

    docker containerd 架构和源码简单分析 本文结合docker1.12简单说明一下docker 的现有框架,简单分析docker containerd的架构和源码. docker发展到现 ...

  3. 【原创】【专栏】《Linux设备驱动程序》--- LDD3源码目录结构和源码分析经典链接

    http://blog.csdn.net/geng823/article/details/37567557 [原创][专栏]<Linux设备驱动程序>--- LDD3源码目录结构和源码分析 ...

  4. android 内核 netlink 上报,Network Daemon(Android Netd)架构和源码分析

    平台: RK3066 ARM9双核 Android4.1 一 Network Daemon(netd)功能概述: Netd是Android的网络守护进程.NetD是个网络管家,封装了复杂的底层各种类型 ...

  5. yum更换本地源、yum下载和源码包安装

    7.6 yum更换国内源 恢复系统默认yum源配置: [root@gaohanwei Packages]# cd /etc/yum.repos.d [root@gaohanwei yum.repos. ...

  6. java底层app_Java底层类和源码分析系列-ArrayBlockingQueue底层架构和源码分析

    ArrayBlockingQueue是一个基于数组实现的有界的阻塞队列. 几个要点 ArrayBlockingQueue是一个用数组实现的队列,所以在效率上比链表结构的LinkedBlockingQu ...

  7. mybatis源码_Mybatis源码之SqlSession

    SqlSession简介 Mybatis是一个强大的ORM框架,它通过接口式编程为开发者屏蔽了传统JDBC的诸多不便,以简单的方式提供强大的扩展能力.其中的接口式编程就是指日常使用的Mapper接口, ...

  8. 【博学谷学习记录】超强总结,用心分享 | 架构师 Mybatis源码学习总结

    Mybatis源码学习 文章目录 Mybatis源码学习 一.Mybatis架构设计 二.源码剖析 1.如何解析的全局配置文件 解析配置文件源码流程 2.如何解析的映射配置文件 Select inse ...

  9. java毕业设计基于BS架构的疫情包联信息管理系统的设计与实现mybatis+源码+调试部署+系统+数据库+lw

    java毕业设计基于BS架构的疫情包联信息管理系统的设计与实现mybatis+源码+调试部署+系统+数据库+lw java毕业设计基于BS架构的疫情包联信息管理系统的设计与实现mybatis+源码+调 ...

最新文章

  1. 【linux驱动】嵌入式 Linux 对内存的直接读写(devmem)
  2. python空类型-python 空类型
  3. 岗岭集团打造中国最大的线上线下一体化的医药健康平台
  4. 老板思维:有支出必须有对应的收入
  5. java面试总结(一)-----如何准备Java初级和高级的技术面试
  6. UI设计师必备的技能|找灵感
  7. 搜狗发布全球首位 3D AI 主播,背后分身技术有玄机
  8. js设置radio单选框值选中
  9. 三星固态硬盘 SM951 NVME win7介绍与安装方法
  10. IM 产品设计思考(4)- 问答机器人
  11. 尺规作图将任意角度三等分
  12. R语言实现 懒惰学习——使用近邻分类
  13. docker安装mssql
  14. ffmpeg视频滤镜中英文对照
  15. 【渝粤教育】 广东开放大学21秋期末考试基础会计10258k2
  16. knn matting matlab,一键抠图,毛发毕现:这个GitHub项目助你快速PS
  17. WebService:跟孔浩学习(契约优先、Schema、WSDL、SOAP、用户管理小系统)
  18. text pad java_错误:无法在Textpad 8中找到或加载主类
  19. python课件百度文库_python教-教学课件.doc
  20. EnglishPlayer ---- 英语听写工具发布了!

热门文章

  1. .net数据源控件绑定mysql_数据源控件与数据绑定控件的进一步简单讨论(1)
  2. 界面 高炉系统_首钢京唐七大系统介绍
  3. Python的3种执行方式
  4. Rainmeter 天气
  5. 面试题:如何编写一个杯子测试用例
  6. vue -resource 文件提交提示process,或者拦截处理
  7. 详解微信开放平台第三方平台代小程序开发业务基本接口调用逻辑
  8. ACM ICPC 2017 Warmup Contest 2[菜鸡选手的成长]
  9. 剑指offer面试题27:二叉搜索树与双向链表
  10. PAT 1052. 卖个萌 (20)