diff --git a/README.md b/README.md
index 0438255c30d..1bc9f498621 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@
`$ ant clean dist`
- - To create maven artifacts (after building .jar using ant), use [following guide](./maven/README.txt).
+ - To create maven artifacts (after building .jar using ant), use [following guide](./maven/README.md).
### How to verify GWT code conventions:
diff --git a/user/build.xml b/user/build.xml
index 2af156012ff..16282cdad22 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -163,6 +163,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -178,6 +189,7 @@
+
diff --git a/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java
index a21e4fb7a3f..bca9fc8d276 100644
--- a/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java
+++ b/user/src/com/google/gwt/user/server/rpc/AbstractRemoteServiceServlet.java
@@ -19,7 +19,6 @@
import java.io.IOException;
-import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@@ -41,9 +40,8 @@ public AbstractRemoteServiceServlet() {
* Standard HttpServlet method: handle the POST. Delegates to
* {@link #processPost(HttpServletRequest, HttpServletResponse)}.
*
- * This doPost method swallows ALL exceptions, logs them in the
- * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code
- * 500.
+ * This doPost method swallows ALL exceptions, logs them,
+ * and returns a GENERIC_FAILURE_MSG response with status code 500.
*/
@Override
public final void doPost(HttpServletRequest request,
@@ -106,9 +104,7 @@ protected void doUnexpectedFailure(Throwable e) {
*/
throw new RuntimeException("Unable to report failure", e);
}
- ServletContext servletContext = getServletContext();
- RPCServletUtils.writeResponseForUnexpectedFailure(servletContext,
- getThreadLocalResponse(), e);
+ RPCServletUtils.writeResponseForUnexpectedFailure(getThreadLocalResponse(), e);
}
/**
diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
index 8a2a50fb012..30cd3eb5327 100644
--- a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
+++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
@@ -15,6 +15,8 @@
*/
package com.google.gwt.user.server.rpc;
+import com.google.gwt.user.server.rpc.logging.RpcLogManager;
+import com.google.gwt.user.server.rpc.logging.RpcLogger;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -37,6 +39,8 @@
*/
public class RPCServletUtils {
+ private static final RpcLogger logger = RpcLogManager.getLogger(RPCServletUtils.class);
+
public static final String CHARSET_UTF8_NAME = "UTF-8";
/**
@@ -62,9 +66,11 @@ public class RPCServletUtils {
private static final String CONTENT_ENCODING_GZIP = "gzip";
- private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";
+ private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 =
+ "application/json; charset=utf-8";
- private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";
+ private static final String GENERIC_FAILURE_MSG =
+ "The call failed on the server; see server log for details";
private static final String GWT_RPC_CONTENT_TYPE = "text/x-gwt-rpc";
@@ -319,12 +325,21 @@ public static boolean shouldGzipResponseContent(HttpServletRequest request,
&& exceedsUncompressedContentLengthLimit(responseContent);
}
+ /**
+ * @deprecated Use {@link #writeResponse(HttpServletResponse, String, boolean)} instead; the
+ * servlet context is no longer needed.
+ */
+ @Deprecated
+ public static void writeResponse(ServletContext ignored, HttpServletResponse response,
+ String responseContent, boolean gzipResponse) throws IOException {
+ writeResponse(response, responseContent, gzipResponse);
+ }
+
/**
* Write the response content into the {@link HttpServletResponse}. If
* gzipResponse is true, the response content will
* be gzipped prior to being written into the response.
*
- * @param servletContext servlet context for this response
* @param response response instance
* @param responseContent a string containing the response content
* @param gzipResponse if true the response content will be gzip
@@ -332,9 +347,8 @@ public static boolean shouldGzipResponseContent(HttpServletRequest request,
* @throws IOException if reading, writing, or closing the response's output
* stream fails
*/
- public static void writeResponse(ServletContext servletContext,
- HttpServletResponse response, String responseContent, boolean gzipResponse)
- throws IOException {
+ public static void writeResponse(HttpServletResponse response, String responseContent,
+ boolean gzipResponse) throws IOException {
byte[] responseBytes = responseContent.getBytes(StandardCharsets.UTF_8);
if (gzipResponse) {
@@ -363,7 +377,7 @@ public static void writeResponse(ServletContext servletContext,
}
if (caught != null) {
- servletContext.log("Unable to compress response", caught);
+ logger.error("Unable to compress response", caught);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
@@ -378,18 +392,26 @@ public static void writeResponse(ServletContext servletContext,
response.getOutputStream().write(responseBytes);
}
+ /**
+ * @deprecated Use {@link #writeResponseForUnexpectedFailure(HttpServletResponse, Throwable)}
+ * instead; the servlet context is no longer needed.
+ */
+ @Deprecated
+ public static void writeResponseForUnexpectedFailure(ServletContext ignored,
+ HttpServletResponse response, Throwable failure) {
+ writeResponseForUnexpectedFailure(response, failure);
+ }
+
/**
* Called when the servlet itself has a problem, rather than the invoked
* third-party method. It writes a simple 500 message back to the client.
*
- * @param servletContext
* @param response
* @param failure
*/
- public static void writeResponseForUnexpectedFailure(
- ServletContext servletContext, HttpServletResponse response,
+ public static void writeResponseForUnexpectedFailure(HttpServletResponse response,
Throwable failure) {
- servletContext.log("Exception while dispatching incoming RPC call", failure);
+ logger.error("Exception while dispatching incoming RPC call", failure);
// Send GENERIC_FAILURE_MSG with 500 status.
//
@@ -403,7 +425,7 @@ public static void writeResponseForUnexpectedFailure(
response.getWriter().write(GENERIC_FAILURE_MSG);
}
} catch (IOException ex) {
- servletContext.log(
+ logger.error(
"respondWithUnexpectedFailure failed while sending the previous failure to the client",
ex);
}
@@ -431,7 +453,8 @@ private static void checkCharacterEncodingIgnoreCase(
* properly parsed character encoding string if we decide to make this
* change.
*/
- if (characterEncoding.toLowerCase(Locale.ROOT).contains(expectedCharSet.toLowerCase(Locale.ROOT))) {
+ if (characterEncoding.toLowerCase(Locale.ROOT)
+ .contains(expectedCharSet.toLowerCase(Locale.ROOT))) {
encodingOkay = true;
}
}
diff --git a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
index 1bd73e142ef..28ff67d5ea1 100644
--- a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
+++ b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
@@ -22,6 +22,8 @@
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RpcTokenException;
import com.google.gwt.user.client.rpc.SerializationException;
+import com.google.gwt.user.server.rpc.logging.RpcLogManager;
+import com.google.gwt.user.server.rpc.logging.RpcLogger;
import java.io.IOException;
import java.io.InputStream;
@@ -45,6 +47,8 @@
public class RemoteServiceServlet extends AbstractRemoteServiceServlet
implements SerializationPolicyProvider {
+ private static final RpcLogger logger = RpcLogManager.getLogger(RemoteServiceServlet.class);
+
/**
* Loads a serialization policy stored as a servlet resource in the same
* ServletContext as this servlet. Returns null if not found.
@@ -62,7 +66,7 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet,
modulePath = new URL(moduleBaseURL).getPath();
} catch (MalformedURLException ex) {
// log the information, we will default
- servlet.log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
+ logger.error("Malformed moduleBaseURL: " + moduleBaseURL, ex);
}
}
@@ -74,19 +78,20 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet,
* this method.
*/
if (modulePath == null || !modulePath.startsWith(contextPath)) {
- String message = "ERROR: The module path requested, "
+ String message = "The module path requested, "
+ modulePath
+ ", is not in the same web application as this servlet, "
+ contextPath
- + ". Your module may not be properly configured or your client and server code maybe out of date.";
- servlet.log(message);
+ + ". Your module may not be properly configured " +
+ "or your client and server code maybe out of date.";
+ logger.error(message);
} else {
// Strip off the context path from the module base URL. It should be a
// strict prefix.
String contextRelativePath = modulePath.substring(contextPath.length());
- String serializationPolicyFilePath = SerializationPolicyLoader.getSerializationPolicyFileName(contextRelativePath
- + strongName);
+ String serializationPolicyFilePath = SerializationPolicyLoader.getSerializationPolicyFileName(
+ contextRelativePath + strongName);
// Open the RPC resource file and read its contents.
InputStream is = servlet.getServletContext().getResourceAsStream(
@@ -98,30 +103,30 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet,
null);
if (serializationPolicy.hasClientFields()) {
if (ENABLE_ENHANCED_CLASSES) {
- servlet.log("WARNING: Service deserializes enhanced JPA/JDO classes, which is " +
- "unsafe. See https://github.com/gwtproject/gwt/issues/9709 for more " +
- "detail on the vulnerability that this presents.");
+ logger.warn("Service deserializes enhanced JPA/JDO classes, which is " +
+ "unsafe. See https://github.com/gwtproject/gwt/issues/9709 for more " +
+ "detail on the vulnerability that this presents.");
} else {
- servlet.log("ERROR: Service deserializes enhanced JPA/JDO classes, which is " +
- "unsafe. Review build logs to see which classes are affected, or set " +
- ENABLE_GWT_ENHANCED_CLASSES_PROPERTY + " to true to allow using this " +
- "service. See https://github.com/gwtproject/gwt/issues/9709 for more " +
- "detail.");
+ logger.error("Service deserializes enhanced JPA/JDO classes, which is " +
+ "unsafe. Review build logs to see which classes are affected, or set " +
+ ENABLE_GWT_ENHANCED_CLASSES_PROPERTY + " to true to allow using this " +
+ "service. See https://github.com/gwtproject/gwt/issues/9709 for more " +
+ "detail.");
serializationPolicy = null;
}
}
} catch (ParseException e) {
- servlet.log("ERROR: Failed to parse the policy file '"
+ logger.error("Failed to parse the policy file '"
+ serializationPolicyFilePath + "'", e);
} catch (IOException e) {
- servlet.log("ERROR: Could not read the policy file '"
+ logger.error("Could not read the policy file '"
+ serializationPolicyFilePath + "'", e);
}
} else {
- String message = "ERROR: The serialization policy file '"
+ String message = "The serialization policy file '"
+ serializationPolicyFilePath
+ "' was not found; did you forget to include it in this deployment?";
- servlet.log(message);
+ logger.error(message);
}
} finally {
if (is != null) {
@@ -144,7 +149,8 @@ static SerializationPolicy loadSerializationPolicy(HttpServlet servlet,
* A cache of moduleBaseURL and serialization policy strong name to
* {@link SerializationPolicy}.
*/
- private final Map serializationPolicyCache = new HashMap();
+ private final Map serializationPolicyCache =
+ new HashMap();
/**
* The implementation of the service.
@@ -177,11 +183,13 @@ public RemoteServiceServlet(Object delegate) {
}
/**
- * Overridden to load the gwt.codeserver.port system property.
+ * Overridden to load the gwt.codeserver.port system property and initialize the
+ * {@link RpcLogManager} with a provider name from system properties or the servlet config.
*/
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
+ RpcLogManager.setServletContext(config.getServletContext());
codeServerPort = getCodeServerPort();
}
@@ -260,12 +268,12 @@ public final SerializationPolicy getSerializationPolicy(String moduleBaseURL,
if (serializationPolicy == null) {
// Failed to get the requested serialization policy; use the default
- log(
- "WARNING: Failed to get the SerializationPolicy '"
+ logger.warn("Failed to get the SerializationPolicy '"
+ strongName
+ "' for module '"
+ moduleBaseURL
- + "'; a legacy, 1.3.3 compatible, serialization policy will be used. You may experience SerializationExceptions as a result.");
+ + "'; a legacy, 1.3.3 compatible, serialization policy will be used. " +
+ "You may experience SerializationExceptions as a result.");
serializationPolicy = RPC.getDefaultSerializationPolicy();
}
@@ -311,7 +319,7 @@ public String processCall(String payload) throws SerializationException {
try {
rpcRequest = RPC.decodeRequest(payload, delegate.getClass(), this);
} catch (IncompatibleRemoteServiceException ex) {
- log(
+ logger.error(
"An IncompatibleRemoteServiceException was thrown while processing this call.",
ex);
return RPC.encodeResponseForFailedRequest(null, ex);
@@ -350,13 +358,12 @@ public String processCall(RPCRequest rpcRequest) throws SerializationException {
rpcRequest.getParameters(), rpcRequest.getSerializationPolicy(),
rpcRequest.getFlags());
} catch (IncompatibleRemoteServiceException ex) {
- log(
+ logger.error(
"An IncompatibleRemoteServiceException was thrown while processing this call.",
ex);
return RPC.encodeResponseForFailedRequest(rpcRequest, ex);
} catch (RpcTokenException tokenException) {
- log("An RpcTokenException was thrown while processing this call.",
- tokenException);
+ logger.error("An RpcTokenException was thrown while processing this call.");
return RPC.encodeResponseForFailedRequest(rpcRequest, tokenException);
}
}
@@ -364,9 +371,8 @@ public String processCall(RPCRequest rpcRequest) throws SerializationException {
/**
* Standard HttpServlet method: handle the POST.
*
- * This doPost method swallows ALL exceptions, logs them in the
- * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code
- * 500.
+ * This doPost method swallows ALL exceptions, logs them,
+ * and returns a GENERIC_FAILURE_MSG response with status code 500.
*
* @throws ServletException
* @throws SerializationException
@@ -465,19 +471,7 @@ protected String getCodeServerPolicyUrl(String strongName) {
* no authentication. It should only be used during development.
*/
protected SerializationPolicy loadPolicyFromCodeServer(String url) {
- SerializationPolicyClient.Logger adapter = new SerializationPolicyClient.Logger() {
-
- @Override
- public void logInfo(String message) {
- RemoteServiceServlet.this.log(message);
- }
-
- @Override
- public void logError(String message, Throwable throwable) {
- RemoteServiceServlet.this.log(message, throwable);
- }
- };
- return CODE_SERVER_CLIENT.loadPolicy(url, adapter);
+ return CODE_SERVER_CLIENT.loadPolicy(url);
}
/**
@@ -541,7 +535,6 @@ private void writeResponse(HttpServletRequest request,
boolean gzipEncode = RPCServletUtils.acceptsGzipEncoding(request)
&& shouldCompressResponse(request, response, responsePayload);
- RPCServletUtils.writeResponse(getServletContext(), response,
- responsePayload, gzipEncode);
+ RPCServletUtils.writeResponse(response, responsePayload, gzipEncode);
}
}
diff --git a/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java b/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java
index dffc45a8841..e228d5598a2 100644
--- a/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java
+++ b/user/src/com/google/gwt/user/server/rpc/SerializationPolicyClient.java
@@ -15,6 +15,9 @@
*/
package com.google.gwt.user.server.rpc;
+import com.google.gwt.user.server.rpc.logging.RpcLogManager;
+import com.google.gwt.user.server.rpc.logging.RpcLogger;
+
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
@@ -30,11 +33,14 @@
* (Intended only for development.)
*/
class SerializationPolicyClient {
+
+ private static final RpcLogger logger = RpcLogManager.getLogger(SerializationPolicyClient.class);
private final int connectTimeout;
private final int readTimeout;
/**
- * Creates an client with the given configuration,
+ * Creates a client to load serialization policies from a dev mode server, with the given
+ * timeouts.
* @param connectTimeoutMs see {@link URLConnection#setConnectTimeout}
* @param readTimeoutMs see {@link URLConnection#setReadTimeout}
*/
@@ -43,12 +49,12 @@ class SerializationPolicyClient {
this.readTimeout = readTimeoutMs;
}
- SerializationPolicy loadPolicy(String url, Logger logger) {
+ SerializationPolicy loadPolicy(String url) {
URL urlObj;
try {
urlObj = new URL(url);
} catch (MalformedURLException e) {
- logger.logError("Can't parse serialization policy URL: " + url, e);
+ logger.error("Can't parse serialization policy URL: " + url, e);
return null;
}
@@ -61,16 +67,16 @@ SerializationPolicy loadPolicy(String url, Logger logger) {
// The code server doesn't redirect. Fail fast if we get a redirect since
// it's likely a configuration error.
if (conn instanceof HttpURLConnection) {
- ((HttpURLConnection)conn).setInstanceFollowRedirects(false);
+ ((HttpURLConnection) conn).setInstanceFollowRedirects(false);
}
conn.connect();
in = conn.getInputStream();
} catch (IOException e) {
- logger.logError("Can't open serialization policy URL: " + url, e);
+ logger.error("Can't open serialization policy URL: " + url, e);
return null;
}
- return readPolicy(in, url, logger);
+ return readPolicy(in, url);
}
/**
@@ -79,34 +85,33 @@ SerializationPolicy loadPolicy(String url, Logger logger) {
* @param sourceName names the source of the input stream for log messages.
* @return the policy or null if unavailable.
*/
- private static SerializationPolicy readPolicy(InputStream in, String sourceName,
- Logger logger) {
+ private static SerializationPolicy readPolicy(InputStream in, String sourceName) {
try {
List errs = new ArrayList();
SerializationPolicy policy = SerializationPolicyLoader.loadFromStream(in, errs);
- logger.logInfo("Downloaded serialization policy from " + sourceName);
+ logger.info("Downloaded serialization policy from " + sourceName);
if (!errs.isEmpty()) {
- logMissingClasses(logger, errs);
+ logMissingClasses(errs);
}
return policy;
} catch (ParseException e) {
- logger.logError("Can't parse serialization policy from " + sourceName, e);
+ logger.error("Can't parse serialization policy from " + sourceName, e);
return null;
} catch (IOException e) {
- logger.logError("Can't read serialization policy from " + sourceName, e);
+ logger.error("Can't read serialization policy from " + sourceName, e);
return null;
} finally {
try {
in.close();
} catch (IOException e) {
- logger.logError("Can't close serialization policy url: " + sourceName, e);
+ logger.error("Can't close serialization policy url: " + sourceName, e);
}
}
}
- private static void logMissingClasses(Logger logger, List errs) {
+ private static void logMissingClasses(List errs) {
StringBuilder message = new StringBuilder();
message.append("Unable to load server-side classes used by policy:\n");
@@ -118,14 +123,7 @@ private static void logMissingClasses(Logger logger, List 0) {
message.append(" (omitted " + omitted + " more classes)\n");
}
- logger.logInfo(message.toString());
+ logger.info(message.toString());
}
- /**
- * Destination for the loader's log messages.
- */
- interface Logger {
- void logInfo(String message);
- void logError(String message, Throwable throwable);
- }
}
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/JulLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/JulLoggerProvider.java
new file mode 100644
index 00000000000..7e8cc14beab
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/logging/JulLoggerProvider.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2025 GWT Project Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.server.rpc.logging;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An {@link RpcLoggerProvider} that delegates to {@link java.util.logging.Logger}.
+ *
+ * @see RpcLogManager
+ */
+public class JulLoggerProvider implements RpcLoggerProvider {
+
+ /**
+ * Public for {@link java.util.ServiceLoader}; not intended for direct use outside this package.
+ */
+ public JulLoggerProvider() {
+ }
+
+ @Override
+ public RpcLogger createLogger(String name) {
+ return new JulLogger(Logger.getLogger(name));
+ }
+
+ private static final class JulLogger implements RpcLogger {
+ private final Logger logger;
+
+ JulLogger(Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void info(String message) {
+ logger.log(Level.INFO, message);
+ }
+
+ @Override
+ public void warn(String message) {
+ logger.log(Level.WARNING, message);
+ }
+
+ @Override
+ public void error(String message) {
+ logger.log(Level.SEVERE, message);
+ }
+
+ @Override
+ public void error(String message, Throwable throwable) {
+ logger.log(Level.SEVERE, message, throwable);
+ }
+ }
+
+}
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java
new file mode 100644
index 00000000000..0b749816b3b
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogManager.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2025 GWT Project Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.server.rpc.logging;
+
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.servlet.ServletContext;
+
+/**
+ * Entry point for obtaining {@link RpcLogger}s. Initializes the {@link RpcLoggerProvider} on class
+ * load using {@link ServiceLoader} to discover available implementations.
+ *
+ * The provider is chosen in this order:
+ *
+ * - The first provider with a fully-qualified class name that matches the system property with
+ * the key {@link #PROVIDER_PROPERTY_KEY} (
gwt.rpc.logging)
+ * - The first provider for which {@link RpcLoggerProvider#isDefault()} returns
+ *
true
+ * - The {@link ServletContextLoggerProvider}
+ *
+ */
+public class RpcLogManager {
+
+ /**
+ * System property key for selecting an {@link RpcLoggerProvider} by fully-qualified class name.
+ */
+ public static final String PROVIDER_PROPERTY_KEY = "gwt.rpc.logging";
+ private static final ConcurrentHashMap loggers = new ConcurrentHashMap<>();
+ private static final RpcLoggerProvider loggerProvider = loadProvider();
+
+ /**
+ * Creates or retrieves a logger for the fully-qualified name of the given class.
+ *
+ * @param clazz the class for which to return a logger
+ * @return a logger
+ */
+ public static RpcLogger getLogger(Class> clazz) {
+ return loggers.computeIfAbsent(clazz.getName(), loggerProvider::createLogger);
+ }
+
+ /**
+ * Sets the servlet context of the {@link ServletContextLoggerProvider} to use for logging. Has no
+ * effect if the provider is not a {@link ServletContextLoggerProvider}.
+ *
+ * @param servletContext the servlet context to use
+ */
+ public static void setServletContext(ServletContext servletContext) {
+ if (loggerProvider instanceof ServletContextLoggerProvider) {
+ ((ServletContextLoggerProvider) loggerProvider).setServletContext(servletContext);
+ }
+ }
+
+ /**
+ * Loads available providers and chooses the first whose class matches the name given with the
+ * {@link #PROVIDER_PROPERTY_KEY} system property, or, failing that, the first for which
+ * {@link RpcLoggerProvider#isDefault()} returns true. If none found, returns a
+ * {@link ServletContextLoggerProvider} as a fallback.
+ *
+ * @return a logger provider
+ */
+ private static RpcLoggerProvider loadProvider() {
+ String providerClassName = System.getProperty(PROVIDER_PROPERTY_KEY);
+ ServiceLoader loaderService = ServiceLoader.load(RpcLoggerProvider.class);
+ for (RpcLoggerProvider provider : loaderService) {
+ if (provider.getClass().getName().equals(providerClassName)) {
+ return provider;
+ }
+ }
+ for (RpcLoggerProvider provider : loaderService) {
+ if (provider.isDefault()) {
+ return provider;
+ }
+ }
+ return new ServletContextLoggerProvider();
+ }
+
+ private RpcLogManager() {
+ // Not instantiable
+ }
+
+}
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLogger.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogger.java
new file mode 100644
index 00000000000..7958d8842d8
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLogger.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 GWT Project Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.server.rpc.logging;
+
+/**
+ * A minimal logging facade used by GWT's server-side RPC package.
+ *
+ * Instances can be obtained from {@link RpcLogManager#getLogger(Class)}. The logging system that
+ * this delegates to is selected by {@link RpcLogManager} at class-initialization.
+ *
+ * @see RpcLogManager
+ * @see RpcLoggerProvider
+ */
+public interface RpcLogger {
+
+ void info(String message);
+
+ void warn(String message);
+
+ void error(String message);
+
+ void error(String message, Throwable throwable);
+
+}
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/RpcLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/RpcLoggerProvider.java
new file mode 100644
index 00000000000..51c9de5ac0e
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/logging/RpcLoggerProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 GWT Project Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.server.rpc.logging;
+
+/**
+ * Service provider interface for obtaining {@link RpcLogger} instances.
+ *
+ * This is not intended to be used directly, but through {@link RpcLogManager#getLogger(Class)},
+ * which discovers implementations with a service loader
+ *
+ * @see RpcLogManager
+ */
+public interface RpcLoggerProvider {
+
+ /**
+ * Creates or retrieves a logger with the given name.
+ *
+ * @param name the name of the logger to create or retrieve
+ * @return the created or retrieved logger
+ */
+ RpcLogger createLogger(String name);
+
+ /**
+ * Indicates whether this provider should be used as the default in the absence of a provider
+ * explicitly selected with the {@link RpcLogManager#PROVIDER_PROPERTY_KEY} system property.
+ *
+ * @return true if this provider should be used when no named provider is found
+ */
+ default boolean isDefault() {
+ return false;
+ }
+
+}
diff --git a/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java b/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java
new file mode 100644
index 00000000000..8fee64ddce0
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/logging/ServletContextLoggerProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2025 GWT Project Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.server.rpc.logging;
+
+import javax.servlet.ServletContext;
+
+/**
+ * An {@link RpcLoggerProvider} that delegates to the servlet context's logging. Used as the
+ * fallback if no other logger provider is found.
+ *
+ * Servlet context logging does not support separate named loggers, so this reuses the same logger
+ * instance. This reuse allows the servlet context to be easily set after the provider is
+ * initialized, and normally the provider will be initialized before the servlet context is even
+ * available. Logs are written to {@link System#out} and {@link System#err} until the servlet
+ * context is set.
+ *
+ * @see RpcLogManager
+ */
+public class ServletContextLoggerProvider implements RpcLoggerProvider {
+
+ private final ServletContextLogger logger = new ServletContextLogger();
+
+ /**
+ * Public for {@link java.util.ServiceLoader}; not intended for direct use outside this package.
+ */
+ public ServletContextLoggerProvider() {
+ }
+
+ /**
+ * Sets the {@link ServletContext} to which log messages will be written.
+ *
+ * @param servletContext the servlet context to use
+ */
+ void setServletContext(ServletContext servletContext) {
+ if (servletContext != null) {
+ logger.servletContext = servletContext;
+ }
+ }
+
+ @Override
+ public RpcLogger createLogger(String ignored) {
+ return logger;
+ }
+
+ private static final class ServletContextLogger implements RpcLogger {
+
+ private volatile ServletContext servletContext;
+
+ private ServletContextLogger() { }
+
+ @Override
+ public void info(String message) {
+ if (servletContext != null) {
+ servletContext.log("INFO: " + message);
+ } else {
+ System.out.println("INFO: " + message);
+ }
+ }
+
+ @Override
+ public void warn(String message) {
+ if (servletContext != null) {
+ servletContext.log("WARNING: " + message);
+ } else {
+ System.out.println("WARNING: " + message);
+ }
+ }
+
+ @Override
+ public void error(String message) {
+ if (servletContext != null) {
+ servletContext.log("ERROR: " + message);
+ } else {
+ System.err.println("ERROR: " + message);
+ }
+ }
+
+ @Override
+ public void error(String message, Throwable throwable) {
+ if (servletContext != null) {
+ servletContext.log("ERROR: " + message, throwable);
+ } else {
+ System.err.println("ERROR: " + message);
+ throwable.printStackTrace();
+ }
+ }
+ }
+
+}