Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support including Module and Action in each JDBC session with Oracle JDBC (take 2) #3252

Closed
wants to merge 9 commits into from
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 issued 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();
}
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) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* 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.Internal;
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.jdbc.config.DataJdbcConfiguration;
import io.micronaut.data.jdbc.connection.annotation.ClientInfo;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.jdbc.BasicJdbcConfiguration;
import io.micronaut.runtime.ApplicationConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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(BasicJdbcConfiguration.class)
@Internal
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(@Parameter DataJdbcConfiguration jdbcConfiguration,
ApplicationContext applicationContext,
@Nullable ApplicationConfiguration applicationConfiguration) {
this.applicationName = applicationConfiguration != null ? applicationConfiguration.getName().orElse(null) : null;
this.enabled = isEnabled(jdbcConfiguration, applicationContext);
this.dataSourceName = jdbcConfiguration.getName();
}

private boolean isEnabled(DataJdbcConfiguration dataJdbcConfiguration, ApplicationContext applicationContext) {
if (!dataJdbcConfiguration.isEnabled()) {
return false;
}
if (dataJdbcConfiguration.getDialect() != Dialect.ORACLE) {
return false;
}
String property = "datasources." + dataJdbcConfiguration.getName() + "." + ORACLE_CLIENT_INFO_ENABLED;
return applicationContext.getProperty(property, Boolean.class).orElse(false);
}

@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 (CollectionUtils.isNotEmpty(connectionClientInfo)) {
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 @NonNull 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.emptyList();
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Repeatable annotation for {@link ClientInfo.Attribute}.
*
* @author radovanradic
* @since 4.11
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ClientInfo {

/**
* Returns the list of the client information attributes.
*
* @return the attribute collection
*/
Attribute[] value() default {};

/**
* An annotation used to set client info for the connection.
*
* @author radovanradic
* @since 4.11
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(ClientInfo.class)
@interface Attribute {

/**
* Returns the name of the client information attribute.
*
* @return the attribute name
*/
String name();

/**
* Returns the value of the client information attribute.
*
* @return the attribute value
*/
String value();
}
}
Loading
Loading