问题引入
MeteoInfoLab可以通过写脚本调用天擎MUSIC Java库来下载数据,详见此公众号的文章使用MeteoInfoLab从天擎气象大数据云平台下载数据。最近将MeteoInfo升级到了4.1.x版本,其中用于读取数据文件的netCDF Java库也升级到了5.9.0版本。有网友指出之前能从天擎下载数据的脚本在MeteoInfo新版本中报错无法下载数据,通过错误信息发现是protobuf Java库版本冲突引起的。netCDF Java库依赖的protobuf版本是4.31.1,而MUSIC依赖的protobuf版本是3.0.0-beta-4。高版本的protobuf库在类加载路径中,MeteoInfoLab启动时会自动加载,在运行MUSIC功能时自然就依赖此高版本protobuf库了,问题是MUSIC需要用到的GeneratedMessage类的makeExtensionsImmutable()方法在高版本protobuf库中因为安全原因被移除了,因此会报错NoSuchMethodError。
尝试了用低版本protobuf库取代高版本,天擎数据下载的问题解决了,但netCDF等数据文件读取又会报错,需要考虑其他解决方案。
隔离类加载
AI时代必须先问问豆包、DeepSeek等,可以用Jython写一个隔离类加载的代码,在天擎数据下载代码中只从MUSIC的依赖库中加载相关类,从而解决依赖库版本冲突的问题。
类加载器隔离是解决Java中依赖冲突的常用方法。它通过创建独立的类加载器来加载特定版本的库,从而避免与父类加载器中的版本冲突。在Jython中,我们可以利用Java的类加载机制来实现这一点。
Java类加载器遵循双亲委派模型,即当一个类加载器需要加载类时,它首先会委托给父类加载器尝试加载,只有在父类加载器无法加载时,自己才尝试加载。通过打破双亲委派模型(不委托父加载器)或者使用独立的类加载器(不委托给系统类加载器),我们可以实现类隔离。
创建隔离的类加载器
from java.io import File
from java.net import URL, URLClassLoader
from java.lang import ClassLoader, Thread
class IsolatedClassLoader:
def __init__(self, jar_paths):
"""Create isolated class loader"""
self.jar_urls = []
for jar_path in jar_paths:
file = File(jar_path)
if file.exists():
self.jar_urls.append(file.toURI().toURL())
# Create a new class loader, no use parent class loader (full isolated)
self.class_loader = URLClassLoader(self.jar_urls, None)
def load_class(self, class_name):
"""Load class"""
return self.class_loader.loadClass(class_name)
def create_instance(self, class_name, *args):
"""Create a class instance"""
clazz = self.load_class(class_name)
constructor = clazz.getConstructor(*[arg.getClass() for arg in args])
return constructor.newInstance(args)
使用隔离类加载器
使用MUSIC的Java依赖库(包括低版本protobuf库)构造隔离类加载器,并从中加载数据下载所需的类:
# load MUSIC java libaries
lib_path = "D:/Working/data/music/music-sdk-java-v2.0"
lib_fns = []
for fn in os.listdir(lib_path):
if fn.endswith(".jar"):
if fn not in sys.path:
lib_fns.append(lib_path + "/" + fn)
isolated_loader = IsolatedClassLoader(lib_fns)
# import classes
retFilesInfo = isolated_loader.create_instance("cma.music.RetFilesInfo")
client = isolated_loader.create_instance("cma.music.client.DataQueryClient")
然后就可以利用client和retFilesInfo对象进行数据下载了。
完整代码
from java.io import File
from java.net import URL, URLClassLoader
from java.lang import ClassLoader, Thread
class IsolatedClassLoader:
def __init__(self, jar_paths):
"""Create isolated class loader"""
self.jar_urls = []
for jar_path in jar_paths:
file = File(jar_path)
if file.exists():
self.jar_urls.append(file.toURI().toURL())
# Create a new class loader, no use parent class loader (full isolated)
self.class_loader = URLClassLoader(self.jar_urls, None)
def load_class(self, class_name):
"""Load class"""
return self.class_loader.loadClass(class_name)
def create_instance(self, class_name, *args):
"""Create a class instance"""
clazz = self.load_class(class_name)
constructor = clazz.getConstructor(*[arg.getClass() for arg in args])
return constructor.newInstance(args)
# load MUSIC java libaries
lib_path = "D:/Working/data/music/music-sdk-java-v2.0"
lib_fns = []
for fn in os.listdir(lib_path):
if fn.endswith(".jar"):
if fn not in sys.path:
lib_fns.append(lib_path + "/" + fn)
isolated_loader = IsolatedClassLoader(lib_fns)
# import classes
retFilesInfo = isolated_loader.create_instance("cma.music.RetFilesInfo")
client = isolated_loader.create_instance("cma.music.client.DataQueryClient")
from java.util import HashMap
# set user and password
userId = "******"
pwd = "******"
# set interface ID
interfaceId = "getSurfEleByTime"
# set parameters
params = HashMap()
params.put("dataCode", "SURF_CHN_MUL_HOR")
# 检索要素:站号、站名、经度、纬度、高度、小时降水、气压、相对湿度、能见度、2分钟平均风速、2分钟风向
params.put("elements", "Station_ID_C,Lat,Lon,Alti,PRE_1h,PRS,RHU,VIS,WIN_S_Avg_2mi,WIN_D_Avg_2mi,Q_PRS")
st = datetime.datetime(2022,11,30,0)
tstr = st.strftime('%Y%m%d%H0000')
params.put("times", tstr)
params.put("orderby", "Station_ID_C:ASC")
# File data format
dataFormat = "CSV"
savePath = "D:/Temp/test/test_{}.csv".format(tstr)
print(savePath)
# call interface
client.initResources()
rst = client.callAPI_to_saveAsFile(userId, pwd, interfaceId, params, dataFormat, savePath, retFilesInfo)
if rst == 0:
print('Download success!')
else:
print('Download failed!')
print('return code: {}'.format(rst))
print('error message: {}'.format(retFilesInfo.request.errorMessage))
# release resources
client.destroyResources()
通过此问题的解决重温了Java类加载的流程,也再次体会了DeepSeek的强大。

