Я создаю библиотеку Java. Некоторые из классов, которые предназначены для использования пользователями библиотеки, содержат собственные системные ресурсы (через JNI). Я хотел бы убедиться, что пользователь «удаляет» эти объекты, поскольку они тяжелые, и в тестовом наборе они могут вызвать утечку между тестовыми наборами (например, мне нужно убедиться, что TearDown будет удалять). Для этой цели я сделал классы Java реализующими AutoCloseable, но этого недостаточно, или я использую его неправильно:
Я не вижу, как использовать оператор try-with-resources в контексте тестов (я использую JUnit5 с Mockito), поскольку «ресурс» не является недолговечным - он является частью тестового приспособления.
Я, как всегда, старался внедрить 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. Однако кажется, что действия по очистке, хотя и зарегистрированы, не вызываются. Я задал дополнительный вопрос здесь.
ваш вопрос только о тестах или о принуждении к использованию метода #close вообще?
Похоже, вы хотите проверить это на своих тестах Приложения, а не на собственных тестах библиотеки. В этом случае вы вполне можете смоделировать объекты AutoCloseable и просто проверить, вызываются ли смоделированные методы close, когда вы ожидаете, что они будут. Есть много способов издеваться над этим, Mockito, JMock...
@MTCoster, вопрос в основном об «их» тестах. Мой класс предоставляет метод close(), и его отсутствие равносильно неправильному использованию моего API. Поэтому я хотел бы как-то обратить внимание на эту ошибку, прежде чем переходить к следующему тестовому набору. Это можно как-то сделать? Я гибкий в том, что я мог другой аналогичный механизм/интерфейс, если это поможет
@AdamSkywalker, так что вопрос относится к использованию моего API в целом, но я иллюстрирую тестами. Приложение должно закрыться, иначе могут произойти неприятные вещи. Чтобы проиллюстрировать, что я имею в виду, подумайте о SO_LINGER для TCP-соединений. На нативной стороне у меня есть такие вещи, как конечные автоматы, которым необходимо войти в определенные конечные состояния (вы можете сказать, что, если они этого не сделают, так как приложение все равно закрывается? ну, не совсем, иногда вам нужно «закрытие», чтобы гарантировать, что все данные были переданы между сверстниками, которые работают в сети)
Из документы на AutoClosable: «Обратите внимание, что в отличие от метода close для Closeable, этот метод close не обязательно должен быть идемпотентным. Другими словами, вызов этого метода close более одного раза может иметь видимый побочный эффект, в отличие от Closeable.close, который не должен иметь никакого эффекта, если вызывается более одного раза.» — вы можете выдать ResourceAlreadyClosedException после первого вызова close, а затем поручить пользователям проверять это исключение в своих тестах.
Это хороший момент, и я занимаюсь случаем, когда close() вызывается дважды, но мой вопрос касается close(), вызываемого ноль раз.
Итак, ваша основная проблема заключается в вызове close перед выключением? Я бы, вероятно, изучил комбинацию хука выключения и статического слабого списка ссылок на экземпляры класса, которые необходимо закрыть.
Больше похоже на то, о чем должен предупреждать компилятор, а не на то, что нужно реализовать во время выполнения. Я думаю, что все потоки будут закрыты ОС, если JVM полностью отключится.
@haelix Что касается finalize - есть новая штука под названием Cleaner, которая в наши дни предпочтительнее финализаторов. Это также хорошо работает с автозакрывающимися элементами, как видно из примера на странице API. Тем не менее, по-прежнему нет никаких гарантий относительно того, когда и даже будут ли они запускаться (если только вы не вызовете очистку вручную).




Если вас интересует согласованность в тестах, просто добавьте в тестовый класс метод 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 рекомендуется использовать для очистки ресурсов.
Я думаю, что решение, включающее Phantom или Weak, — это то, что нужно, в основном какой-то механизм уведомления, поэтому моя библиотека знает, когда пользователь больше не держит Strong ref. В этот момент она должна была закрыться() ресурс, иначе библиотека развязывает ад.
«Должен был закрыться()» — если нет, то в тестах вы можете пометить этот кейс как неудавшийся, в продакшне вы можете молча закрыть его для них, освободив ресурс.
Я понимаю. В этом случае я бы рассмотрел два варианта: 1) создать поток менеджера, который создает и закрывает объекты по запросу, который будет работать даже в среде prod, 2) отслеживать поток и трассировку стека, где были созданы отдельные объекты, и сообщать о них при обнаружении неисправности. Это поможет обнаружению ошибок.
я бы предоставил экземпляры для этих объектов через 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() ресурса после каждой операции.
В общем, если бы вы могли надежно проверить, был ли закрыт ресурс, вы могли бы просто закрыть его самостоятельно.
Первое, что нужно сделать, это упростить работу с ресурсом для клиента. Используйте идиому 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, например, имеет средство расширения на основе аннотаций, которое позволяет вам предоставлять контекст в качестве аргумента, а также применять проверки после выполнения каждого теста. (Но я им мало пользовался, так что больше ничего не скажу.)
Тесты вашей библиотеки не должны терпеть неудачу, если ресурсы не закрыты. Поскольку эта операция закрытия явно предоставляется пользователю, это то, что должны проверять тесты их. Однако вы можете проверить, что вызов операции закрытия действительно закрывает системные ресурсы, но в этом случае вы можете явно закрыть ресурс в своем тесте.