Java8: работа с методами по умолчанию

При написании класса полезности криптографии я столкнулся с проблемой со следующим методом:

public static void destroy(Key key) throws DestroyFailedException {
    if(Destroyable.class.isInstance(key)) {
        ((Destroyable)key).destroy();
    }
}

@Test
public void destroySecretKeySpec() {
    byte[] rawKey = new byte[32];
    new SecureRandom().nextBytes(rawKey);
    try {
        destroy(new SecretKeySpec(rawKey , "AES"));
    } catch(DestroyFailedException e) {
        Assert.fail();
    }
}

В конкретном случае javax.crypto.spec.SecretKeySpec описанный выше метод отлично работает в java7, потому что SecretKeySpec (javadocs 7) не реализует Destroyable (javadocs 7 )

Теперь с java8 класс SecretKeySpec (javadocs 8) был сделал удаляемым (javadocs 8) и метод Destroyable#destroy теперь равен default, что соответствует этому утверждение

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

то код компилируется без проблем несмотря на то, что сам класс ScretKeySpec не изменился, только интерфейс SecretKey был.

Проблема в том, что в oracle's jdk8 метод destroy имеет следующую реализацию:

public default void destroy() throws DestroyFailedException {
    throw new DestroyFailedException();
}

что приводит к исключению во время выполнения.

Таким образом, бинарная совместимость могла не быть нарушена, но существующий код был нарушен. Приведенный выше тест проходит с java7, но не с java8

Итак, мои вопросы:

  • Как вообще поступить с методами по умолчанию, которые могут привести к исключениям из-за того, что они не реализованы или не поддерживаются, или к неожиданному поведению во время выполнения? помимо выполнения

    Method method = key.getClass().getMethod("destroy");
    if(! method.isDefault()) {
        ((Destroyable)key).destroy();
    }
    

    который действителен только для java8 и может быть неправильным в будущих выпусках, поскольку метод по умолчанию может получить осмысленную реализацию.

  • Не лучше ли было бы оставить этот метод по умолчанию пустым, вместо того, чтобы генерировать исключение (которое IMO вводит в заблуждение, поскольку, кроме законного вызова для уничтожения, ничего не было предпринято для эффективного уничтожения ключа, UnsupportedOperationException подойдет лучше, и вы сразу поймете, что происходит)

  • Является ли мой подход (проверка типа/приведение/вызов)

    if(Destroyable.class.isInstance(key))
        ((Destroyable)key).destroy();
    

    для определения, уничтожить или не ошибиться? Что может быть альтернативой?

  • Это заблуждение или они просто забыли добавить осмысленную реализацию в ScretKeySpec?


person A4L    schedule 20.07.2014    source источник
comment
Есть ли причина, по которой вы не используете instanceof?   -  person Jeffrey    schedule 20.07.2014
comment
@Джеффри, не совсем так, разве isInstance не является динамическим эквивалентом instance of? Но, согласно этому ответу, я думаю, что instance of должно подойти для моего случая. Спасибо, что указали на это.   -  person A4L    schedule 20.07.2014
comment
Ага. isInstance является динамическим эквивалентом instanceof.   -  person Jeffrey    schedule 20.07.2014


Ответы (2)


Это заблуждение или они просто забыли добавить осмысленную реализацию в SecretKeySpec?

Ну не забыли. SecretKeySpec нужна реализация, но она еще не реализована. См. ошибку JDK-8008795. Извините, нет ожидаемого времени, когда это будет исправлено.

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

Понятие «бинарная совместимость» в приведенном вами учебнике является довольно строгим определением, которое используется Спецификация языка Java, глава 13. По сути, речь идет о допустимых преобразованиях библиотечных классов, которые не вызывают ошибок загрузки или связывания классов во время выполнения в сочетании с классами, скомпилированными для более старых версий этих библиотечных классов. Это контрастирует с несовместимостью исходного кода, которая вызывает ошибки во время компиляции, и поведенческой несовместимостью, которая обычно вызывает нежелательные изменения в поведении системы во время выполнения. Например, создание исключений, которые не были созданы ранее.

Это не для того, чтобы свести к минимуму тот факт, что ваш код был сломан. Это все равно несовместимость. (Извиняюсь.)

В качестве обходного пути вы можете добавить instanceof PrivateKey || instanceof SecretKey (поскольку это, по-видимому, классы, которым не хватает реализации destroy) и заставить тест утверждать, что они выдают DestroyFailedException, иначе, если instanceof Destroyable, выполнит оставшуюся часть логики в вашем тесте. Тест снова потерпит неудачу, когда эти экземпляры получат разумные destroy реализации в какой-нибудь будущей версии Java; это будет сигналом к ​​изменению теста обратно на вызов destroy на всех разрушителях. (Альтернативой может быть полное игнорирование этих классов, но тогда допустимые пути кода могут остаться незамеченными в течение некоторого времени.)

person Stuart Marks    schedule 21.07.2014
comment
Большое спасибо за этот ответ и извините, если мой последний вопрос прозвучал немного резко. Что касается обходного пути, мне удалось реализовать метод isDestroyable(Key), который возвращает key instanceof Destroyable && ! ReflectUtil.isDefault(key, "destroy"), последний делает *кашель* (аб) *кашель* *кашель* использование отражения, он в основном получает объект java.lang.reflect.Method isDefault m1 из того же класса, если такой метод присутствует, затем получите объект java.lang.reflect.Method destroy m2 из класса ключа и верните результат m1.invoke(m2). - person A4L; 21.07.2014
comment
... Это предполагает, что реализация по умолчанию всегда будет выдавать DestroyFailedException, что вряд ли изменится, поскольку это контракт: Реализация по умолчанию создает исключение DestroyFailedException.. Эти методы будут работать для всех версий Java без каких-либо изменений. Если в более поздней версии тесты проваливаются, то это потому, что попытка уничтожения действительно была, и она не удалась, а это уже другая история. Еще раз спасибо. - person A4L; 21.07.2014

Я только предполагаю, но я думаю, что идея создания исключения в реализации destroy по умолчанию состоит в том, чтобы предупредить вас, что конфиденциальные данные не были уничтожены. Если реализация по умолчанию была пустой и нет реализации, переопределяющей реализацию по умолчанию, вы можете по ошибке предположить, что конфиденциальные данные были уничтожены.

Я думаю, вам все равно следует перехватывать исключение DestroyFailedException, независимо от того, выброшено ли оно из реализации по умолчанию или из реальной реализации, поскольку оно предупреждает вас о том, что ничего не было уничтожено, и вам следует решить, как поступить в этой ситуации.

Контракт метода destroy, который не изменился между Java 7 и Java 8 (кроме комментария о реализации по умолчанию), гласит - Sensitive information associated with this Object is destroyed or cleared. Subsequent calls to certain methods on this Object will result in an IllegalStateException being thrown.

а также :

Выдает:
DestroyFailedException — если операция уничтожения не удалась.

Если уничтожить не удается, последующие вызовы определенных методов этого объекта не приведут к выдаче IllegalStateException. Это по-прежнему верно, если уничтожение ничего не сделало, и поэтому реализация по умолчанию (которая ничего не делает) выдает DestroyFailedException.

person Eran    schedule 20.07.2014
comment
Оставлять метод пустым — не лучший выбор, это правильно. Исключение проверяется, поэтому его нужно поймать и обработать, но чтобы сделать это правильно, я думаю, необходимо знать, не прошел ли процесс уничтожения или потому, что уничтожать вообще нечего. Вот почему я заявил, что UnsupportedOperationException был бы более полезным. Вы примете решение в зависимости от типа исключения. Как вы думаете, как правильно это обработать, кроме проверки типа конкретной реализации, то есть SecreKeySpec или супертипа SecretKey? - person A4L; 20.07.2014
comment
@ A4L Я не знаю, как вы планируете обрабатывать DestroyFailedException, но я не вижу причин для другой обработки в случае, если он был выброшен из реализации по умолчанию. То, что была вызвана реализация по умолчанию для уничтожения, не означает, что вообще нечего уничтожать, это означает, что экземпляр, для которого вы вызываете destroy, не имеет доступной реализации для выполнения этого уничтожения. - person Eran; 20.07.2014
comment
идея создания исключения в реализации destroy по умолчанию состоит в том, чтобы предупредить вас о том, что конфиденциальные данные не были уничтожены --- но старый статус, когда класс даже не объявлял себя разрушаемым, по-видимому, сигнализировал, что даже лучше и во время разработки. Итак, реальный вопрос в том, зачем навязывать Destroyable всему, будь то действительно разрушаемое или нет? - person William F. Jameson; 20.07.2014
comment
@Eran Моя формулировка потому что вообще нечего уничтожать вводит в заблуждение, извините за это. Я имел в виду именно то, что вы сказали, экземпляр, для которого вы вызываете уничтожение, не имеет доступной реализации для выполнения этого уничтожения. На самом деле, в отличие от открытых ключей, я считаю, что секретный ключ следует уничтожить, поскольку он содержит конфиденциальные данные. Класс ScretKeySpec - это просто реальный пример, который выявил общую проблему, которая может возникнуть с методами по умолчанию и к которой на самом деле относится мой вопрос. - person A4L; 20.07.2014
comment
@A4L Я не уверен, есть ли другие реализации интерфейса по умолчанию, которые просто генерируют исключения, и применимо ли к ним то, что я здесь написал, или нет, но в этом конкретном случае вам все равно придется обрабатывать DestroyFailedException (будь то Java 7 или Java 8), так что не вижу проблемы. - person Eran; 20.07.2014