(02)Cartographer源码无死角解析-(54) 2D后端优化→PoseGraphInterfacePoseGraphPoseGraph2D::AddNode()

Posted 江南才尽,年少无知!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(02)Cartographer源码无死角解析-(54) 2D后端优化→PoseGraphInterfacePoseGraphPoseGraph2D::AddNode()相关的知识,希望对你有一定的参考价值。

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下:
(02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 \\colorblue文末正下方中心提供了本人 \\colorred 联系方式,\\colorblue点击本人照片即可显示WX→官方认证 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

通过上一篇博客,再次对 global_trajectory_builder.cc 文件中的 AddSensorData() 进行了分析,同时简要提及位姿图。在Cartographer 中,位姿图优化核心类为 src/cartographer/cartographer/mapping/internal/2d/pose_graph_2d.cc 文件中的 PoseGraph2D。其函数 PoseGraph2D::AddNode() 起到了前端与后端交互作用。

对于接下来的讲解,本人比较纠结,①先讲整体流程,再梳理细节;②按照源码执行过程,见招拆招,按顺序讲解;这两种方式,最终选择了后者。因为如果直接讲整体流程,没有基础的情况下是很难看懂的。所示,接下来按照源码执行过程,一一进行讲解,类似于本人分析代码的过程。等把细节都梳理清楚之后,再反过来做一个总结,这样应该理解就比较深刻了。这里复制一下 PoseGraph2D::AddNode() 被调用过程,即 GlobalTrajectoryBuilder::AddSensorData() 函数中代码段如下:

      // 将匹配后的结果 当做节点 加入到位姿图中
      auto node_id = pose_graph_->AddNode(
          matching_result->insertion_result->constant_data,  //子图相关的
          trajectory_id_, //轨迹id
          matching_result->insertion_result->insertion_submaps); //子图

该些变量都再前面进行分析过,其上 constant_data 为 const TrajectoryNode::Data 类型的智能指针:

  struct Data 
    common::Time time; //点云数据时间戳

    // Transform to approximately gravity align the tracking frame as
    // determined by local SLAM.
    Eigen::Quaterniond gravity_alignment; //重力校正旋转四元数

    // Used for loop closure in 2D: voxel filtered returns in the
    // 'gravity_alignment' frame.
    sensor::PointCloud filtered_gravity_aligned_point_cloud; //经过滤波之后的点云数据

    // Used for loop closure in 3D.用于3D回环检测,
    sensor::PointCloud high_resolution_point_cloud; //高分辨率点云数据
    sensor::PointCloud low_resolution_point_cloud; //低分辨率点云数据
    Eigen::VectorXf rotational_scan_matcher_histogram; //旋转扫描匹配直方图

    // The node pose in the local SLAM frame.
    transform::Rigid3d local_pose; //节点相对于local SLAM frame(可以理解为lcoal地图)的位姿,MatchingResult::local_pose是一致的
  ;

在了解这些之后,下面就开始对PoseGrap2D继续进行一个详细分析了。首先其继承关系如下:

PoseGraph2D:PoseGraph:PoseGraphInterface

根据继承关系, 首先要理解PoseGraphInterface,其在 src/cartographer/cartographer/mapping/pose_graph_interface.h 定义。
 

二、PoseGraphInterface

首先其是定义了一个结构提体类型struct Constraint,代码注释之前,可以看下图,有助于后续的理解( 用于参考,并不一定完全正确 \\colorred用于参考,并不一定完全正确 用于参考,并不一定完全正确)

图一

1、黑色坐标系: 表示Global全局坐标系,O表示原点
2、红色坐标系: 子图坐标系,O表示原点
3、紫色矩形: 子图
4、Submapx: 分别代表子图1、子图2、子图3
5、蓝色虚线:分别表示子图间约束、子图内约束

该图示中有些地方可能是带有疑问的,如Global全局坐标系是否在左上角?子图local坐标系是否在左上角?子图Submap1坐标系与Global全局坐标系是否相同?这些疑问暂时就不解答了,后续如果有时间,再来分析,到时候发现不对的地方再做纠正。

1、结构体Constrain

首先PoseGraphInterface中定义了结构体Constraint,其是用于描述一个约束,也就是图优化中的边,这里先贴一下代码注释:

  // 包含了子图的id, 节点的id, 节点j相对于子图i的坐标变换, 以及节点是在子图内还是子图外的标志
  struct Constraint 
    struct Pose 
      transform::Rigid3d zbar_ij;  
      double translation_weight;
      double rotation_weight;
    ;

    SubmapId submap_id;  // 'i' in the paper.
    NodeId node_id;      // 'j' in the paper.

    // Pose of the node 'j' relative to submap 'i'.
    Pose pose;

    // Differentiates between intra-submap (where node 'j' was inserted into
    // submap 'i') and inter-submap constraints (where node 'j' was not inserted
    // into submap 'i').
    enum Tag  INTRA_SUBMAP, INTER_SUBMAP  tag;
  ;

对应于论文中如下内容:

在上一篇文中提到,简单的来说,可以直接把一个位姿变换看作一个约束。其上代码中定义的Constraint就是描述一个位姿变换: Constraint::submap_id对应的子图到Constraint::node_id对应的节点位姿变换, submap_id 等价于论文中 c i c_i ci,node_id等价于论文中 c j c_j cj,如果觉得这里所谓的节点不好理解,直接看成机器人位姿即可。总的来说 Constraint:pose表示节点 j 相对于子图 i 的位姿。

另外,其定义的一个枚举类型Tag,如图1所示,因为机器人位于Submap7中,其蓝色虚线INTRA_SUBMA表示 Submap7(原点)到机器人的位姿变换,等价于机器人在Submap7中的位姿,即机器人与在所属子图的位姿(等价于该子图到机器人位姿变换),称为INTER_SUBMAP 子图内约束。

除此之外,如图1还可以看到Submap2原点与机器人连接的蓝色虚线,其表示Submap2系到机器人系的位姿变换,可机器人并不处于子图Submap2中,所以该属于INTRA_SUBMAP子图间约束。

2、结构体LandmarkNode

除结构体结构体类型 struct Constrain,pose_graph_interface.h中还定义了结构体类型 struct LandmarkNode,其主要用来描述一个Landmark节点,首先机器人在某个时刻位姿下,可能同时观测到多个Landmark,这些被观测到的Landmark数据存储于成员变量 LandmarkNode::landmark_observations 之中,LandmarkNode::global_landmark_pose 表示这帧 Landmark 数据对应的 tracking_frame 在 globa l坐标系下的位姿,如下所示:

  struct LandmarkNode 
    // landmark数据是相对于tracking_frame的相对坐标变换
    struct LandmarkObservation 
      int trajectory_id;
      common::Time time;
      transform::Rigid3d landmark_to_tracking_transform;
      double translation_weight;
      double rotation_weight;
    ;
    // 同一时刻可能会观测到多个landmark数据
    std::vector<LandmarkObservation> landmark_observations;
    // 这帧数据对应的tracking_frame在global坐标系下的位姿
    absl::optional<transform::Rigid3d> global_landmark_pose;
    bool frozen = false;
  ;

可以看到单个 landmark 数据是使用 LandmarkNode::LandmarkObservation 类型结构体描述的,首先包含了trajectory_id、time。以及 landmark_to_tracking_transform,其表示单个landmark相对于tracking_frame(机器人)的位姿,另外包含了该位姿旋转与平移的权重 translation_weight、rotation_weight。最后关于参数LandmarkNode::frozen,其含义是冻结,这里猜测其是对 LandmarkNode 进行冻结,即表示其内的成员数据不会被改变了。

3、SubmapPose、SubmapData

  struct SubmapPose 
    int version;
    transform::Rigid3d pose;
  ;

  struct SubmapData 
    std::shared_ptr<const Submap> submap;
    transform::Rigid3d pose;
  ;

这两个结构体类型比较简单,分别用来表述子图的位姿与子图的数据,SubmapData 中实际上存储一个指向于 Submap 类型指针,可以注意到成员变量 SubmapData::pose,目前估计其也是子图的位姿→相对于全局坐标系。

4、TrajectoryData、TrajectoryState

  // tag: TrajectoryData
  struct TrajectoryData 
    double gravity_constant = 9.8; //重力常数
    std::array<double, 4> imu_calibration1., 0., 0., 0.; //初始默认值,单位四元数
    absl::optional<transform::Rigid3d> fixed_frame_origin_in_map; //GPS帧原点相对于map的位姿。
  ;

  enum class TrajectoryState  ACTIVE, FINISHED, FROZEN, DELETED ;

TrajectoryData 是用来秒速轨迹数据的、这里看起来不知道给来干嘛的,后续使用到了再做具体分析。枚举类型 TrajectoryState 定义了轨迹4种状态,分别为激活(ACTIVE)、完成(FINISHED)、冻结(FINISHED)、删除(DELETED)。

5、GlobalSlamOptimizationCallback

最后还定义了一个回调函数类型,该类型函数无返回值、输入类型分别为 std::map<int,SubmapId>、 std::map<int, NodeId> 。该类函数的功能是做全局优化。

  using GlobalSlamOptimizationCallback =
      std::function<void(const std::map<int /* trajectory_id */, SubmapId>&,
                         const std::map<int /* trajectory_id */, NodeId>&)>;

下面都是一些虚函数了,后续对PoseGraph2D讲解时,再对其进行具体分析。
 

三、PoseGraph

前面以及提到PoseGraph由PoseGraphInterface派生而来,其声明于src/cartographer/cartographer/mapping/pose_graph.h 文件中,其中定义了结构体类型 struct InitialTrajectoryPose,用来秒速一个轨迹的初始位姿,不过值得注意的是,该初始位姿是一个先对值,相对于 to_trajectory_id 的位置,后续使用到了,再分析一遍,代码如下:

  // 相对于to_trajectory_id的初始位姿
  struct InitialTrajectoryPose 
    int to_trajectory_id;
    transform::Rigid3d relative_pose;
    common::Time time;
  ;

 

四、PoseGraph2D::AddNode

那先现在就是关于PoseGraph2D部分了,其继承至PoseGraph。类声明于 src/cartographer/cartographer/mapping/internal/2d/pose_graph_2d.h 文件中,其主要声明一些重写的虚函数,后续一个一个的为大家讲解。该篇不可只对其中的PoseGraph2D::AddNode() 函数进行分析。上一篇博客中提到,该函数中在 src/cartographer/cartographer/mapping/internal/global_trajectory_builder.cc 文件中有被调用。PoseGraph2D::AddNode() 函数总体流程可以说是十分的简单、但是如果深入了解其调用函数,可以说是复杂至极。但是关于 PoseGraph2D::AddNode() 函数的逻辑还是十分简单的。其主要流程如下:

( 01 ) \\colorblue(01) (01) 首先其需要传递三个参数:①参数constant_data就是上面的struct Data类型,主要是关于插入子图中相关的固化信息;②trajectory_id表示轨迹id;③insertion_submaps去通常包含两个活跃子图。

( 02 ) \\colorblue(02) (02)首先调用GetLocalToGlobalTransform() 函数传入 trajectory_id 获得由local系到global系的坐标变换矩阵,然后左乘constant_data->local_pose,其目的是把local坐标系下的节点坐标(把扫描匹配出来的位姿看成一个节点)变换到global系下。

( 03 ) \\colorblue(03) (03) 把变换到global系下的节点(位姿)通过PoseGraph2D::AppendNode()函数加入到节点列表 PoseGraph2D::data_trajectory_nodes 之中,该成员变量的定义为:MapById<NodeId, TrajectoryNode> trajectory_nodes;加入之后会获得一个NodeId node_id,其为整个函数的返回值。

( 04 ) \\colorblue(04) (04) 把计算约束的工作放入workitem中等待执行,需要注意的是,这里并还没有执行计算节点约束函数 ComputeConstraintsForNode(),仅仅是把该函数作为一个任务项添加至线程池→关于线程部分后续再进行详细分析。

/**
 * @brief 增加节点, 并计算跟这个节点相关的约束
 * 
 * @param[in] constant_data 节点信息
 * @param[in] trajectory_id 轨迹id
 * @param[in] insertion_submaps 子地图 active_submaps
 * @return NodeId 返回节点的ID
 */
NodeId PoseGraph2D::AddNode(
    std::shared_ptr<const TrajectoryNode::Data> constant_data,
    const int trajectory_id,
    const std::vector<std::shared_ptr<const Submap2D>>& insertion_submaps) 
  
  // 将节点在local坐标系下的坐标转成global坐标系下的坐标
  const transform::Rigid3d optimized_pose(
      GetLocalToGlobalTransform(trajectory_id) * constant_data->local_pose);

  // 向节点列表加入节点,并得到节点的id
  const NodeId node_id = AppendNode(constant_data, trajectory_id,
                                    insertion_submaps, optimized_pose);

  // We have to check this here, because it might have changed by the time we
  // execute the lambda.
  // 获取第一个submap是否是完成状态
  const bool newly_finished_submap =
      insertion_submaps.front()->insertion_finished();

  // 把计算约束的工作放入workitem中等待执行
  AddWorkItem([=]() LOCKS_EXCLUDED(mutex_) 
    return ComputeConstraintsForNode(node_id, insertion_submaps,
                                     newly_finished_submap);
  );

  return node_id;

 

五、结语

到目前为止,对PoseGraph2D继承关系,以及其中包含的结构体定义与成员变量进行了简单讲解。那么下面就是对细节的分析。后续主要围绕 PoseGraph2D::AddNode() 函数进行展开。接下里要讲解的,首先就是其调用代码:

 // local系下的位姿变换到global系下
  const transform::Rigid3d optimized_pose(
      GetLocalToGlobalTransform(trajectory_id) * constant_data->local_pose);

如何把local系下的位姿变换到global系下,其上的 constant_data->local_pose 就表示local系下的位姿,可以理解为机器人Robot位姿。

 
 
 

精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)

前言

开始上班了,小编继上篇博客精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)(二),今天讲结果集处理与映射的倒数第二篇,本来以为三篇能结束的结果发现不够用了,先讲懒加载吧。话不多说进入主题。

懒加载

懒加载是为改善解析对象属性时,大量的嵌套子查询的并发问题。设置懒加载后,只有在使用指定属性时才会加载,从而分散SQL请求。mybatis懒加载原理是基于动态代理实现的。
基本配置以及代码示例(小编依旧使用公司下多个部门的嵌套查询,设置部门查询的时候使用懒加载):

<resultMap id="DepartmentMap" type="entity.Department">
        <id property="id" column="id"/>
        <result property="companyId" column="company_id" />
        <result property="departmentName" column="department_name"/>
    </resultMap>

<resultMap id="CompanyMap2" type="entity.Company">
        <id property="id" column="id"/>
        <result property="companyName" column="company_name" jdbcType="VARCHAR"/>
        <collection property="departmentList" column="id" select="selectDepartmentByCompanyId" fetchType="lazy"/>
    </resultMap>
    
 <select id="selectDepartmentByCompanyId" resultMap="DepartmentMap">
        select * from department where company_id = #{companyId}
    </select>

    <select id="selectCompanyById" resultMap="CompanyMap2"  parameterType="java.lang.Long">
        select * from company where id = #{id}
    </select>

测试代码:

public class LazyLoadTest {

    SqlSessionFactory sqlSessionFactory;
    Configuration configuration;

    @Before
    public void init() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        configuration = sqlSessionFactory.getConfiguration();
        configuration.setLazyLoadTriggerMethods(new HashSet<>());
    }

    @Test
    public void collectionLazyTest() {
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            System.out.println(company.getDepartmentList().size());
        }
    }
}

好了小编根据示例先讲一下懒加载的配置和细节:

  1. 在嵌套子查询中指定 fetchType=“lazy” 即可设置懒加载。在调用company.getDepartmentList时才会真正加载。此外调用company的:“equals”, “clone”, “hashCode”, “toString” 均会触发当前对象所有未执行的懒加载。(“equals”, “clone”, “hashCode”, “toString” 这些方法是在configuration中lazyLoadTriggerMethods的属性,类型为Set,上面小编为了不让company直接懒加载[因为debug下会执行toString方法]从而设置了空hashSet)
  2. 通过设置全局参数aggressiveLazyLoading=true ,也可指定调用对象任意方法触发所有懒加载。
参数描述
lazyLoadingEnabled全局懒加载开关 默认false
aggressiveLazyLoading任意方法触发加载 默认false。
fetchType加载方式 eager实时 lazy懒加载。默认eager
  1. set方法覆盖,当调用setXXX方法手动设置属性之后,对应的属性懒加载将会被移除,不会覆盖手动设置的值。
  2. 当对象经过序列化和反序列化之后,默认不在支持懒加载。但如果在全局参数中设置了configurationFactory类,而且采用JAVA原生序列化是可以正常执行懒加载的。其原理是将懒加载所需参数以及配置一起进行序列化,反序列化后在通过configurationFactory获取configuration构建执行环境。需要配置:
public static class ConfigurationFactory {       
	public static Configuration getConfiguration() {        
 		return configuration; } 
}

第三与第四点小编通过代码再次说明一下:
set方法覆盖

	//set方法覆盖
 	@Test
    public void collectionLazySetTest() {
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            company.setDepartmentList(new ArrayList<>());
            System.out.println(company.getDepartmentList().size());
        }
    }

测试结果:
set方法覆盖
在这里插入图片描述
没set方法覆盖:
在这里插入图片描述
对象序列化反序列化

	@Test
    public void collectionLazySerializableTest() throws Exception{
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            byte[] bytes = writeObject(company);
            Company companySer = (Company)readObject(bytes);
            System.out.println(companySer.getDepartmentList());
        }
    }

    private  byte[] writeObject(Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(out);
        outputStream.writeObject(obj);
        return out.toByteArray();
    }

    private  Object readObject(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        ObjectInputStream inputStream = new ObjectInputStream(in);
        return inputStream.readObject();
    }

测试结果:
在这里插入图片描述

修改完毕后:

    <settings>
    	//这里是内部静太类用$标识
        <setting name="configurationFactory" value="test.LazyLoadTest$ConfigurationFactory"/>
    </settings>
@Test
 public void collectionLazySerializableTest() throws Exception{
        try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
            CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
            Company company = companyMapper.selectCompanyById(1L);
            byte[] bytes = writeObject(company);
            Company companySer = (Company)readObject(bytes);
            System.out.println(companySer.getDepartmentList());
        }
    }
public static class ConfigurationFactory {
        public static Configuration getConfiguration() {
            return configuration; }
    }

测试结果:
在这里插入图片描述

懒加载原理及内部结构

讲完了懒加载的结论,那小编肯定得证明原理,首先根据上面的示例代码,查看一下company的结构:
在这里插入图片描述
代理之后Bean会包含一个MethodHandler,内部在包含一个Map用于存放待执行懒加载,执行前懒加载前会移除。LoadPair用于针对反序列化的Bean准备执行环境。ResultLoader用于执行加载操作,执行前原执行器如果关闭会创建一个新的执行器(这里会引出一个小编之前没有讲解的Executor => ClosedExecutor)。特定属性如果加载失败,不会在进行二次加载。
内部结构图以及执行流程:
在这里插入图片描述
代理bean的创建过程
在这里插入图片描述
代理过程发生在结果集解析,创建对象之后(DefaultResultSetHandler.createResultObject),如果对应的属性设置了懒加载,则会通过ProxyFactory 创建代理对象,该对象继承自原对象,然后将对象的值全部拷贝到代理对像。并设置相应MethodHandler(原对象直接抛弃)

懒加载源码阅读

接下来小编带大家来一起阅读一下源码:
先是代理对象的创建:
DefaultResultSetHandler#createResultObject方法

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
   ...
    //创建company对象
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
    if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
      //三个映射 ,再department的时候为子查询且是懒加载
      for (ResultMapping propertyMapping : propertyMappings) {
        // issue gcode #109 && issue #149
        if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
          //对company对象进行代理
          resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
          break;
        }
      }
    }
    this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
  }

JavassistProxyFactory#createProxy方法

public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
      final Class<?> type = target.getClass();
      //创建EnhancedResultObjectProxyImpl 
      EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
      //根据EnhancedResultObjectProxyImpl 创建compay的代理对象
      Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
      //之后是将原对象的属性copy进入代理对象
      PropertyCopier.copyBeanProperties(type, target, enhanced);
      return enhanced;
    }

创建完代理对象后,然后获取:
JavassistProxyFactory#invoke方法

@Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
     //原对象的method方法
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
         //不会进这儿
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            Object original;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
              return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              return original;
            }
          } else {
            //懒加载大于0并且不是finalize方法
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
              //是否任意方法就加载或者在"equals", "clone", "hashCode", "toString"方法里面
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                lazyLoader.loadAll();
                //如果是set方法 则将懒加载移除,直接赋值
              } else if (PropertyNamer.isSetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                lazyLoader.remove(property);
                //为get方法,根据属性名称获取懒加载器有就加载
              } else if (PropertyNamer.isGetter(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

ResultLoaderMap#load

public boolean load(String property) throws SQLException {
	//取出LoadPair ,并且移除懒加载属性(懒加载之前所以如果pair.load()失败则不会进行二次加载)
    LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
    if (pair != null) {
     //开始加载
      pair.load();
      return true;
    }
    return false;
  }

ResultLoaderMap$LoadPair#load

public void load(final Object userObject) throws SQLException {
      if (this.metaResultObject == null || this.resultLoader == null) {
        if (this.mappedParameter == null) {
          throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
                  + "required parameter of mapped statement ["
                  + this.mappedStatement + "] is not serializable.");
        }

        final Configuration config = this.getConfiguration();
        final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
        if (ms == null) {
          throw new ExecutorException("Cannot lazy load property [" + this.property
                  + "] of deserialized object [" + userObject.getClass()
                  + "] because configuration does not contain statement ["
                  + this.mappedStatement + "]");
        }

        this.metaResultObject = config.newMetaObject(userObject);
        //这里有个ClosedExecutor 继承BaseExecutor,只是将isClosed设置为true
        this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
                metaResultObject.getSetterType(this.property), null, null);
      }

      /* We are using a new executor because we may be (and likely are) on a new thread
       * and executors aren't thread safe. (Is this sufficient?)
       *
       * A better approach would be making executors thread safe. */
      //这段代码有点多余,因为上面已经赋值了,即使进去也是一样的
      if (this.serializationCheck == null) {
        final ResultLoader old = this.resultLoader;
        this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
                old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
      }
	 //设置属性值
	 //this.resultLoader.loadResult()
      this.metaResultObject.setValue(property, this.resultLoader.loadResult());
    }

ResultLoader#selectList

public Object loadResult() throws SQLException {
	//通过executor 直接查询库
    List<Object> list = selectList();
    //设置值
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
  }

  private <E> List<E> selectList() throws SQLException {
    Executor localExecutor = executor;
    if (Thread.currentThread(以上是关于(02)Cartographer源码无死角解析-(54) 2D后端优化→PoseGraphInterfacePoseGraphPoseGraph2D::AddNode()的主要内容,如果未能解决你的问题,请参考以下文章

谷歌Cartographer学习-原理阐述与源码解析

cartographer源码解析

cartographer源码解析

cartographer源码解析

cartographer源码解析

精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)