В Java, как проверить, что AutoCloseable.close() был вызван?

Я создаю библиотеку Java. Некоторые из классов, которые предназначены для использования пользователями библиотеки, содержат собственные системные ресурсы (через JNI). Я хотел бы убедиться, что пользователь «удаляет» эти объекты, поскольку они тяжелые, и в тестовом наборе они могут вызвать утечку между тестовыми наборами (например, мне нужно убедиться, что TearDown будет удалять). Для этой цели я сделал классы Java реализующими AutoCloseable, но этого недостаточно, или я использую его неправильно:

  1. Я не вижу, как использовать оператор try-with-resources в контексте тестов (я использую JUnit5 с Mockito), поскольку «ресурс» не является недолговечным - он является частью тестового приспособления.

  2. Я, как всегда, старался внедрить finalize() и протестировать его на замыкание, но оказалось, что finalize() даже не вызывается (Java10). Это также помечено как устаревшее, и я уверен, что эта идея будет осуждена.

Как это делается? Чтобы было ясно, я хочу, чтобы тесты приложения (использующие мою библиотеку) не удавались, если они не вызывают close() для моих объектов.


Редактировать: добавлю код, если поможет. Это немного, но это то, что я пытаюсь сделать.

@SuppressWarnings("deprecation") // finalize() provided just to assert closure (deprecated starting Java 9)
@Override
protected final void finalize() throws Throwable {
    if (nativeHandle_ != 0) {
         // TODO finalizer is never called, how to assert that close() gets called?
        throw new AssertionError("close() was not called; native object leaking");
    }
}

Edit2, результат щедрости Спасибо всем за ответы, половина награды была автоматически присуждена. Я пришел к выводу, что для моего случая лучше всего попробовать решение с участием Cleaner. Однако кажется, что действия по очистке, хотя и зарегистрированы, не вызываются. Я задал дополнительный вопрос здесь.

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

MTCoster 29.01.2019 20:52

ваш вопрос только о тестах или о принуждении к использованию метода #close вообще?

AdamSkywalker 29.01.2019 20:56

Похоже, вы хотите проверить это на своих тестах Приложения, а не на собственных тестах библиотеки. В этом случае вы вполне можете смоделировать объекты AutoCloseable и просто проверить, вызываются ли смоделированные методы close, когда вы ожидаете, что они будут. Есть много способов издеваться над этим, Mockito, JMock...

Will Cain 29.01.2019 21:04

@MTCoster, вопрос в основном об «их» тестах. Мой класс предоставляет метод close(), и его отсутствие равносильно неправильному использованию моего API. Поэтому я хотел бы как-то обратить внимание на эту ошибку, прежде чем переходить к следующему тестовому набору. Это можно как-то сделать? Я гибкий в том, что я мог другой аналогичный механизм/интерфейс, если это поможет

haelix 29.01.2019 21:05

@AdamSkywalker, так что вопрос относится к использованию моего API в целом, но я иллюстрирую тестами. Приложение должно закрыться, иначе могут произойти неприятные вещи. Чтобы проиллюстрировать, что я имею в виду, подумайте о SO_LINGER для TCP-соединений. На нативной стороне у меня есть такие вещи, как конечные автоматы, которым необходимо войти в определенные конечные состояния (вы можете сказать, что, если они этого не сделают, так как приложение все равно закрывается? ну, не совсем, иногда вам нужно «закрытие», чтобы гарантировать, что все данные были переданы между сверстниками, которые работают в сети)

haelix 29.01.2019 21:10

Из документы на AutoClosable: «Обратите внимание, что в отличие от метода close для Closeable, этот метод close не обязательно должен быть идемпотентным. Другими словами, вызов этого метода close более одного раза может иметь видимый побочный эффект, в отличие от Closeable.close, который не должен иметь никакого эффекта, если вызывается более одного раза.» — вы можете выдать ResourceAlreadyClosedException после первого вызова close, а затем поручить пользователям проверять это исключение в своих тестах.

MTCoster 29.01.2019 21:12

Это хороший момент, и я занимаюсь случаем, когда close() вызывается дважды, но мой вопрос касается close(), вызываемого ноль раз.

haelix 29.01.2019 21:15

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

Coderino Javarino 31.01.2019 22:10

Больше похоже на то, о чем должен предупреждать компилятор, а не на то, что нужно реализовать во время выполнения. Я думаю, что все потоки будут закрыты ОС, если JVM полностью отключится.

plalx 01.02.2019 02:25

@haelix Что касается finalize - есть новая штука под названием Cleaner, которая в наши дни предпочтительнее финализаторов. Это также хорошо работает с автозакрывающимися элементами, как видно из примера на странице API. Тем не менее, по-прежнему нет никаких гарантий относительно того, когда и даже будут ли они запускаться (если только вы не вызовете очистку вручную).

gustafc 01.02.2019 15:30
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
29
10
5 942
4

Ответы 4

Если вас интересует согласованность в тестах, просто добавьте в тестовый класс метод destroy(), отмеченный аннотацией @AfterClass, и закройте в нем все ранее выделенные ресурсы.

Если вас интересует подход, позволяющий защитить ресурс от того, чтобы он не был закрыт, вы можете предоставить способ, который не предоставляет ресурс пользователю явно. Например, ваш код может контролировать жизненный цикл ресурса и принимать от пользователя только Consumer<T>.

Если вы не можете этого сделать, но все же хотите быть уверены, что ресурс будет закрыт, даже если пользователь использует его неправильно, вам придется сделать несколько хитрых вещей. Вы можете разделить свой ресурс на sharedPtr и сам resource. Затем откройте sharedPtr пользователю и поместите его во внутреннее хранилище, завернутое в WeakReference. В результате вы сможете поймать момент, когда GC удалит sharedPtr и вызовет close() на resource. Имейте в виду, что resource не должен открываться пользователю. Я подготовил пример, он не очень точен, но надеюсь, что он показывает идею:

public interface Resource extends AutoCloseable {

    public int jniCall();
}
class InternalResource implements Resource {

    public InternalResource() {
        // Allocate resources here.
        System.out.println("Resources were allocated");
    }

    @Override public int jniCall() {
        return 42;
    }

    @Override public void close() {
        // Dispose resources here.
        System.out.println("Resources were disposed");
    }
}
class SharedPtr implements Resource {

    private final Resource delegate;

    public SharedPtr(Resource delegate) {
        this.delegate = delegate;
    }

    @Override public int jniCall() {
        return delegate.jniCall();
    }

    @Override public void close() throws Exception {
        delegate.close();
    }
}
public class ResourceFactory {

    public static Resource getResource() {
        InternalResource resource = new InternalResource();
        SharedPtr sharedPtr = new SharedPtr(resource);

        Thread watcher = getWatcherThread(new WeakReference<>(sharedPtr), resource);
        watcher.setDaemon(true);
        watcher.start();

        Runtime.getRuntime().addShutdownHook(new Thread(resource::close));

        return sharedPtr;
    }

    private static Thread getWatcherThread(WeakReference<SharedPtr> ref, InternalResource resource) {
        return new Thread(() -> {
            while (!Thread.currentThread().isInterrupted() && ref.get() != null)
                LockSupport.parkNanos(1_000_000);

            resource.close();
        });
    }
}

Если бы я был вами, я бы сделал следующее:

  • Напишите статическую оболочку вокруг ваших вызовов, которая возвращает «тяжелые» объекты.
  • Создайте коллекцию PhantomReferences для хранения всех ваших тяжелых объектов в целях очистки.
  • Создайте коллекцию Слабые ссылки для хранения всех ваших тяжелых объектов, чтобы проверить, являются ли они GC'd или нет (имеют ссылку от вызывающего или нет)
  • При разрыве я бы проверил оболочку, чтобы увидеть, какие ресурсы были GC'd (имеют ссылку в Phantom, но не в Weak), и я бы проверил, закрыты ли они или нет.
  • Если вы добавите некоторую информацию об отладке/вызывающем объекте/стеке во время обслуживания ресурса, будет легче отследить утечку тестового примера.

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

Что касается ссылочных типов, я рекомендую эта ссылка. PhantomReferences рекомендуется использовать для очистки ресурсов.

Я думаю, что решение, включающее Phantom или Weak, — это то, что нужно, в основном какой-то механизм уведомления, поэтому моя библиотека знает, когда пользователь больше не держит Strong ref. В этот момент она должна была закрыться() ресурс, иначе библиотека развязывает ад.

haelix 07.02.2019 09:16

«Должен был закрыться()» — если нет, то в тестах вы можете пометить этот кейс как неудавшийся, в продакшне вы можете молча закрыть его для них, освободив ресурс.

gaborsch 07.02.2019 10:26
молча закрыть его для них - в моем случае это не вариант. Собственный объект чувствителен и должен быть закрыт от создавшего его потока, который может быть пользовательским потоком. Надеюсь, это проливает больше света на то, почему пользователь действительно должен детерминистически закрывать вещь, и почему все обсуждение
haelix 07.02.2019 12:10

Я понимаю. В этом случае я бы рассмотрел два варианта: 1) создать поток менеджера, который создает и закрывает объекты по запросу, который будет работать даже в среде prod, 2) отслеживать поток и трассировку стека, где были созданы отдельные объекты, и сообщать о них при обнаружении неисправности. Это поможет обнаружению ошибок.

gaborsch 07.02.2019 12:16

я бы предоставил экземпляры для этих объектов через Factory methods, и с этим я контролирую их создание, и я буду кормить потребителей с помощью Proxies, который выполняет логику закрытие объекта

interface Service<T> {
 T execute();
 void close();
}

class HeavyObject implements Service<SomeObject> {
  SomeObject execute() {
  // .. some logic here
  }
  private HeavyObject() {}

  public static HeavyObject create() {
   return new HeavyObjectProxy(new HeavyObject());
  }

  public void close() {
   // .. the closing logic here
  }
}

class HeavyObjectProxy extends HeavyObject {

  public SomeObject execute() {
    SomeObject value = super.execute();
    super.close();
    return value;
  }
}

Я не хочу close() ресурса после каждой операции.

haelix 07.02.2019 09:13

В общем, если бы вы могли надежно проверить, был ли закрыт ресурс, вы могли бы просто закрыть его самостоятельно.

Первое, что нужно сделать, это упростить работу с ресурсом для клиента. Используйте идиому Execute Around.

Насколько я знаю, единственное использование execute вокруг для обработки ресурсов в библиотеке Java — это java.security.AccessController.doPrivileged, и это особенное (ресурс — это волшебный фрейм стека, который вы В самом деле не хотите оставлять открытым). Я считаю, что у Spring уже давно есть столь необходимая библиотека JDBC для этого. Я определенно использовал execute-around (в то время не знал, что это так называется) для JDBC вскоре после того, как Java 1.1 сделала его смутно практичным.

Код библиотеки должен выглядеть примерно так:

@FunctionalInterface
public interface WithMyResource<R> {
    R use(MyResource resource) throws MyException;
}
public class MyContext {
// ...
    public <R> R doAction(Arg arg, WithMyResource<R> with) throws MyException {
        try (MyResource resource = acquire(arg)) {
            return with.use(resource);
        }
    }

(Получите объявления параметров типа в нужном месте.)

Использование на стороне клиента выглядит примерно так:

MyType myResult = yourContext.doContext(resource -> {
    ...blah...;
    return ...thing...;
});

Вернемся к тестированию. Как упростить тестирование, даже если испытуемый извлекает ресурс из среды выполнения или доступен какой-то другой механизм?

Очевидный ответ заключается в том, что вы предоставляете решение для выполнения теста. Вам нужно будет предоставить некоторый API-интерфейс для выполнения, чтобы убедиться, что все ваши ресурсы, которые были получены в области, также были закрыты. Это должно быть связано с контекстом, из которого получен ресурс, а не с использованием глобального состояния.

В зависимости от того, какую среду тестирования используют ваши клиенты, вы можете предложить что-то лучшее. JUnit5, например, имеет средство расширения на основе аннотаций, которое позволяет вам предоставлять контекст в качестве аргумента, а также применять проверки после выполнения каждого теста. (Но я им мало пользовался, так что больше ничего не скажу.)

В общем, если бы вы могли надежно проверить, был ли закрыт ресурс, вы могли бы просто закрыть его самостоятельно. — ложь. В моем случае это не вариант. Собственный объект чувствителен и должен быть закрыт от создавшего его потока, который может быть пользовательским потоком. Надеюсь, это проливает больше света на то, почему пользователь действительно должен детерминистически закрывать вещь, и почему все обсуждение
haelix 07.02.2019 12:11

Другие вопросы по теме