В Java, как проверить, что AutoCloseable.close () был вызван? - PullRequest
0 голосов
/ 29 января 2019

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

  1. Я не вижу, как использовать *Оператор 1006 * в контексте тестов (я использую 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.Однако, похоже, что действия по очистке, хотя и зарегистрированы, не вызываются.Я задал дополнительный вопрос здесь .

Ответы [ 5 ]

0 голосов
/ 07 февраля 2019

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

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

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

0 голосов
/ 01 февраля 2019

я бы предоставил экземпляры этим объектам через 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;
  }
}
0 голосов
/ 01 февраля 2019

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

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

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

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

0 голосов
/ 01 февраля 2019

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

Один из способов заставить ваших клиентов постоянно звонить 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);
}

Как использовать библиотеку сейчас?

  • Она должна потребляться только через *Интерфейс 1034 *.
  • Действия потребителей:

Например:

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

Плюсы

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

Минусы

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

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

Вдохновлен Spring Framework

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

См. например:

Обновление 2/7/19

Как я могу кешировать / повторно использовать ресурс?

Это что-то вродеиз пул :

пул - это набор ресурсов, которые хранятся готовыми к использованию, а не приобретаются при использовании и высвобождаются

некоторые пулы в Java:

WhПри реализации пула возникает несколько вопросов:

  • Когда ресурс действительно должен быть close d?
  • Как ресурс должен быть распределен между несколькими потоками?

Когда ресурс должен быть close d?

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

Какон может быть разделен между несколькими потоками?

Это зависит от вида самого ресурса.

Обычно требуется, чтобы только один поток обращался к одному ресурсу.

Это можно сделать с помощью некоторого типа блокировки

Демо

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

IpAndPort.java

@Value
public class IpAndPort {
  InetAddress address;
  int port;
}

TcpConnection.java

@Data
public class TcpConnection {
  private static final AtomicLong counter = new AtomicLong();

  private final IpAndPort ipAndPort;
  private final long instance = counter.incrementAndGet();

  public void close() {
    System.out.println("Closed " + this);
  }
}

CachingTcpConnectionTemplate.java

public class CachingTcpConnectionTemplate implements TcpConnectionOperations {
  private final Map<IpAndPort, TcpConnection> cache
      = new HashMap<>();
  private boolean closed; 
  public CachingTcpConnectionTemplate() {
    System.out.println("Created new template");
  }

  @Override
  public synchronized <T> T doWithConnectionTo(IpAndPort ipAndPort, TcpConnectionAction<T> action) {
    if (closed) {
      throw new IllegalStateException("Closed");
    }
    TcpConnection tcpConnection = cache.computeIfAbsent(ipAndPort, this::getConnection);
    try {
      System.out.println("Executing action with connection " + tcpConnection);
      return action.doWithConnection(tcpConnection);
    } finally {
      System.out.println("Returned connection " + tcpConnection);
    }
  }

  private TcpConnection getConnection(IpAndPort ipAndPort) {
    return new TcpConnection(ipAndPort);
  }


  @Override
  public synchronized void close() {
    if (closed) {
      throw new IllegalStateException("closed");
    }
    closed = true;
    for (Map.Entry<IpAndPort, TcpConnection> entry : cache.entrySet()) {
      entry.getValue().close();
    }
    System.out.println("Template closed");
  }
}
Тестирование инфраструктуры

TcpConnectionOperationsParameterResolver.java

public class TcpConnectionOperationsParameterResolver implements ParameterResolver, AfterAllCallback {
  private final CachingTcpConnectionTemplate tcpConnectionTemplate = new CachingTcpConnectionTemplate();

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    return parameterContext.getParameter().getType().isAssignableFrom(CachingTcpConnectionTemplate.class)
        && parameterContext.isAnnotated(ReuseTemplate.class);
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    return tcpConnectionTemplate;
  }

  @Override
  public void afterAll(ExtensionContext context) throws Exception {
    tcpConnectionTemplate.close();
  }
}

ParameterResolver и AfterAllCallbackот JUnit.

@ReuseTemplate - это пользовательская аннотация

ReuseTemplate.java:

@Retention(RetentionPolicy.RUNTIME)
public @interface ReuseTemplate {
}

Наконец, тест:

@ExtendWith(TcpConnectionOperationsParameterResolver.class)
public class Tests2 {
  private final TcpConnectionOperations tcpConnectionOperations;

  public Tests2(@ReuseTemplate TcpConnectionOperations tcpConnectionOperations) {
    this.tcpConnectionOperations = tcpConnectionOperations;
  }

  @Test
  void google80() throws UnknownHostException {
    tcpConnectionOperations.doWithConnectionTo(new IpAndPort(InetAddress.getByName("google.com"), 80), tcpConnection -> {
      System.out.println("Using " + tcpConnection);
      return tcpConnection.toString();
    });
  }

  @Test
  void google80_2() throws Exception {
    tcpConnectionOperations.doWithConnectionTo(new IpAndPort(InetAddress.getByName("google.com"), 80), tcpConnection -> {
      System.out.println("Using " + tcpConnection);
      return tcpConnection.toString();
    });
  }

  @Test
  void google443() throws Exception {
    tcpConnectionOperations.doWithConnectionTo(new IpAndPort(InetAddress.getByName("google.com"), 443), tcpConnection -> {
      System.out.println("Using " + tcpConnection);
      return tcpConnection.toString();
    });
  }
}

Запуск:

$ mvn test

Выход:

Created new template
[INFO] Running Tests2
Executing action with connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Using TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Returned connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Executing action with connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Using TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Returned connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Executing action with connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Using TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Returned connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Closed TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Closed TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Template closed

Ключевое наблюдение здесь заключается в том, что соединения используются повторно (см. "instance=")

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

Возвращаясь к вопросу

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

См. Пользователь Junit 5Руководство.Модель расширения

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

Вы перегрузили finalize, так что это вызывает исключение, но они игнорируются.

См.Object#finalize

Если метод finalize создает необработанное исключение, оно игнорируется и завершается завершение этого объекта.

лучшее, что вы можете здесь сделать - это зарегистрировать утечку ресурса и close ресурс

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

Как тесты приложений используют ваш ресурс?Они создают его, используя оператор new?Если да, то я думаю, что PowerMock может помочь вам (но я не уверен)

Если у вас есть скрытая реализация ресурса за какой-то фабрикой, то вы можете дать тесты приложениякакая-то фальшивая фабрика


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

0 голосов
/ 01 февраля 2019

Если вас интересует согласованность тестов, просто добавьте метод 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();
        });
    }
}
...