Подтвердить что ты не робот

Обработка изменений в зависимых сторонних библиотеках

У меня есть проект, который зависит от нескольких сторонних библиотек, сам проект упакован как банку и распространяется среди других разработчиков в виде библиотеки. Эти разработчики добавляют зависимости к их пути к классам и используют мою библиотеку в своем коде.

Недавно у меня возникла проблема с одной из сторонних зависимостей, 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)), который совместим с обратной связью, это более общий вопрос.

4b9b3361

Ответ 1

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, который изменил его способ.

EDIT Вот причина изменения - https://issues.apache.org/jira/browse/CODEC-99.

Ответ 2

Вы спрашиваете, что такое "лучшая практика" для этой проблемы. Я предполагаю, что под "этой проблемой" вы подразумеваете проблему обновления сторонних библиотек, и в частности, эти два вопроса:

  • Когда вы должны обновить?

  • Что вы должны сделать, чтобы защитить себя от плохих обновлений (например, ошибка сообщества-кодека, упомянутая в вашем примере)?

Чтобы ответить на первый вопрос, "когда вам следует обновить?", в отрасли существует множество стратегий. В большинстве коммерческих Java-сетей я считаю, что нынешняя доминирующая практика заключается в том, что "вы должны обновиться, когда будете готовы". Другими словами, как разработчик, вам сначала нужно понять, что доступна новая версия библиотеки (для каждой из ваших библиотек!), Тогда вам необходимо интегрировать ее в свой проект, и именно вы делаете финальную решение go/no-go на основе собственной тестовой кровати --- юнит, регрессия, ручное тестирование и т.д.... что бы вы ни делали, чтобы обеспечить качество. Maven облегчает этот подход (я называю его "pinning" ), создавая несколько версий большинства популярных библиотек для автоматической загрузки в вашу систему сборки и молчаливо поддерживая эту традицию "закрепления".

Но существуют и другие методы, например, в дистрибутиве Debian Linux теоретически можно делегировать большую часть этой работы сторонним разработчикам пакетов Debian. Вы бы просто набрали свой уровень комфорта в соответствии с 4 уровнями, доступными Debian, выбором новизны над риском или наоборот. Доступны 4 уровня Debian: OLDSTABLE, STABLE, TESTING, UNSTABLE. Нестабильный является чрезвычайно стабильным, несмотря на его имя, и OLDSTABLE предлагает библиотеки, которые могут истечь 3 года по сравнению с последними и лучшими версиями, доступными на их исходных веб-сайтах проекта "вверх".

Что касается второго вопроса, как защитить себя, я думаю, что нынешняя "лучшая практика" в отрасли двоякая: выберите свои библиотеки на основе репутации (Apache, как правило, очень хорош), и подождите немного до обновления, например, не всегда спешите, чтобы быть на последнем и самом большом. Возможно, выберете публичный выпуск библиотеки, которая уже была доступна от 3 до 6 месяцев, в надежде, что любые критические ошибки были очищены и исправлены с момента выпуска.

Вы можете пойти дальше, написав тесты JUnit, которые специально защищают поведение, на которое вы полагаетесь в своих зависимостях. Таким образом, когда вы снимете более новую версию библиотеки, ваш JUnit сразу выйдет из строя, предупредив вас об этой проблеме. Но я не вижу, чтобы многие люди это делали, по моему опыту. И часто бывает трудно осознавать точный поступок, на который вы полагаетесь.

И, между прочим, я Джулиус, парень, ответственный за эту ошибку! Примите мои извинения за эту проблему. Вот почему я думаю, что это произошло. Я буду говорить только для себя. Чтобы узнать, что думают другие члены сообщества apache-codec, вы должны сами спросить их (например, ggregory, sebb).

  • Когда я работал над Base64 в версиях 1.4 и 1.5, я был очень сфокусирован на основной проблеме Base64, то есть кодировании двоичных данных в ASCIi с более низким значением 127, а декодирование обратно в двоичный.

  • Итак, в моем уме (и здесь, где я поступил не так) разница между "aGk =\r\n" и "aGk =" несущественна. Они оба декодируются до одного и того же двоичного результата!

  • Но, думая об этом в более широком смысле после прочтения вашей публикации в stackoverflow, я понимаю, что, вероятно, очень популярная система, которую я никогда не рассматривал. То есть проверка паролем таблицы шифрованных паролей в базе данных. В этом случае вы, вероятно, будете делать следующее:

    // a.  store user 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.

Итак, конечно, этот usecase терпит неудачу, если commons-codec.jar изменяет свое поведение в кодировке, даже несущественным образом в соответствии с спецификацией base64. Мне очень жаль!

Я думаю, что даже со всеми "лучшими практиками", которые я изложил в начале этого сообщения, все еще существует высокая вероятность напортачиться на эту. Тестирование Debian уже содержит commons-codec-1.5, версию с ошибкой и исправить эту ошибку, по сути, означает накручивать людей, которые использовали версию 1.5 вместо версии 1.4, где вы это делали. Но я попытаюсь поместить некоторую документацию на веб-сайт apache, чтобы предупредить людей. Спасибо, что упомянул об этом здесь, о переполнении стека (я прав насчет usecase?).

пс. Я подумал Пол Гримрешение было довольно аккуратным, но я подозреваю, что он полагается на проекты, нажимающие информацию о версии в файле Jar META-INF/MANIFEST.MF. Я думаю, что все библиотеки Java Apache делают это, но другие проекты могут и не быть. Этот подход - хороший способ привязать себя к версиям во время сборки: вместо того, чтобы понять, что вы зависите от "\ r\n", и написав JUnit, который защищает от этого, вы можете вместо этого написать намного проще JUnit: assertTrue(desiredLibVersion.equals(actualLibVersion)).

(Это предполагает, что исполняемые файлы libs не изменяются по сравнению с libs!)

Ответ 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, который дает вам возможность упаковывать все сторонние библиотеки (maven артефакты) в один JAR файл, готовый к развертыванию. Все другие решения просто уступают - я говорю из своего личного опыта - я был там, сделал это... Даже написал свой собственный плагин-менеджер и т.д. Используйте Maven, это мой дружеский совет.

Ответ 5

замена новой строки пустой строкой может быть решением?

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

Ответ 6

Я бы создал 2 + разные версии библиотеки, чтобы дополнить соответствующую версию сторонней библиотеки и предоставить руководство, которое нужно использовать. Наверное, напишите для этого правильный pom.

Ответ 7

Чтобы решить вашу проблему, я думаю, что лучший способ - использовать контейнер OSGi, чтобы вы могли выбрать свою версию третьей стороны зависимые и другие библиотеки могут безопасно использовать другую версию без каких-либо конфликтов.

Если вы не можете полагаться на контейнер OSGi, вы можете использовать версию реализации в MANIFEST.MF

Maven - отличный инструмент, но не может решить проблему самостоятельно.