简介面向物流、配送及地理信息处理开发者这套基于高德地图API的Java工具专门解决批量地理位置距离计算问题支持地址批量输入、距离矩阵计算、CSV文件导入导出、多线程并发处理与结果可视化展示适合需要处理成百上千个地址数据的场景。资源包共16个文件约695KB包含Java源码Const.java、Main.java、Data.java、编译后的class文件、fastjson-1.2.62.jar依赖库、多个xml/iml配置与工程文件以及README.md、说明文件.txt、附赠资源.docx等使用文档目录结构清晰便于检索。通过源码可学习高德地图API调用、多线程任务拆分、CSV数据流转和距离矩阵算法可视化模块能直观展示地点间距离关系为物流调度和运输路线优化提供数据支撑。附带的说明文件与附赠资源可帮助快速上手开发者也能在此基础上二次开发扩展为更专业的地理位置处理工具。已有178人学习下载。1. 批量距离计算的开箱工具高德地图API与物流距离矩阵的匹配度做物流调度的人应该都有这种体验手上几十个配送点要算两两之间的距离人工打开高德地图一个个量量完填进Excel腰酸背痛还容易抄错行。这个基于高德地图API的批量地理位置距离计算工具就是来干这件事的。把地址列表整理成CSV丢进去程序自动调高德地图API的距离矩阵接口用多线程并发把几百个点的两两距离算完再导出成结果表。它不帮你做路径优化但能在一顿饭的功夫里给你一张可靠的距离矩阵。适合物流调度、同城配送、门店划区这类需要大量位置计算的人也适合想参考高德API调用和Java多线程写法的开发者。2. 拆解Distance-master三个Java类如何撑起CSV到距离矩阵的流水线2.1 目录结构先看清Main、Const、Data三个类各管一段拿到了解压后的目录第一反应是把整个文件夹结构过一遍。src下面只有三个Java文件这意味着它是一个典型的工具型项目不是那种动辄几十个类的企业工程读起来负担小很多。Distance-master/ ├── lib/fastjson-1.2.62.jar # JSON解析依赖 ├── src/ │ ├── Const.java # 常量API Key、请求URL │ ├── Main.java # 入口流程编排 │ └── Data.java # 数据模型地址与计算结果 ├── out/production/ # 编译输出目录 ├── .idea/ # IntelliJ IDEA 工程配置 └── Distance.iml # IDEA 模块文件Const.java 管的是高德API的 Key 和接口地址Main.java 负责把“读CSV到调接口到写结果”这条链路串起来Data.java 定义一行地址数据在内存里长什么样。lib 目录下只有 fastjson-1.2.62.jar 一个依赖说明作者刻意避开了Spring这类重框架整个工具拷贝到哪都能编译运行。这种轻量结构的实际好处是数据流单一清晰运营同学改CSV输入开发同学只动Main.java互不干扰。出问题时按“输入文件 → 中间结果 → 输出文件”三段排查定位很快。我第一次跑通这个工程从看代码到出结果大概花了半小时其中一半时间花在申请Key和改文件路径上。2.2 CSV进内存Data模型与坐标系的选择Data.java是整个数据模型的核心。它把CSV里的一行地址映射成一个Java对象再带着计算结果一起进入后续流程。常见的设计是保留名称、经纬度和距离三个基础字段。public class Data { private String name; // 网点或配送点名称 private double lng; // 经度注意是GCJ-02坐标系 private double lat; // 纬度 public Data(String name, double lng, double lat) { this.name name; this.lng lng; this.lat lat; } // getter / setter 略 }这里的经度纬度必须强调坐标系。高德地图API默认使用GCJ-02坐标系俗称“火星坐标系”。如果你手里是从GPS设备直接读出来的WGS-84经纬度不经过转换就传给高德算出来的距离会整体偏移几百米甚至更多。我见过有人拿GPS裸坐标算了两百多个点的距离矩阵结果和实际路线差出一大截最后查到就是坐标系没对齐。CSV的读取没有引入额外的库直接用BufferedReader按行读配合split做切分。注意表头要跳过空行要过滤这是最容易忽略的边界条件。try (BufferedReader br new BufferedReader(new InputStreamReader( new FileInputStream(inputFile), StandardCharsets.UTF_8))) { br.readLine(); // 跳过表头 String line; while ((line br.readLine()) ! null) { if (line.trim().isEmpty()) continue; String[] cols line.split(,); Data d new Data(cols[0].trim(), // 名称 Double.parseDouble(cols[1].trim()), // 经度 Double.parseDouble(cols[2].trim())); // 纬度 list.add(d); } }这段逻辑的要点在于CSV里三列的顺序必须固定为“名称,经度,纬度”。如果是从Excel另存的CSV经常会出现UTF-8编码问题建议在另存时选“CSV UTF-8”格式。另外split(,)这种方式对简单场景够用但列内容里如果带了英文逗号就会切错列正式用的时候可以换成CSVReader这类库。2.3 两个API怎么选地理编码geocode与距离distance的参数对比高德地图Web服务里有两个接口和这个工具强相关。一个是地理编码接口/v3/geocode/geo把文字地址转成经纬度另一个是距离测量接口/v3/distance直接算两个坐标点之间的距离。整个工具要跑通需要先搞清楚这两个接口的分工。接口作用输入配额消耗/v3/geocode/geo地址转坐标结构化地址字符串按次计/v3/distance坐标点之间距离origins destination按公里与次数叠加如果CSV里已经是经纬度直接调distance接口就行。如果只有文字地址比如“杭州市西湖区文三路138号”必须先调geocode把地址换成GCJ-02坐标再拿坐标去算距离。geocode的返回结构里有个 geocodes 数组第一个元素就是最匹配的结果里面包含 location 字段格式是“经度,纬度”。distance接口的请求参数比较讲究核心是 origins、destination 和 type 三个字段。origins 支持多个起点用竖线分隔destination 是本次请求的目标点。type0表示直线距离type1表示驾车导航距离。驾车距离更贴近实际配送场景但配额消耗更大也会受道路不可达影响。# 一次请求一个目的地、多个起点的距离 https://restapi.amap.com/v3/distance?key你的Key origins116.481028,39.989643|116.410886,39.880949 destination116.434446,39.90816 type13. 核心实现多线程并发、QPS控制与距离矩阵计算细节3.1 HTTP请求与fastjson解析status才是判断标准Java里调HTTP接口最常见的做法就是用HttpURLConnection不引额外的HTTP库轻量且JDK自带。整个请求的核心是拼参数、发GET、读返回流这三步。高德的返回是JSON格式用项目里自带的fastjson-1.2.62.jar解析就够了。private static String httpGet(String urlStr) throws IOException { URL url new URL(urlStr); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(GET); conn.setConnectTimeout(5000); conn.setReadTimeout(10000); int code conn.getResponseCode(); if (code ! 200) { conn.disconnect(); throw new IOException(HTTP code); } try (BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } return sb.toString(); } finally { conn.disconnect(); } }连接超时设5秒、读超时设10秒这个配置在批量场景下比单次调用更敏感。如果某个请求卡住不返回默认的无限超时会拖死整个线程池。读超时设短一点失败批次重试就是了千万别让一个坏点卡住整批任务。返回的JSON先用 status 字段判断成败status为1才算成功为0时要去 info 字段读错误码。这是和普通HTTP请求最大的区别HTTP 200不代表业务成功。拿到 results 数组后遍历每个元素里取 origin_id 和 distance就能知道某个起点到这个目的地的距离了。JSONObject obj JSON.parseObject(response); if (!1.equals(obj.getString(status))) { throw new RuntimeException(API错误: obj.getString(info)); } JSONArray results obj.getJSONArray(results); for (int i 0; i results.size(); i) { JSONObject r results.getJSONObject(i); String originId r.getString(origin_id); // 对应传入的起点索引 long distance r.getLongValue(distance); // 单位米 // 按 originId 回填到对应的 Data 对象 }origin_id 是回填结果的关键。请求里传了多少个 origins返回数组里就会有多少个元素每个结果通过 origin_id 和传入的起点顺序对应。如果请求是按批次发的回填时一定要带上批次号否则并发一高结果对不上号就全乱了。3.2 组合地址对下三角矩阵的任务切分距离矩阵的本质是N个点两两算距离。N个点全量计算需要N乘N次但实际场景里A到B和B到A往往可以合并所以只算下三角矩阵就能覆盖全部点对。Listint[] pairs new ArrayList(); for (int i 0; i points.size(); i) { for (int j i 1; j points.size(); j) { pairs.add(new int[]{i, j}); } }这段代码生成的是所有“i小于j”的组合数量是 N*(N-1)/2。把生成的每个点对作为一个基本任务后续再按批次合并。合并的原则是单次请求的 origins 数量控制在50个以内一方面是因为高德对单次请求的 origins 数量有上限另一方面是URL长度太长容易被服务端拒绝。分组逻辑上我一般会把同一批次的点对放在一个Batch对象里Batch里记录源点索引、目标点索引和请求参数。这样某个批次失败后重试只需要重新提交这个Batch不用全部重算。3.3 线程池与信号量先压住QPS再谈并发多线程并发是这个工具提升效率的核心手段但很多人一上来就把线程数调到20甚至50结果高德返回一堆限流错误。高德API对单个Key的并发有严格限制个人开发者默认的QPS额度很小线程再多也只能排队。正确做法是线程池控制并发数信号量控制QPS两层配合。Semaphore semaphore new Semaphore(qpsLimit); // qpsLimit3每秒最多3个请求 ExecutorService pool Executors.newFixedThreadPool(8); CountDownLatch latch new CountDownLatch(batches.size()); for (Batch b : batches) { pool.submit(() - { try { semaphore.acquire(); // 拿不到许可就阻塞等待 callDistanceApi(b); // 发一次真实请求 Thread.sleep(1000 / qpsLimit); // 兜底节流防止突发 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { failedBatches.add(b); // 记录失败批次 } finally { semaphore.release(); latch.countDown(); } }); } latch.await(30, TimeUnit.MINUTES); pool.shutdown();这里有两个参数要好好调qpsLimit 我一般设3到5线程数设4到8。这是个经验值因为高德的限流不是精确到秒的给一点余量更稳妥。Thread.sleep(1000 / qpsLimit) 是兜底节流防止信号量刚释放就瞬间打满QPS。latch.await 的作用是等所有批次跑完超时设30分钟数据量大的时候不至于无限等。日志输出上我习惯每个批次打印开始和结束时间这样能看出整体耗时和单个批次的瓶颈。实际跑300个点大约4.5万个点对拆成900个批次8线程配合3 QPS差不多十几分钟能跑完这个速度人工量距离想都不敢想。3.4 失败批次重试与结果写回批量计算不可能一次全成功网络抖动、服务端限流、某个坐标格式错误都会导致个别批次失败。重试机制必须做进流程里。// 第一轮结束后统一重试失败批次 int retry 0; while (!failedBatches.isEmpty() retry 3) { ListBatch toRetry new ArrayList(failedBatches); failedBatches.clear(); for (Batch b : toRetry) { try { Thread.sleep(2000); // 等待2秒再重试 callDistanceApi(b); } catch (Exception ex) { failedBatches.add(b); // 仍然失败进入下一轮 } } retry; }重试策略很简单第一轮结束后收集所有失败批次统一重试最多三轮。每轮重试前等2秒给服务端留出恢复时间。三轮之后仍然失败的批次单独写进一个 error.csv里面带上点对名称和错误信息人工去查是地址问题还是道路不可达。结果写回时如果只算了上三角下三角的值直接对称复制。导出的CSV表头我一般这样设计起点,终点,距离(米),驾车时长(秒)这个格式直接可以被Excel识别也方便后续透视。写入文件时注意编码见下一章的避坑内容。4. 避坑指南高德API批量计算翻车率最高的五个配置点4.1 返回INVALID_USER_KEY但Key没抄错先查IP白名单现象直接用浏览器访问请求URL能正常返回数据但代码里跑就报 INVALID_USER_KEYKey确认没抄错复制粘贴了三遍都对。原因高德控制台里创建的Web服务Key默认绑定IP白名单或域名白名单。浏览器访问时带的是你的出口IP可能刚好在白名单里而服务器或本地Java进程的出口IP不同被白名单拦住了返回的错误码也是 INVALID_USER_KEY。这个错误码是“Key不合法”的泛化表达很多情况下是白名单问题。解决登录高德开放平台控制台找到对应应用把调用方服务器的公网IP加进IP白名单。如果是在本地跑就把本地出口IP加进去。加了之后等一两分钟生效再跑一次就通了。从那以后我拿到Key第一件事就是查白名单省得被这个错误码骗走半小时。4.2 一并发就返回CUQPS_HAS_EXCEEDED_THE_LIMIT现象单线程跑一切正常换成8线程后大量请求失败错误码是 CUQPS_HAS_EXCEEDED_THE_LIMIT翻译过来是并发超限。原因单个Key的QPS配额是固定的线程数增加并不会提高配额只是把请求更密集地打过去瞬间就撞上了限流阈值。多线程在这里变成了“加速撞墙”。解决用信号量或令牌桶把整体请求速率压到配额以下。qpsLimit设为3意味着每秒最多3个请求线程池大小设为8只是保证有8个线程在排队执行真正放行请求的是信号量。参数上我建议先保守跑通后再逐步调大。4.3 距离偏几百米WGS-84与GCJ-02坐标系混用现象算出的距离和高德地图上手动测距的结果差出一截有时候偏几百米方向还不固定。原因输入的经纬度是WGS-84坐标系GPS原始坐标而高德所有API都在GCJ-02坐标系下工作。两个坐标系在全国范围内有几百米不等的偏移在距离计算上会直接体现在结果偏差里。解决如果原始坐标是GPS设备拿的先调用高德的坐标转换API或者用公开的坐标系转换算法把WGS-84转成GCJ-02再传入。反过来如果坐标是从高德地图上直接取的那本身就是GCJ-02直接传就行。我一般在Data模型里加一个坐标系标记字段防止不同来源的坐标混在一个文件里。4.4 Excel打开CSV中文乱码缺一个BOM头现象Java写出的CSV文件用记事本打开正常用Excel打开中文全部变成乱码数字和英文正常。原因Java默认以UTF-8无BOM格式写出CSVExcel在Windows环境下默认用ANSI编码解析CSV没有BOM头就识别不了UTF-8。这是跨平台文件处理的经典问题和具体工具无关。解决写文件时在输出流最前面手动写入一个UTF-8 BOM标记也就是三个字节 0xEF 0xBB 0xBF。OutputStream os new FileOutputStream(outputFile); os.write(0xEF); os.write(0xBB); os.write(0xBF); // UTF-8 BOM Writer writer new OutputStreamWriter(os, StandardCharsets.UTF_8);写完BOM之后再用OutputStreamWriter写内容Excel打开就是正常中文。这个细节不贵但没加之前每次导出都要在Excel里折腾一次导入编码做过一次就记住了。4.5 distance返回0或结果为空坐标串了还是道路不可达现象某些点对算出来距离是0但看坐标明显不可能是同一个位置还有的返回结果里直接少了某个起点。原因第一种情况是经纬度填反了比如把“纬度,经度”当成“经度,纬度”传进去坐标落到海里的概率极高高德返回0或不可达第二种情况是type1的驾车模式要求两点之间存在可通行的道路如果目的地在小岛或封闭区域内接口可能不返回结果。解决先在CSV里随机抽几个点对用高德地图网页版手动搜一下确认坐标没有填反。其次在代码里对distance为0的点对做标记单独导出名单人工复核。我实际处理过一批仓库坐标查出来三条记录是手填Excel时把经纬度两列填反了数据质量问题在源头工具本身没毛病。5. 结果可视化与物流落地把CSV矩阵变成调度决策依据5.1 结果表列设计给Excel透视留口子距离矩阵算完直接面对的问题是这一大堆数字怎么用。原始的“起点,终点,距离”三列当然能看但要做决策需要更细的维度。我一般会在导出CSV时多算两列直线距离和驾车距离的比值以及按距离分档的区间标记。起点终点驾车距离(米)直线距离(米)绕路系数距离分档虹桥仓静安点14200108001.3110-20km虹桥仓浦东点27800196001.4220-30km绕路系数是驾车距离除以直线距离的值这个系数在城市配送里很有参考意义。同一批点对里系数偏高说明两点之间道路绕行严重要么是河流桥梁阻隔要么是单行道密集。距离分档则是给后续成本估算用的按区间设置不同的单公里成本。CSV导出后Excel透视表能快速给出每行每列的平均距离和总距离。物流调度的典型问题是“从哪个仓出发覆盖周边所有点最划算”这个透视表就能回答对每个仓库所在行求平均距离平均值最小的那个就是相对最优的辐射点。5.2 三种可视化热力表、聚类散点、距离直方图结果可视化展示这个功能项目里的做法是先把CSV结果整理成标准格式再丢给可视化工具画图。不需要在Java里做大屏系统三张图就能支撑大部分决策。第一张是热力矩阵。把N个点的两两距离映射成颜色深浅N乘N的表格直接以热力图形式展现深色表示距离远浅色表示距离近。这张图用来发现“离群点”特别直观如果某个点对应的整行都是深色说明它离所有其他点都很远选址上就要单独考虑。把CSV导入常见的数据分析工具用自带的热力图功能就能生成不需要额外开发界面。第二张是聚类散点图。把经纬度直接按坐标画在二维平面叠加距离矩阵的结果不同聚类簇用不同颜色标记。物流划分配送区域时把几十个配送点按距离聚类每个簇就是一个潜在的配送分区再给每个簇分配一个仓库或中转站。这种做法比人工在地图上画圈靠谱得多因为聚类结果是基于真实坐标和距离算出来的不是拍脑袋。第三张是距离直方图。把所有点对的距离值按区间统计数量横轴是距离区间纵轴是点对数量。这张图用来判断整体配送半径是否合理。如果大量点对都集中在长距离区间说明现有网点分布覆盖不足需要考虑增加中转点。反过来如果大量点对集中在近距离说明网点密度过高部分站点可能产能冗余。5.3 边界要清楚距离矩阵不等于路径规划这个工具的目标很明确生成距离矩阵但拿到矩阵之后做什么决定了它能不能真正落地。距离矩阵解决的是“两点之间多远”的问题路径规划解决的是“怎么走最省时间”的问题两者不能混为一谈。高德地图API提供的驾车路径规划接口返回的是经过红绿灯、限行、路况等实时因素优化后的路线。距离矩阵里的数字是静态的适合做网络规划、成本预估、分区决策这类宏观分析。如果需要给司机具体导航路线还是要接路径规划接口让司机在高德地图App里按实时导航走。我在实际物流项目里是把两者串起来用的先用这个工具算距离矩阵完成配送区域划分区域定下来之后再对区域内每个具体配送任务调路径规划接口生成当天的实际送货顺序。矩阵管规划规划管执行各司其职。6. 进阶改造让工具变成可复用的批量计算服务6.1 参数化改造把Key和文件路径提到命令行现在Main.java里的Key和文件路径大概率是写死的常量。要做成可复用服务第一步就是把这两个东西提成启动参数。改造思路是Main方法接收args数组按固定顺序解析输入文件、输出文件、Key。java -cp Distance-master/lib/fastjson-1.2.62.jar:Distance-master/out/production \ Main input.csv output.csv your_key_here改造后配合脚本使用就能实现定时批量计算。比如每天凌晨仓库点位更新后自动重算一次距离矩阵把结果推给调度系统。退出码也建议加上正常结束返回0有批次重试三次仍失败返回1方便外部脚本判断任务是否成功。6.2 抽验习惯用真实地图验证五个随机点对代码跑出来的数据永远要抽样验证。我的方法是先从结果里随机抽5个点对打开高德地图网页版手动搜索两个地点用自带测距工具对比。误差在5%以内说明坐标系、API参数、单位转换都对了如果某个点对偏差特别大优先怀疑坐标顺序和坐标系。这个习惯救过我很多次。最典型的一次是换了一批数据源新数据里纬度在前经度在后和之前的格式反了结果整张矩阵全乱。如果直接拿去给调度系统用配送成本估算会偏得离谱。从那以后我每次换数据源都强制走一遍抽验流程。这个工具本身能用多久不好说但多花十分钟做抽样验证永远不会亏。希望帮到你。本文还有配套的精品资源点击获取
