精通MyBatis原理,看这两篇就够了!(二)

news2024/11/24 12:53:25

本文是关于MyBatis源码的第二篇,解读了MyBatis的核心执行SQL流程,对源码做了详细注释。内容较长,推荐电脑阅读。

点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

执行阶段流程

第一篇文章讲解了Mybatis启动阶段的流程,大家自行阅读。

这里将讲解剩余的执行阶段流程,涉及的主要内容如下:

  1. 根据方法签名,获得mapper对应的代理对象。

  2. 通过JDK动态代理找到对应的执行逻辑,获得数据库连接,然后执行SQL。

  3. 拿到执行结果,并且根据映射关系处理为Java对象。

相关测试用例的代码如下:

// 获取数据库的会话,创建出数据库连接的会话对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取要调用的接口类,创建出对应的mapper的动态代理对象
UserDao mapper = sqlSession.getMapper(UserDao.class);
// 调用方法开始执行SQL,并且将返回结果映射为Java对象
User userInfo = mapper.findUserInfo(1);

从代码可以便看出,对于JDBC来说那些繁琐的SQL执行流程都变得如此简单。比如准备statement、设置参数、解析结果、根据不同类型设置到Java对象中等,这里只需要两行代码,MyBatis 的优势就在于此,解决了JDBC开发中的那些痛点。

一.获得代理对象

之前的流程介绍过,SqlSession 的实现类是 DefaultSqlSession,configuration 全局配置对象作为其成员变量,sqlSession 的 getMapper方法本质就是调用 configuration 的 getmapper方法,最后会去调用 MapperRegistry.getMapper()

0fb302672be8a3ed9269ec35c7d636c0.png

紧接着 MapperRegistry 中根据接口类型拿到 mapperProxyFactory ,这是在启动阶段根据xml配置的package中读取时put进knownMappers 里的(可以回顾第一篇文章)。

//DefaultSqlSession
public <T> T getMapper(Class<T> type) {
  //最后会去调用MapperRegistry.getMapper
  return configuration.getMapper(type, this);
}
//configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

//MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 查找指定type对应MapperProxyFactory对象
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
……
    // 创建实现了type接口的代理对象
    return mapperProxyFactory.newInstance(sqlSession);
……
}

拿到 mapperProxyFactory 后便可以调用其 newInstance()方法开始对 mapper 的代理对象进行实例化。在 newInstance() 方法中会首先实例化 MapperProxy ,MapperProxy继承了 InvocationHandler 接口。看到这里你应该懂了,MyBatis 正是运用了JDK代理的方式来实现代理对象。

mapperProxyFactory.newInstance的本质便是那段熟悉的代码,调用 Proxy 的 newProxyInstance 来创建动态代理对象,mapperInterface 就是需要被代理的接口。

在本例中,执行到这里就拿到了 UserDao 接口的动态代理实现类。

protected T newInstance(MapperProxy<T> mapperProxy) {
  // 创建实现了mapperInterface接口的代理对象
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public class MapperProxy<T> implements InvocationHandler, Serializable {
}
protected T newInstance(MapperProxy<T> mapperProxy) {
  // 创建实现了mapperInterface接口的代理对象
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}


JDK动态代理的详细知识可以回顾这篇文章:

快速深入理解JDK动态代理原理

2022-05-19

667e4ffeb14ddbac93e15e6a2ddc17d6.jpeg

二.执行代理方法

拿到mapper的对象后便可以开始执行SQL,这里以select语句为例:User userInfo = mapper.findUserInfo(1);

之前提到,所有的 mapper 接口实际的实现类是通过动态代理的方式实现的,并且是通过重要的实现类 MapperProxy。

MapperProxy 实现了 InvocationHandler 接口,在真正发生方法调用时通过 InvocationHandler.invoke 方法调用真正的实现逻辑。每一个具体实现方法都在 methodCache 这个 map 中保存,根据 method name从map中拿出来然后调用 invoke。

// MapperProxy
private final Map<Method, MapperMethodInvoker> methodCache;

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 如果目标方法继承自Object,则直接调用目标方法
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else {
      
      // 根据被调用接口方法的method对象,从缓存中获取MapperMethodInvoker对象,如果没有则创建一个并放入缓存,然后调用invoke
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
}

真正Mapper的实现类

JDK动态代理生成了自定义mapper接口真正的实现类,其实是可以生成并保存在文件系统中的。这里我贴了出来,方便大家理解MyBatis的实现逻辑。里面一些无关紧要的成员变量和方法已经删除,只留下最重要的实现部分。

2fb6270ab9f0f6cb4a5358f2cf41d1fc.png

可以看到成员变量 m7就代表了方法 findUserInfo,在创建代理对象的时候需要传入实现 InvocationHandler接口类的实力对象,这里的 InvocationHandler var1 其实就是 MapperProxy,m7方法就是 UserDao接口中 findUserInfo方法的代理实现。可以看出其中真正调用的是 InvocationHandlerinvoke方法,也就是 MapperProxy.invoke()

public final class $Proxy8 extends Proxy implements UserDao {
  // findUserInfo
    private static Method m7;
  // 实际在父类Proxy中,为了方便理解加到这里。
  protected InvocationHandler h;

    public $Proxy8(InvocationHandler var1) throws  {
        super(var1);
    }

  // findUserInfo 
    public final User findUserInfo(Integer var1) throws  {
        try {
            return (User)super.h.invoke(this, m7, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }
    static {
     m7 = Class.forName("com.daley.dao.UserDao").getMethod("findUserInfo", Class.forName("java.lang.Integer"));
    }
}

三.查找方法对应 MapperMethodInvoker

MethodCache 用于缓存MapperMethod对象,其中 key 是mapper接口中方法对应的Method对象,value是MapperMethodInvoker ,里面包含对应的MapperMethod对象,MapperMethod对象会完成参数转换,以及SQL语句的执行功能,需要注意的是,MapperMethod中并不记录任何状态相关的信息,所以可以在多个代理对象之间共享。他们之间的关系如下图:

362459d81e91145982e4cac414e1f351.png

从缓存中查找 method,如果是接口默认方法直接执行,如果是普通方法,创建 PlainMethodInvoker 保存到 methodCache 中,key 为 Method 类型。

这样SQL和方法便绑定了起来,注意现在只是创建过程,绑定接口、方法、configuration三者的关系,还没有开始执行。

// 获取缓存中MapperMethodInvoker,如果没有则创建一个,而MapperMethodInvoker内部封装这一个MethodHandler
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  ……
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      if (m.isDefault()) {
        // 如果调用接口的是默认方法
       …… 省略try
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          } else {
            return new DefaultMethodInvoker(getMethodHandleJava9(method));
          }
       …… 省略try
      } else {
        // 如果调用的普通方法,则创建一个PlainMethodInvoker并放入缓存,其中MapperMethod保存对应接口方法的SQL以及入参和出参的数据类型等信息
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
…… 省略 catch
}

public class MapperMethod {

  // 记录了SQL语句的名称和类型
  private final SqlCommand command;
  // mapper接口中对应方法的相关信息
  private final MethodSignature method;

  // mepperinferface 为当前被代理的 mapper 接口
  // method 为当前执行的方法
  // config 是当前全局配置对象
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
}

绑定SQL和method关系

还记得上篇文章提到的 mappedStatements吗,里面保存了方法签名和SQL对应的信息。在这里创建 MapperMethod 的成员 SqlCommand 的时候,就会根据方法签名把这里面绑定的 mappedStatement 从这个Map结构中取出来。

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  this.command = new SqlCommand(config, mapperInterface, method);
  this.method = new MethodSignature(config, mapperInterface, method);
}
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    final String methodName = method.getName();
    final Class<?> declaringClass = method.getDeclaringClass();
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
  ……
}

与此同时,关于方法的信息 MethodSignature 也会在这里做解析,比如返回的数据类型、@Param注解的参数名等等,所有信息都会保存在这个变量中,具体逻辑可以看 MethodSignature类的构造方法。

这样,SQL和方法的关系便关联了起来。

代码调试截图如下:

54fa772f76a3d52d5a4752bf005d18de.png

四.正式执行SQL

创建好 MapperMethod 之后会put到 methodCache 中,Method作为key,便于下次访问这个方法的时候直接取出来使用。

接着便可以调用Invoke方法,最终会调用 mapperMethod.execute(sqlSession, args)开始真正执行SQL。

execute 方法需要根据 SQL 类型和返回值类型确定执行哪一段逻辑,中间代码做了省略,道理一样大家自行调试,这里我们执行SELECT逻辑。

//MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    // 根据SQL语句的类型调用SqlSession对应的方法
    switch (command.getType()) {
      ……
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
     ……
      case SELECT:
        // 处理返回值为void是ResultSet通过ResultHandler处理的方法
        if (method.returnsVoid() && method.hasResultHandler()) {
          // 如果有结果处理器
          executeWithResultHandler(sqlSession, args);
          result = null;
          // 处理返回值为集合和数组的方法
        ……
        } 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;
      ……
    return result;
  }

因为本方法是根据id查询的,只有一个查询结果,所以执行 DefaultSQLSession.selectOne方法。

最终会需要执行器方法执行查询操作,也就是 cachingExecutor.query()

// DefaultSQLSession
public <T> T selectOne(String statement, Object parameter) {
  // 从这里的返回值可以看出,尽量使用包装类,否则返回结果为空会报 NPE
  List<T> list = this.selectList(statement, parameter);
  if (list.size() == 1) {
    return list.get(0);
    }
  ……
}

public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

//核心selectList
  private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
      //根据方法签名找到对应的MappedStatement(SQL)
      MappedStatement ms = configuration.getMappedStatement(statement);
      //用执行器来查询结果,注意这里传入的ResultHandler是null
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  }

执行器查询结果

在cachingExecutor的query方法中,会首先准备好boundSql,里面包含了SQL以及参数、返回等信息。然后会根据参数、sql等信息生产一个缓存 key,在本例中,这个cacheKey等于 1148877182:3356992236:com.daley.dao.UserDao.findUserInfo:0:2147483647:select * from user_info where id = ?:1:development。这就是二级缓存的key,其目的当然是为了在用同样参数执行的时候可以直接返回缓存中的结果,只有在开启缓存后才会生效。

214c678d7fe74fbd8a0444b4d315b780.png
//cachingExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // 获取BoundSql对象
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // 创建CacheKey对象
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

执行cachingExecutor 中的query方法,本质是执行BaseExecutor.query,这里使用了装饰者模式。这里的 delegate 就是 BaseExecutor 。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // 是否开启了二级缓存 如果没有使用缓存直接看最后一行
    if (cache != null) {
      ……
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 二级缓存没有相应的结果对,调用封装的Executor对象的query方法
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 将查询结果保存到TransactionalCache.entriesToAddOnCommit集合中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
  
    // 没有启动二级缓存,直接调用底层Executor执行数据库查询操作
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

中间的链路很简单,这里做了省略,最终会追到 doQuery方法。看到这里的你应该感觉很熟悉了吧,就像JDBC的执行流程一样使用 PreparedStatement 开始执行查询。

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    // 获取配置对象
    Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler对象,实际返回的是RoutingStatementHandler对象
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 完成Statement的创建和初始化
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 调用query方法执行sql语句,并通过ResultSetHandler完成结果集的映射
    return handler.query(stmt, resultHandler);
  } finally {
    // 关闭Statement对象
    closeStatement(stmt);
  }
}

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
  //执行SQL
    ps.execute();
  //处理返回结果
    return resultSetHandler.handleResultSets(ps);
 }

五.处理返回结果

在 PreparedStatement 执行完 execute 方法后便拿到了MySQL服务器返回的结果,这就可以开始结果处理流程了。

具体过程代码中已经详细注释,简单来说就是根据ResultMap的类型找到对应的类型处理handler,然后根据返回结果类型调用 constructor.newInstance()创建结果对象,然后解析属性名,利用反射不同字段对应的value设置到Java对象中。

public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  // 该集合用于保存映射结果得到的结果对象
  final List<Object> multipleResults = new ArrayList<>();

  int resultSetCount = 0;
  // 获取第一个ResultSet对象
  ResultSetWrapper rsw = getFirstResultSet(stmt);

  // 获取MappedStatement.resultMaps集合
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  // 如果集合集不为空,则resultMaps集合不能为空,否则抛出异常
  validateResultMapsCount(rsw, resultMapCount);
  // 遍历resultMaps集合
  while (rsw != null && resultMapCount > resultSetCount) {
    // 获取该结果集对应的ResultMap对象
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // 根据ResultMap中定义的映射规则对ResultSet进行映射,并将映射的结果对象添加到multipleResult集合中保存
    handleResultSet(rsw, resultMap, multipleResults, null);
    // 获取下一个结果集
    rsw = getNextResultSet(stmt);
    // 清空nestedResultObjects集合
    cleanUpAfterHandlingResultSet();
    // 递增resultSetCount
    resultSetCount++;
  }

  // 获取MappedStatement.resultSets属性,该属性对多结果集的情况使用,该属性将列出语句执行后返回的结果集,并给每个结果集一个名称,名称是逗号分隔的,
  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
      // 根据resultSet的名称,获取未处理的ResultMapping
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        // 根据ResultMap对象映射结果集
        handleResultSet(rsw, resultMap, null, parentMapping);
      }
      // 获取下一个结果集
      rsw = getNextResultSet(stmt);
      // 清空nestedResultObjects集合
      cleanUpAfterHandlingResultSet();
      // 递增resultSetCount
      resultSetCount++;
    }
  }

  return collapseSingleResultList(multipleResults);
}

到这里,MyBatis的主要执行流程就全部讲解完了。

最后,欢迎大家提问和交流。

如果觉得对你有帮助,欢迎点赞、标🌟分享

大厂程序员常用的几款「高效工具」,已整理资源!

2022-12-20

6f07490d76ae228fe7cdd63e13176be5.jpeg

MySQL主从复制太慢,怎么办?

2022-12-15

9fcff535d2ec23562b5cae44b0175f12.jpeg

引入新模块都在用这个注解,它是如何生效的?|原创

2022-12-11

928ddd2306258de3552f7dde016e830a.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/115510.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【jdk11+jprofiler 11进行java程序性能调优案例之--内存溢出原因分析】

1.安装jprofiler jprofiler_windows-x64_11_0_2.exe 2.使用KeyGen.exe生成注册码然后输入 3.idea中安装jprofiler插件 File-->Setting-->Plugins 搜索jprofiler插件然后安装 4.以一个内存溢出的程序为例子进行分析(一直分配内存&#xff0c;List容器引用着Student导致…

Java创建线程的三种方式

Java创建线程的三种方式 一、通过Thread类的方式进行创建 步骤&#xff1a; 1、创建Thread的子类&#xff0c;重写run方法&#xff0c;run方法就表示线程需要完成的任务 2、创建Thread实例&#xff0c;也就是创建线程对象 3、使用start来启动线程&#xff08;线程启动的唯一方…

显著性分析

选择图 为什么要分Non-parametric & parametric 方法 为了找到更符合数据的分析方法。每个方法有自己的假设&#xff0c;如果违背了结果会不精准。 Sign Test 是一个可以用于任何数据分布情况的pairwise 方法。 检查normality: Sample 数量 < 50,适用 Shapiro-Wilk&am…

【Kotlin 协程】Flow 操作符 ① ( 过渡操作符 | map 操作符 | transform 操作符 | 限长操作符 | take 操作符 )

文章目录一、过渡操作符1、map 操作符2、transform 操作符二、限长操作符 ( take 操作符 )一、过渡操作符 过渡操作符 相关概念 : 转换流 : 使用 过渡操作符 转换 Flow 流 ;作用位置 : 过渡操作符作用 于 流的上游 , 返回 流的下游 ;非挂起函数 : 过渡操作符 不是挂起函数 , 属…

大话JMeter2|正确get参数传递和HTTP如何正确使用

上节课展示了JMeter的基础用法&#xff1a;录制回放功能&#xff0c;断言&#xff0c;聚合报告。但是在无UI下如何进行接口的访问呢&#xff1f;如何正确get参数传递和HTTP如何正确使用。尤其是在无UI下进行接口的访问。小哥哥带着你用漫画来学习JMeter&#xff0c;让你在轻松的…

【MMAsia 2021】Patch-Based Deep Autoencoder for Point Cloud Geometry Compression

文章目录Patch-Based Deep Autoencoder for Point Cloud Geometry Compression压缩流程自编码架构实验结果Patch-Based Deep Autoencoder for Point Cloud Geometry Compression https://arxiv.org/abs/2110.09109 这篇论文使用深度自编码器&#xff0c;提出了一种基于分块&am…

发票识别OCR及查验API接口为企业化解难题

对于当今的现代企业来说&#xff0c;分散的财务管理模式效率不高&#xff0c;管理成本反而相对较高&#xff0c;制约了集团企业发展战略的实施&#xff0c;因而需要建设财务共享模式。一个企业要建成财务共享中心&#xff0c;面临的难题是大量的数据采集和信息处理工作&#xf…

一组类型相同的数据【C 数组】总结

作者 &#xff1a; 会敲代码的Steve 墓志铭&#xff1a;博学笃志 切问静思 前言&#xff1a;本文旨在复习C语言数组章节的知识点、分为以下几个部分&#xff1a; 什么是数组一维数组、一维数组的初始化、一维数组的遍历、冒泡排序。二维数组、二维数组的创建和初始化、二维数…

多功能采集仪VH03接口使用说明

传感器接口 传感器接口须使用设备专门配备的测线&#xff0c;一端为 DB9 一端为用颜色区分的多个鳄鱼夹&#xff0c;线&#xff08;鳄鱼夹&#xff09;颜色和功能定义详见“设备组成和接口定义” 。 充电和通讯接口 VH03 使用标准的 USB Type-C 接口完成设备充电和通讯&…

创建一个vue项目

文章目录前言一、安装node.js二、vue ui命令没有反应原因1.vue ui命令是vue 3.x版本以上才支持&#xff0c;因此需要更新vue的版本。2.更新vue版本2.1首先使用以下命令卸载旧版本2.2然后使用下面命令安装最新版本2.3查看是当前版本号2.4此时&#xff0c;输入 vue -h 命令查看co…

HMS Core 3D流体仿真技术,打造移动端PC级流体动效

移动设备硬件的高速发展&#xff0c;让游戏行业发生翻天覆地的变化&#xff0c;许多酷炫的游戏效果不再局限于电脑端&#xff0c;玩家在移动端就能享受到场景更逼真、画质更清晰、体验更流畅的游戏服务。但由于移动设备算力不足&#xff0c;为了实现真实感的水体效果&#xff0…

cesium地形上面绘点时,山背面点位始终显示在地形上

cesium地形上面绘点时&#xff0c;山背面点位始终显示在地形上&#xff0c;如下图&#xff1a; 深度检测也是打开的&#xff0c;各种方法试完之后&#xff0c;也没有找到问题&#xff0c;把viewer属性注释之后&#xff0c;就没有出现这个问题&#xff0c;于是一个个属性&#…

【LeetCode】C语言实现---用队列实现栈用栈实现队列

目录&#x1f449;用队列实现栈&#x1f449;用栈实现队列&#x1f449;用队列实现栈 入口&#xff1a;OJ 题目描述&#xff1a; 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0c;并支持普通栈的全部四种操作&#xff08;push、top、pop 和 em…

redis的消息发布订阅实现

文章目录前言一、创建好springboot项目,引入核心依赖二、使用步骤1. 自定义一个消息接受类2.声名一个消息配置类3.编写一个测试类总结前言 一般项目中都会使用redis作为缓存使用,加速用户体验,实现分布式锁等等,redis可以说为项目中的优化,关键技术实现立下了汗马功劳.今天带来…

YonBuilder应用构建教程之移动端基础配置

在YonBuilder中除了PC端应用的构建外&#xff0c;我们还可以构建配套的移动端页面。对于同一个数据实体可以实现PC端和移动端的数据同步修改&#xff0c;使数据录入、修改、审批等更加便捷。本篇文章通过对员工信息实体的移动端页面构建来对YonBuilder移动端配置的基础流程进行…

利用ENVI对遥感图像校正

1.几何校正 引起图像几何变形一般分为两大类:系统性和非系统性。系统性一般由传感器本身引起&#xff0c;有规律可循和可预测性&#xff0c;可以用传感器模型来校正&#xff0c;卫星地面接收站已经完成这项工作;非系统性几何变形是不规律的&#xff0c;它可以是传感器平台本身…

【Axure高保真原型】移动端钱包原型模板

今天和大家分享移动端钱包的原型模板&#xff0c;里面包含了11大模块&#xff0c;各个模块都是高保真高交互的原型模板&#xff0c;大家可以在演示地址里体验哦 【原型预览及下载地址】 https://axhub.im/ax9/4c3757a85d201a4c/#c1 这个原型还可以在手机上演示哦&#xff0c…

Bitmiracle Docotic.Pdf Library 8.8.14015 Crack

C# 和 VB.NET 的 PDF 库 Docotic.Pdf 是用于 .NET 的高性能 C# PDF 库。您可以使用它在 .NET Core、ASP.NET、Windows Forms、WPF、Xamarin、Blazor、Unity 和 HoloLense 应用程序中创建、阅读和编辑 PDF 文档。 该库支持 .NET 6、.NET 5、.NET Standard/.NET Core 和 .NET 4.…

Opencv(C++)笔记--模板匹配cv::matchTemplate()和最值计算cv::minMaxLoc()

目录 1--模板匹配 1-1--OpenCV API 1-2--六种匹配方法 1-3--代码实例 2--最值计算 2-1--OpenCV API 1--模板匹配 使用模板图像与原图像进行匹配&#xff0c;OpenCV提供了相应的模板匹配函数cv::matchTemplate()&#xff0c;并支持六种模板匹配方法。 1-1--OpenCV API vo…

【Linux】Linux项目自动化构建工具——make/Makefile

我举报&#xff0c;有人不学习&#xff01;&#xff01;&#xff01; 文章目录一、makefile原理二、初步理解makefile的语法1.gcc如何得知&#xff0c;源文件不需要再编译了呢&#xff1f;2.为什么执行的指令是make和make clean呢&#xff1f;三、makefile的推导规则四、Linux…