В 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");
    }
}

Ответ 1

Этот пост не дает прямого ответа на ваш вопрос, но дает другую точку зрения.

Один из подходов, чтобы ваши клиенты постоянно называют close, чтобы освободить их от этой ответственности.

Как ты можешь это сделать?

Используйте шаблон шаблона.

Эскиз реализации

Вы упомянули, что работаете с TCP, поэтому предположим, что у вас есть класс TcpConnection, у которого есть метод close().

Давайте определим интерфейс TcpConnectionOpertaions:

public interface TcpConnectionOperations {
  <T> T doWithConnection(TcpConnectionAction<T> action);
}

и реализовать это:

public class TcpConnectionTemplate implements TcpConnectionOperations {
  @Override
  public <T> T doWithConnection(TcpConnectionAction<T> action) {
    try (TcpConnection tcpConnection = getConnection()) {
      return action.doWithConnection(tcpConnection);
    }
  }
}

TcpConnectionAction - это просто обратный вызов, ничего особенного.

public interface TcpConnectionAction<T> {
  T doWithConnection(TcpConnection tcpConnection);
}

Как библиотека должна потребляться сейчас?

  • Он должен использоваться только через интерфейс TcpConnectionOperations.
  • Потребители предлагают акции

Например:

String s = tcpConnectionOperations.doWithConnection(connection -> {
  // do what we with with the connection
  // returning to string for example
  return connection.toString();
});

Pros

  • Клиенты не должны беспокоиться о:
    • получение TcpConnection
    • закрытие соединения
  • Вы управляете созданием соединений:
    • вы можете их кешировать
    • войти их
    • собирать статистику
    • много других вариантов использования...
  • В тестах вы можете предоставить фиктивные TcpConnectionOperations и фиктивные TcpConnections и сделать против них утверждения

Cons

Этот подход может не работать, если жизненный цикл ресурса длиннее action. Например, клиенту необходимо хранить ресурс дольше.

Тогда вы можете захотеть углубиться в ReferenceQueue/ Cleaner (начиная с Java 9) и связанных API.

Вдохновленный рамками Spring

Этот шаблон широко используется в рамках Spring.

Смотрите, например:


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

Ответ 2

На вашем месте я бы сделал следующее:

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

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

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

Ответ 3

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

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

Если вы не можете этого сделать, но по-прежнему хотите быть уверены, что ресурс будет закрыт, даже если пользователь не использует его правильно, вам придется сделать несколько хитрых вещей. Вы можете разделить свой ресурс на sharedPtr и сам resource. Затем предоставьте пользователю sharedPtr и поместите его во внутреннюю память, WeakReference в 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();
        });
    }
}

Ответ 4

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

@ExtendWith(MockitoExtension.class)
class Test {
    private static class Resource implements AutoCloseable {
        boolean isTrue() {
            return true;
        }

        @Override
        public void close() throws Exception {
            // dispose a resource
        }
    }

    private Resource resource = Mockito.spy(new Resource());

    @AfterEach
    void tearDown() throws Exception {
        Mockito.verify(resource, Mockito.atLeastOnce()).close();
    }

    @Test
    void testTrue() {
        Assert.assertTrue(resource.isTrue());
    }
}

Однако приведенный выше пример применим, когда вы управляете созданием ресурса (и явно оборачиваете его шпионом).

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

Одним из способов достижения желаемого поведения было бы разоблачение ваших Ресурсов через какую-то Фабрику. Это позволит вам вернуть контроль над созданием Ресурсов. Получив обратно элемент управления, вы можете обернуть ресурсы в шпионы и убедиться, что close() был вызван в tearDown(). Это только идея, и реализации могут быть очень разными по сложности.

Самая простая реализация, которая приходит на ум, - это определить собственный класс AbstractTest и задокументировать, что клиенты библиотеки должны расширять ваш класс AbstractTest в своих тестовых примерах. Например, это может выглядеть примерно так:

// Library client test
class TestCase extends AbstractTest {

    @Test
    void testTrue() {
        Assert.assertTrue(resource.isTrue());
    }

}

// Library code
@ExtendWith(MockitoExtension.class)
abstract class AbstractTest {

    protected Resource resource = Mockito.spy(Factory.create());

    @AfterEach
    void tearDown() throws Exception {
        Mockito.verify(resource, Mockito.atLeastOnce()).close();
    }
}

interface Resource extends AutoCloseable {
    boolean isTrue();
}

class Factory {
    public static Resource create() {
        return new ResourceImpl();
    }

    private static class ResourceImpl implements Resource {

        public boolean isTrue() {
            return true;
        }

        @Override
        public void close() throws Exception {
            // dispose a resource
        }
    }
}

Более продвинутая реализация может заключаться в создании собственного расширения junit, которое клиенты библиотеки будут использовать, просто аннотируя свои тесты с помощью @ExtendWith(CustomExtension.class). В своем расширении вы должны каким-то образом создать Proxy of the Factory, который оборачивает все ресурсы в шпионы, и написать свернутый код проверки.

Ответ 5

я бы предоставил экземпляры для этих объектов с помощью 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;
  }
}

Ответ 6

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

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

Насколько я знаю, единственное использование execute для обработки ресурсов в библиотеке Java - это java.security.AccessController.doPrivileged и это специальное (ресурс представляет собой фрейм магического стека, который вы действительно не хотите оставлять открытым)., Я считаю, что у Spring уже давно есть необходимая библиотека JDBC для этого. Я, конечно, использовал JDBC для выполнения 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, например, имеет средство расширения на основе аннотаций, которое позволяет вам предоставлять контекст в качестве аргумента, а также применять проверки после каждого теста. (Но я этим мало пользовался, поэтому больше ничего не скажу.)