
简介一款面向物流与地理信息处理场景的批量地理位置距离计算工具基于高德地图服务接口构建支持批量地址输入、距离矩阵计算、数据表导入导出、多线程并发处理与结果可视化解决大量地址数据高效求距及配送路线优化问题。资源压缩包共16个文件、约695KB含Java源码.java、配置文件.xml、项目结构.iml、编译产物.class、依赖库.jar及说明文档.md/.docx/.txt其中Distance-master目录方便二次开发附赠资源与说明文件提供详细指导。目前已有178人学习下载适合物流从业者、地理信息开发人员及数据分析学习者参考。凭借高德地图精准定位与路线规划能力工具可显著缩短批量计算耗时电子表格友好导入导出便于数据管理可视化界面直观展示地点间距离优化配送路径提升调度效率。1. 批量距离计算不是把地址扔进for循环那么简单做物流调度或者供应链分析的人大概率经历过这种场景手里拿着几百个客户地址需要算出任意两个点之间的距离来排配送路线。一个一个在高德地图网页里复制粘贴查询几万个组合能查到怀疑人生。这份资源的价值就在于把这条手工链路整条自动化地址批量进、距离矩阵出中间的地理编码、往返距离计算、多线程加速、结果导出全部封装好。它适合三类人需要定期做配送路径预规划的物流运营、做网点覆盖分析的供应链从业者以及想摸清高德Web服务API批量化调用套路的开发者。工具本身是基于高德地图API的Java项目下载包里带完整源码和fastjson依赖下面我按自己拆项目的顺序把实现和踩过的坑讲一遍。2. 高德API调用链Key申请到Const、Data两个基础文件的定位2.1 先搞清高德Web服务API里你需要哪几个接口这个工具的核心链路是“地址→坐标→距离”所以它绕不开高德Web服务API的两种接口地理编码和路径规划。地理编码接口负责把“北京市朝阳区XX路XX号”这种文本地址转成经纬度坐标路径规划接口负责把两个坐标之间的实际行驶距离算出来。很多第一次看源码的人会误以为高德有现成的“批量距离矩阵”接口直接把一堆坐标POST过去就返回矩阵。高德Web服务API确实有名为“距离测量”的接口但它只支持单起点对单终点而且返回的是直线距离不是驾车距离。物流场景要的是货车实际能走的里程所以这个工具的真实设计是先用地理编码批量把地址转成坐标再用驾车路径规划接口逐个算距离最后自己拼出距离矩阵。这个理解很关键否则你读Main.java的时候会觉得“怎么调了两个接口是不是多此一举”。高德控制台的Key申请没什么门槛个人开发者直接创建应用就能拿到Web服务Key。注意你创建的Key类型必须是“Web服务”而不是“Web端(JS API)”或“Android/iOS”否则请求会报INVALID_USER_KEY。这个细节后面避坑章还会专门展开很多人第一步就卡在这。2.2 Const.java与Data.java把Key、URL和数据模型钉死打开下载包里的src目录最先该看的是Const.java和Data.java。这两个文件是一个项目的骨架。Const顾名思义管常量我一般会建议在这个文件里集中管理所有跟高德API相关的固定值而不是散落在Main.java各个方法里。public class Const { // 高德地理编码接口地址转坐标 public static final String GEOCODE_URL https://restapi.amap.com/v3/geocode/geo; // 高德驾车路径规划接口坐标对算距离 public static final String DRIVING_URL https://restapi.amap.com/v3/direction/driving; // 高德Web服务Key去控制台申请后替换 public static final String AMAP_KEY 你的高德Web服务Key; // 请求超时时间单位毫秒 public static final int TIMEOUT 5000; // 单次批量地理编码的地址数量上限高德限制最多20个 public static final int BATCH_SIZE 20; }这里的AMAP_KEY是全局唯一的敏感配置建议后续改成从配置文件读取而不是硬编码在Java文件里。BATCH_SIZE设成20不是拍脑袋高德地理编码接口支持批量提交单次请求最多20个地址超过会报错。TIMEOUT设5000毫秒是基于“单请求平均200~500毫秒返回”的经验值后面多线程并发的时候你就能体会到这个值的意义——线程多起来某一个请求挂住会拖垮整批任务。Data.java负责定义单条地址数据的结构它决定了你在整个计算过程中能拿什么字段、往结果里写什么字段。这个工具里的Data类大致长这样public class Data { private String name; // 地点名称比如北京分拨中心 private String address; // 原始地址文本传给高德做地理编码 private double lng; // 地理编码返回的经度 private double lat; // 地理编码返回的纬度 private boolean geocoded; // 是否地理编码成功失败的要做记录 public Data(String name, String address) { this.name name; this.address address; this.geocoded false; } // getter和setter略实际项目里需要补全 }把geocoded作为一个显式标记字段是我比较推荐的做法。批量地理编码时高德对个别地址返回空结果是很常见的事比如地址里带了生僻地名或者门牌号不规范。如果一个地址转坐标失败后面的距离矩阵计算就不能带它玩否则那个点会变成坐标(0,0)算出来的距离完全失真。标记好失败状态最后导出CSV的时候能单独生成一份失败清单方便人工补录后再算。2.3 Main.java主线地址批量转坐标的请求设计与JSON解析把Main.java的主流程简化一下核心是先读CSV拿到地址列表然后分批调用地理编码接口把返回的坐标回填到Data对象里。这个步骤是整个工具的地基坐标错了后面全错。// 批量地理编码每次最多处理Const.BATCH_SIZE个地址 for (int i 0; i dataList.size(); i Const.BATCH_SIZE) { int end Math.min(i Const.BATCH_SIZE, dataList.size()); ListData batch dataList.subList(i, end); // 拼接批量地址高德要求地址之间用|竖线分隔 StringBuilder addrParam new StringBuilder(); for (Data d : batch) { addrParam.append(d.getAddress()).append(|); } // 去除末尾多余的竖线 addrParam.setLength(addrParam.length() - 1); // 构建请求URL使用URLEncoder对参数做编码 String url Const.GEOCODE_URL ?key Const.AMAP_KEY address URLEncoder.encode(addrParam.toString(), UTF-8) city URLEncoder.encode(全国, UTF-8) batchtrueoutputJSON; // 发送HTTP请求并用fastjson解析响应 String response HttpUtil.get(url); JSONObject obj JSON.parseObject(response); if (1.equals(obj.getString(status))) { JSONArray geocodes obj.getJSONArray(geocodes); for (int j 0; j geocodes.size(); j) { JSONObject geo geocodes.getJSONObject(j); String location geo.getString(location); // 返回格式经度,纬度 if (location ! null !location.isEmpty()) { String[] lngLat location.split(,); Data d batch.get(j); d.setLng(Double.parseDouble(lngLat[0])); d.setLat(Double.parseDouble(lngLat[1])); d.setGeocoded(true); } } } else { // status字段不是1说明请求本身失败记录infocode供排查 System.err.println(地理编码失败, infocode obj.getString(infocode) , msg obj.getString(info)); } }这里有几个参数值得细说。city参数传“全国”是一种推荐做法如果不传或者传错城市高德在地址归属地模糊时可能返回空结果。batchtrue必须显式声明否则高德默认按单地址解析“|”分隔的批量格式会被当成一段完整地址返回一个空坐标。outputJSON指定返回格式高德还支持XML用JSON配合fastjson解析最顺手。response字符串拿到之后先判断status是否为“1”。高德的status字段是字符串类型的“1”和“0”不是布尔值或者数字1用equals(1)判断才稳妥。geocodes是个JSONArray顺序和你提交的地址顺序一致所以可以用下标j直接对应batch里的数据。location字段的格式是“经度,纬度”注意是经度在前千万别split之后赋值反了——经纬度颠倒的后果是距离矩阵计算出来全是几百上千公里的离谱结果这种错误不抽检很难发现。下载包里自带的fastjson-1.2.62.jar是阿里开源的JSON解析库直接放在lib目录下导入项目即可。用过fastjson的人都知道它对JSONObject的getString、getJSONArray封装很省事但1.2.62这个版本在解析某些极端嵌套结构时存在已知安全漏洞公告生产环境建议升级到fastjson2或者高版本不过作为本地工具跑数据1.2.62完全可以胜任。3. CSV批量输入与结果导出字段映射、编码问题和文件组织3.1 CSV导入的字段约定与地址清洗这个工具的输入侧是CSV文件所以第一步要把Excel或者数据库导出的地址数据规整成标准CSV。推荐的字段格式是两列name和address第一行是表头表头本身会被程序跳过。name是给地点起个简称比如“北京朝阳站”address是完整地址文本比如“北京市朝阳区建国路93号院”。如果是从Excel导出注意别直接复制粘贴到记事本存成txt——Excel导出CSV最省事的方式是“另存为 → CSV UTF-8”这样表头和数据字段的编码不会乱。地址清洗这一步我觉得是很多使用者最容易忽略的。从Excel导出的地址经常带不可见字符行尾有多余的换行符、地址中间混入全角空格、还有粘贴时带上的制表符。高德地理编码对这类字符的容忍度有限地址里多个空格可能就导致匹配不到。常见做法是先用正则把所有空白字符统一替换成普通空格再trim掉首尾。public static String cleanAddress(String raw) { if (raw null) return ; // 把全角空格、制表符、换行符统一替换为半角空格 String cleaned raw.replaceAll([\\s\\u3000], ).trim(); // 如果清洗后为空多半是原始数据本身就是空白行 return cleaned; }清洗逻辑里\u3000是Unicode全角空格的编码Excel里很多地址输入法切换时混入全角空格这种字符在高德接口里会被当成非法字符。replaceAll用正则一次处理掉所有空白类字符比逐个字符判断高效得多。清洗完了之后建议做一次抽样随机挑10个地址去高德网页版地图搜索框里验证一下能不能搜到如果网页都搜不到那API返回空结果也是正常的。3.2 结果矩阵的CSV导出UTF-8 BOM和Excel乱码的关系输入侧搞定之后输出侧同样有坑。计算完的距离矩阵如果要给别人用大概率是导出CSV再用Excel打开。这里有一个非常经典的编码陷阱Java里直接写FileWriter默认用平台编码Windows下是GBKLinux下是UTF-8。如果你在Linux服务器上跑完导出CSV拿回Windows的Excel打开中文列名全是乱码。// 输出距离矩阵CSV带上UTF-8 BOM头Excel才能正确识别 FileOutputStream fos new FileOutputStream(distance_matrix.csv); fos.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}); // BOM头 OutputStreamWriter osw new OutputStreamWriter(fos, UTF-8); BufferedWriter bw new BufferedWriter(osw); // 第一行表头第一个单元格留空后面依次是各地点名称 bw.write(,); for (Data d : dataList) { bw.write(d.getName() ,); } bw.newLine(); // 每一行行首是地点名后面是到其他地点的距离单位米 for (int i 0; i distanceMatrix.length; i) { bw.write(dataList.get(i).getName() ,); for (int j 0; j distanceMatrix[i].length; j) { // 同一地点到自身的距离写0其余写实际计算值 bw.write(String.format(%.0f, distanceMatrix[i][j]) ,); } bw.newLine(); } bw.close();BOM头是微软的私有约定但已经成了Excel识别UTF-8的实际标准。那三个字节0xEF 0xBB 0xBF写在文件开头Excel看到就知道这是UTF-8编码中文表头才不会乱码。如果你用记事本打开CSV可能会看到开头多了个“ ”字符那是BOM头的正常显示Excel不会展示它。这里还有个细节是距离值用%.0f格式化把小数位去掉—物流场景展示到米级别就够了带小数点反而让CSV在Excel里被识别成浮点数后续做数据透视表时格式不干净。结果可视化展示是很多人忽略的环节其实不需要额外开发什么前端页面。把距离矩阵CSV导入Excel后用“条件格式 → 色阶”就能做出热力矩阵距离近的格子显示绿色距离远的显示红色配送路线规划时一眼就能看出哪些点扎堆、哪些点孤悬在外。下载包的说明文件.txt里也提到了这个思路它对物流调度员来说比看冰冷的数字直观得多。3.3 说明文件与附赠文档的定位下载包里有两个文件名容易被忽略“说明文件.txt”和“附赠资源.docx”。前者是快速上手手册讲了CSV格式要求和Key替换位置适合不想读源码的人先按文档跑通流程后者里面有高德API的请求参数速查表和一些地址数据整理的模板属于辅助性质的内容。我的建议是先把说明文件从头读一遍再动代码它能帮你省掉至少半小时的摸索时间。4. 多线程并发距离矩阵任务拆分、线程池与fastjson解析4.1 距离矩阵的对称性任务量直接减半进入真正耗时的环节——两两距离计算。如果有个点暴力算所有点对的距离需要算n×n对组合。但实际上距离矩阵是对称的点A到点B的距离和点B到点A的距离一样对角线上的点自己到自己距离为0。这样一来实际需要计算的组合数是n×(n-1)/2直接少了一半。这个优化在n50时能省掉1250次API调用n越大省得越多。// 生成所有待计算的点对组合利用对称性只算上三角 Listint[] pairs new ArrayList(); for (int i 0; i n; i) { for (int j i 1; j n; j) { pairs.add(new int[]{i, j}); } }这里的pairs列表保存的是两个点在dataList里的下标。后面多线程计算时每个线程从pairs里领取一个组合调用高德驾车路径规划接口拿到距离再同时写回矩阵的上三角和下三角保证矩阵对称。如果你不利用对称性不仅多花一倍时间还可能因为并发请求翻倍提前触发高德的限流。4.2 线程池与CountDownLatch控制并发度和等待所有任务完成多线程并发处理的实现有不少细节要抠。Java里实现并发最直接的手段是Executors和线程池但不能无脑开几十个线程同时打高德接口因为高德对单个Key的并发连接数有限制。个人开发者默认每秒允许的并发请求数是有限额的具体数值以控制台展示为准——我自己的经验是单Key开8个线程比较稳再多就会出现偶发的超时。// 固定8线程的线程池加上CountDownLatch等待全部完成 int n dataList.size(); double[][] distanceMatrix new double[n][n]; ExecutorService pool Executors.newFixedThreadPool(8); CountDownLatch latch new CountDownLatch(pairs.size()); for (int[] pair : pairs) { pool.execute(() - { try { int i pair[0]; int j pair[1]; double dist callDrivingApi(dataList.get(i), dataList.get(j)); // 同时写入上三角和下三角保持矩阵对称 distanceMatrix[i][j] dist; distanceMatrix[j][i] dist; } catch (Exception e) { // 单个点位失败不能拖垮整体记为-1并打印日志 distanceMatrix[pair[0]][pair[1]] -1; distanceMatrix[pair[1]][pair[0]] -1; System.err.println(距离计算失败: e.getMessage()); } finally { latch.countDown(); } }); } latch.await(); // 主线程等待所有子任务结束 pool.shutdown();CountDownLatch的作用是让主线程在提交完所有任务之后阻塞等待直到每个任务都执行完countDown也就是所有点对都算完再继续后续的CSV导出。不用它的话主线程可能跑到导出结果时后面还有一堆线程在写距离矩阵导出的CSV里出现一堆0或者null。try-catch-finally的结构很关键任何一个点对的高德请求失败不能让异常中断整个程序而是记录成-1交给后续处理。并发写同一个distanceMatrix数组是安全的因为每个任务只写自己负责的那两个下标位置不同任务之间没有重叠。如果有人在并发环境下用List来存结果反而会有线程安全问题数组在这里比集合更合适。多线程的调试有个特点本地跑正常、服务器上跑全是问题大概率是线程安全和资源竞争后面避坑章再展开。4.3 callDrivingApi的核心逻辑与fastjson返回值解析每个点对的距离计算都封装在callDrivingApi方法里它负责拼参数、发请求、解析返回。高德驾车路径规划接口的核心参数是origin和destination格式都是“经度,纬度”注意这里仍然是经度在前。另外还可以用extensionsbase来只拿基础字段返回体更小解析更快。private static double callDrivingApi(Data from, Data to) throws Exception { String origin from.getLng() , from.getLat(); String destination to.getLng() , to.getLat(); String url Const.DRIVING_URL ?key Const.AMAP_KEY origin URLEncoder.encode(origin, UTF-8) destination URLEncoder.encode(destination, UTF-8) extensionsbase strategy0 outputJSON; String response HttpUtil.get(url); JSONObject obj JSON.parseObject(response); if (!1.equals(obj.getString(status))) { throw new RuntimeException(高德接口返回错误, infocode obj.getString(infocode) , info obj.getString(info)); } // path数组里只有一个元素取第一个就够 JSONObject path obj.getJSONObject(route).getJSONArray(paths).getJSONObject(0); // distance字段单位是米 return path.getDoubleValue(distance); }strategy0是高德推荐的默认策略表示速度优先综合路况后推荐最快路线。做物流距离估算用这个策略比较合理它接近一个熟悉路况的司机实际会走的路线。如果算的是货车配送可以考虑strategy10或者优先高速等策略但要注意不同策略算出来的距离可能差5%~15%同一个项目里必须固定一种策略否则距离矩阵内部就乱套了。返回体里route.paths是个JSONArray正常情况下只有一个元素取第一个就行。distance字段单位是米不是公里导出CSV的时候别忘了这个单位。下载包里Main.java有一段是直接读取这些数据然后启动线程池的整体流程就是你看到的这样先地理编码再并发算距离最后导出。我个人跑过一个100个地址的矩阵总计算量是4950次驾车路径规划8线程并发下大概12分钟左右跑完。如果换成单线程串行差不多要1个小时。这就是这个工具把多线程并发处理作为核心卖点的原因。5. 距离计算避坑五个最容易翻车的位置与排查方法5.1 现象跑了几百个请求后突然大量报错CU批量计算进行到一半控制台突然刷出一片错误infocode是CU开头的。这是因为高德对单Key有并发和每日配额的双重限制。每日配额一般够用但每秒并发限制很容易被打满。8个线程持续请求单Key的每秒并发数超限后高德会暂时拒绝后续请求。解决方案是控制并发度和频率。把线程数从8降到4或者在线程里加一个20毫秒的Sleep控制请求节奏都能让整体速度降到高德许可范围内。另外高德控制台可以查看当日配额使用量跑之前瞄一眼还剩多少避免跑了一半到晚间才发现配额不足。这个工具本身跑的是个人Key如果业务量真的很大建议申请企业认证配额会上一个台阶。5.2 现象工控机上跑出来的CSVExcel打开全是乱码前面章节提过UTF-8 BOM的问题这里再明确一下排查方法。如果你打开CSV看到的是锟斤拷之类的字符大概率是文件被写成了UTF-8无BOM格式。Windows的Excel默认按ANSI(即GBK)解析无BOM的UTF-8文件中文必然乱码。解决方式只有一条路写文件时手动加BOM头。下载包里这份工具已经处理好了但如果你自己改写了导出逻辑务必保留那三个字节的BOM写入。另外如果是在Linux服务器上跑完再用WinSCP下载CSV传输模式建议选二进制而不是文本模式文本模式可能二次转码把UTF-8破坏掉。5.3 现象个别地址地理编码返回的坐标是0.000000高德地理编码对地址的规范性要求比大家想的要高。地址文本里如果混入了“附近”“旁边”这类模糊方位词或者门牌号格式不对高德返回的geocodes数组里对应位置就是空的。如果你在代码里直接把空值转成0.0这个地址就变成了非洲西海岸的坐标算出的距离动辄上万公里。解决方式是在地理编码阶段就拦截失败项Data.java里的geocoded字段就是干这个的。每一批地址解析完后遍历检查geocodes的长度和location字段是否为空空的直接标记失败并写入单独的错误清单CSV后续人工处理。千万不要把location为空的情况当成经纬度“0,0”继续算那会让整个距离矩阵失真。5.4 现象多线程跑起来后偶发算出的距离一会儿对一会儿离谱这是并发环境下的经典问题。如果你在多个线程里共享了同一个非线程安全的对象——比如SimpleDateFormat实例或者同一个Apache HttpClient实例——并发高的时候对象内部状态被多个线程同时修改返回值就会错乱。特别是SimpleDateFormat它的parse和format方法内部有共享的Calendar状态线程不安全是出了名的。解决方式是每个线程内创建自己的实例或者改成Java 8的DateTimeFormatterThreadLocal包装也行。另外如果你用了HttpClient连接池注意连接池本身的线程安全性一般来说Apache HttpClient 4.x的CloseableHttpClient是线程安全的但别自己创建PoolingHttpClientConnectionManager之后又手动关闭连接那会导致连接被并发释放请求突然失败。5.5 现象本地运行正常换台电脑报INVALID_USER_KEY这个是最容易被人忽略的Key配置问题。高德Web服务API的Key在控制台创建时有一个“白名单”设置限制哪些IP或域名可以调用。如果你本地开发时把白名单设成了localhost换个IP的服务器跑Key就失效了。解决方案是在高德控制台把该Key的调用限制改成“无白名单”或者把当前运行环境的出口IP加入白名单。注意公司网络的出口IP经常是动态的今天能跑明天可能又失效。用“无白名单”模式最省心代价是Key泄露后任何人都能调你的配额所以Key本身别提交到公开仓库里。排查这些问题时我一般会在Main.java的入口加一个自检模式拿两个已知地址先单线程算一次距离把高德返回的原始JSON打出来看看。如果这一步就报错后面批量和多线程都不用跑了优先解决Key、网络和参数问题再继续。下载包源码里没有这个自检开关但加起来也就20行代码我强烈建议跑正式数据前先做这一步。6. 用球面距离公式给高德结果做交叉验证距离矩阵算完不是直接就能交差的至少得验证一下结果靠不靠谱。最有效的验证手段是拿球面距离公式算一遍任意两点间的直线距离然后和高德返回的驾车距离对比。理论上同一对点的驾车距离一定大于等于直线距离而且绕路系数通常在1.3到2.5之间——物流干线运输可能接近1.3城市配送可能到2.0以上。如果某个点对的驾车距离比直线距离还小10%以上那这个数据一定有问题。// 球面距离计算输入两点的经纬度输出直线距离米 public static double sphereDistance(double lng1, double lat1, double lng2, double lat2) { double R 6371000; // 地球平均半径单位米 double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double a radLat1 - radLat2; double b Math.toRadians(lng1) - Math.toRadians(lng2); double s 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2))); return R * s; }这个公式是Haversine公式的标准实现6371000米是常用的地球平均半径近似值。验证逻辑不复杂随机抽30个点对把高德驾车距离和这个直线距离放在一起算一个比值。如果比值小于1说明要么驾车距离算错了要么坐标用错了。如果比值普遍大于3可能高德返回的是绕行严重的路线或者地址本身就在山区、水系附近实际绕路确实夸张。这两种情况都值得回查一下原始数据。坐标系的坑在这个环节也会暴露。高德地图用的GCJ-02坐标系和GPS设备直接输出的WGS-84坐标系有大约300到500米的偏移。如果有人在CSV里提前用GPS测好的坐标替代了地理编码结果拿WGS-84坐标去调用高德驾车路径规划API距离偏差会体现在每条路线上但肉眼从单个数值又看不出来。验证阶段如果发现高德驾车距离和直线距离的比值整体偏大就可以怀疑坐标系混用了。最常见的修正方式是把WGS-84坐标做一次火星坐标转换再交给高德但最好还是让高德自己地理编码避免混用坐标体系的麻烦。从那以后我每次跑完距离矩阵都会写个小脚本自动抽30%的点对做这个交叉验证确认比值分布落在1.3到2.5之间才敢把CSV发给调度部门。多花这几分钟比返工重跑一个小时的矩阵划算得多。这个习惯是我第一次把算好的距离矩阵交给仓库那边、结果对方反馈“有两对点距离明显不对”之后养成的——距离矩阵这种黑匣子不验证真不知道里面藏着什么。希望这个验证手法也能帮你在用这套工具时省掉一次返工。本文还有配套的精品资源点击获取