flink watermark 生成机制与总结

flink watermark 生成机制与总结

    • watermark 介绍
    • watermark生成方式
      • watermark 的生成值算法策略
      • watermark策略设置代码
    • watermark源码分析
      • watermark源码调用流程debug(重要)
      • 测试思路
    • 迟到时间处理
    • FlinkSql 中的watermark
    • 引出问题与源码分析

watermark 介绍

本质上watermark是flink为了处理eventTime窗口计算提出的一种机制,本质上也是一种时间戳,由flink souce或者自定义的watermark生成器按照需求定期或者按条件生成一种系统event,与普通数据流event一样流转到对应的下游operations,接收到watermark数据的operator以此不断调整自己管理的window event time clock。

首先,eventTime计算意味着flink必须有一个地方用于抽取每条消息中自带的时间戳,所以TimestampAssigner的实现类都要具体实现
long (T element, long previousElementTimestamp);方法用来抽取当前元素的eventTime,这个eventTime会用来决定元素落到下游的哪个或者哪几个window中进行计算。

其次,在数据进入window前,需要有一个Watermarker生成当前的event time对应的水位线,flink支持两种后置的Watermarker:Periodic和Punctuated,一种是定期产生watermark(即使没有消息产生),一种是在满足特定情况的前提下触发。两种Watermark分别需要实现接口为
Watermark getCurrentWatermark()和Watermark checkAndGetNextWatermark(T lastElement, long extractedTimestamp);

如果一个下游算子实例消费者多个上游算子实例,则选择上游最小的watermark作为自己的watermark发往下游,这也是为什么要对齐在这里插入图片描述
另外
在这里插入图片描述

watermark生成方式

在 Flink 中,数据处理中需要通过调⽤ DataStream 中的 assignTimestampsAndWatermarks ⽅法来分配时间和⽔印,该⽅法可以传⼊两种参数,⼀个是 AssignerWithPeriodicWatermarks(周期性生成watermark),另⼀个是 AssignerWithPunctuatedWatermarks(已过期,按指定标记性事件生成 watermark),通常建议在数据源(source)之后就进⾏⽣成⽔印,或者做些简单操作⽐如 filter/map/flatMap 之 后再⽣成⽔印,越早⽣成⽔印的效果会更好,也可以直接在数据源头就做⽣成⽔印。

  • With Periodic Watermarks(常用):周期性(一定时间间隔或者达到一定的记录条数)生成watermark

    • 需要实现AssignerWithPeriodicWatermarks接口
    • 默认周期是200ms,可通过env.getConfig.setAutoWatermarkInterval进行修改
    • 实际生产环境用得多,但必须结合时间或者累计条数两个维度,否则在极端情况下会有很大的延时
  • With Punctuated Watermarks(不常用):在满足自定义条件时生成watermark,每一个元素都有机会判断是否生成一个watermark。

    • 需要实现AssignerWithPunctuatedWatermarks接口
    • 在TPS很高的生产环境下会产生大量的 Watermark,可能在一定程度上对下游算子造成一定的压力,只有在实时性很高的场景才会选择这种方式来进行生成水印
    • 新版 Flink 源码中已经标记为 @Deprecated

watermark 的生成值算法策略

  • 紧跟最大事件时间的 watermark 生成策略(完全不容忍乱序)
    WatermarkStrategy.forMonotonousTimestamps();
  • 允许乱序的 watermark 生成策略(最大事件时间-容错时间)
    WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10)); // 根据实际数据的最大乱序情况来设置
  • 自定义 watermark 生成策略
    WatermarkStrategy.forGenerator(new WatermarkGenerator(){ … } );

watermark策略设置代码


import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import java.time.Duration;public class sinkFunction {public static void main(String[] args) {StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();DataStreamSource<String> stream = env.socketTextStream("local", 9999);stream.assignTimestampsAndWatermarks(WatermarkStrategy.noWatermarks()); //禁用时间时间的推进机制stream.assignTimestampsAndWatermarks(WatermarkStrategy.forMonotonousTimestamps()); //紧跟最大时间时间stream.assignTimestampsAndWatermarks(WatermarkStrategy.forGenerator()); //自定义watermark生成算法stream.assignTimestampsAndWatermarks(WatermarkStrategy.<String>forBoundedOutOfOrderness(Duration.ofSeconds(10)).withTimestampAssigner(new SerializableTimestampAssigner<String>() {@Overridepublic long extractTimestamp(String s, long l) {return Long.parseLong(s.split(",")[0]);}}));}
}

watermark源码分析

背景代码: source.map(s->bean).assignWatermarkAndTimestamps( ).process().print();

watermark源码调用流程debug(重要)

想要知道代码是如何调用的,我们通过debug的方式来查看数据调用:
我们测试的代码是这样的:

package Launcher;import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import java.time.Duration;
//nc -lk 9999
public class sinkFunction {public static void main(String[] args) throws Exception {StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();DataStreamSource<String> stream = env.socketTextStream("localhost", 9999);
//        stream.assignTimestampsAndWatermarks(WatermarkStrategy.noWatermarks()); //禁用时间时间的推进机制
//        stream.assignTimestampsAndWatermarks(WatermarkStrategy.forMonotonousTimestamps()); //紧跟最大时间时间
//        stream.assignTimestampsAndWatermarks(WatermarkStrategy.forGenerator()); //自定义watermark生成算法stream.assignTimestampsAndWatermarks(WatermarkStrategy.<String>forBoundedOutOfOrderness(Duration.ofSeconds(10)).withTimestampAssigner(new SerializableTimestampAssigner<String>() {@Overridepublic long extractTimestamp(String s, long l) {return Long.parseLong(s.split(",")[0]);}}));stream.print();env.execute();}
}

代码很简单,什么都没有,就一个设置watermark
我们先打了两个断点在源码的BoundedOutOfOrdernessWatermarks.class中:
在这里插入图片描述

然后开启nc窗口:
在这里插入图片描述

当我们debug后,没有任何输入就会在output.emitWatermark(new Watermark(this.maxTimestamp - this.outOfOrdernessMillis - 1L));处 断点停止,因为这个函数是有定时器触发的,只要程序跑起来,每200ms就会触发一次:
在这里插入图片描述

这个两百是怎么来的呢?点这里可以看到:
在这里插入图片描述
这是task类封装的,一个底层算子类TimestampsAndWatermarksOperator,看这个类的open方法中的这里:
在这里插入图片描述
这里注册了一个计时器,时间是当前时间加watermarkInterval 后触发,这个watermarkInterval 就是200ms。

在这里插入图片描述而触发方法,这里在调用了watermarkGenerator.onPeriodicEmit,并且有重新注册了一个200ms后的定时,实现了 每隔200ms触发一次的效果。

如果想修改源码,看一些效果,可以完全按照源码的包名,类名,写一个一模一样的java类,类加载器会优先加载自己写的代码,而不会去加载引入的源码中代码。

在这里插入图片描述
这里可以看到,数据来了,他是先数据 collect,后触发水印的onevent方法,也就是后更新watermark值。

测试思路

构造两条单并行度流,合并成一个单并行度流,来 watermark,及重点观察“接收多个上游分区”的 算子的 watermark 推进规律;
在这里插入图片描述

/*** 作者:深海 "deep as the sea"* 日期:2022/4/10* 联系方式:qq:657270652 wx:doitedu2018* 网站:多易教育 www.51doit.cn* 描述:watermark 推进测试观察**/
public class WatermarkTest {public static void main(String[] args) throws Exception {StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1);// 构造一个无乱序延迟的 watermark 生成策略WatermarkStrategy<String> stringWatermarkStrategy = WatermarkStrategy.<String>forMonotonousTimestamps().withTimestampAssigner(new SerializableTimestampAssigner<String>() {@Overridepublic long extractTimestamp(String element, long recordTimestamp) {return Long.parseLong(element.split(",")[1]);}});// 构造单并行度流 1DataStreamSource<String> source1 = env.socketTextStream("localhost", 9988);SingleOutputStreamOperator<String> s1 = source1.assignTimestampsAndWatermarks(stringWatermarkStrategy);// 构造单并行度流 2DataStreamSource<String> source2 = env.socketTextStream("localhost", 9999);SingleOutputStreamOperator<String> s2 = source2.assignTimestampsAndWatermarks(stringWatermarkStrategy);// 两条单并行度流,合并到一条单并行度流DataStream<String> s = s1.union(s2);// 打印 watermark 信息,观察 watermark 推进情况s.process(new ProcessFunction<String, String>() {@Overridepublic void processElement(String value, Context ctx, Collector<String> out) throws Exception {// 获取当前的 watermark(注意:此处所谓当前 watermark 是指处理当前数据前的 watermark)long currentWatermark = ctx.timerService().currentWatermark();Long timestamp = ctx.timestamp();System.out.println("s" + " : " + timestamp + " : " + currentWatermark + " => " + value);out.collect(value);}}).print();env.execute();}
}

测试结果

在 9988 端口输入数据: a,1000
在 9999 端口输入数据: b,1000 b,4000 b,5000
输出结果:
s: 1000 : -9223372036854775808 => a,1000
s: 1000 : -9223372036854775808 => b,1000
s: 4000 : 999 => b,4000
s: 5000 : 999 => b,5000

迟到时间处理

  • 直接丢弃:将迟到事件视为错误消息并丢弃(flink默认处理方式)。
  • Side Output机制:可以将迟到事件单独放入一个数据流分支,这会作为 window 计算结果的副产品,以便用户获取并对其进行特殊处理。
  • Allowed Lateness机制:允许用户设置一个允许的最大迟到时长。Flink 会再窗口关闭后一直保存窗口的状态直至超过允许迟到时长,这期间的迟到事件不会被丢弃,而是默认会触发窗口重新计算。因为保存窗口状态需要额外内存,并且如果窗口计算使用了 ProcessWindowFunction API 还可能使得每个迟到事件触发一次窗口的全量计算,代价比较大,所以允许迟到时长不宜设得太长,迟到事件也不宜过多,否则应该考虑降低水位线提高的速度或者调整算法。

FlinkSql 中的watermark

在创建表的 DDL 中定义
事件时间属性可以用 WATERMARK 语句在 CREATE TABLE DDL 中进行定义。WATERMARK 语句在一个已有字段上定义一个 Watermark 生成表达式,同时标记这个已有字段为时间属性字段。

CREATE TABLE user_actions (
user_name STRING,data STRING,
user_action_time TIMESTAMP(3),-- 声明 user_action_time 是事件时间属性,并且用 延迟 5 秒的策略来生成 watermark
WATERMARK FOR user_action_time AS user_action_time - INTERVAL '5' SECOND
) WITH (
...
);SELECT TUMBLE_START(user_action_time, INTERVAL '10' MINUTE), COUNT(DISTINCT user_name)
FROM user_actions
GROUP BY TUMBLE(user_action_time, INTERVAL '10' MINUTE);

如果源中的时间戳数据表示为一个 epoch time,通常是一个长值,例如 1618989564564,建议将事件时间属性定义为 TIMESTAMP_LTZ 列

CREATE TABLE user_actions (
user_name STRING,data STRING,
ts BIGINT,
time_ltz AS TO_TIMESTAMP_LTZ(ts, 3),-- declare time_ltz as event time attribute and use 5 seconds delayed watermark strategy
WATERMARK FOR time_ltz AS time_ltz - INTERVAL '5' SECOND
) WITH (
...
);SELECT TUMBLE_START(time_ltz, INTERVAL '10' MINUTE), COUNT(DISTINCT user_name)
FROM user_actions
GROUP BY TUMBLE(time_ltz, INTERVAL '10' MINUTE);

引出问题与源码分析

我们这里来引出一个问题,不能排除有同学就是想用ProcessTime。

那么问题来了,EventTime 情况的watermark 很好理解,
可是ProcessTime的watermark到底做了什么,也不需要用它来过滤数据,本来就没有用数据内的时间,根本就不知道数据的顺序,更谈不上乱序了,那ProcessTime起了什么用呢?

我们首先来看下env.setStreamTimeCharacteristic() 这个方法

/*** Sets the time characteristic for all streams create from this environment, e.g., processing* time, event time, or ingestion time.** <p>If you set the characteristic to IngestionTime of EventTime this will set a default* watermark update interval of 200 ms. If this is not applicable for your application* you should change it using {@link ExecutionConfig#setAutoWatermarkInterval(long)}.** @param characteristic The time characteristic.*/@PublicEvolvingpublic void setStreamTimeCharacteristic(TimeCharacteristic characteristic) {this.timeCharacteristic = Preconditions.checkNotNull(characteristic);if (characteristic == TimeCharacteristic.ProcessingTime) {getConfig().setAutoWatermarkInterval(0);} else {getConfig().setAutoWatermarkInterval(200);}}

这个方法设置用户使用的是eventtime还是processtime

由源码可以看到 ,如果设置的是ProcessingTime ,会把autoWatermarkInterval这个属性值设为0,如果是EventTime,会设置为 200,我们追踪这个值发现,用户自定义的watermark类,需要注册在assignTimestampsAndWatermarks中,而在assignTimestampsAndWatermarks类中能够找到TimestampsAndPeriodicWatermarksOperator,
TimestampsAndPeriodicWatermarksOperator的open方法中有autoWatermarkInterval这个属性值。

public SingleOutputStreamOperator<T> assignTimestampsAndWatermarks(AssignerWithPeriodicWatermarks<T> timestampAndWatermarkAssigner) {// match parallelism to input, otherwise dop=1 sources could lead to some strange// behaviour: the watermark will creep along very slowly because the elements// from the source go to each extraction operator round robin.final int inputParallelism = getTransformation().getParallelism();final AssignerWithPeriodicWatermarks<T> cleanedAssigner = clean(timestampAndWatermarkAssigner);TimestampsAndPeriodicWatermarksOperator<T> operator =new TimestampsAndPeriodicWatermarksOperator<>(cleanedAssigner);return transform("Timestamps/Watermarks", getTransformation().getOutputType(), operator).setParallelism(inputParallelism);}
@Overridepublic void open() throws Exception {super.open();currentWatermark = Long.MIN_VALUE;watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();if (watermarkInterval > 0) {long now = getProcessingTimeService().getCurrentProcessingTime();getProcessingTimeService().registerTimer(now + watermarkInterval, this);}}

我们来看这个open的初始方法,这个open是TimestampsAndPeriodicWatermarksOperator的初始方法,其实也是assignTimestampsAndWatermarks启动的条件,这个open给定了watermark的初始值。

这里初始化了两个值,

  • 一个是其实的watermark的初始值,最小的long值,-9223372036854775808
  • 另一个是初始的 watermark的间隔 如果是 EventTime就是当前时间加200ms,如果是ProcessTime就是当前时间。

再来看下面的定时任务

	@Overridepublic void onProcessingTime(long timestamp) throws Exception {// register next timerWatermark newWatermark = userFunction.getCurrentWatermark();if (newWatermark != null && newWatermark.getTimestamp() > currentWatermark) {currentWatermark = newWatermark.getTimestamp();// emit watermarkoutput.emitWatermark(newWatermark);}long now = getProcessingTimeService().getCurrentProcessingTime();getProcessingTimeService().registerTimer(now + watermarkInterval, this);}

再来看下我们自定义注册的watermark方法

     @Overridepublic Watermark getCurrentWatermark() {// return the watermark as current highest timestamp minus the out-of-orderness boundreturn new Watermark(currentMaxTimestamp - maxOutOfOrderness);}

autoWatermarkInterval为0的话 super.open() 不会被调用

这里面now,就是System.currentTimeMillis(); 所以如果时间间隔不为0,那么下一次调用的时间就是 当前时间 + 方法运行的时间 + 时间间隔,由于方法运行的时间约等于0ms,所以基本就是每个时间间隔(默认200ms),运行一次获取wakermark的方法。

所以如果是ProcessingTime,那么默认时间间隔是0,所以matermarks时间就是一直-9223372036854775808,所以就一直不会过滤时间。
所以想要启动ProcessingTime 来做 时间戳 ,就一定要设置
env.getConfig().setAutoWatermarkInterval(200);

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot实现缓存预热的几种常用方案

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&…

TRS 2024 论文阅读 | 基于点云处理和点Transformer网络的人体活动连续识别

无线感知/雷达成像部分最新工作<持续更新>: 链接地址 注1:本文系“无线感知论文速递”系列之一,致力于简洁清晰完整地介绍、解读无线感知领域最新的顶会/顶刊论文(包括但不限于 Nature/Science及其子刊; MobiCom, Sigcom, MobiSys, NSDI, SenSys, Ubicomp; JSAC, 雷达学…

ubuntu使用LLVM官方发布的tar.xz来安装Clang编译器

ubuntu系统上的软件相比CentOS更新还是比较快的&#xff0c;但是还是难免有一些软件更新得不那么快&#xff0c;比如LLVM Clang编译器&#xff0c;目前ubuntu 22.04版本最高还只能安装LLVM 15&#xff0c;而LLVM 18 rc版本都出来了。参见https://github.com/llvm/llvm-project/…

go使用trpc案例

1.go下载trpc go install trpc.group/trpc-go/trpc-cmdline/trpclatest 有报错的话尝试配置一些代理&#xff08;选一个&#xff09; go env -w GOPROXYhttps://goproxy.cn,direct go env -w GOPROXYhttps://goproxy.io,direct go env -w GOPROXYhttps://goproxy.baidu.com/…

NXP实战笔记(六):S32K3xx基于RTD-SDK在S32DS上配置PWM发波

目录 1、概述 2、SDK配置 2.1、Port配置 2.2、Emios_Mcl_Ip 2.3、Emios_Pwm 2.4、代码示例 1、概述 针对S32K3xx芯片&#xff0c;产生PWM的硬件支持单元仅有两个&#xff0c;分别是eMiosx与Flexio. 生成PWM的顺序&#xff0c;按照单片机所用资源进行初始化执行如下 初始化…

工业信息安全的神秘武器——德迅卫士(主机安全)

前言&#xff1a; 服务器作为承载公司业务及内部运转的底层平台&#xff0c;其稳定、安全地运行是公司的正常发展的前提保障。由于主机上运行着各种各样的业务&#xff0c;会存在着各类漏洞及安全问题。攻击者以此为目标&#xff0c;通过对服务器的攻击来获利&#xff0c;给公司…

day40打卡

day40打卡 343. 整数拆分 状态表示 ​ dp[i] 表示将正整数i拆分成至少两个正整数的和之后&#xff0c;这些正整数的最大乘积 状态转移方程 ​ i > 2 时&#xff0c;对正整数i拆出的第一个正整数是j&#xff0c;则有&#xff1a; 将i拆分为 j 和 i-j&#xff0c;且 i-j…

【计网】TCP的三次握手四次挥手

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;JAVA ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 三次握手&#xff08;Connection Establishment&#xff09; 四次挥手&#xff08;Connection Termination&#xff09; 结语 我…

linux0.11 源码阅读 head.s setup.s bootsect.s加载位置

从github上下载linux0.11源码 linux0.11源码 将0x10000处的代码往下复制到0开始的地址处。 移动后的内存布局如下 setup中存在gdt和idt的相关数据。此时需要用gdtr和idtr寄存器指向对应的数据。 实模式下&#xff0c;访问内存方式。最多访问1M内存。 分页模式下&…

SQL注入:网鼎杯2018-unfinish

目录 使用dirmap扫描 使用dirsearch扫描 使用acunetix扫描 爆破后端过滤的字符 绕过限制获取数据 这次的进行SQL注入的靶机是&#xff1a;BUUCTF在线评测 进入到主页面后发现是可以进行登录的&#xff0c;那么我们作为一个安全人员&#xff0c;那肯定不会按照常规的方式来…

如何用GPT进行论文写作?

一&#xff1a;AI领域最新技术 1.OpenAI新模型-GPT-5 2.谷歌新模型-Gemini Ultra 3.Meta新模型-LLama3 4.科大讯飞-星火认知 5.百度-文心一言 6.MoonshotAI-Kimi 7.智谱AI-GLM-4 二&#xff1a;GPT最新技术 1.最新大模型GPT-4 Turbo 2.最新发布的高级数据分析&#x…

基于SVM的功率分类,基于支持向量机SVM的功率分类识别,Libsvm工具箱详解

目录 支持向量机SVM的详细原理 SVM的定义 SVM理论 Libsvm工具箱详解 简介 参数说明 易错及常见问题 完整代码和数据下载链接:基于SVM的功率分类,基于支持向量机SVM的功率分类识别资源-CSDN文库 https://download.csdn.net/download/abc991835105/88862836 SVM应用实例, 基于…

QT-串口工具

一、演示效果 二、关键程序 &#xff1a; #include "mainwindow.h" #include "ui_mainwindow.h"#include <QMessageBox>MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow),listPlugins(QList<TabPluginInt…

JavaScript字符串的常用方法(非常详细)

文章目录 一、操作方法增concat 删改trim()、trimLeft()、trimRight()repeat()padEnd() toLowerCase()、 toUpperCase()查charAt()indexOf()startWith()、includes() 二、转换方法split 三、模板匹配方法match()search()replace() 一、操作方法 我们也可将字符串常用的操作方法…

zemax凯尔纳目镜

也叫做凯涅尔目镜 凯尔纳目镜 是由单片透镜和双胶合透镜组成的。 一种改进型的冉斯登目镜&#xff0c;二片组成的接目镜及双凸透镜作为场镜。 它能校正倍率色差 &#xff0c;同时也减小了位置色差 、像散和畸变。 视场角大于40&#xff0c;可达50。 目前在一些中低倍望远镜中…

在Linux操作系统的ECS实例上安装Hive

目录 1. 完成hadoop安装配置2. 安装配置MySql安装配置 3. 安装Hive4. 配置元数据到MySQL5. hiveserver2服务配置文件测试 1. 完成hadoop安装配置 在Linux操作系统的ECS实例上安装hadoop 以上已安装并配置完jdk、hadoop也搭建了伪分布集群 2. 安装配置MySql 安装 下下一步…

Jenkins使用遇到的一些问题

一&#xff1a;插件依赖报错 比如遇到一堆插件报错&#xff0c;不是提示版本对不上&#xff0c;就是启用不了 这样直接把Jenkins升级就行了&#xff0c;比如我这个是命令行启动的&#xff0c;直接把他替换就好了 如果是遇到插件依赖报错&#xff0c;比如A插件异常 则点击这个插…

RISC-V知识总结 —— 指令集

资源1: RISC-V China – RISC-V International 资源2: RISC-V International – RISC-V: The Open Standard RISC Instruction Set Architecture 资源3: RV32I, RV64I Instructions — riscv-isa-pages documentation 1. 指令集架构的类型 在讨论RISC-V或任何处理器架构时&…

解决IDEA中Maven下载依赖包过慢或报错的问题

由于公司项目迭代&#xff0c;越来越多的项目开始转型新版本&#xff0c;由于我对Java一直不感冒&#xff0c;但要顺应公司项目要求&#xff0c;遂自己要逐步开始完善Java相关的知识层面&#xff0c;此篇是我在学习SpringBoot时对一些不懂地方及遇到问题时的记录。 学习视频链…

[更新]ARCGIS之土地耕地占补平衡、进出平衡系统报备坐标txt格式批量导出工具(定制开发版)

序言 之前开发的耕地占补平衡报备格式&#xff0c;现在之前的基础上集成了耕地进出平衡报备格式导出。 之前版本软件详见&#xff1a;软件介绍 一、软件简介 本软件是基于arcgis二次开发的工具&#xff08;插件&#xff09;&#xff0c;需要授权后才能使用&#xff1b; 本软件…