解决geotools处理矢量数据格式转换NPE异常
1.背景简介
项目中需要实现矢量数据的shp文件转成geojson的需求,计划通过geotools工具来实现。
geotools:GeoTools是一个开源的Java库,用于处理地理空间数据和执行空间分析。它提供了丰富的GIS(地理信息系统)功能和工具,可以处理包括矢量数据、栅格数据、影像数据等不同类型的地理数据,支持空间对象操作、地图投影和坐标转换、空间查询和空间分析等能力。
2.引入依赖
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>29.0</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>29.0</version>
</dependency>
3.代码示例
下面这段代码实现将shp文件转化成geojson格式的文件:
@SneakyThrows
@Test
public void shpToGeo() {
// shp文件
File file = new File("D:\\test\\shp\\city.shp");
Map<String, Object> map = new HashMap<>();
map.put("url", URLs.fileToUrl(file));
DataStore dataStore = DataStoreFinder.getDataStore(map);
String typeName = dataStore.getTypeNames()[0];
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
// geojson文件
File geojsonFile = new File("D:\\test\\shp\\city.geojson");
SimpleFeatureCollection featureCollection = featureSource.getFeatures();
FileOutputStream geoJsonOutputStream = new FileOutputStream(geojsonFile);
// 写入数据
new FeatureJSON().writeFeatureCollection(featureCollection, geoJsonOutputStream);
}
4.报错详情
运行上面的代码进行格式转换,可能会报NPE异常:
5.原因分析
根据异常信息,直接定位到源头(上图红框内),查看一下SystemUtils.isJavaVersionAtLeast的处理逻辑。
下面是SystemUtils.isJavaVersionAtLeast的处理逻辑与代码调用链路:
/**
* 判断java版本是否符合要求
*/
public static boolean isJavaVersionAtLeast(JavaVersion requiredVersion) {
return JAVA_SPECIFICATION_VERSION_AS_ENUM.atLeast(requiredVersion);
}
public boolean atLeast(JavaVersion requiredVersion) {
return this.value >= requiredVersion.value;
}
// java 指定版本
JAVA_SPECIFICATION_VERSION_AS_ENUM = JavaVersion.get(JAVA_SPECIFICATION_VERSION);
public static final String J AVA_SPECIFICATION_VERSION = getSystemProperty("java.specification.version");
// 读取系统属性
private static String getSystemProperty(String property) {
try {
return System.getProperty(property);
} catch (SecurityException var2) {
System.err.println("Caught a SecurityException reading the system property '" + property + "'; the SystemUtils property value will default to null.");
return null;
}
}
首先是判断JAVA_SPECIFICATION_VERSION_AS_ENUM与requiredVersion的值,这里的JAVA_SPECIFICATION_VERSION_AS_ENUM指的是java的指定版本,requiredVersion指的是运行该段代码要求的Java版本。下面看一下这两个值分别来自哪里。
JAVA_SPECIFICATION_VERSION_AS_ENUM是JavaVersion(org.apache.commons.lang3这个包)的一个枚举值:
static JavaVersion get(String nom) {
if ("0.9".equals(nom)) {
return JAVA_0_9;
} else if ("1.1".equals(nom)) {
return JAVA_1_1;
} else if ("1.2".equals(nom)) {
return JAVA_1_2;
} else if ("1.3".equals(nom)) {
return JAVA_1_3;
} else if ("1.4".equals(nom)) {
return JAVA_1_4;
} else if ("1.5".equals(nom)) {
return JAVA_1_5;
} else if ("1.6".equals(nom)) {
return JAVA_1_6;
} else if ("1.7".equals(nom)) {
return JAVA_1_7;
} else if ("1.8".equals(nom)) {
return JAVA_1_8;
} else if ("9".equals(nom)) {
return JAVA_9;
} else if ("10".equals(nom)) {
return JAVA_10;
} else if (nom == null) {
return null;
} else {
float v = toFloatVersion(nom);
if ((double)v - 1.0 < 1.0) {
int firstComma = Math.max(nom.indexOf(46), nom.indexOf(44));
int end = Math.max(nom.length(), nom.indexOf(44, firstComma));
if (Float.parseFloat(nom.substring(firstComma + 1, end)) > 0.9F) {
return JAVA_RECENT;
}
}
return null;
}
}
从上面的代码可以看出对Java版本的判断只到Java10,Java10以后的结果直接返回null(NPE就来自这里)了,猜测原因应该是工程中引入的lang3依赖的版本比较老(3.7),该版本发布时,Java10以后的jdk版本尚未发布。
下面看一下requiredVersion的值,根据报错信息org.geotools.util.NIOUtilities.clean这个方法调用了 org.apache.commons.lang3.SystemUtils.isJavaVersionAtLeast这个方法触发了NPE,下面看一下clean()这个方法的实现逻辑:
public static boolean clean(ByteBuffer buffer) {
if (buffer != null && buffer.isDirect()) {
PrivilegedAction<Boolean> action = SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9) ? () -> {
return (new CleanupAfterJdk8(buffer)).clean();
} : () -> {
return (new CleanupPriorJdk9(buffer)).clean();
};
return (Boolean)AccessController.doPrivileged(action);
} else {
return true;
}
}
clean()这个方法中调用了SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9),也就是说传递给SystemUtils.isJavaVersionAtLeast(JavaVersion requiredVersion)这个方法的requiredVersion参数的值是9。
根据上面的分析,可以在下面这个地方打个断点验证一下:
根据上面的断点信息可以看出requiredVersion9,JAVA_SPECIFICATION_VERSION_AS_ENUMnull。
再在下面这个地方打个断点看一下:
可以看出从系统属性中读取出来的JAVA_SPECIFICATION_VERSION==11,也就是工程指定的jdk版本,然后从下面这段代码读取JAVA_SPECIFICATION_VERSION_AS_ENUM枚举值的时候,返回了null:
static JavaVersion get(String nom) {
if ("0.9".equals(nom)) {
return JAVA_0_9;
} else if ("1.1".equals(nom)) {
return JAVA_1_1;
} else if ("1.2".equals(nom)) {
return JAVA_1_2;
} else if ("1.3".equals(nom)) {
return JAVA_1_3;
} else if ("1.4".equals(nom)) {
return JAVA_1_4;
} else if ("1.5".equals(nom)) {
return JAVA_1_5;
} else if ("1.6".equals(nom)) {
return JAVA_1_6;
} else if ("1.7".equals(nom)) {
return JAVA_1_7;
} else if ("1.8".equals(nom)) {
return JAVA_1_8;
} else if ("9".equals(nom)) {
return JAVA_9;
} else if ("10".equals(nom)) {
return JAVA_10;
} else if (nom == null) {
return null;
} else {
float v = toFloatVersion(nom);
if ((double)v - 1.0 < 1.0) {
int firstComma = Math.max(nom.indexOf(46), nom.indexOf(44));
int end = Math.max(nom.length(), nom.indexOf(44, firstComma));
if (Float.parseFloat(nom.substring(firstComma + 1, end)) > 0.9F) {
return JAVA_RECENT;
}
}
return null;
}
}
6.解决方式
上面根据代码调用链已经分析出了导致NPE的原因,原因在于org.apache.commons.lang3.JavaVersion的get()方法返回了null,上面也猜测了原因,应该是工程中引入的lang3依赖的版本比较老(3.7版本),该版本发布时,Java10以后的jdk版本尚未发布。那么沿着这个思路,升级commons-lang3的版本,将commons-lang3升级至3.12.0
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
升级版本后,查看org.apache.commons.lang3.JavaVersion的get()方法:
static JavaVersion get(String versionStr) {
if (versionStr == null) {
return null;
} else {
switch (versionStr) {
case "0.9":
return JAVA_0_9;
case "1.1":
return JAVA_1_1;
case "1.2":
return JAVA_1_2;
case "1.3":
return JAVA_1_3;
case "1.4":
return JAVA_1_4;
case "1.5":
return JAVA_1_5;
case "1.6":
return JAVA_1_6;
case "1.7":
return JAVA_1_7;
case "1.8":
return JAVA_1_8;
case "9":
return JAVA_9;
case "10":
return JAVA_10;
case "11":
return JAVA_11;
case "12":
return JAVA_12;
case "13":
return JAVA_13;
case "14":
return JAVA_14;
case "15":
return JAVA_15;
case "16":
return JAVA_16;
case "17":
return JAVA_17;
default:
float v = toFloatVersion(versionStr);
if ((double)v - 1.0 < 1.0) {
int firstComma = Math.max(versionStr.indexOf(46), versionStr.indexOf(44));
int end = Math.max(versionStr.length(), versionStr.indexOf(44, firstComma));
if (Float.parseFloat(versionStr.substring(firstComma + 1, end)) > 0.9F) {
return JAVA_RECENT;
}
} else if (v > 10.0F) {
return JAVA_RECENT;
}
return null;
}
}
}
从上面代码可以看出升级版本后的代码能够正常判断jdk17及以前的java版本。