Release instances blocking GC when JAXB has PerThread injections
Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
index e05cb5f..548a7e8 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientMessageBodyFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,6 +16,7 @@
package org.glassfish.jersey.client;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.BootstrapBag;
import org.glassfish.jersey.internal.BootstrapConfigurator;
import org.glassfish.jersey.internal.inject.Bindings;
@@ -29,7 +30,7 @@
import javax.ws.rs.core.Configuration;
-class ClientMessageBodyFactory extends MessageBodyFactory {
+class ClientMessageBodyFactory extends MessageBodyFactory implements SafelyClosable {
/**
* Keep reference to {@link ClientRuntime} so that {@code finalize} on it is not called
@@ -39,7 +40,7 @@
* but if the finalizer is invoked before that, the HK2 injection manager gets closed.
* </p>
*/
- private final LazyValue<ClientRuntime> clientRuntime;
+ private LazyValue<ClientRuntime> clientRuntime;
/**
* Create a new message body factory.
@@ -52,6 +53,11 @@
clientRuntime = Values.lazy(clientRuntimeValue);
}
+ @Override
+ public void close() {
+ clientRuntime = null;
+ }
+
/**
* Configurator which initializes and register {@link MessageBodyWorkers} instance into {@link InjectionManager} and
* {@link BootstrapBag}.
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java b/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java
new file mode 100644
index 0000000..33983b6
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/io/SafelyClosable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.innate.io;
+
+/**
+ * A SafelyClosable is a resource that can be closed.
+ * The close method is invoked to release resources that the object is holding.
+ * Closing the resource is safe in a sense that no Exception is being thrown.
+ */
+public interface SafelyClosable {
+
+ /**
+ * Close the resource, no checked exception thrown.
+ */
+ void close();
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java b/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
index 4df032c..0eb86f3 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/MessageBodyWorkers.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -32,6 +32,7 @@
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.WriterInterceptor;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.PropertiesDelegate;
/**
@@ -43,7 +44,7 @@
* @see MessageBodyReader
* @see MessageBodyWriter
*/
-public interface MessageBodyWorkers {
+public interface MessageBodyWorkers extends SafelyClosable {
/**
* Get the map of media type to list of message body writers that are compatible with
* a media type.
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
index a9c6dc1..26c4651 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -23,17 +23,13 @@
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
-import java.net.URI;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
@@ -41,21 +37,17 @@
import javax.ws.rs.ProcessingException;
import javax.ws.rs.core.Configuration;
-import javax.ws.rs.core.Cookie;
-import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Link;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.NewCookie;
import javax.ws.rs.ext.ReaderInterceptor;
-import javax.ws.rs.ext.RuntimeDelegate;
import javax.xml.transform.Source;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.LocalizationMessages;
import org.glassfish.jersey.internal.PropertiesDelegate;
-import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
import org.glassfish.jersey.internal.util.collection.LazyValue;
import org.glassfish.jersey.internal.util.collection.Value;
@@ -67,7 +59,7 @@
*
* @author Marek Potociar
*/
-public abstract class InboundMessageContext extends MessageHeaderMethods {
+public abstract class InboundMessageContext extends MessageHeaderMethods implements SafelyClosable {
private static final InputStream EMPTY = new InputStream() {
@@ -729,6 +721,9 @@
*/
public void close() {
entityContent.close(true);
+ if (workers != null) {
+ workers.close();
+ }
setWorkers(null);
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
index f8d31a2..698ab14 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/MessageBodyFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -1189,4 +1189,11 @@
}
return false;
}
+
+
+ @Override
+ public void close() {
+ // NOOP
+ }
+
}
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
index 8e10cdf..fbadb05 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -39,6 +39,7 @@
import javax.ws.rs.core.MultivaluedMap;
import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.innate.io.SafelyClosable;
import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap;
@@ -52,7 +53,7 @@
*
* @author Marek Potociar
*/
-public class OutboundMessageContext extends MessageHeaderMethods {
+public class OutboundMessageContext extends MessageHeaderMethods implements SafelyClosable {
private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
private static final List<MediaType> WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
Collections.<MediaType>singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java
new file mode 100644
index 0000000..8a836b7
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/JerseyClientRuntimeTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.client;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JerseyClientRuntimeTest {
+
+ private static int COUNT = 10;
+ private List<WeakReference<Object>> list = new ArrayList<>();
+ private ReferenceQueue queue = new ReferenceQueue();
+
+ @Test
+ public void testClientRuntimeInstancesAreGCed() throws InterruptedException {
+ Client c = ClientBuilder.newClient();
+ c.register(new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.abortWith(Response
+ .ok("<myDTO xmlns=\"http://org.example.dtos\"/>")
+ .type(MediaType.APPLICATION_XML_TYPE)
+ .build());
+ }
+ });
+
+ WebTarget target = c.target("http://localhost/nowhere");
+ for (int i = 0; i != COUNT; i++) {
+ target = target.property("SOME", "PROPERTY");
+ ClientConfig config = (ClientConfig) target.getConfiguration();
+ Object clientRuntime = getClientRuntime(config);
+ addToList(clientRuntime);
+ try (Response response = target.request().get()) {
+ MatcherAssert.assertThat(response.getStatus(), Matchers.is(200));
+ MyDTO dto = response.readEntity(MyDTO.class);
+ MatcherAssert.assertThat(dto, Matchers.notNullValue());
+ }
+ }
+
+ System.gc();
+ do {
+ Thread.sleep(100L);
+ } while (queueIsEmpty(queue));
+
+ c.close();
+
+ }
+
+ private static Object getClientRuntime(ClientConfig config) {
+ try {
+ Method m = ClientConfig.class.getDeclaredMethod("getRuntime");
+ m.setAccessible(true);
+ return m.invoke(config);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static boolean queueIsEmpty(ReferenceQueue queue) {
+ return queue.poll() == null;
+ }
+
+ private void addToList(Object object) {
+ list.add(new WeakReference<>(object, queue));
+ }
+
+ @XmlRootElement(name = "myDTO", namespace = "http://org.example.dtos")
+ public static class MyDTO {
+
+ }
+}