-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support including Module and Action in each JDBC session with Oracle …
…JDBC Trying different approach
- Loading branch information
1 parent
86a54bb
commit aa356d9
Showing
11 changed files
with
569 additions
and
24 deletions.
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/ConnectionCustomizer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* Copyright 2017-2024 original 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 | ||
* | ||
* https://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 io.micronaut.data.jdbc.connection; | ||
|
||
import io.micronaut.core.annotation.NonNull; | ||
import io.micronaut.core.naming.Named; | ||
import io.micronaut.core.order.Ordered; | ||
|
||
import java.sql.Connection; | ||
|
||
/** | ||
* Handles before and after JDBC call for given connection {@link Connection}. | ||
* | ||
* Implementations of this interface can modify the behavior of connections created by Micronaut Data | ||
* or do what might be needed before or after JDBC call for given connection. | ||
* | ||
* @author radovanradic | ||
* @since 4.11 | ||
*/ | ||
public interface ConnectionCustomizer extends Named, Ordered { | ||
|
||
/** | ||
* Called before JDBC call is issues for given connection. | ||
* | ||
* This method allows implementations to perform additional setup or configuration on the connection. | ||
* | ||
* @param connection The JDBC connection | ||
* @param methodInfo The method info | ||
*/ | ||
void beforeCall(@NonNull Connection connection, @NonNull MethodInfo methodInfo); | ||
|
||
/** | ||
* Called after JDBC call for given connection has been issued. | ||
* | ||
* This method allows implementations to release any resources or perform cleanup tasks related to the connection. | ||
* | ||
* @param connection The JDBC connection | ||
*/ | ||
void afterCall(@NonNull Connection connection); | ||
|
||
/** | ||
* Returns the name of this listener. Used for logging purposes. By default, returns class simple name. | ||
* | ||
* @return the name of this customizer | ||
*/ | ||
@Override | ||
@NonNull | ||
default String getName() { | ||
return getClass().getSimpleName(); | ||
} | ||
|
||
/** | ||
* Indicates whether this customizer is enabled. | ||
* | ||
* By default, all customizers are enabled. Subclasses may override this method to provide dynamic enabling/disabling logic. | ||
* | ||
* @return true if this customizer is enabled, false otherwise | ||
*/ | ||
default boolean isEnabled() { | ||
return true; | ||
} | ||
|
||
/** | ||
* Returns the name of the data source associated with this customizer. | ||
* | ||
* This method provides access to the name of the data source that this customizer is configured for. | ||
* | ||
* @return the name of the data source | ||
*/ | ||
String getDataSourceName(); | ||
} |
32 changes: 32 additions & 0 deletions
32
data-jdbc/src/main/java/io/micronaut/data/jdbc/connection/MethodInfo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* Copyright 2017-2024 original 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 | ||
* | ||
* https://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 io.micronaut.data.jdbc.connection; | ||
|
||
import io.micronaut.core.annotation.AnnotationMetadata; | ||
import io.micronaut.core.annotation.Internal; | ||
import io.micronaut.core.annotation.NonNull; | ||
|
||
/** | ||
* The method info used for {@link ConnectionCustomizer} calls providing method metadata | ||
* when might be needed. | ||
* | ||
* @param clazz The class name where method belongs | ||
* @param methodName The method name | ||
* @param annotationMetadata The annotation metadata for the method | ||
*/ | ||
@Internal | ||
public record MethodInfo(@NonNull Class<?> clazz, @NonNull String methodName, @NonNull AnnotationMetadata annotationMetadata) { | ||
} |
218 changes: 218 additions & 0 deletions
218
...src/main/java/io/micronaut/data/jdbc/connection/OracleClientInfoConnectionCustomizer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
/* | ||
* Copyright 2017-2024 original 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 | ||
* | ||
* https://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 io.micronaut.data.jdbc.connection; | ||
|
||
import io.micronaut.context.ApplicationContext; | ||
import io.micronaut.context.annotation.EachBean; | ||
import io.micronaut.context.annotation.Parameter; | ||
import io.micronaut.core.annotation.AnnotationMetadata; | ||
import io.micronaut.core.annotation.AnnotationValue; | ||
import io.micronaut.core.annotation.NonNull; | ||
import io.micronaut.core.annotation.Nullable; | ||
import io.micronaut.core.util.CollectionUtils; | ||
import io.micronaut.core.util.StringUtils; | ||
import io.micronaut.data.connection.jdbc.advice.DelegatingDataSource; | ||
import io.micronaut.data.jdbc.config.DataJdbcConfiguration; | ||
import io.micronaut.data.jdbc.connection.annotation.ClientInfo; | ||
import io.micronaut.data.model.query.builder.sql.Dialect; | ||
import io.micronaut.runtime.ApplicationConfiguration; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import javax.sql.DataSource; | ||
import java.sql.Connection; | ||
import java.sql.SQLClientInfoException; | ||
import java.sql.SQLException; | ||
import java.util.Collections; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Properties; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
/** | ||
* A customizer for Oracle database connections that sets client information before and after issuing JDBC call for given connection. | ||
* | ||
* This customizer checks if the connection is an Oracle database connection and then sets the client information | ||
* (client ID, module, and action) before issuing JDBC call for the connection. It also clears these properties after the JDBC call for given connection. | ||
* | ||
* @author radovanradic | ||
* @since 4.11 | ||
*/ | ||
@EachBean(DataSource.class) | ||
//@Requires(condition = OracleClientInfoCondition.class) | ||
final class OracleClientInfoConnectionCustomizer implements ConnectionCustomizer { | ||
|
||
private static final String NAME_MEMBER = "name"; | ||
private static final String VALUE_MEMBER = "value"; | ||
private static final String INTERCEPTED_SUFFIX = "$Intercepted"; | ||
private static final String ORACLE_CLIENT_INFO_ENABLED = "enable-oracle-client-info"; | ||
|
||
/** | ||
* Constant for the Oracle connection client info client ID property name. | ||
*/ | ||
private static final String ORACLE_CLIENT_ID = "OCSID.CLIENTID"; | ||
/** | ||
* Constant for the Oracle connection client info module property name. | ||
*/ | ||
private static final String ORACLE_MODULE = "OCSID.MODULE"; | ||
/** | ||
* Constant for the Oracle connection client info action property name. | ||
*/ | ||
private static final String ORACLE_ACTION = "OCSID.ACTION"; | ||
/** | ||
* Constant for the Oracle connection database product name. | ||
*/ | ||
private static final String ORACLE_CONNECTION_DATABASE_PRODUCT_NAME = "Oracle"; | ||
|
||
private static final Logger LOG = LoggerFactory.getLogger(OracleClientInfoConnectionCustomizer.class); | ||
|
||
private static final Map<Class<?>, String> MODULE_CLASS_MAP = new ConcurrentHashMap<>(100); | ||
|
||
@Nullable | ||
private final String applicationName; | ||
private final String dataSourceName; | ||
|
||
private final boolean enabled; | ||
|
||
OracleClientInfoConnectionCustomizer(@NonNull DataSource dataSource, | ||
@Parameter DataJdbcConfiguration jdbcConfiguration, | ||
ApplicationContext applicationContext, | ||
@Nullable ApplicationConfiguration applicationConfiguration) { | ||
this.applicationName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(null) : null; | ||
this.enabled = isEnabled(jdbcConfiguration, applicationContext, dataSource); | ||
this.dataSourceName = jdbcConfiguration.getName(); | ||
} | ||
|
||
private boolean isEnabled(DataJdbcConfiguration dataJdbcConfiguration, ApplicationContext applicationContext, DataSource dataSource) { | ||
if (dataJdbcConfiguration.getDialect() != Dialect.ORACLE) { | ||
return false; | ||
} | ||
String property = "datasources." + dataJdbcConfiguration.getName() + "." + ORACLE_CLIENT_INFO_ENABLED; | ||
if (!applicationContext.getProperty(property, Boolean.class).orElse(false)) { | ||
return false; | ||
} | ||
boolean customizerEnabled; | ||
try { | ||
Connection connection = DelegatingDataSource.unwrapDataSource(dataSource).getConnection(); | ||
customizerEnabled = isOracleConnection(connection); | ||
} catch (SQLException e) { | ||
LOG.error("Failed to get connection for oracle connection customizer", e); | ||
customizerEnabled = false; | ||
} | ||
return customizerEnabled; | ||
} | ||
|
||
@Override | ||
public void beforeCall(@NonNull Connection connection, @NonNull MethodInfo methodInfo) { | ||
// Set client info for the connection if Oracle connection before JDBC call | ||
Map<String, String> connectionClientInfo = getConnectionClientInfo(methodInfo); | ||
if (connectionClientInfo != null && !connectionClientInfo.isEmpty()) { | ||
LOG.trace("Setting connection tracing info to the Oracle connection"); | ||
try { | ||
for (Map.Entry<String, String> additionalInfo : connectionClientInfo.entrySet()) { | ||
String name = additionalInfo.getKey(); | ||
String value = additionalInfo.getValue(); | ||
connection.setClientInfo(name, value); | ||
} | ||
} catch (SQLClientInfoException e) { | ||
LOG.debug("Failed to set connection tracing info", e); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public void afterCall(@NonNull Connection connection) { | ||
// Clear client info for connection if it was Oracle connection and client info was set previously | ||
Properties properties = null; | ||
try { | ||
properties = connection.getClientInfo(); | ||
} catch (SQLException e) { | ||
LOG.debug("Failed to get connection client info", e); | ||
} | ||
if (properties != null && !properties.isEmpty()) { | ||
try { | ||
for (String key : properties.stringPropertyNames()) { | ||
connection.setClientInfo(key, null); | ||
} | ||
} catch (SQLClientInfoException e) { | ||
LOG.debug("Failed to clear connection tracing info", e); | ||
} | ||
} | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return "Oracle Connection Client Info Customizer"; | ||
} | ||
|
||
@Override | ||
public boolean isEnabled() { | ||
return enabled; | ||
} | ||
|
||
@Override | ||
public String getDataSourceName() { | ||
return dataSourceName; | ||
} | ||
|
||
/** | ||
* Checks whether current connection is Oracle database connection. | ||
* | ||
* @param connection The connection | ||
* @return true if current connection is Oracle database connection | ||
*/ | ||
private boolean isOracleConnection(Connection connection) { | ||
try { | ||
String databaseProductName = connection.getMetaData().getDatabaseProductName(); | ||
return StringUtils.isNotEmpty(databaseProductName) && databaseProductName.equalsIgnoreCase(ORACLE_CONNECTION_DATABASE_PRODUCT_NAME); | ||
} catch (SQLException e) { | ||
LOG.debug("Failed to get database product name from the connection", e); | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Gets connection client info from the {@link ClientInfo} annotation. | ||
* | ||
* @param methodInfo The method info | ||
* @return The connection client info or null if not configured to be used | ||
*/ | ||
private @Nullable Map<String, String> getConnectionClientInfo(@NonNull MethodInfo methodInfo) { | ||
AnnotationMetadata annotationMetadata = methodInfo.annotationMetadata(); | ||
AnnotationValue<ClientInfo> annotation = annotationMetadata.getAnnotation(ClientInfo.class); | ||
List<AnnotationValue<ClientInfo.Attribute>> clientInfoValues = annotation != null ? annotation.getAnnotations(VALUE_MEMBER) : Collections.EMPTY_LIST; | ||
Map<String, String> clientInfoAttributes = new LinkedHashMap<>(clientInfoValues.size()); | ||
if (CollectionUtils.isNotEmpty(clientInfoValues)) { | ||
for (AnnotationValue<ClientInfo.Attribute> clientInfoValue : clientInfoValues) { | ||
String name = clientInfoValue.getRequiredValue(NAME_MEMBER, String.class); | ||
String value = clientInfoValue.getRequiredValue(VALUE_MEMBER, String.class); | ||
clientInfoAttributes.put(name, value); | ||
} | ||
} | ||
// Fallback defaults if not provided in the annotation | ||
if (StringUtils.isNotEmpty(applicationName)) { | ||
clientInfoAttributes.putIfAbsent(ORACLE_CLIENT_ID, applicationName); | ||
} | ||
clientInfoAttributes.putIfAbsent(ORACLE_MODULE, | ||
MODULE_CLASS_MAP.computeIfAbsent(methodInfo.clazz(), | ||
clazz -> clazz.getName().replace(INTERCEPTED_SUFFIX, "")) | ||
); | ||
clientInfoAttributes.putIfAbsent(ORACLE_ACTION, methodInfo.methodName()); | ||
|
||
return clientInfoAttributes; | ||
} | ||
} |
Oops, something went wrong.