V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
matepi
V2EX  ›  Java

Java 有否 在不预配置类路径类名的情况下,运行态动态装载类及其依赖类的方法?

  •  
  •   matepi · 2023-02-06 14:32:45 +08:00 · 1925 次点击
    这是一个创建于 655 天前的主题,其中的信息可能已经有所发展或是发生改变。

    需求:运行态、不重启,动态装载本地 /远程类。

    SPI 带类路径类名配置的比较容易做到。OSGi 是不是也类似的搞法?

    但如果要求不配置的情况下,此时如果只是装载单个无依赖类(严格说是依赖均已存在的类),也比较容易做到。如果要进一步装载这个无依赖子类的父类啥的,想办法搞搞也能做到。

    但难的就是,如何把这个类的所有依赖也装载进来。

    想来想去,想到两个办法:

    1 、要么就是装载类里面自己先搞个成员,自声明需要依赖的类(但这个就要保证对所有的依赖、依赖的依赖……都要声明出来、挺难保证的);

    2 、还有一种就是给个保证 100%覆盖率的对应 Tester 方法或者类内方法,预先加载 Tester 方法,循环处理掉每个 ClassNotFound 异常,做对应依赖的装载(但这个要保证 100%覆盖率、甚至是依赖类的覆盖率也要做到,也是挺容易出问题的)

    第 1 条附言  ·  2023-02-07 09:42:41 +08:00
    更新一下这个问题的目前解决思路、非最优:

    妥协在非最优的先实现了的原因——
    如果在 jar 包内部通过 Class.forName 、或者自定义 ClassLoader 方法去装载类,这样的类装载肯定是要到运行态才能发现的——因此配置不可避免,和大家说的一样。

    那么即便是非最优、但提的上 [有点优] ,那就还是要搞点事情的:
    1 、可只配置依赖 jar 包文件路径,而不必明确每个需要依赖的类名
    2 、当出现依赖不完整时,即出现 ClassNotFound/NoSuchMethodException 异常时,自动尝试寻找更进一步的企业内框架 jar 库路径自动解决依赖,作为托底,并对应 warning log 提示动态装载类开发者明确依赖(这里有点安全性问题、企业内应该还好)

    先阅读背景材料:
    https://stackoverflow.com/questions/60764/how-to-load-jar-files-dynamically-at-runtime
    然后跟着写了一个代码,没有发现所说的 java<=8 和>=9 两者存在区别,用 URLClassLoader 足以解决问题

    [但 这 里 有 个 大 大 大 坑]
    URLClassLoader 是一个实现 Closeable 的 resource ,IDE 会提示你 close ,否则一直 warning
    如果你 close 了,那么恭[进]喜[坑]

    例:
    你在 A.class 里面动态加载 B.class ,在需调用计算的 B.someCaller 里面,引用了一个 C.jar 里面的 D.class 的 D.someCallee

    伪码:
    myClsLoader = new URLClassLoader(someClsPathURL, someDefaultLoader)
    Class<?> bClass = myClsLoader.loadClass(BclassName)
    myClsLoader.close()
    return bClass;
    —— bClass 返回的很好,close 正常,啥错都没有。然后再进一步调用
    bInstance = bClass.newInstance()
    ——也没问题,但
    bInstance.someCaller()
    ——此时,就会报错 ClassNotFound: D.class
    摸不着头脑了吧,一开始还以为是 java<=8 和>=9 的区别,搞了一圈 hack ,结果实际是——

    [这个 URLClassLoader 是在真正的 Call 没有发生前,是不能关闭的;关了就没办法加载进一步 Call 内引用的 C.jar 内依赖 D.class]
    这样 URLClassLoader 的生命周期就要和 Caller 的生命周期一样了,从加载框架层、跨越到逻辑执行层,味道太烂了。不能关。

    那不关是不是就 resource leak 了呢?单纯这么写,当然是 leak 的
    参: https://blog.csdn.net/moneyshi/article/details/81939477 ,但按这文章里面讲的办法关闭估计还是无法加载 D.class 的

    那么怎么解决 leak 呢?
    有不能关闭的 resource ,把它 cache 起来,保证 resource load once and only once ,这样不就行了嘛。同时也能 cache 各种 loader 过程和反射方法等等,一举多得。

    但是进一步的又有点 [担心] :
    a ,被 cache 的 URLClassLoader 是否会在对象回收时,被 JVM 主动释放,造成 cache stale 现象
    b ,毕竟是个外部 resource ,是否会影响 JVM 的优雅停止

    因此在 [有点优] 的事情 2 上,本身就是要 try 包住 caller 过程的,其中考虑用户依赖不完整时,同时也处理掉 staled-cache ,重建 URLClassLoader 重新 cache 。不过担心 b 还是存在啊

    这样一路实现下来,感觉 [有点优] 里不优雅的事情还是很多啊……大家有什么更好的设计思路?
    16 条回复    2023-02-14 10:46:22 +08:00
    assiadamo
        1
    assiadamo  
       2023-02-06 14:35:52 +08:00
    agent + instrument
    matepi
        2
    matepi  
    OP
       2023-02-06 14:59:58 +08:00
    @assiadamo 你说的应该是 instrumentation.appendToSystemClassLoaderSearch 之类的吧?这个也就是 premain 一次性的。进一步的还是要解决问题 1 吧?
    assiadamo
        3
    assiadamo  
       2023-02-06 15:08:32 +08:00
    嗯,这只是临时热更解决问题,之后还是需要带上改动出版本更新维护的
    assiadamo
        4
    assiadamo  
       2023-02-06 15:10:59 +08:00
    运行时可以通过 rmi 或 jmx ,去调用 instrumentation 的 api 去替换 class 的字节码
    matepi
        5
    matepi  
    OP
       2023-02-06 15:31:42 +08:00
    @assiadamo 我的需求不是临时热更新处理一些临时问题。而是功能特性上,在一些处理本地文件分析的节点、在设计上,就要能够动态加载 节点使用者所提交的类。由于处理文件量很大,设计上必须是把处理逻辑分发到每个节点上,而不是把文件整体提交到专有的处理节点。
    registerrr
        6
    registerrr  
       2023-02-06 15:42:09 +08:00
    @matepi 按你这描述,不如让节点使用者单独起一个服务并保证其可用性,让你的服务去调用它,而不是把它化成你的一部分。
    aguesuka
        7
    aguesuka  
       2023-02-06 15:46:07 +08:00
    osgi 也是配置 require-bundle 才能实现的. 也就是加载整个模块, 并且在模块的 MANIFEST 里指定依赖.
    janwarlen
        8
    janwarlen  
       2023-02-06 15:55:52 +08:00
    应该不行的吧,java 的类路径相当于 class 的 id 了
    可以尝试将类打入 jre ,然后不用 package ,扁平化存放
    jdk9 的 jlink 可以自定义 jre 的

    不过还不知道有人这么干过,如果成功了踢我一脚
    matepi
        9
    matepi  
    OP
       2023-02-06 16:18:52 +08:00
    @registerrr 上面回复中也提到了:调用的入参是一个大文件,做的功能大文件的数据处理;如果变为远程调用、以服务方式 把大文件整体传输过去,效率上不能行。对大文件此时的设计,需要是分发计算,而不是分发数据。
    2han9wen71an
        10
    2han9wen71an  
       2023-02-06 17:29:17 +08:00
    你可能需要的是 URLClassLoader
    assiadamo
        11
    assiadamo  
       2023-02-06 19:06:49 +08:00 via Android
    为了实现不停机的需求的话,可不可以使用脚本,比如 JavaScript 或 Lua
    pursuer
        12
    pursuer  
       2023-02-06 19:07:57 +08:00
    如果只要求标准 jre 平台,可以重载 classloader 的 findClass ,调用 defineClass 就行。
    如果要 android 平台则需要返回一个由另一个子 classloader 加载的对应类。
    第二种方法我之前开发一个框架的时候写过类似的东西,可以参考一下
    https://github.com/partic2/xplatj/blob/main/commonj/src/main/java/xplatj/javaplat/pursuer/lang/IntegratedClassLoader.java
    aristotll
        13
    aristotll  
       2023-02-06 22:12:08 +08:00
    Groovy 脚本
    ql562482472
        14
    ql562482472  
       2023-02-07 09:35:59 +08:00
    我觉得你可以写自己的类加载器,当被加载时,如果符合某些特征就进行初始化,否则就光加载连接
    nekoneko
        15
    nekoneko  
       2023-02-07 21:07:19 +08:00
    Groovy
    Aresxue
        16
    Aresxue  
       2023-02-14 10:46:22 +08:00
    动态装载的类也是有它的开发环境和过程的,可以考虑在开发过程中把这个类使用的类打包为一个新的 jar ,装载时使用自定义的 classloader 去 load 这个 jar ,至于实际实现的话可以用静态分析( import 的递归和对反射的分析)和动态分析(限定类的指定运行方法,开发时自己 run 一遍将 jvm 中所有类都记下来,将 jdk 和已有依赖的类排除其它的类合并为一个新的 jar ),这东西简单想想还行实际做起来确实挺复杂
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1233 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 23:57 · PVG 07:57 · LAX 15:57 · JFK 18:57
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.