0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

源码学习之MyBatis的底层查询原理

OSC开源社区 ? 2022-10-10 11:42 ? 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

导读

本文通过MyBatis一个低版本的bug(3.4.5之前的版本)入手,分析MyBatis的一次完整的查询流程,从配置文件的解析到一个查询的完整执行过程详细解读MyBatis的一次查询流程,通过本文可以详细了解MyBatis的一次查询过程。在平时的代码编写中,发现了MyBatis一个低版本的bug(3.4.5之前的版本),由于现在很多工程中的版本都是低于3.4.5的,因此在这里用一个简单的例子复现问题,并且从源码角度分析MyBatis一次查询的流程,让大家了解MyBatis的查询原理

01问题现象 在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!

1.1 场景问题复现

如下图所示,在示例Mapper中,下面提供了一个方法queryStudents,从student表中查询出符合查询条件的数据,入参可以为student_name或者student_name的集合,示例中参数只传入的是studentName的List集合

 List studentNames = new LinkedList<>();
 studentNames.add("lct");
 studentNames.add("lct2");
 condition.setStudentNames(studentNames);
  


        select * from student
        
            
                AND student_name IN
                
                    #{studentName, jdbcType=VARCHAR}
                 foreach>
             if>


            
                AND student_name = #{studentName, jdbcType=VARCHAR}
             if>
         where>
     select>


 mapper>

2.示例代码

public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //1.获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //2.获取对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //3.获取接口的代理类对象
        StudentDao mapper = sqlSession.getMapper(StudentDao.class);
        StudentCondition condition = new StudentCondition();
        List studentNames = new LinkedList<>();
        studentNames.add("lct");
        studentNames.add("lct2");
        condition.setStudentNames(studentNames);
        //执行方法
        List students = mapper.queryStudents(condition);
    }

2.2.3 查询过程分析

1.SqlSessionFactory的构建

先看SqlSessionFactory的对象的创建过程

//1.获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

代码中首先通过调用SqlSessionFactoryBuilder中的build方法来获取对象,进入build方法

 public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

调用自身的build方法

bb27fc9c-484c-11ed-a3b6-dac502259ad0.png

图1 build方法自身调用调试图例

在这个方法里会创建一个XMLConfigBuilder的对象,用来解析传入的MyBatis的配置文件,然后调用parse方法进行解析

bb586698-484c-11ed-a3b6-dac502259ad0.png

图2 parse解析入参调试图例

在这个方法中,会从MyBatis的配置文件的根目录中获取xml的内容,其中parser这个对象是一个XPathParser的对象,这个是专门用来解析xml文件的,具体怎么从xml文件中获取到各个节点这里不再进行讲解。这里可以看到解析配置文件是从configuration这个节点开始的,在MyBatis的配置文件中这个节点也是根节点

 

        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">



    
           
     properties>
然后将解析好的xml文件传入parseConfiguration方法中,在这个方法中会获取在配置文件中的各个节点的配置

bb98ffe6-484c-11ed-a3b6-dac502259ad0.png

图3 解析配置调试图例

以获取mappers节点的配置来看具体的解析过程

 
        
     mappers>
进入mapperElement方法
mapperElement(root.evalNode("mappers"));

bbb688ae-484c-11ed-a3b6-dac502259ad0.png

图4 mapperElement方法调试图例

看到MyBatis还是通过创建一个XMLMapperBuilder对象来对mappers节点进行解析,在parse方法中

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }


  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

通过调用configurationElement方法来解析配置的每一个mapper文件

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

以解析mapper中的增删改查的标签来看看是如何解析一个mapper文件的

进入buildStatementFromContext方法

private void buildStatementFromContext(List list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

可以看到MyBatis还是通过创建一个XMLStatementBuilder对象来对增删改查节点进行解析,通过调用这个对象的parseStatementNode方法,在这个方法里会获取到配置在这个标签下的所有配置信息,然后进行设置

bbf41318-484c-11ed-a3b6-dac502259ad0.png

图5 parseStatementNode方法调试图例

解析完成以后,通过方法addMappedStatement将所有的配置都添加到一个MappedStatement中去,然后再将mappedstatement添加到configuration中去

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    resultSetTypeEnum, flushCache, useCache, resultOrdered, 
    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

bc14bf64-484c-11ed-a3b6-dac502259ad0.png

图6 增加解析完成的mapper方法调试图例

可以看到一个mappedstatement中包含了一个增删改查标签的详细信息

bc7ef898-484c-11ed-a3b6-dac502259ad0.png

图7 mappedstatement对象方法调试图例

而一个configuration就包含了所有的配置信息,其中mapperRegistertry和mappedStatements

bcbddaea-484c-11ed-a3b6-dac502259ad0.png

图8 config对象方法调试图例

具体的流程

bcebfc40-484c-11ed-a3b6-dac502259ad0.png

图9 SqlSessionFactory对象的构建过程

2.SqlSession的创建过程

SqlSessionFactory创建完成以后,接下来看看SqlSession的创建过程

SqlSession sqlSession = sqlSessionFactory.openSession();

首先会调用DefaultSqlSessionFactory的openSessionFromDataSource方法

@Override
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

在这个方法中,首先会从configuration中获取DataSource等属性组成对象Environment,利用Environment内的属性构建一个事务对象TransactionFactory

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);
    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();
  }
}

事务创建完成以后开始创建Executor对象,Executor对象的创建是根据 executorType创建的,默认是SIMPLE类型的,没有配置的情况下创建了SimpleExecutor,如果开启二级缓存的话,则会创建CachingExecutor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

创建executor以后,会执行executor = (Executor) interceptorChain.pluginAll(executor)方法,这个方法对应的含义是使用每一个拦截器包装并返回executor,最后调用DefaultSqlSession方法创建SqlSession

bd01a34c-484c-11ed-a3b6-dac502259ad0.png

图10 SqlSession对象的创建过程

3.Mapper的获取过程

有了SqlSessionFactory和SqlSession以后,就需要获取对应的Mapper,并执行mapper中的方法

StudentDao mapper = sqlSession.getMapper(StudentDao.class);

在第一步中知道所有的mapper都放在MapperRegistry这个对象中,因此通过调用org.apache.ibatis.binding.MapperRegistry#getMapper方法来获取对应的mapper

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

在MyBatis中,所有的mapper对应的都是一个代理类,获取到mapper对应的代理类以后执行newInstance方法,获取到对应的实例,这样就可以通过这个实例进行方法的调用

public class MapperProxyFactory {


  private final Class mapperInterface;
  private final Map methodCache = new ConcurrentHashMap();


  public MapperProxyFactory(Class mapperInterface) {
    this.mapperInterface = mapperInterface;
  }


  public Class getMapperInterface() {
    return mapperInterface;
  }


  public Map getMethodCache() {
    return methodCache;
  }


  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }


  public T newInstance(SqlSession sqlSession) {
    final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }


}

获取mapper的流程为

bd1adefc-484c-11ed-a3b6-dac502259ad0.png

图11 Mapper的获取过程

4.查询过程

获取到mapper以后,就可以调用具体的方法

//执行方法
List students = mapper.queryStudents(condition);

首先会调用org.apache.ibatis.binding.MapperProxy#invoke的方法,在这个方法中,会调用org.apache.ibatis.binding.MapperMethod#execute

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:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } 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);
      }
      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;
}

首先根据SQL的类型增删改查决定执行哪个方法,在此执行的是SELECT方法,在SELECT中根据方法的返回值类型决定执行哪个方法,可以看到在select中没有selectone单独方法,都是通过selectList方法,通过调用org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)方法来获取到数据

@Override
public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

在selectList中,首先从configuration对象中获取MappedStatement,在statement中包含了Mapper的相关信息,然后调用org.apache.ibatis.executor.CachingExecutor#query()方法

bd79d010-484c-11ed-a3b6-dac502259ad0.png

图12 query()方法调试图示

在这个方法中,首先对SQL进行解析根据入参和原始SQL,对SQL进行拼接

bdc8acf8-484c-11ed-a3b6-dac502259ad0.png

图13 SQL拼接过程代码图示

调用MapperedStatement里的getBoundSql最终解析出来的SQL为

bde17670-484c-11ed-a3b6-dac502259ad0.png

图14 SQL拼接过程结果图示

接下来调用org.apache.ibatis.parsing.GenericTokenParser#parse对解析出来的SQL进行解析

be180d5c-484c-11ed-a3b6-dac502259ad0.png

图15 SQL解析过程图示

最终解析的结果为

be404ad8-484c-11ed-a3b6-dac502259ad0.png

图16 SQL解析结果图示

最后会调用SimpleExecutor中的doQuery方法,在这个方法中,会获取StatementHandler,然后调用org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize这个方法进行参数和SQL的处理,最后调用statement的execute方法获取到结果集,然后 利用resultHandler对结进行处理

bef01c9c-484c-11ed-a3b6-dac502259ad0.png

图17 SQL处理结果图示

查询的主要流程为

bf1a73a2-484c-11ed-a3b6-dac502259ad0.png

bf2f3a6c-484c-11ed-a3b6-dac502259ad0.png

图18 查询流程处理图示

5.查询流程总结

总结整个查询流程如下

bf749d46-484c-11ed-a3b6-dac502259ad0.png

图19 查询流程抽象

2.3场景问题原因及解决方案

2.3.1 个人排查

这个问bug出现的地方在于绑定SQL参数的时候再源码中位置为

 @Override
 public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   BoundSql boundSql = ms.getBoundSql(parameter);
   CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

由于所写的SQL是一个动态绑定参数的SQL,因此最终会走到org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql这个方法中去

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }


  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }


  return boundSql;
}

在这个方法中,会调用 rootSqlNode.apply(context)方法,由于这个标签是一个foreach标签,因此这个apply方法会调用到org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply这个方法中去

@Override
public boolean apply(DynamicContext context) {
  Map bindings = context.getBindings();
  final Iterable  iterable = evaluator.evaluateIterable(collectionExpression, bindings);
  if (!iterable.iterator().hasNext()) {
    return true;
  }
  boolean first = true;
  applyOpen(context);
  int i = 0;
  for (Object o : iterable) {
    DynamicContext oldContext = context;
    if (first) {
      context = new PrefixedContext(context, "");
    } else if (separator != null) {
      context = new PrefixedContext(context, separator);
    } else {
        context = new PrefixedContext(context, "");
    }
    int uniqueNumber = context.getUniqueNumber();
    // Issue #709 
    if (o instanceof Map.Entry) {
      @SuppressWarnings("unchecked") 
      Map.Entry mapEntry = (Map.Entry) o;
      applyIndex(context, mapEntry.getKey(), uniqueNumber);
      applyItem(context, mapEntry.getValue(), uniqueNumber);
    } else {
      applyIndex(context, i, uniqueNumber);
      applyItem(context, o, uniqueNumber);
    }
    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
    if (first) {
      first = !((PrefixedContext) context).isPrefixApplied();
    }
    context = oldContext;
    i++;
  }
  applyClose(context);
  return true;
}

当调用appItm方法的时候将参数进行绑定,参数的变量问题都会存在bindings这个参数中区

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

进行绑定参数的时候,绑定完成foreach的方法的时候,可以看到bindings中不止绑定了foreach中的两个参数还额外有一个参数名字studentName->lct2,也就是说最后一个参数也是会出现在bindings这个参数中的,

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

bf86da60-484c-11ed-a3b6-dac502259ad0.png

图20 参数绑定过程

最后判定

org.apache.ibatis.scripting.xmltags.IfSqlNode#apply

@Override
public boolean apply(DynamicContext context) {
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    contents.apply(context);
    return true;
  }
  return false;
}

可以看到在调用evaluateBoolean方法的时候会把context.getBindings()就是前边提到的bindings参数传入进去,因为现在这个参数中有一个studentName,因此在使用Ognl表达式的时候,判定为这个if标签是有值的因此将这个标签进行了解析

bfb17dba-484c-11ed-a3b6-dac502259ad0.png

图21 单个参数绑定过程

最终绑定的结果为

c015c9be-484c-11ed-a3b6-dac502259ad0.png

图22 全部参数绑定过程

因此这个地方绑定参数的地方是有问题的,至此找出了问题的所在。

2.3.2 官方解释

翻阅MyBatis官方文档进行求证,发现在3.4.5版本发行中bug fixes中有这样一句

c05977d6-484c-11ed-a3b6-dac502259ad0.png

图23 此问题官方修复github记录

修复了foreach版本中对于全局变量context的修改的bug

issue地址为https://github.com/mybatis/mybatis-3/pull/966

修复方案为https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a

可以看到官方给出的修改方案,重新定义了一个对象,分别存储全局变量和局部变量,这样就会解决foreach会改变全局变量的问题。

c07f0e10-484c-11ed-a3b6-dac502259ad0.png

图24 此问题官方修复代码示例

2.3.3 修复方案

升级MyBatis版本至3.4.5以上

如果保持版本不变的话,在foreach中定义的变量名不要和外部的一致

03源码阅读过程总结 理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。

MyBatis源代码的目录是比较清晰的,基本上每个相同功能的模块都在一起,但是如果直接去阅读源码的话,可能还是有一定的难度,没法理解它的运行过程,本次通过一个简单的查询流程从头到尾跟下来,可以看到MyBatis的设计以及处理流程,例如其中用到的设计模式:

c0bbc3fa-484c-11ed-a3b6-dac502259ad0.png

图25 MyBatis代码结构图

组合模式:如ChooseSqlNode,IfSqlNode等

模板方法模式:例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler

Builder模式:例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder

工厂模式:例如SqlSessionFactory、ObjectFactory、MapperProxyFactory

代理模式:MyBatis实现的核心,比如MapperProxy、ConnectionLogger

04文档参考

https://mybatis.org/mybatis-3/zh/index.html

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 源码
    +关注

    关注

    8

    文章

    672

    浏览量

    30384
  • BUG
    BUG
    +关注

    关注

    0

    文章

    156

    浏览量

    16042
  • Suite
    +关注

    关注

    0

    文章

    13

    浏览量

    8268
  • mybatis
    +关注

    关注

    0

    文章

    64

    浏览量

    6951
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    产品详情查询API接口

    ? 在现代电子商务和软件开发中,产品详情查询API接口扮演着至关重要的角色。它允许开发者通过编程方式从远程服务器获取产品的详细信息,如名称、价格、描述和库存状态等。这种接口通常基于RESTful架构
    的头像 发表于 07-24 14:39 ?80次阅读
    产品详情<b class='flag-5'>查询</b>API接口

    Mybatis 源码畅谈软件设计(八):从根上理解 Mybatis 二级缓存

    的 cache 标签指定了 readOnly 属性,因为该配置相对比较重要,所以我们在这里把它讲解一下: readOnly 默认为 false ,这种情况下通过二级缓存查询出来的数据会进行一次 序列化深拷贝 。在这里大家需要回想一下介绍一级缓存时
    的头像 发表于 06-23 11:35 ?141次阅读
    由 <b class='flag-5'>Mybatis</b> <b class='flag-5'>源码</b>畅谈软件设计(八):从根上理解 <b class='flag-5'>Mybatis</b> 二级缓存

    京东中台化底层支撑框架技术分析及随想

    架构涉及的变化和影响,只是从中台化演进的思路,及使用的底层支撑技术框架进行分析探讨,重点对中台及前台协作涉及到的扩展点及热部署包的底层技术细节,结合京东实际落地情况,对涉及的核心技术框架进行源码初探分析,探讨技术框架的考虑
    的头像 发表于 04-08 11:29 ?285次阅读
    京东中台化<b class='flag-5'>底层</b>支撑框架技术分析及随想

    底层开发与应用开发到底怎么选?

    理解计算机系统的底层原理,掌握核心技术。 稳定性与稀缺性:底层开发人才相对稀缺,市场需求稳定,薪资待遇通常较高。 行业基础:底层开发是整个计算机技术的基石,对个人技术成长有极大帮助。 5. 挑战
    发表于 03-06 10:10

    IP地址查询技术

    IP查询****的价值 根据2023年国际互联网数据中心统计,全球每天产生的IP查询请求超过50亿次,这一数字就能够清晰的看出广大群众对于IP查询技术的需求以及它的价值。 而传统IP查询
    的头像 发表于 02-12 11:13 ?455次阅读
    IP地址<b class='flag-5'>查询</b>技术

    什么是AI查询引擎

    AI 查询引擎可高效处理、存储和检索大量数据,以增强生成式 AI 模型的输入。
    的头像 发表于 01-10 10:00 ?1592次阅读

    超级大全贴片元件代码查询

    如题:超级大全贴片元件代码查询
    发表于 01-08 13:49 ?34次下载

    Mybatis 源码畅谈软件设计(九):“能用就行” 其实远远不够

    作者:京东保险 王奕龙 到本节 Mybatis 源码中核心逻辑基本已经介绍完了,在这里我想借助 Mybatis 其他部分源码来介绍一些我认为在编程中能 最快提高编码质量的小方法 ,它们
    的头像 发表于 01-03 10:39 ?444次阅读

    SSM框架的源码解析与理解

    SSM框架(Spring + Spring MVC + MyBatis)是一种在Java开发中常用的轻量级企业级应用框架。它通过整合Spring、Spring MVC和MyBatis三个框架,实现了
    的头像 发表于 12-17 09:20 ?1020次阅读

    源码开放 智能监测电源管理教程宝典!

    源码开放,今天我们学习的是电源管理系统的核心功能模块,手把手教你如何通过不同的技术手段实现有效的电源管理。
    的头像 发表于 12-11 09:26 ?692次阅读
    <b class='flag-5'>源码</b>开放  智能监测电源管理教程宝典!

    根据ip地址查网页怎么查询

    一、通过命令提示符查询查网页(Windows系统) ①按“Win+R”键,打开运营窗口。 根据ip地址查网页怎么查询? ②输入“cmd”+“回车”,打开命令提示符窗口。 ③输入“nslookup
    的头像 发表于 09-29 10:56 ?2680次阅读
    根据ip地址查网页怎么<b class='flag-5'>查询</b>?

    【免费领取】AI人工智能学习资料(学习路线图+100余讲课程+虚拟仿真平台体验+项目源码+AI论文)

    想要深入学习AI人工智能吗?现在机会来了!我们为初学者们准备了一份全面的资料包,包括学习路线、100余讲视频课程、AI在线实验平合体验、项目源码、AI论文等,所有资料全部免费领取。01完整学习
    的头像 发表于 09-27 15:50 ?877次阅读
    【免费领取】AI人工智能<b class='flag-5'>学习</b>资料(<b class='flag-5'>学习</b>路线图+100余讲课程+虚拟仿真平台体验+项目<b class='flag-5'>源码</b>+AI论文)

    常见的IP地址查询技术

    1. 在线IP地址查询工具 ? 在线IP地址查询服务是获取IP地址信息的最用户友好方法。像IP数据云IP地址查询这样的网页提供直观的界面,用户只需输入IP地址即可获得详细信息。这些工具通常提供
    的头像 发表于 09-26 10:21 ?897次阅读
    常见的IP地址<b class='flag-5'>查询</b>技术

    【免费分享】嵌入式Linux开发板【入门+项目,应用+底层】资料包一网打尽,附教程/视频/源码...

    ?想要深入学习嵌入式Linux开发吗?现在机会来了!我们为初学者们准备了一份全面的资料包,包括原理图、教程、课件、视频、项目、源码等,所有资料全部免费领取,课程视频可试看(购买后看完整版),让你
    的头像 发表于 09-05 10:45 ?643次阅读
    【免费分享】嵌入式Linux开发板【入门+项目,应用+<b class='flag-5'>底层</b>】资料包一网打尽,附教程/视频/<b class='flag-5'>源码</b>...

    北京迅为RK3568开发板嵌入式学习Linux驱动全新更新-CAN+

    北京迅为RK3568开发板嵌入式学习Linux驱动全新更新-CAN+
    的头像 发表于 09-04 15:29 ?1105次阅读
    北京迅为RK3568开发板嵌入式<b class='flag-5'>学习</b><b class='flag-5'>之</b>Linux驱动全新更新-CAN+