ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Hazelcast Jet 代码部署增强:`addClass` 嵌套类与 `addPackage` 包级部署机制全解析

2026/10/7 16:15:57 拓冰建站 浏览量
Hazelcast Jet 代码部署增强:`addClass` 嵌套类与 `addPackage` 包级部署机制全解析 缓存KV存储消息队列流处理后端【免费下载链接】hazelcastHazelcast is a unified real-time data platform combining stream processing with a fast data store, allowing customers to act instantly on>项目地址https://gitcode.com/gh_mirrors/ha/hazelcast点击查看免费下载本文围绕 Hazelcast Jet现已并入 Hazelcast 统一实时数据平台自 4.1 版本引入的代码部署增强Code Deployment Improvements展开作业提交时如何把内部类、匿名类乃至整个包递归地注入到 Jet 作业的 classpath 中。文章将以设计文档 docs/design/jet/001-code-deployment-improvements.md 为骨架并结合仓库源码与测试深入讲解JobConfig.addClass()/addPackage()的底层实现、classgraph 库的着色使用方式、资源在集群中的存储与内存开销以及可验证的测试行为。读完本文你将能够正确地为 Jet 作业配置类与包级资源理解“资源以 IMap 存储、主副本 备份副本双份驻留内存”的容量规划要点并能针对常见坑点如 light job 限制、非现有包名行为写出可运行的部署代码。背景为什么需要“嵌套类”与“包级”部署在 Hazelcast Jet 中一个作业Job由 Pipeline 或 DAG 描述作业的代码类、JAR、classpath 资源通过JobConfig提交到集群由 Jet 的专用 classloader 在成员节点上加载执行。早期JobConfig只支持添加单个类、单个 JAR 或单个 classpath 资源用户面临两个典型的部署痛点嵌套类无法整体携带一个业务类往往附带内部类inner class与匿名类anonymous class例如 Kotlin lambda、Runnable匿名实现、EntryProcessor 中常见的匿名内部类。手工逐个addClass()不现实且容易遗漏。包级资源无法批量携带用户希望一次把某个包package下的所有类与资源一次性注入作业 classpath而 Java 标准 API 并不支持“列出某个包下的所有类”。设计文档把目标概括为User should be able to add nested (inner anonymous) classes as well as entire packages to Jet jobs classpath.问题陈述Java 反射与 ClassLoader 的能力边界为什么这个需求在技术上并不平凡设计文档明确指出两个硬性约束Java 反射无法列出匿名类给定一个类标准反射 API 只能通过getDeclaredClasses()拿到声明在源码中的成员类却无法获知该类在方法体内创建的匿名类如new Runnable() {...}、Kotlin lambda 生成的Foo$1之类的类。这些匿名类同样需要被 JVM 加载才能反序列化执行。ClassLoader 不支持列出包内容ClassLoader.getResources()只能按确定的资源名查找无法回答“这个包下有哪些类文件”这类目录列举问题。换句话说仅靠Class.forName 反射无法穷举一个类“实际依赖的所有字节码文件”。解决方案借助 classgraph 扫描包资源设计文档给出的解决思路非常直接既然嵌套类与根类位于同一个包package下那么可以列出该包关联的所有资源 URL再提取并过滤出想要的 class 文件——这一策略同时要处理“列目录、从任意嵌套 JAR 中解压、处理自定义 URL scheme”等场景同样的策略也可递归用于找出一个包内的全部类。与其从零实现一套 classpath 扫描器Jet 选择引入一个轻量级开源库 ——classgraph文档记载体积约 470KBJet 通过 Maven Shade 将其着色打包进自己的发行物中避免与用户 classpath 冲突。classgraph 支持广泛的 classpath 规格机制可以扫描目录、嵌套 JAR、file:/jar:等常见 URL scheme提供包级package与路径级path的接受规则返回类信息ClassInfo及其内部类关系getInnerClasses()返回非 class 资源如包内的package.properties、静态资源文件。在仓库源码中可以看到 Jet 对 classgraph 的实际使用见 ReflectionUtils.javaio.github.classgraph.ClassGraph、ClassInfo、ScanResult的导入与调用。API 变更JobConfig的两个新能力设计文档为JobConfig提出两项 API 扩展两者在 JobConfig.java 中均有完整实现。1. 增强addClass()递归添加嵌套类原文档给出的签名since 4.1Nonnull SuppressWarnings(rawtypes) public JobConfig addClass(Nonnull Class... classes)仓库中的实际实现JobConfig.javaNonnull public JobConfig addClass(Nonnull Class?... classes) { throwIfLocked(); checkNotNull(classes, Classes cannot be null); ResourceConfig.fromClass(classes).forEach(cfg - resourceConfigs.put(cfg.getId(), cfg)); return this; }其语义Javadoc 原文要点递归添加给定类及其所有嵌套内部与匿名类到 Jet 作业 classpath这些类只对挂接在该 Pipeline / DAG 上的代码可见对其他代码不可见——重要示例是IMap数据源它只能实例化 Jet 实例 classpath 中的类即集群侧代码不能用于 light jobJetService#newLightJob(Pipeline)底层存储是默认备份数为 1 的IMap因此添加大文件时需按内存扩容集群每个文件在集群内会有2 份拷贝主副本 备份副本。注意类被加入后返回this以支持 Fluent API 链式调用addClass与addPackage调用前都会先throwIfLocked()——JobConfig一旦被提交locked便不可再修改。2. 新增addPackage()递归添加整个包原文档给出的签名Nonnull public JobConfig addPackage(Nonnull String... packages)仓库中的实际实现JobConfig.javaNonnull public JobConfig addPackage(Nonnull String... packages) { checkNotNull(packages, Packages cannot be null); Resources resources ReflectionUtils.resourcesOf(packages); resources.classes().forEach(classResource - add(classResource.getUrl(), classResource.getId(), CLASS)); resources.nonClasses().forEach(this::addClasspathResource); return this; }语义要点递归添加给定包内的全部类与资源非 class 文件如package.properties到 Jet 作业 classpath包内每个 class 文件以ResourceType.CLASS注册资源 ID 即“包路径/类名.class”非类资源则以addClasspathResource方式注册同样不能用于 light job同样受 IMap 双副本内存开销约束与addClass不同addPackage对传入的包名数组做了checkNotNull但不调用throwIfLocked()——从源码结构看包内资源是在扫描完成后逐条通过私有add(...)注册的。配套 API 一览设计文档提到可参考的addJar与addClasspathResource仓库中 JobConfig.java 提供了一系列重载方法入参形式资源 ID 规则说明addJar(URL)URLURL 最后一段路径filename若 ID 已存在则不替换addJar(File)File文件名同上要求是文件ensureIsFileaddJar(String)路径字符串文件名内部转File再转 URLaddJarsInZip(URL/File/String)ZIP 内 JAR按 JAR 文件名一次添加 ZIP 包内多个 JARaddClasspathResource(URL/File/String)单资源URL/文件名另有带显式id的重载addClass(Class...)类对象包路径/类名.class递归嵌套类addPackage(String...)包名数组各 class/资源的路径递归包内类与资源所有资源的实际存储形态为ResourceConfigResourceConfig.java它是一个实现IdentifiedDataSerializable的配置对象携带URL、可空id与ResourceTypeCLASS/JAR/CLASSPATH_RESOURCE。源码级原理ReflectionUtils如何用 classgraph 干活addClass与addPackage的核心逻辑都汇聚在工具类ReflectionUtilsReflectionUtils.java。nestedClassesOf()找出一个类及其全部嵌套类ResourceConfig.fromClass()的实现ResourceConfig.javapublic static StreamResourceConfig fromClass(Nonnull Class?... classes) { return ReflectionUtils.nestedClassesOf(classes).stream().map(ResourceConfig::new); }nestedClassesOf()ReflectionUtils.java的关键步骤构造ClassGraph并开启.enableClassInfo()与.ignoreClassVisibility()——后者保证即使嵌套类是私有的也能被发现收集传入类的classloader去重后逐个addClassLoader并把每个类所在的包名作为acceptPackages规则传入scan()后先按类名过滤出目标ClassInfo再对其调用getInnerClasses()取出所有内部/匿名类信息最后loadClass()得到真实的Class?对象返回传入类本身 全部嵌套类的合并集合。这一步正是设计文档中“先列出包资源、再过滤目标类文件”策略的实现由于嵌套类与根类同包acceptPackages加上getInnerClasses()的关系遍历即可补全反射无法枚举的匿名类。resourcesOf()递归扫描包内的类与资源addPackage()调用的resourcesOf(String... packages)ReflectionUtils.java把包名com.example.foo转换成路径com/example/foo.replace(., /)ClassGraph同时acceptPackages(packages)与acceptPaths(paths)ignoreClassVisibility()scan()后getAllClasses()得到包内全部类包装成ClassResource集合getAllResources().nonClassFilesOnly()得到包内非 class 资源 URL 集合打包返回Resources含 classes 与 nonClasses 两部分。回到JobConfig.addPackage()classes 以CLASS类型逐条add(...)nonClasses 交给addClasspathResource从而实现对“包内一切内容”的完整携带。测试验证从单测看行为契约设计文档特别提到“In particular created unit test proving Kotlin lambdas (as anonymous classes) are added to the classpath”即专门有单测证明 Kotlin lambda本质是匿名类能被加入 classpath。仓库中的ResourceConfigTestResourceConfigTest.java用四类用例锁定了行为when_addClassWithClass第 62-71 行addClass(ResourceConfigTest.class)后资源 ID 等于ReflectionUtils.toClassResourceId(类)即com/hazelcast/jet/config/ResourceConfigTest.class资源类型为ResourceType.CLASSwhen_addResourcesWithPackage第 73-92 行addPackage(本类所在包名)后资源配置集合中既包含本类的 CLASS 资源也包含package.properties这类 CLASSPATH_RESOURCE——验证“包内类 资源一并携带”when_addResourcesWithNonExistingPackage第 94-103 行对不存在的包名thispackage.does.not.exist调用addPackage返回空集合而不抛异常空操作语义JAR 相关用例第 105-159 行addJar(URL)以 URL 末段为资源 IDURL 无路径段时抛IllegalArgumentException相同资源 ID 重复添加抛异常不静默覆盖。此外JobConfigTest.java 覆盖了addClass/addPackage在JobConfig层面的注册行为JetTest.java 展示了new JobConfig().addClass(JetTest.class)的典型用法部署相关测试如 AbstractDeploymentTest.java、ClientDeployment_StandaloneClusterTest.java则验证了带用户代码部署配置的客户端如何通过addClass把类随作业提交到集群。典型用法示例把上述 API 组合起来一个典型的 Jet 作业配置如下Fluent API 风格JobConfig jobConfig new JobConfig() // 递归携带该类的所有内部类与匿名类含 Kotlin lambda 生成的类 .addClass(MyEntryProcessor.class, MyFunction.class) // 递归携带整个包内的类与资源 .addPackage(com.example.jet.functions, com.example.jet.mappers) // 携带独立 JAR 与任意 classpath 资源 .addJar(/path/to/lib-extra.jar) .addClasspathResource(/path/to/config.properties); Pipeline p Pipeline.create(); p.readFrom(Sources.list(input)) .map(new MyFunction()) // 引用 addClass 加入的类 .writeTo(Sinks.list(output)); JetInstance jet Jet.newJetClient(); jet.newJob(p, jobConfig).join();使用须知来自 Javadoc 与源码行为作用域这些资源只对附着于本作业 Pipeline / DAG 的代码可见IMap数据源等集群侧组件无法使用它们light job 限制addClass/addPackage/addJar/addClasspathResource均不可用于 light job内存规划资源以IMap存储、默认备份数为 1每个资源在集群中驻留2 份主副本 备份副本提交大文件前需按此评估成员内存ID 唯一性JAR 类资源以文件名URL 末段为 ID重复 ID 会抛IllegalArgumentExceptionaddClass/addPackage生成的资源 ID 为包路径形式重复添加同一类/包会覆盖同 ID 条目源码中resourceConfigs.put(cfg.getId(), cfg)空包行为addPackage对不存在的包名表现为空操作不抛异常见测试用例。局限与演进方向设计文档在末尾列出了一项“未来改进”ImprovementsMaybe instead of forcing user to manually add (addClass(),addPackage(),addJar()…) all the required resources to the classpath we could scan the classpath and add them automatically - however we would need to figure out the way to filter out unneeded ones so we don’t end up with bloated job’sIMapstate.即理想方案是自动扫描 classpath 并推断作业所需资源免去用户手工addClass/addPackage/addJar的负担难点在于如何过滤无关类避免作业的IMap状态被无用资源撑爆——这与本文反复强调的内存双副本约束直接相关也是自动部署方案必须解决的核心权衡。从当前仓库源码结构看addClass的递归嵌套类能力与addPackage的包级扫描能力已经完整落地自动扫描仍属于文档层面的设计展望而非已实现功能。总结能力自 4.1 起JobConfig.addClass()递归携带嵌套类含匿名类与 Kotlin lambda新增的addPackage()递归携带包内全部类与资源原理Java 反射无法枚举匿名类、ClassLoader 无法列出包内容Jet 借助轻量级开源库 classgraph 扫描包级资源并过滤目标类相关逻辑集中在 ReflectionUtils.java实现落点JobConfig.java 的addClass/addPackage、ResourceConfig.java 的fromClass行为契约由 ResourceConfigTest.java 等测试锁定类/包/资源 ID 规则、空包空操作、JAR ID 冲突异常运维要点资源经IMap双副本存储需按内存扩容light job 不可用作用域仅限作业自身 Pipeline / DAG。在设计文档 001-code-deployment-improvements.md 之外读者可继续深入 JobConfig.java、ReflectionUtils.java 与 ResourceConfigTest.java 获取一手实现与验证证据。赞分享缓存KV存储消息队列流处理后端【免费下载链接】hazelcastHazelcast is a unified real-time data platform combining stream processing with a fast data store, allowing customers to act instantly on>项目地址https://gitcode.com/gh_mirrors/ha/hazelcast点击查看免费下载相关推荐使用 Helm Chart 在 Kubernetes 上部署 Hazelcast Jet 流处理集群的完整指南使用 Helm Chart 在 Kubernetes 上部署 Hazelcast Jet 流处理集群的完整指南 Hazelcast Jet 是一个构建在 Haz不只是 Loadingeasy-loading-cj Toast 功能实战图标、偏移、尺寸一次讲透不只是 Loadingeasy loading cj Toast 功能实战图标、偏移、尺寸一次讲透 easy loading cj 是一款面向 Cangji缓存KV存储消息队列流处理后端DeepMosaics 预训练模型完全指南模型分类、下载部署与源码级调用机制解析DeepMosaics 预训练模型完全指南模型分类、下载部署与源码级调用机制解析 本篇指南以 DeepMosaics 仓库官方文档 docs/pre trai人工智能深度学习计算机视觉图像处理视频处理上一篇Flet Map 地图控件完整指南在 Python 应用中集成可交互的矢量地图下一篇GSD-2 项目迁移指南从 v1 .planning 平滑迁移到 DB 驱动的 .gsd 格式创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考