【问题标题】:Handling changes in dependent 3rd party libraries处理依赖 3rd 方库中的更改
【发布时间】:2012-02-09 20:45:06
【问题描述】:

我有一个项目,它依赖于几个 3rd 方库,项目本身被打包为一个 jar,并作为一个库分发给其他开发人员。 这些开发人员将依赖项添加到他们的类路径中,并在他们的代码中使用我的库。

最近我遇到了第 3 方依赖项之一的问题,即 apache commons 编解码器库, 问题是这样的:

byte[] arr = "hi".getBytes();
// Codec Version 1.4
Base64.encodeBase64String(arr) == "aGk=\r\n" // this is true

// Codec Version 1.6
Base64.encodeBase64String(arr) == "aGk=" // this is true

正如您所见,该方法的输出随着次要版本的增加而发生了变化。

我的问题是,我不想强​​制我的库的用户使用 3rd 方库的特定次要版本。假设我知道对依赖库的更改,无论如何我可以识别哪个库版本包含在类路径中并做出相应的行为?或者,对于这类场景,什么被认为是最佳做法?

P.S - 我知道对于上面的示例,我可以只使用向后兼容的 new String(Base64.encodeBase64(data, false)),这是一个更普遍的问题。

【问题讨论】:

    标签: java dependencies


    【解决方案1】:

    你问这个问题的“最佳实践”是什么。我将假设您所说的“这个问题”是指第 3 方库升级的问题,特别是这两个问题:

    1. 什么时候应该升级?

    2. 您应该如何保护自己免受不良升级(例如您的示例中提到的 commons-codec 错误)?

    要回答第一个问题,“您应该什么时候升级?”行业中存在许多策略。在大多数商业 Java 世界中,我相信当前的主流做法是“当你准备好升级时应该升级”。换句话说,作为开发人员,您首先需要意识到有一个新版本的库可用(适用于您的每个库!),然后您需要将它集成到您​​的项目中,并且您是制作最终版本的人基于您自己的测试平台的 go/no-go 决定 --- junit、回归、手动测试等......无论您做什么来确保质量。 Maven 通过使最流行的库的多个版本可用于自动下载到您的构建系统中并默认培养这种“固定”传统来促进这种方法(我称之为版本“固定”)。

    但确实存在其他实践,例如,在 Debian Linux 发行版中,理论上可以将大量工作委托给 Debian 软件包维护者。您只需根据 Debian 提供的 4 个级别调整您的舒适度,选择新鲜度而不是风险,反之亦然。 Debian 提供的 4 个级别是:OLDSTABLE、STABLE、TESTING、UNSTABLE。尽管 Unstable 名称如此,但它非常稳定,而且 OLDSTABLE 提供的库与其原始“上游”项目网站上提供的最新最好的版本相比可能已经过时了 3 年之久。

    至于第二个问题,如何保护自己,我认为目前业界的“最佳实践”是双重的:根据声誉选择您的库(Apache 的通常相当不错),并等待一段时间再升级,例如,不要总是急于成为最新最好的。也许选择一个已经发布 3 到 6 个月的库的公开版本,希望自最初发布以来所有严重的错误都已被清除和修补。

    您可以走得更远,通过编写专门保护您在依赖项中依赖的行为的 JUnit 测试。这样,当您关闭较新版本的库时,您的 JUnit 将立即失败,并警告您该问题。但根据我的经验,我没有看到很多人这样做。而且通常很难意识到您所依赖的精确行为。

    而且,顺便说一下,我是 Julius,负责这个错误的人!请接受我对这个问题的歉意。这就是为什么我认为它发生了。我只会为我自己说话。要了解 apache commons-codec 团队中其他人的想法,您必须自己询问他们(例如 ggregory、sebb)。

    1. 当我在 1.4 和 1.5 版本中处理 Base64 时,我非常关注 Base64 的主要问题,即将二进制数据编码为低 127 ASCIi,并将其解码回二进制.

    2. 所以在我看来(这就是我出错的地方)“aGk=\r\n”和“aGk=”之间的区别并不重要。它们都解码为相同的二进制结果!

    3. 但在阅读了您在此处发布的 stackoverflow 帖子后,从更广泛的意义上考虑它,我意识到可能有一个非常流行的用例,我从未考虑过。也就是说,根据数据库中的加密密码表检查密码。在该用例中,您可能会执行以下操作:

    // 一种。将用户的密码存储在数据库中 // 使用加密和盐,最后, // commons-codec-1.4.jar(带有“\r\n”)。 // // b.每次用户登录时,加密他们的 // 密码使用适当的加密算法,加上盐, // 最后使用最新版本的 commons-codec.jar 进行 base64 编码, // 然后检查数据库中的加密密码 // 看是否匹配。

    因此,如果 commons-codec.jar 更改其编码行为,即使根据 base64 规范以非物质方式更改,这个用例当然会失败。非常抱歉!

    我认为,即使我在本文开头阐明了所有“最佳实践”,但仍有很大可能会被搞砸。 Debian 测试已经包含了 commons-codec-1.5,即有 bug 的版本,修复这个 bug 本质上意味着让那些使用 1.5 版而不是 1.4 版的人搞砸。但我会尝试在 apache 网站上放一些文档来警告人们。感谢您在 stack-overflow 上提到它(我对用例的看法是对的吗?)。

    ps。我认为Paul Grime 的解决方案非常简洁,但我怀疑它依赖于在 Jar 的META-INF/MANIFEST.MF 文件中推送版本信息的项目。我认为所有 Apache Java 库都这样做,但其他项目可能不会。不过,这种方法是一种在构建时将自己固定到版本的好方法:与其意识到您依赖“\r\n”并编写防止这种情况发生的 JUnit,您可以编写一个更简单的 JUnit: assertTrue(desiredLibVersion.equals(actualLibVersion)).

    (假设运行时库与构建时库相比没有变化!)

    【讨论】:

    • 这就是为什么我喜欢 SO,你问一个问题,然后让真正编码的人首先做出回应。谢谢!。我喜欢 Debian 的方法,不知道如何使用 Java。 UnitTests 确实发现了这个问题并修复了它,我想知道将来解决这个问题的最佳方法是什么。
    • 哦,我有 2 个用例:1)我将资产(一些字节内容)上传到分布式缓存。我使用带有 Base64 编码数据的 HTTP PUT 请求来执行此操作。从缓存中读回数据时,单元测试失败,因为编码的字符串不同。 2)就像你说的 - 密码 + 随机字节盐 + 散列 + Base64
    • 感谢您的用例!我从来没有想过缓存的,谢谢你提到它。我正在开发一个 Debian-2-Maven 适配器。我也许能在一两个月内推出它(我这几天有点忙,没时间做很多开源工作)......如果它实现了,我会告诉你的。
    【解决方案2】:
    package stackoverflow;
    
    import org.apache.commons.codec.binary.Base64;
    
    public class CodecTest {
        public static void main(String[] args) {
            byte[] arr = "hi".getBytes();
            String s = Base64.encodeBase64String(arr);
            System.out.println("'" + s + "'");
            Package package_ = Package.getPackage("org.apache.commons.codec.binary");
            System.out.println(package_);
            System.out.println("specificationVersion: " + package_.getSpecificationVersion());
            System.out.println("implementationVersion: " + package_.getImplementationVersion());
        }
    }
    

    生成(针对 v1.6):

    'aGk='
    package org.apache.commons.codec.binary, Commons Codec, version 1.6
    specificationVersion: 1.6
    implementationVersion: 1.6
    

    生成(针对 v1.4):

    'aGk=
    '
    package org.apache.commons.codec.binary, Commons Codec, version 1.4
    specificationVersion: 1.4
    implementationVersion: 1.4
    

    所以你可以使用包对象来测试。

    但我会说 API 改变了它的做法有点顽皮。

    编辑这是更改的原因 - https://issues.apache.org/jira/browse/CODEC-99

    【讨论】:

    • 这将适用于特定问题,但是依赖于 jar 的清单是否被认为是合理的解决方案?还是只看方法的输出更好?哦,是的,在次要版本之间更改 API 的输出非常顽皮 :)
    • 只要清单包含正确的信息,这将是一个合理的解决方案。对不起,说的有点明显!但是 Apache 在这方面往往做得很好。有很多时髦的方法来检查是否不是——比如类/方法检测、类/jar 大小、类/jar 签名等。如果你问我,所有这些都会涉及更多,但有时是必要的。用你的应用打包你的依赖是最简单的方法。
    【解决方案3】:

    您可以计算实际类文件的 md5 总和并将其与预期进行比较。可以这样工作:

    String classname = "java.util.Random"; //fill in the your class
    MessageDigest digest = MessageDigest.getInstance("MD5");
    Class test = Class.forName(classname);
    InputStream in = test.getResourceAsStream("/" + classname.replace(".", "/") + ".class");
    byte[] buffer = new byte[8192];
    int read = 0;
    
    while ((read = in.read(buffer)) > 0) {
        digest.update(buffer, 0, read);
    }
    byte[] md5sum = digest.digest();
    BigInteger bigInt = new BigInteger(1, md5sum);
    String output = bigInt.toString(16);
    System.out.println(output);
    
    in.close();
    

    或者也许您可以遍历类路径中的文件名。当然,这只有在开发人员使用原始文件名时才有效。

    String classpath = System.getProperty("java.class.path");
    for(String path:classpath.split(";")){
        File o = new File(path);
        if(o.isDirectory()){
            ....        
        }    
    }
    

    【讨论】:

      【解决方案4】:

      Asaf,我使用 Maven 解决了这个问题。 Maven 对您在项目中使用的所有工件都有很好的版本控制支持。最重要的是,我使用了出色的 Maven Shade Plugin,它使您能够将所有 3rd 方库(maven 工件)打包到一个 JAR 文件中,以便部署。所有其他解决方案都逊色 - 我是根据我的个人经验说的 - 我去过那里,做过......甚至写了我自己的插件管理器等。使用 Maven,这是我的友好建议。

      【讨论】:

        【解决方案5】:

        用空字符串替换换行符可能是一种解决方案?

        Base64.encodeBase64String(arr).replace("\r\n","");
        

        【讨论】:

        • 我想过这样做,但它似乎是一个脆弱的解决方案,最终可能会导致噩梦般的错误。
        • 它适用于每个版本,而且是一个非常小的工作!只有半行代码。便于日后维护。
        【解决方案6】:

        我会创建 2 个以上不同版本的库来补充适当的第三方库版本,并提供使用哪个版本的手册。可能为它写正确的pom。

        【讨论】:

          【解决方案7】:

          为了解决您的问题,我认为最好的方法是使用 OSGi 容器,这样您就可以选择您的第 3 方依赖项的版本,其他库可以安全地使用其他版本而不会发生任何冲突。

          如果您不能依赖 OSGi 容器,那么您可以使用 MANIFEST.MF 中的实现版本

          Maven 是一个很棒的工具,但不能单独解决您的问题。

          【讨论】:

            猜你喜欢
            • 2013-10-12
            • 1970-01-01
            • 1970-01-01
            • 2022-10-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-04-25
            相关资源
            最近更新 更多