From 7dfd3f86da7e1616e01eac84901582f176cd5e50 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Fri, 9 Aug 2024 02:16:03 +0200 Subject: [PATCH 01/10] SQLTraceListener: More info in the trace record Fixed poolName. Added applicationName, moduleName, sqlQuery. --- .../connectors/ActiveResourceAdapterImpl.java | 21 ++-- .../gjc/spi/ManagedConnectionFactoryImpl.java | 8 +- .../com/sun/gjc/util/SQLTraceDelegator.java | 101 +++++++++++------- .../glassfish/api/jdbc/SQLTraceRecord.java | 92 ++++++++++++++-- 4 files changed, 165 insertions(+), 57 deletions(-) diff --git a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java index ee8c13c4f29..fcb47eefb39 100755 --- a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java +++ b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java @@ -32,6 +32,7 @@ import com.sun.enterprise.deployment.runtime.connector.ResourceAdapter; import com.sun.enterprise.util.i18n.StringManager; import com.sun.logging.LogDomains; +import jakarta.inject.Inject; import jakarta.resource.spi.ManagedConnectionFactory; @@ -41,6 +42,7 @@ import org.glassfish.api.naming.SimpleJndiName; import org.glassfish.hk2.api.PerLookup; +import org.glassfish.hk2.api.ServiceLocator; import org.glassfish.resourcebase.resources.api.PoolInfo; import org.glassfish.resourcebase.resources.api.ResourceInfo; import org.jvnet.hk2.annotations.Service; @@ -64,13 +66,15 @@ public class ActiveResourceAdapterImpl implements ActiveResourceAdapter { private static Logger _logger = LogDomains.getLogger(ActiveResourceAdapterImpl.class, LogDomains.RSR_LOGGER); private final StringManager localStrings = StringManager.getManager(ActiveResourceAdapterImpl.class); + @Inject + protected ServiceLocator locator; + protected ConnectorDescriptor desc_; protected String moduleName_; protected ClassLoader jcl_; protected ConnectionDefDescriptor[] connectionDefs_; protected ConnectorRuntime connectorRuntime_; - /** * Constructor. * @@ -385,17 +389,22 @@ private void setLogWriter(ManagedConnectionFactory mcf) { } - protected ManagedConnectionFactory instantiateMCF(String mcfClass, ClassLoader loader) throws Exception { + protected ManagedConnectionFactory instantiateMCF(String mcfClassName, ClassLoader loader) throws Exception { ManagedConnectionFactory mcf = null; + Class mcfClass; if (jcl_ != null) { - mcf = (ManagedConnectionFactory) jcl_.loadClass(mcfClass).getDeclaredConstructor().newInstance(); + mcfClass = jcl_.loadClass(mcfClassName); +// mcf = (ManagedConnectionFactory) mcfClass.getDeclaredConstructor().newInstance(); } else if (loader != null) { - mcf = (ManagedConnectionFactory) loader.loadClass(mcfClass).getDeclaredConstructor().newInstance(); + mcfClass = loader.loadClass(mcfClassName); +// mcf = (ManagedConnectionFactory) loader.loadClass(mcfClass).getDeclaredConstructor().newInstance(); } else { + mcfClass = Thread.currentThread().getContextClassLoader().loadClass(mcfClassName); // mcf = (ManagedConnectionFactory) Class.forName(mcfClass).newInstance(); - mcf = (ManagedConnectionFactory) Thread.currentThread().getContextClassLoader().loadClass(mcfClass) - .getDeclaredConstructor().newInstance(); +// mcf = (ManagedConnectionFactory) Thread.currentThread().getContextClassLoader().loadClass(mcfClass) +// .getDeclaredConstructor().newInstance(); } + mcf = locator.createAndInitialize((Class)mcfClass); setLogWriter(mcf); return mcf; } diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java index 082c8f6d513..776718e4e75 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java @@ -84,6 +84,9 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; +import jakarta.inject.Inject; +import org.glassfish.api.invocation.InvocationManager; + /** * ManagedConnectionFactory implementation for Generic JDBC * Connector. This class is extended by the DataSource specific @@ -101,6 +104,9 @@ public abstract class ManagedConnectionFactoryImpl private static Logger _logger = LogDomains.getLogger(ManagedConnectionFactoryImpl.class, LogDomains.RSR_LOGGER); protected static final StringManager localStrings = StringManager.getManager(DataSourceObjectBuilder.class); + @Inject + protected InvocationManager invocationManager; + protected DataSourceSpec spec = new DataSourceSpec(); protected transient DataSourceObjectBuilder dataSourceObjectBuilder; protected PrintWriter logWriter; @@ -553,7 +559,7 @@ private void detectSqlTraceListeners() { String delimiter = ","; if (sqlTraceListeners != null && !sqlTraceListeners.equals("null")) { - sqlTraceDelegator = new SQLTraceDelegator(getPoolName(), getApplicationName(), getModuleName()); + sqlTraceDelegator = new SQLTraceDelegator(getPoolName(), invocationManager); StringTokenizer st = new StringTokenizer(sqlTraceListeners, delimiter); while (st.hasMoreTokens()) { diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java index 3946bfcf017..858fa36afef 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Contributors to the Eclipse Foundation + * Copyright (c) 2022,2024 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -14,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package com.sun.gjc.util; import com.sun.gjc.monitoring.JdbcRAConstants; @@ -31,6 +30,9 @@ import static java.util.logging.Level.FINEST; +import org.glassfish.api.invocation.ComponentInvocation; +import org.glassfish.api.invocation.InvocationManager; + /** * Implementation of SQLTraceListener to listen to events related to a sql * record tracing. The registry allows multiple listeners to listen to the sql @@ -45,24 +47,21 @@ public class SQLTraceDelegator implements SQLTraceListener { // List of listeners protected List sqlTraceListenersList; private final SimpleJndiName poolName; - private final String appName; - private final String moduleName; + private InvocationManager invocationManager; private SQLTraceProbeProvider probeProvider = null; public SQLTraceProbeProvider getProbeProvider() { return probeProvider; } - public SQLTraceDelegator(SimpleJndiName poolName, String appName, String moduleName) { + public SQLTraceDelegator(SimpleJndiName poolName, InvocationManager invocationManager) { this.poolName = poolName; - this.appName = appName; - this.moduleName = moduleName; + this.invocationManager = invocationManager; probeProvider = new SQLTraceProbeProvider(); } /** - * Add a listener to the list of sql trace listeners maintained by this - * registry. + * Add a listener to the list of sql trace listeners maintained by this registry. * * @param listener */ @@ -75,50 +74,70 @@ public void registerSQLTraceListener(SQLTraceListener listener) { @Override public void sqlTrace(SQLTraceRecord record) { - if (sqlTraceListenersList != null) { - for (SQLTraceListener listener : sqlTraceListenersList) { - try { - listener.sqlTrace(record); - } catch (Exception e) { - // it is possible that any of the implementations may fail processing a trace - // record. - // do not propagate such failures. Log them as FINEST. - if (_logger.isLoggable(FINEST)) { - _logger.log(FINEST, - "exception from one of the SQL trace listeners [" + listener.getClass().getName() + "]", - e); + if (record != null) { + record.setPoolName(poolName.toString()); + + String sqlQuery = null; + record.setSqlQuery(findSqlQuery(record, sqlQuery)); + record.setApplicationName(getAppName()); + record.setModuleName(getModuleName()); + + if (record.getSqlQuery() != null) { + probeProvider.traceSQLEvent(poolName.toString(), record.getApplicationName(), + record.getModuleName(), record.getSqlQuery()); + } + + if (sqlTraceListenersList != null) { + for (SQLTraceListener listener : sqlTraceListenersList) { + try { + listener.sqlTrace(record); + } catch (Exception e) { + // it is possible that any of the implementations may fail processing a trace + // record. + // do not propagate such failures. Log them as FINEST. + if (_logger.isLoggable(FINEST)) { + _logger.log(FINEST, + "exception from one of the SQL trace listeners [" + listener.getClass().getName() + "]", + e); + } } } } } + } - if (record != null) { - record.setPoolName(poolName.toString()); - String methodName = record.getMethodName(); - // Check if the method name is one in which sql query is used - if (isMethodValidForCaching(methodName)) { - Object[] params = record.getParams(); - if (params != null && params.length > 0) { - String sqlQuery = null; - for (Object param : params) { - if (param instanceof String) { - sqlQuery = param.toString(); - } - break; - } - if (sqlQuery != null) { - probeProvider.traceSQLEvent(poolName.toString(), appName, moduleName, sqlQuery); + private String findSqlQuery(SQLTraceRecord record, String sqlQuery) { + // Check if the method name is one in which sql query is used + String methodName = record.getMethodName(); + + if (isMethodValidForCaching(methodName)) { + Object[] params = record.getParams(); + if (params != null && params.length > 0) { + for (Object param : params) { + if (param instanceof String) { + sqlQuery = param.toString(); } + break; } } } + return sqlQuery; + } + + private String getAppName() { + ComponentInvocation currentInvocation = invocationManager.getCurrentInvocation(); + return currentInvocation != null ? currentInvocation.getAppName() : null; + } + + private String getModuleName() { + ComponentInvocation currentInvocation = invocationManager.getCurrentInvocation(); + return currentInvocation != null ? currentInvocation.getModuleName(): null; } /** - * Check if the method name from the sql trace record can be used to retrieve a - * sql string for caching purpose. Most of the method names do not contain a sql - * string and hence are unusable for caching the sql strings. These method names - * are filtered in this method. + * Check if the method name from the sql trace record can be used to retrieve a sql string for caching purpose. Most + * of the method names do not contain a sql string and hence are unusable for caching the sql strings. These method + * names are filtered in this method. * * @param methodName * @return true if method name can be used to get a sql string for caching. diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index 240b5d8b330..46bad0eba90 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -1,4 +1,5 @@ /* + * Copyright (c) 2024 Contributors to the Eclipse Foundation * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the @@ -13,7 +14,6 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 */ - package org.glassfish.api.jdbc; import java.io.Serializable; @@ -26,6 +26,7 @@ * @author Shalini M */ public class SQLTraceRecord implements Serializable { + /** * */ @@ -46,6 +47,16 @@ public class SQLTraceRecord implements Serializable { */ private String poolName; + /** + * Application Name which executed the SQL statement + */ + private String applicationName; + + /** + * Module Name which executed the SQL statement + */ + private String moduleName; + /** * Type of SQL query. Could be PreparedStatement, CallableStatement or other object types. */ @@ -66,6 +77,11 @@ public class SQLTraceRecord implements Serializable { */ private Object[] params; + /** + * The SQL query + */ + private String sqlQuery; + /** * Gets the class name of the SQL query expressed as a String. * @@ -194,21 +210,79 @@ public void setParams(Object[] params) { this.params = params; } + /** + * SQL query related to the database operation or null if not applicable + * + * @return SQL query or null + */ + public String getSqlQuery() { + return sqlQuery; + } + + /** + * Set the SQL query related to the database operation + * + * @param sqlQuery + */ + public void setSqlQuery(String sqlQuery) { + this.sqlQuery = sqlQuery; + } + + /** + * Application Name which executed the SQL statement + * + * @return Application name + */ + public String getApplicationName() { + return applicationName; + } + + /** + * Set the application name which executed the SQL statement + * + * @param applicationName + */ + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + /** + * Module name of an application which executed the SQL statement + * + * @return Module name or null if the application doesn't have modules + */ + public String getModuleName() { + return moduleName; + } + + /** + * Set the module name which executed the SQL statement + * + * @param moduleName + */ + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + @Override public String toString() { - StringBuffer sb = new StringBuffer(); - sb.append("ThreadID=" + getThreadID() + " | "); - sb.append("ThreadName=" + getThreadName() + " | "); - sb.append("TimeStamp=" + getTimeStamp() + " | "); - sb.append("ClassName=" + getClassName() + " | "); - sb.append("MethodName=" + getMethodName() + " | "); + StringBuilder sb = new StringBuilder(); + sb.append("PoolName=").append(getPoolName()).append(" | "); + sb.append("ThreadID=").append(getThreadID()).append(" | "); + sb.append("ThreadName=").append(getThreadName()).append(" | "); + sb.append("TimeStamp=").append(getTimeStamp()).append(" | "); + sb.append("SQL=").append(getSqlQuery()).append(" | "); + sb.append("AppName=").append(getApplicationName()).append(" | "); + sb.append("ModuleName=").append(getModuleName()).append(" | "); + sb.append("ClassName=").append(getClassName()).append(" | "); + sb.append("MethodName=").append(getMethodName()).append(" | "); if (params != null && params.length > 0) { int index = 0; for (Object param : params) { - sb.append("arg[" + index++ + "]=" + (param != null ? param.toString() : "null") + " | "); + sb.append("arg[").append(index++).append("]=") + .append(param != null ? param.toString() : "null").append(" | "); } } - // TODO add poolNames and other fields of this record. return sb.toString(); } } From 370e474f80f52db00cd7c86d246885297c3f5cbd Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Fri, 9 Aug 2024 04:11:19 +0200 Subject: [PATCH 02/10] SQLTraceListener: Report calling class and method from application --- .../com/sun/gjc/util/SQLTraceDelegator.java | 33 ++++++++++++++++++- .../glassfish/api/jdbc/SQLTraceRecord.java | 8 +++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java index 858fa36afef..ced6c7e4d08 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java @@ -30,6 +30,7 @@ import static java.util.logging.Level.FINEST; +import java.util.Arrays; import org.glassfish.api.invocation.ComponentInvocation; import org.glassfish.api.invocation.InvocationManager; @@ -87,7 +88,12 @@ public void sqlTrace(SQLTraceRecord record) { record.getModuleName(), record.getSqlQuery()); } - if (sqlTraceListenersList != null) { + if (sqlTraceListenersList != null && !sqlTraceListenersList.isEmpty()) { + StackTraceElement callingElement = findCallingApplicationMethod(); + if (callingElement != null) { + record.setClassName(callingElement.getClassName()); + record.setMethodName(callingElement.getMethodName()); + } for (SQLTraceListener listener : sqlTraceListenersList) { try { listener.sqlTrace(record); @@ -145,4 +151,29 @@ private String getModuleName() { private boolean isMethodValidForCaching(String methodName) { return JdbcRAConstants.validSqlTracingMethodNames.contains(methodName); } + + private StackTraceElement findCallingApplicationMethod() { + return Arrays.stream(Thread.currentThread().getStackTrace()) + .skip(1) + .filter(this::isMethodFromApplication) + .findFirst().orElse(null); + + } + + private boolean isMethodFromApplication(StackTraceElement ste) { + try { + ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + Class cls = appClassLoader.loadClass(ste.getClassName()); + ClassLoader classLoader = cls.getClassLoader(); + while (classLoader != null) { + if (classLoader.equals(appClassLoader)) { + return true; + } + classLoader = classLoader.getParent(); + } + } catch (ClassNotFoundException ex) { + } + return false; + } + } diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index 46bad0eba90..9266fed6545 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -83,7 +83,8 @@ public class SQLTraceRecord implements Serializable { private String sqlQuery; /** - * Gets the class name of the SQL query expressed as a String. + * Gets the class name in the application which executed the SQL query, expressed as a String. + * If it's not possible to detect the application, it will be the class name which directly invoked the connection. * * @return The class name of the SQL query expressed as a String. */ @@ -92,7 +93,7 @@ public String getClassName() { } /** - * Sets the class name of the SQL query expressed as a String. + * Set the class name which executed the SQL query. * * @param className class name of the SQL query. */ @@ -101,7 +102,8 @@ public void setClassName(String className) { } /** - * Gets the method name that executed the SQL query. + * Gets the method name in the application which executed the SQL query, expressed as a String. + * If it's not possible to detect the application, it will be the method name which directly invoked the connection. * * @return methodName that executed the SQL query. */ From cad09da4db2cd8b925293e9f5d1b9dd602a9ab21 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Sat, 10 Aug 2024 01:35:44 +0200 Subject: [PATCH 03/10] SQLTraceListener: Retrieve calling application class and method --- .../com/sun/gjc/util/SQLTraceDelegator.java | 30 ----------------- .../glassfish/api/jdbc/SQLTraceRecord.java | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java index ced6c7e4d08..40e365a5236 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java @@ -30,7 +30,6 @@ import static java.util.logging.Level.FINEST; -import java.util.Arrays; import org.glassfish.api.invocation.ComponentInvocation; import org.glassfish.api.invocation.InvocationManager; @@ -89,11 +88,6 @@ public void sqlTrace(SQLTraceRecord record) { } if (sqlTraceListenersList != null && !sqlTraceListenersList.isEmpty()) { - StackTraceElement callingElement = findCallingApplicationMethod(); - if (callingElement != null) { - record.setClassName(callingElement.getClassName()); - record.setMethodName(callingElement.getMethodName()); - } for (SQLTraceListener listener : sqlTraceListenersList) { try { listener.sqlTrace(record); @@ -152,28 +146,4 @@ private boolean isMethodValidForCaching(String methodName) { return JdbcRAConstants.validSqlTracingMethodNames.contains(methodName); } - private StackTraceElement findCallingApplicationMethod() { - return Arrays.stream(Thread.currentThread().getStackTrace()) - .skip(1) - .filter(this::isMethodFromApplication) - .findFirst().orElse(null); - - } - - private boolean isMethodFromApplication(StackTraceElement ste) { - try { - ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); - Class cls = appClassLoader.loadClass(ste.getClassName()); - ClassLoader classLoader = cls.getClassLoader(); - while (classLoader != null) { - if (classLoader.equals(appClassLoader)) { - return true; - } - classLoader = classLoader.getParent(); - } - } catch (ClassNotFoundException ex) { - } - return false; - } - } diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index 9266fed6545..5b1b5b86ba7 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -17,6 +17,7 @@ package org.glassfish.api.jdbc; import java.io.Serializable; +import java.util.Arrays; /** * Information related to SQL operations executed by the applications are stored in this object. @@ -266,6 +267,37 @@ public void setModuleName(String moduleName) { this.moduleName = moduleName; } + /** + * Get the stack trace element for the method call in the application that triggered SQL execution. + * + * This call analyzes the stacktrace on the current thread and finds the first element that + * represents a call in the application (the class is loaded by the application classloader). + * + * @return Stack trace element that represents a call to a server component that triggered SQL execution + */ + public StackTraceElement getCallingApplicationMethod() { + return Arrays.stream(Thread.currentThread().getStackTrace()) + .skip(1) + .filter(this::isMethodFromApplication) + .findFirst().orElse(null); + } + + private boolean isMethodFromApplication(StackTraceElement ste) { + try { + ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + Class cls = appClassLoader.loadClass(ste.getClassName()); + ClassLoader classLoader = cls.getClassLoader(); + while (classLoader != null) { + if (classLoader.equals(appClassLoader)) { + return true; + } + classLoader = classLoader.getParent(); + } + } catch (ClassNotFoundException ex) { + } + return false; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); From 26f91fd82694aa4fdb4c934912d1e82a1a395fe0 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Sat, 10 Aug 2024 18:25:26 +0200 Subject: [PATCH 04/10] SQLTraceListener: Test --- appserver/tests/application/pom.xml | 8 +- .../lib/LastTraceSQLTraceListener.java | 41 +++++ .../test/app/connpool/webapp/Employee.java | 70 ++++++++ .../webapp/SqlListenerApplication.java | 32 ++++ .../connpool/webapp/SqlListenerEndpoint.java | 83 ++++++++++ .../app/connpool/webapp/META-INF/load.sql | 8 + .../connpool/webapp/META-INF/persistence.xml | 39 +++++ .../app/connpool/SQLTraceListenerTest.java | 156 ++++++++++++++++++ 8 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java create mode 100644 appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/Employee.java create mode 100644 appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerApplication.java create mode 100644 appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java create mode 100644 appserver/tests/application/src/main/resources/org/glassfish/main/test/app/connpool/webapp/META-INF/load.sql create mode 100644 appserver/tests/application/src/main/resources/org/glassfish/main/test/app/connpool/webapp/META-INF/persistence.xml create mode 100644 appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java diff --git a/appserver/tests/application/pom.xml b/appserver/tests/application/pom.xml index b27195f3437..b0b77e70bc8 100755 --- a/appserver/tests/application/pom.xml +++ b/appserver/tests/application/pom.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java new file mode 100644 index 00000000000..e4e2dcedd53 --- /dev/null +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * 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.main.test.app.connpool; + +import org.glassfish.main.test.app.connpool.lib.LastTraceSQLTraceListener; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +import org.glassfish.main.itest.tools.GlassFishTestEnvironment; +import org.glassfish.main.itest.tools.asadmin.Asadmin; +import org.glassfish.main.itest.tools.asadmin.AsadminResult; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static java.lang.System.Logger.Level.INFO; +import static org.glassfish.main.itest.tools.GlassFishTestEnvironment.openConnection; +import static org.glassfish.main.itest.tools.asadmin.AsadminResultMatcher.asadminOK; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.glassfish.main.test.app.connpool.webapp.Employee; +import org.glassfish.main.test.app.connpool.webapp.SqlListenerApplication; +import org.glassfish.main.test.app.connpool.webapp.SqlListenerEndpoint; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +public class SQLTraceListenerTest { + + private static final System.Logger LOG = System.getLogger(SQLTraceListenerTest.class.getName()); + + private static final String RESOURCE_ROOT = "src/main/resources/" + SqlListenerApplication.class.getPackageName().replace(".", "/"); + + private static final String LIB_FILE_NAME = "lib.jar"; + + private static final String WEBAPP_FILE_NAME = "webapp.war"; + + private static final String WEBAPP_NAME = "webapp"; + + private static final String POOL_NAME = "DerbyPool"; + + private static final Asadmin ASADMIN = GlassFishTestEnvironment.getAsadmin(); + + @TempDir + private static File appLibDir; + + @TempDir + private static File webAppDir; + + @BeforeAll + public static void deployAll() throws IOException { + AsadminResult result; + + File webApp = createWebApp(); + + File lib = createSqlTraceListenerLib(); + + result = ASADMIN.exec("add-library", lib.getAbsolutePath()); + assertThat(result, asadminOK()); + + // add-library requires restart + result = ASADMIN.exec("restart-domain"); + assertThat(result, asadminOK()); + + result = ASADMIN.exec("set", "resources.jdbc-connection-pool." + POOL_NAME + + ".sql-trace-listeners=" + LastTraceSQLTraceListener.class.getName()); + assertThat(result, asadminOK()); + + result = ASADMIN.exec("deploy", + "--contextroot", "/" + WEBAPP_NAME, + "--name", WEBAPP_NAME, + webApp.getAbsolutePath()); + assertThat(result, asadminOK()); + } + + @AfterAll + public static void undeployAll() { + assertAll( + () -> assertThat(ASADMIN.exec("undeploy", WEBAPP_NAME), asadminOK()), + () -> assertThat(ASADMIN.exec("set", "resources.jdbc-connection-pool." + POOL_NAME + + ".sql-trace-listeners="), asadminOK()), + () -> assertThat(ASADMIN.exec("remove-library", LIB_FILE_NAME), asadminOK()) + ); + } + + @Test + public void testSQLQueryWithListener() throws IOException { + createWebApp(); + assertValidTraceRecordReceived(WEBAPP_NAME, "validate-trace-listener"); + } + + private void assertValidTraceRecordReceived(String contextRoot, String endpoint) throws IOException { + HttpURLConnection connection = openConnection(8080, "/" + contextRoot + "/" + endpoint); + connection.setRequestMethod("GET"); + try { + try { + assertThat(connection.getResponseCode(), equalTo(200)); + } catch (AssertionError e) { + throw new AssertionError(readErrorResponse(connection), e); + } + } finally { + connection.disconnect(); + } + } + + private String readErrorResponse(HttpURLConnection connection) throws IOException { + try (InputStream inputStream = connection.getErrorStream()) { + return new String(inputStream.readAllBytes()).trim(); + } + } + + private static File createSqlTraceListenerLib() throws IOException { + JavaArchive javaArchive = ShrinkWrap.create(JavaArchive.class) + .addClasses(LastTraceSQLTraceListener.class); + + LOG.log(INFO, javaArchive.toString(true)); + + File appLib = new File(appLibDir, LIB_FILE_NAME); + javaArchive.as(ZipExporter.class).exportTo(appLib, true); + return appLib; + } + + private static File createWebApp() throws IOException { + WebArchive webArchive = ShrinkWrap.create(WebArchive.class) + .addClass(SqlListenerApplication.class) + .addClass(SqlListenerEndpoint.class) + .addClass(Employee.class) + .addAsResource(new File(RESOURCE_ROOT, "/META-INF/persistence.xml"), "/META-INF/persistence.xml") + .addAsResource(new File(RESOURCE_ROOT, "/META-INF/load.sql"), "/META-INF/load.sql"); + + LOG.log(INFO, webArchive.toString(true)); + + File webApp = new File(webAppDir, WEBAPP_FILE_NAME); + webArchive.as(ZipExporter.class).exportTo(webApp, true); + return webApp; + } +} From e90768076fedf5219c5bc52a41766ef0b37c63fa Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Mon, 12 Aug 2024 16:15:05 +0200 Subject: [PATCH 05/10] SQLTraceListener: Use Optional for values that can be empty --- .../com/sun/gjc/util/SQLTraceDelegator.java | 11 +++++----- .../lib/LastTraceSQLTraceListener.java | 18 ++++++++++------- .../connpool/webapp/SqlListenerEndpoint.java | 9 +++++++++ .../glassfish/api/jdbc/SQLTraceRecord.java | 20 ++++++++++--------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java index 40e365a5236..a7e22017d17 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java @@ -77,14 +77,14 @@ public void sqlTrace(SQLTraceRecord record) { if (record != null) { record.setPoolName(poolName.toString()); - String sqlQuery = null; - record.setSqlQuery(findSqlQuery(record, sqlQuery)); + String sqlQuery = findSqlQuery(record); + record.setSqlQuery(sqlQuery); record.setApplicationName(getAppName()); record.setModuleName(getModuleName()); - if (record.getSqlQuery() != null) { + if (sqlQuery != null) { probeProvider.traceSQLEvent(poolName.toString(), record.getApplicationName(), - record.getModuleName(), record.getSqlQuery()); + record.getModuleName(), sqlQuery); } if (sqlTraceListenersList != null && !sqlTraceListenersList.isEmpty()) { @@ -106,9 +106,10 @@ public void sqlTrace(SQLTraceRecord record) { } } - private String findSqlQuery(SQLTraceRecord record, String sqlQuery) { + private String findSqlQuery(SQLTraceRecord record) { // Check if the method name is one in which sql query is used String methodName = record.getMethodName(); + String sqlQuery = null; if (isMethodValidForCaching(methodName)) { Object[] params = record.getParams(); diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java index 90f42ca3d31..80fb732ef83 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java @@ -27,15 +27,19 @@ public class LastTraceSQLTraceListener implements SQLTraceListener { @Override public void sqlTrace(SQLTraceRecord sqltr) { - logger.fine("Trace record: " + sqltr); - if (sqltr.getSqlQuery() != null) { + logger.fine(() -> "Trace record: " + sqltr); + boolean shouldRememberRecord = sqltr.getSqlQuery().isPresent(); + if (shouldRememberRecord) { lastTraceRecord = sqltr; } - StackTraceElement caller = sqltr.getCallingApplicationMethod(); - logger.fine("Method calling SQL: " + caller); - if (sqltr.getSqlQuery() != null) { - lastCallingApplicationMethod = caller; - } + // We need to remember the calling method explicitly because getCallingApplicationMethod() + // is based on the context of the current thread + sqltr.getCallingApplicationMethod().ifPresent(caller -> { + logger.fine(() -> "Method calling SQL: " + caller); + if (shouldRememberRecord) { + lastCallingApplicationMethod = caller; + } + }); } } diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java index 1fa63db64cb..075db9a3c77 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java @@ -24,6 +24,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.ws.rs.core.MediaType; +import java.util.Optional; import org.glassfish.api.jdbc.SQLTraceRecord; import org.glassfish.main.test.app.connpool.lib.LastTraceSQLTraceListener; @@ -65,6 +66,14 @@ private void assertTraceListenerCalled() { assertTrue("application method should be callSomeQuery", caller.getMethodName().equals("callSomeQuery")); } + private void assertValueSet(String valueDescription, Optional applicationName) { + if (applicationName.isPresent()) { + assertValueSet(valueDescription, applicationName.get()); + } else { + throw new AssertionError(valueDescription + " has no value"); + } + } + private void assertValueSet(String valueDescription, String applicationName) { if (applicationName == null) { throw new AssertionError(valueDescription + " is null"); diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index 5b1b5b86ba7..770a3b5d883 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -18,6 +18,7 @@ import java.io.Serializable; import java.util.Arrays; +import java.util.Optional; /** * Information related to SQL operations executed by the applications are stored in this object. @@ -214,12 +215,12 @@ public void setParams(Object[] params) { } /** - * SQL query related to the database operation or null if not applicable + * SQL query related to the database operation if applicable. * - * @return SQL query or null + * @return SQL query */ - public String getSqlQuery() { - return sqlQuery; + public Optional getSqlQuery() { + return Optional.ofNullable(sqlQuery); } /** @@ -250,9 +251,10 @@ public void setApplicationName(String applicationName) { } /** - * Module name of an application which executed the SQL statement + * Module name of an application which executed the SQL statement. + * If the application doesn't have modules it will be equal to the application name * - * @return Module name or null if the application doesn't have modules + * @return Module name */ public String getModuleName() { return moduleName; @@ -270,16 +272,16 @@ public void setModuleName(String moduleName) { /** * Get the stack trace element for the method call in the application that triggered SQL execution. * - * This call analyzes the stacktrace on the current thread and finds the first element that + * This call analyzes the stacktrace of the current thread and finds the first element that * represents a call in the application (the class is loaded by the application classloader). * * @return Stack trace element that represents a call to a server component that triggered SQL execution */ - public StackTraceElement getCallingApplicationMethod() { + public Optional getCallingApplicationMethod() { return Arrays.stream(Thread.currentThread().getStackTrace()) .skip(1) .filter(this::isMethodFromApplication) - .findFirst().orElse(null); + .findFirst(); } private boolean isMethodFromApplication(StackTraceElement ste) { From 6cda55108a33594d70a19c13a723fbff230dbde3 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Mon, 12 Aug 2024 21:23:51 +0200 Subject: [PATCH 06/10] SQLTraceListener: Format code to pass checkstyle --- .../sun/enterprise/connectors/ActiveResourceAdapterImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java index fcb47eefb39..1d18cedaaa4 100755 --- a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java +++ b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java @@ -32,8 +32,8 @@ import com.sun.enterprise.deployment.runtime.connector.ResourceAdapter; import com.sun.enterprise.util.i18n.StringManager; import com.sun.logging.LogDomains; -import jakarta.inject.Inject; +import jakarta.inject.Inject; import jakarta.resource.spi.ManagedConnectionFactory; import java.util.Set; From 026a6a336a41b036e9befc0fdb83f2c823c71d72 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Mon, 12 Aug 2024 22:39:58 +0200 Subject: [PATCH 07/10] SQLTraceListener: Redesign the method SQLTraceRecord.getCallingApplicationMethod - use StackWalker - eagerly find application method and set it to SQLTraceRecord (only if there are some listeners) This should be efficient enough and leads to a simpler design. --- .../gjc/spi/ManagedConnectionFactoryImpl.java | 5 +-- .../com/sun/gjc/util/SQLTraceDelegator.java | 39 +++++++++++++++-- .../lib/LastTraceSQLTraceListener.java | 10 +---- .../test/app/connpool/webapp/Employee.java | 8 ++-- .../webapp/SqlListenerApplication.java | 1 - .../connpool/webapp/SqlListenerEndpoint.java | 12 +++--- .../app/connpool/SQLTraceListenerTest.java | 13 +++--- .../glassfish/api/jdbc/SQLTraceRecord.java | 43 ++++++++----------- 8 files changed, 76 insertions(+), 55 deletions(-) diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java index 776718e4e75..70d71ae10cf 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/spi/ManagedConnectionFactoryImpl.java @@ -25,6 +25,7 @@ import com.sun.gjc.util.SQLTraceDelegator; import com.sun.logging.LogDomains; +import jakarta.inject.Inject; import jakarta.resource.ResourceException; import jakarta.resource.spi.ConfigProperty; import jakarta.resource.spi.ConnectionManager; @@ -65,6 +66,7 @@ import javax.sql.DataSource; import javax.sql.PooledConnection; +import org.glassfish.api.invocation.InvocationManager; import org.glassfish.api.jdbc.ConnectionValidation; import org.glassfish.api.jdbc.SQLTraceListener; import org.glassfish.api.jdbc.objects.TxIsolationLevel; @@ -84,9 +86,6 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; -import jakarta.inject.Inject; -import org.glassfish.api.invocation.InvocationManager; - /** * ManagedConnectionFactory implementation for Generic JDBC * Connector. This class is extended by the DataSource specific diff --git a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java index a7e22017d17..7da4bd7318a 100644 --- a/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java +++ b/appserver/jdbc/jdbc-ra/jdbc-core/src/main/java/com/sun/gjc/util/SQLTraceDelegator.java @@ -21,18 +21,20 @@ import com.sun.logging.LogDomains; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; +import org.glassfish.api.invocation.ComponentInvocation; +import org.glassfish.api.invocation.InvocationManager; import org.glassfish.api.jdbc.SQLTraceListener; import org.glassfish.api.jdbc.SQLTraceRecord; import org.glassfish.api.naming.SimpleJndiName; import static java.util.logging.Level.FINEST; -import org.glassfish.api.invocation.ComponentInvocation; -import org.glassfish.api.invocation.InvocationManager; - /** * Implementation of SQLTraceListener to listen to events related to a sql * record tracing. The registry allows multiple listeners to listen to the sql @@ -88,6 +90,7 @@ public void sqlTrace(SQLTraceRecord record) { } if (sqlTraceListenersList != null && !sqlTraceListenersList.isEmpty()) { + getCallingApplicationStackFrame().ifPresent(record::setCallingApplicationMethod); for (SQLTraceListener listener : sqlTraceListenersList) { try { listener.sqlTrace(record); @@ -106,6 +109,36 @@ public void sqlTrace(SQLTraceRecord record) { } } + private Optional getCallingApplicationStackFrame() { + Set> checkedClasses = new HashSet<>(); + checkedClasses.add(this.getClass()); + return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(traces -> { + return traces + .filter(trace -> { + Class declaringClass = trace.getDeclaringClass(); + final ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); + boolean result = !checkedClasses.contains(declaringClass) && isClassFromApplication(declaringClass, appClassLoader); + if (!result) { + checkedClasses.add(declaringClass); + } + return result; + }) + .findFirst(); + }); + } + + private boolean isClassFromApplication(Class cls, ClassLoader appClassLoader) { + ClassLoader clsClassLoader = cls.getClassLoader(); + while (clsClassLoader != null) { + if (clsClassLoader.equals(appClassLoader)) { + return true; + } + clsClassLoader = clsClassLoader.getParent(); + } + return false; + } + private String findSqlQuery(SQLTraceRecord record) { // Check if the method name is one in which sql query is used String methodName = record.getMethodName(); diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java index 80fb732ef83..a34821d83d2 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java @@ -15,6 +15,7 @@ */package org.glassfish.main.test.app.connpool.lib; import java.util.logging.Logger; + import org.glassfish.api.jdbc.SQLTraceListener; import org.glassfish.api.jdbc.SQLTraceRecord; @@ -23,22 +24,15 @@ public class LastTraceSQLTraceListener implements SQLTraceListener { private static final Logger logger = Logger.getLogger(LastTraceSQLTraceListener.class.getName()); public static SQLTraceRecord lastTraceRecord; - public static StackTraceElement lastCallingApplicationMethod; @Override public void sqlTrace(SQLTraceRecord sqltr) { logger.fine(() -> "Trace record: " + sqltr); - boolean shouldRememberRecord = sqltr.getSqlQuery().isPresent(); - if (shouldRememberRecord) { + if (sqltr.getSqlQuery().isPresent()) { lastTraceRecord = sqltr; } - // We need to remember the calling method explicitly because getCallingApplicationMethod() - // is based on the context of the current thread sqltr.getCallingApplicationMethod().ifPresent(caller -> { logger.fine(() -> "Method calling SQL: " + caller); - if (shouldRememberRecord) { - lastCallingApplicationMethod = caller; - } }); } diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/Employee.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/Employee.java index 16a7baf11ee..3a56b2cec4c 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/Employee.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/Employee.java @@ -15,10 +15,6 @@ */ package org.glassfish.main.test.app.connpool.webapp; -import static jakarta.persistence.GenerationType.IDENTITY; - -import java.io.Serializable; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -26,6 +22,10 @@ import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; +import java.io.Serializable; + +import static jakarta.persistence.GenerationType.IDENTITY; + /** * @author Arun Gupta */ diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerApplication.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerApplication.java index 615b078cf6d..c43d2f1f130 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerApplication.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerApplication.java @@ -16,7 +16,6 @@ package org.glassfish.main.test.app.connpool.webapp; -import org.glassfish.main.test.app.applib.webapp.*; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java index 075db9a3c77..bc848fe6b52 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/webapp/SqlListenerEndpoint.java @@ -15,16 +15,17 @@ */ package org.glassfish.main.test.app.connpool.webapp; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; - import jakarta.ejb.Stateless; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; + import java.util.Optional; + import org.glassfish.api.jdbc.SQLTraceRecord; import org.glassfish.main.test.app.connpool.lib.LastTraceSQLTraceListener; @@ -61,7 +62,8 @@ private void assertTraceListenerCalled() { assertTrue("threadID > 0", sqlTr.getThreadID() > 0); assertTrue("timeStamp > 0", sqlTr.getTimeStamp() > 0); - StackTraceElement caller = LastTraceSQLTraceListener.lastCallingApplicationMethod; + assertTrue("application method stack frame should be present", sqlTr.getCallingApplicationMethod().isPresent()); + StackWalker.StackFrame caller = sqlTr.getCallingApplicationMethod().get(); assertTrue("application class should be " + SqlListenerEndpoint.class, caller.getClassName().equals(SqlListenerEndpoint.class.getName())); assertTrue("application method should be callSomeQuery", caller.getMethodName().equals("callSomeQuery")); } diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java index e4e2dcedd53..8ebac0a0792 100644 --- a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java @@ -15,7 +15,6 @@ */ package org.glassfish.main.test.app.connpool; -import org.glassfish.main.test.app.connpool.lib.LastTraceSQLTraceListener; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -24,10 +23,16 @@ import org.glassfish.main.itest.tools.GlassFishTestEnvironment; import org.glassfish.main.itest.tools.asadmin.Asadmin; import org.glassfish.main.itest.tools.asadmin.AsadminResult; +import org.glassfish.main.test.app.connpool.lib.LastTraceSQLTraceListener; +import org.glassfish.main.test.app.connpool.webapp.Employee; +import org.glassfish.main.test.app.connpool.webapp.SqlListenerApplication; +import org.glassfish.main.test.app.connpool.webapp.SqlListenerEndpoint; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -38,12 +43,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import org.glassfish.main.test.app.connpool.webapp.Employee; -import org.glassfish.main.test.app.connpool.webapp.SqlListenerApplication; -import org.glassfish.main.test.app.connpool.webapp.SqlListenerEndpoint; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; - public class SQLTraceListenerTest { private static final System.Logger LOG = System.getLogger(SQLTraceListenerTest.class.getName()); diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index 770a3b5d883..7f33e95c74a 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -17,7 +17,6 @@ package org.glassfish.api.jdbc; import java.io.Serializable; -import java.util.Arrays; import java.util.Optional; /** @@ -84,6 +83,11 @@ public class SQLTraceRecord implements Serializable { */ private String sqlQuery; + /** + * Info about the method in the application that triggered the SQL query + */ + private StackWalker.StackFrame callingApplicationMethod; + /** * Gets the class name in the application which executed the SQL query, expressed as a String. * If it's not possible to detect the application, it will be the class name which directly invoked the connection. @@ -270,36 +274,27 @@ public void setModuleName(String moduleName) { } /** - * Get the stack trace element for the method call in the application that triggered SQL execution. + * Get the stack frame for the method call in the application that triggered SQL execution, if the call comes from a deployed application. * - * This call analyzes the stacktrace of the current thread and finds the first element that - * represents a call in the application (the class is loaded by the application classloader). + * This returns a frame which returns a class from the {@link StackWalker.StackFrame#getDeclaringClass()} method + * ({@link StackWalker.Option.RETAIN_CLASS_REFERENCE} is enabled). * - * @return Stack trace element that represents a call to a server component that triggered SQL execution + * @return Stack frame that represents a call to a server component that triggered SQL execution */ - public Optional getCallingApplicationMethod() { - return Arrays.stream(Thread.currentThread().getStackTrace()) - .skip(1) - .filter(this::isMethodFromApplication) - .findFirst(); + public Optional getCallingApplicationMethod() { + return Optional.ofNullable(callingApplicationMethod); } - private boolean isMethodFromApplication(StackTraceElement ste) { - try { - ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader(); - Class cls = appClassLoader.loadClass(ste.getClassName()); - ClassLoader classLoader = cls.getClassLoader(); - while (classLoader != null) { - if (classLoader.equals(appClassLoader)) { - return true; - } - classLoader = classLoader.getParent(); - } - } catch (ClassNotFoundException ex) { - } - return false; + /** + * Set the stack frame for the method call in the application that triggered SQL execution. + * The {@link StackWalker.StackFrame#getDeclaringClass()} should return a class and never throw {@link UnsupportedOperationException}. + */ + public void setCallingApplicationMethod(StackWalker.StackFrame callingApplicationMethod) { + this.callingApplicationMethod = callingApplicationMethod; } + + @Override public String toString() { StringBuilder sb = new StringBuilder(); From cf2fa28967b5066f449889826ff34ed7f3426065 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Mon, 12 Aug 2024 23:10:21 +0200 Subject: [PATCH 08/10] SQLTraceListener: Add caller method to toString() And call it from the test. --- .../test/app/connpool/lib/LastTraceSQLTraceListener.java | 4 ++-- .../src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java index a34821d83d2..2bd20462bf9 100644 --- a/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java +++ b/appserver/tests/application/src/main/java/org/glassfish/main/test/app/connpool/lib/LastTraceSQLTraceListener.java @@ -27,8 +27,8 @@ public class LastTraceSQLTraceListener implements SQLTraceListener { @Override public void sqlTrace(SQLTraceRecord sqltr) { - logger.fine(() -> "Trace record: " + sqltr); - if (sqltr.getSqlQuery().isPresent()) { + logger.info(() -> "Trace record: " + sqltr); + if (sqltr.getSqlQuery().isPresent() && sqltr.getCallingApplicationMethod().isPresent()) { lastTraceRecord = sqltr; } sqltr.getCallingApplicationMethod().ifPresent(caller -> { diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index 7f33e95c74a..fd39c675e2c 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -314,6 +314,11 @@ public String toString() { .append(param != null ? param.toString() : "null").append(" | "); } } + if (callingApplicationMethod != null) { + sb.append("CallingMethod=").append(callingApplicationMethod).append(" | "); + } else { + sb.append("CallingMethod=(null)").append(" | "); + } return sb.toString(); } } From 212540ac1c3e862b039166f25e7f38ec7aeced5f Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Sat, 17 Aug 2024 02:42:19 +0200 Subject: [PATCH 09/10] SQLTraceListener: Use embedded Derby for tests --- .../itest/tools/asadmin/DomainSettings.java | 75 +++++++++++++++++++ .../app/connpool/SQLTraceListenerTest.java | 7 ++ .../JtaDataSourceResourceRefTest.java | 38 ++-------- 3 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/asadmin/DomainSettings.java diff --git a/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/asadmin/DomainSettings.java b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/asadmin/DomainSettings.java new file mode 100644 index 00000000000..04c00d60eb9 --- /dev/null +++ b/appserver/itest-tools/src/main/java/org/glassfish/main/itest/tools/asadmin/DomainSettings.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 Eclipse Foundation 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.main.itest.tools.asadmin; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.glassfish.main.itest.tools.asadmin.AsadminResultMatcher.asadminOK; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * + * @author Ondro Mihalyi + */ +public class DomainSettings { + + private final Asadmin asadmin; + List settingsBackup = new ArrayList<>(); + + public DomainSettings(Asadmin asadmin) { + this.asadmin = asadmin; + } + + public void backupSettings(String getKey) { + final AsadminResult result = asadmin.exec(5_000, "get", getKey); + Stream.of(result.getStdOut().split("\n")) + // Exclude "command successful + .filter(line -> line.contains("=")) + // Exclude .name for connection pools which cannot be changed + .filter(line -> !line.startsWith("resources.jdbc-connection-pool") || !line.contains(".name=")) + .forEach(settingsBackup::add); + } + + public void restoreSettings() { + String[] args = new String[settingsBackup.size() + 1]; + args[0] = "set"; + for (int i = 1; i < args.length; i++) { + args[i] = settingsBackup.get(i - 1); + } + settingsBackup.clear(); + final AsadminResult result = asadmin.exec(5_000, args); + assertThat(result, asadminOK()); + } + + public void backupDerbyPoolSettings() { + backupSettings("resources.jdbc-connection-pool.DerbyPool.*"); + } + + /** Default is org.apache.derby.jdbc.ClientDataSource */ + public void setDerbyPoolEmbededded() { + final AsadminResult result = asadmin.exec(5_000, "set", + "resources.jdbc-connection-pool.DerbyPool.datasource-classname=org.apache.derby.jdbc.EmbeddedDataSource", + "resources.jdbc-connection-pool.DerbyPool.property.PortNumber=", + "resources.jdbc-connection-pool.DerbyPool.property.serverName=", + "resources.jdbc-connection-pool.DerbyPool.property.URL="); + assertThat(result, asadminOK()); + asadmin.exec(5_000, "get", "resources.jdbc-connection-pool.DerbyPool.*"); + } + + +} diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java index 8ebac0a0792..c41c24fdb39 100644 --- a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/connpool/SQLTraceListenerTest.java @@ -23,6 +23,7 @@ import org.glassfish.main.itest.tools.GlassFishTestEnvironment; import org.glassfish.main.itest.tools.asadmin.Asadmin; import org.glassfish.main.itest.tools.asadmin.AsadminResult; +import org.glassfish.main.itest.tools.asadmin.DomainSettings; import org.glassfish.main.test.app.connpool.lib.LastTraceSQLTraceListener; import org.glassfish.main.test.app.connpool.webapp.Employee; import org.glassfish.main.test.app.connpool.webapp.SqlListenerApplication; @@ -59,6 +60,8 @@ public class SQLTraceListenerTest { private static final Asadmin ASADMIN = GlassFishTestEnvironment.getAsadmin(); + private static final DomainSettings DOMAIN_SETTINGS = new DomainSettings(ASADMIN); + @TempDir private static File appLibDir; @@ -80,6 +83,9 @@ public static void deployAll() throws IOException { result = ASADMIN.exec("restart-domain"); assertThat(result, asadminOK()); + DOMAIN_SETTINGS.backupDerbyPoolSettings(); + DOMAIN_SETTINGS.setDerbyPoolEmbededded(); + result = ASADMIN.exec("set", "resources.jdbc-connection-pool." + POOL_NAME + ".sql-trace-listeners=" + LastTraceSQLTraceListener.class.getName()); assertThat(result, asadminOK()); @@ -93,6 +99,7 @@ public static void deployAll() throws IOException { @AfterAll public static void undeployAll() { + DOMAIN_SETTINGS.restoreSettings(); assertAll( () -> assertThat(ASADMIN.exec("undeploy", WEBAPP_NAME), asadminOK()), () -> assertThat(ASADMIN.exec("set", "resources.jdbc-connection-pool." + POOL_NAME diff --git a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/persistence/resourceref/JtaDataSourceResourceRefTest.java b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/persistence/resourceref/JtaDataSourceResourceRefTest.java index cd8e1dea7d6..a2c4e54f6ee 100644 --- a/appserver/tests/application/src/test/java/org/glassfish/main/test/app/persistence/resourceref/JtaDataSourceResourceRefTest.java +++ b/appserver/tests/application/src/test/java/org/glassfish/main/test/app/persistence/resourceref/JtaDataSourceResourceRefTest.java @@ -18,11 +18,11 @@ import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; -import java.util.stream.Stream; import org.glassfish.main.itest.tools.TestUtilities; import org.glassfish.main.itest.tools.asadmin.Asadmin; import org.glassfish.main.itest.tools.asadmin.AsadminResult; +import org.glassfish.main.itest.tools.asadmin.DomainSettings; import org.glassfish.main.test.app.persistence.resourceref.webapp.ResourceRefApplication; import org.glassfish.main.test.app.persistence.resourceref.webapp.ResourceRefResource; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -51,12 +51,12 @@ public class JtaDataSourceResourceRefTest { private static final String CONTEXT_ROOT = "/"; private static final Asadmin ASADMIN = getAsadmin(); - private static String[] derbyPoolSettingsBackup; + private static final DomainSettings DOMAIN_SETTINGS = new DomainSettings(ASADMIN); @BeforeAll public static void deploy() throws Exception { - backupDerbyPoolSettings(); - setDerbyPoolEmbededded(); + DOMAIN_SETTINGS.backupDerbyPoolSettings(); + DOMAIN_SETTINGS.setDerbyPoolEmbededded(); final File warFile = createDeployment(); try { AsadminResult result = ASADMIN.exec("deploy", "--contextroot", CONTEXT_ROOT, "--name", APP_NAME, @@ -72,7 +72,7 @@ public static void deploy() throws Exception { static void undeploy() { AsadminResult result = ASADMIN.exec("undeploy", APP_NAME); assertThat(result, asadminOK()); - restoreDerbyPoolSettings(); + DOMAIN_SETTINGS.restoreSettings(); } @@ -87,34 +87,6 @@ public void test() throws IOException { } } - private static void backupDerbyPoolSettings() { - final AsadminResult result = ASADMIN.exec(5_000, "get", "resources.jdbc-connection-pool.DerbyPool.*"); - // Exclude "command successful and .name which cannot be changed - derbyPoolSettingsBackup = Stream.of(result.getStdOut().split("\n")) - .filter(line -> line.contains("=") && !line.contains(".name=")).toArray(String[]::new); - } - - private static void restoreDerbyPoolSettings() { - String[] args = new String[derbyPoolSettingsBackup.length + 1]; - args[0] = "set"; - for (int i = 1; i < args.length; i++) { - args[i] = derbyPoolSettingsBackup[i - 1]; - } - final AsadminResult result = ASADMIN.exec(5_000, args); - assertThat(result, asadminOK()); - } - - /** Default is org.apache.derby.jdbc.ClientDataSource */ - private static void setDerbyPoolEmbededded() { - final AsadminResult result = ASADMIN.exec(5_000, "set", - "resources.jdbc-connection-pool.DerbyPool.datasource-classname=org.apache.derby.jdbc.EmbeddedDataSource", - "resources.jdbc-connection-pool.DerbyPool.property.PortNumber=", - "resources.jdbc-connection-pool.DerbyPool.property.serverName=", - "resources.jdbc-connection-pool.DerbyPool.property.URL="); - assertThat(result, asadminOK()); - ASADMIN.exec(5_000, "get", "resources.jdbc-connection-pool.DerbyPool.*"); - } - private static File createDeployment() throws IOException { final WebArchive webArchive = ShrinkWrap.create(WebArchive.class) .addClass(ResourceRefResource.class) From 3ebd31953292505008511d788202413c4af9dad8 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Mon, 19 Aug 2024 16:30:18 +0200 Subject: [PATCH 10/10] SQLTraceListener: Cleanup code base on review --- .../enterprise/connectors/ActiveResourceAdapterImpl.java | 6 +----- .../main/java/org/glassfish/api/jdbc/SQLTraceRecord.java | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java index 1d18cedaaa4..b7454dd14e8 100755 --- a/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java +++ b/appserver/connectors/connectors-runtime/src/main/java/com/sun/enterprise/connectors/ActiveResourceAdapterImpl.java @@ -394,15 +394,11 @@ protected ManagedConnectionFactory instantiateMCF(String mcfClassName, ClassLoad Class mcfClass; if (jcl_ != null) { mcfClass = jcl_.loadClass(mcfClassName); -// mcf = (ManagedConnectionFactory) mcfClass.getDeclaredConstructor().newInstance(); } else if (loader != null) { mcfClass = loader.loadClass(mcfClassName); -// mcf = (ManagedConnectionFactory) loader.loadClass(mcfClass).getDeclaredConstructor().newInstance(); + } else { mcfClass = Thread.currentThread().getContextClassLoader().loadClass(mcfClassName); - // mcf = (ManagedConnectionFactory) Class.forName(mcfClass).newInstance(); -// mcf = (ManagedConnectionFactory) Thread.currentThread().getContextClassLoader().loadClass(mcfClass) -// .getDeclaredConstructor().newInstance(); } mcf = locator.createAndInitialize((Class)mcfClass); setLogWriter(mcf); diff --git a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java index fd39c675e2c..3c8d6f30d55 100644 --- a/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java +++ b/nucleus/common/glassfish-api/src/main/java/org/glassfish/api/jdbc/SQLTraceRecord.java @@ -314,10 +314,10 @@ public String toString() { .append(param != null ? param.toString() : "null").append(" | "); } } - if (callingApplicationMethod != null) { - sb.append("CallingMethod=").append(callingApplicationMethod).append(" | "); - } else { + if (callingApplicationMethod == null) { sb.append("CallingMethod=(null)").append(" | "); + } else { + sb.append("CallingMethod=").append(callingApplicationMethod).append(" | "); } return sb.toString(); }