大数跨境
0
0

通过隔离类加载解决依赖库版本冲突的问题 - 天擎数据下载

通过隔离类加载解决依赖库版本冲突的问题 - 天擎数据下载 Amanda跨境运营
2025-10-24
21

问题引入

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的强大。


【声明】内容源于网络
0
0
Amanda跨境运营
跨境分享集 | 每天一点跨境见解
内容 42460
粉丝 3
Amanda跨境运营 跨境分享集 | 每天一点跨境见解
总阅读227.7k
粉丝3
内容42.5k