我有一个依赖于几个第三方库的项目,项目本身被打包为jar并作为库分发给其他开发人员。 这些开发人员将依赖项添加到其类路径中,并在其代码中使用我的库。
最近我遇到了第三方依赖项之一的问题,apache commons codec libary, 问题是:
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
正如您所看到的,方法的输出随着次要版本的变化而发生了变化。
我的问题是,我不想强迫我的图书馆用户使用第三方图书馆的特定次要版本。假设我知道对依赖库的更改,无论如何我可以识别哪个库版本被包含在类路径中并且相应地表现?或者,什么被认为是这种情景的最佳做法?
P.S - 我知道对于上面的例子,我可以使用向后兼容的new String(Base64.encodeBase64(data, false))
,这是一个更普遍的问题。
答案 0 :(得分:12)
你问这个问题的“最佳实践”是什么。我将假设“这个问题”是指第三方库升级的问题,具体而言,这两个问题:
什么时候升级?
你应该做些什么来保护自己免受不良升级(比如你的例子中提到的commons-codec bug)?
要回答第一个问题,“你应该什么时候升级?”,行业中存在许多策略。在大多数商业Java世界中,我认为目前的主导做法是“当你准备好时,你应该进行升级。”换句话说,作为开发人员,您首先需要意识到可以使用新版本的库(对于您的每个库!),然后您需要将其集成到您的项目中,并且您是最终的人根据您自己的测试床进行/不进行决定--- junit,回归,手动测试等...无论您做什么来确保质量。 Maven通过使多个版本的大多数流行库可以自动下载到您的构建系统中,并通过默认地培养这种“固定”传统来促进这种方法(我称之为“固定”版本)。
但是其他实践确实存在,例如,在Debian Linux发行版中,理论上可以将大量此类工作委托给Debian软件包维护者。您只需根据Debian提供的4个级别拨打您的舒适级别,选择新风险,或反之亦然。 Debian提供的4个级别是:OLDSTABLE,STABLE,TESTING,UNSTABLE。不稳定是非常稳定的,尽管它的名字,OLDSTABLE提供的库可能比他们原来的“上游”项目网站上提供的最新和最好的版本长达3年。
至于第二个问题,如何保护自己,我认为当前行业中的“最佳实践”是双重的:根据声誉选择你的库(Apache通常都很不错),并在升级之前等待一段时间,例如,不要总是急于做最新的和最伟大的。也许选择已经有3至6个月可用的库的公开发布版本,希望自首次发布以来,任何关键错误都已被刷新并修补。
通过编写专门保护依赖项中依赖的行为的JUnit测试,您可以走得更远。这样,当您关闭较新版本的库时,您的JUnit会立即失败,并警告您该问题。但根据我的经验,我没有看到很多人这样做。而且通常很难意识到您所依赖的精确行为。
顺便说一下,我是Julius,负责这个bug的人!请接受我对此问题的歉意。这就是为什么我认为它发生了。我只会为自己说话。要找出apache commons-codec团队认为的其他人,你必须自己问问他们(例如,ggregory,sebb)。
当我在版本1.4和1.5中使用Base64时,我非常关注Base64的主要问题,即将二进制数据编码到低127的ASCIi中,并将其解码回二进制
所以在我的脑海里(这里我出错了)“aGk = \ r \ n”和“aGk =”之间的区别并不重要。它们都解码为相同的二进制结果!
但是在阅读了你的stackoverflow帖子之后,在更广泛的意义上思考它,我意识到可能有一个我从未考虑过的非常流行的用例。也就是说,对数据库中的加密密码表进行密码检查。在该用例中,您可能会执行以下操作:
// a. store user's password in the database // using encryption and salt, and finally, // commons-codec-1.4.jar (with "\r\n"). // // b. every time the user logs in, encrypt their // password using appropriate encryption alg., plus salt, // finally base64 encode using latest version of commons-codec.jar, // and then check against encrypted password in the database // to see if it matches.
所以当然,如果commons-codec.jar改变了它的编码行为,那么这个用例就会失败,即使是根据base64规范的非物质方式。我很抱歉!
我认为即使我在本文开头所阐述的所有“最佳实践”,仍然很有可能被搞砸了。 Debian Testing已经包含了commons-codec-1.5,带有bug的版本,并且修复这个bug本质上意味着搞砸使用1.5版而不是版本1.4的人。但我会尝试在apache网站上放一些文档来警告人们。谢谢你在堆栈溢出这里提到它(我是否正确使用usecase?)。
PS。我认为Paul Grime的解决方案非常简洁,但我怀疑它依赖于在Jar的META-INF/MANIFEST.MF
文件中推送版本信息的项目。我认为所有的Apache Java库都可以做到这一点,但其他项目可能没有。这种方法是一种很好的方法,可以在构建时将自己固定到版本:不是意识到你依赖于“\ r \ n”,而是编写了防止它的JUnit,你可以编写一个更简单的JUnit: assertTrue(desiredLibVersion.equals(actualLibVersion))
。
(这假设运行时库与构建时库相比没有变化!)
答案 1 :(得分:6)
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。
答案 2 :(得分:1)
您可以计算实际类文件的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()){
....
}
}
答案 3 :(得分:1)
Asaf,我使用Maven解决了这个问题。 Maven对您在项目中使用的所有工件提供了很好的版本控制支持。最重要的是,我使用了优秀的Maven Shade Plugin,它使您能够将所有第三方库(maven工件)打包在一个JAR文件中,为部署做好准备。所有其他解决方案都是低劣的 - 我正在谈论我的个人经历 - 我去过那里,完成了......甚至写了我自己的插件管理器等。使用Maven,这是我友好的建议。
答案 4 :(得分:0)
用空字符串替换换行符可能是一个解决方案吗?
Base64.encodeBase64String(arr).replace("\r\n","");
答案 5 :(得分:0)
我会创建2个以上不同版本的库来补充适当的第三方库版本,并提供使用哪个版本的手册。可能会为它写出正确的pom。
答案 6 :(得分:0)
要解决您的问题,我认为最好的方法是使用OSGi容器,这样您就可以选择第三方依赖项的版本,其他库可以安全地使用其他版本而不会产生任何冲突。
如果您不能依赖OSGi容器,那么您可以使用MANIFEST.MF中的实现版本
Maven是一个很棒的工具,但不能单独解决你的问题。