All Downloads are FREE. Search and download functionalities are using the official Maven repository.

customplugins.README.md Maven / Gradle / Ivy

# AWS JDBC Driver for MySQL Custom Connection Plugin Sample Application

This README walks through the steps to create two additional custom connection plugins for the AWS
JDBC Driver for MySQL and use them in a simple JDBC application. All of the code used in this README
is available within this directory.

### Prerequisites

To follow along this tutorial or to run this sample program directly, you need to have:

1. Amazon Corretto 8+ or Java 8+
2. A MySQL database
3. The [AWS JDBC Driver for MySQL](https://github.com/awslabs/aws-mysql-jdbc#obtaining-the-aws-jdbc-driver-for-mysql) in your classpath.

## Connection Plugins

Connection plugins are widgets attached to each `Connection` object to help execute additional or
supplementary logic related to that `Connection`. All of your connection plugins are chained together; the prior connection plugin calls the next plugin. The AWS JDBC Driver for MySQL attaches the
`DefaultConnectionPlugin` to the tail of the connection plugin chain and actually executes the given
JDBC method.

Since all the connection plugins are chained together, the prior connection plugin affects the
latter plugins. If the connection plugin at the head of the connection plugin chain measures the 
execution time, this measurement would encompass the time spent in all the connection plugins down
the chain.

> **NOTE**: The `DefaultConnectionPlugin` will always be the last plugin to execute in the connection plugin chain.

## Creating Custom Connection Plugins

This tutorial will walk through the steps to create two connection plugins. The first connection
plugin is called `MethodCountConnectionPlugin`, it tracks the number of method calls throughout the
lifespan of the connection. The second connection plugin `ExecutionTimeConnectionPlugin` tracks the
total execution time of each type of the method calls. We will use the two custom plugins together
with the built-in plugins. The final connection plugin chain will look like this:
![](./diagrams/connection_plugin_chain.png)

The following is an example of the execution process of a JDBC method:
![](../../../../docs/files/images/connection_plugin_manager_diagram.png)

### Creating a Top Level Connection Plugin

All connection plugins have to implement the `IConnectionPlugin` interface.

```java
/**
 * This connection plugin counts the total number of executed JDBC methods throughout the
 * lifespan of the current connection.
 *
 * 

All connection plugins must implement the {@link IConnectionPlugin} interface. Since * all the connection plugins are chained together, the prior connection plugin needs to * invoke the next plugin. * Once registered, every connection will create an instance of this connection plugin. */ public class MethodCountConnectionPlugin implements IConnectionPlugin { private final IConnectionPlugin nextPlugin; private final Log logger; private final Map methodCount = new HashMap<>(); public MethodCountConnectionPlugin(IConnectionPlugin nextPlugin, Log logger) { this.nextPlugin = nextPlugin; this.logger = logger; } /** * All method calls related to the connection object will be passed to this method as * {@code Callable executeJdbcMethod}. * This includes methods that may be called frequently, such as: *

    *
  • {@link ResultSet#next()}
  • *
  • {@link ResultSet#getString(int)}
  • *
*/ @Override public Object execute( Class methodInvokeOn, String methodName, Callable executeJdbcMethod) throws Exception { // Increment the number of calls to this method. methodCount.merge(methodName, 1, Integer::sum); // Traverse the connection plugin chain by invoking the `execute` method in the // next plugin. return this.nextPlugin.execute(methodInvokeOn, methodName, executeJdbcMethod); } @Override public void transactionBegun() { this.nextPlugin.transactionBegun(); } @Override public void transactionCompleted() { this.nextPlugin.transactionCompleted(); } /** * This method is called when the connection closes. * If this connection plugin has any background threads this is the time to clean up * these dangling resources. However, you can also perform other steps you wish before * closing the plugin. This sample outputs all the aggregated information during this * step. */ @Override public void releaseResources() { // Output the aggregated information from all methods called throughout the lifespan // of the current connection. final String leftAlignFormat = "| %-19s | %-10d |\n"; final StringBuilder logMessage = new StringBuilder(); logMessage .append("** MethodCountConnectionPlugin Summary **\n") .append("+---------------------+------------+\n") .append("| Method Executed | Frequency |\n") .append("+---------------------+------------+\n"); methodCount.forEach((key, val) -> logMessage.append(String.format( leftAlignFormat, key, val))); logMessage.append("+---------------------+------------+\n"); logger.logInfo(logMessage); methodCount.clear(); // Traverse the connection plugin chain by calling the next plugin. This step allows // all connection plugins a chance to clean up any dangling resources or perform any // last tasks before shutting down. // In this sample, `ExecutionTimeConnectionPlugin#releaseResources()` will be called // to print out the total execution time of each method. this.nextPlugin.releaseResources(); } } ``` ### Creating the Second Connection Plugin The next custom plugin is the `ExecutionTimeConnectionPlugin`, which tracks the time spent executing the given method in the remaining plugins. ```java /** * This connection plugin tracks the execution time of all the given JDBC methods throughout * the lifespan of the current connection. * *

During the cleanup phase when {@link ExecutionTimeConnectionPlugin#releaseResources()} * is called, the plugin logs all the methods executed and time spent on each execution * in milliseconds. */ public class ExecutionTimeConnectionPlugin implements IConnectionPlugin { final long initializeTime; final IConnectionPlugin nextPlugin; private final Log logger; private final Map methodExecutionTimes = new HashMap<>(); public ExecutionTimeConnectionPlugin( IConnectionPlugin nextPlugin, Log logger) { this.nextPlugin = nextPlugin; this.logger = logger; initializeTime = System.currentTimeMillis(); } @Override public Object execute( Class methodInvokeOn, String methodName, Callable executeJdbcMethod) throws Exception { // This `execute` measures the time it takes for the remaining connection plugins to // execute the given method call. final long startTime = System.nanoTime(); final Object executeResult = this.nextPlugin.execute(methodInvokeOn, methodName, executeJdbcMethod); final long elapsedTime = System.nanoTime() - startTime; methodExecutionTimes.merge( methodName, elapsedTime / 1000000, Long::sum); return executeResult; } @Override public void transactionBegun() { this.nextPlugin.transactionBegun(); } @Override public void transactionCompleted() { this.nextPlugin.transactionCompleted(); } @Override public void releaseResources() { // Output the aggregated information from all methods called throughout the lifespan // of the current connection. final long connectionUptime = System.nanoTime() - initializeTime; final String leftAlignFormat = "| %-19s | %-10s |\n"; final StringBuilder logMessage = new StringBuilder(); logMessage.append("** ExecutionTimeConnectionPlugin Summary **\n"); logMessage.append(String.format( "Connection Uptime: %ds\n", connectionUptime / 1000000 )); logMessage .append("** Method Execution Time **\n") .append("+---------------------+------------+\n") .append("| Method Executed | Total Time |\n") .append("+---------------------+------------+\n"); methodExecutionTimes.forEach((key, val) -> logMessage.append(String.format( leftAlignFormat, key, val + "ms"))); logMessage.append("+---------------------+------------+\n"); logger.logInfo(logMessage); methodExecutionTimes.clear(); // Traverse the connection plugin chain by calling the next plugin. This step allows // all connection plugins a chance to clean up any dangling resources or perform any // last tasks before shutting down. this.nextPlugin.releaseResources(); } } ``` ### Creating the Connection Plugin Factories Before we can register the connection plugins in the `ConnectionPluginManager`, we need to create factory classes that implement the `IConnectionPluginFactory` interface. Each `IConnectionPluginFactory` implementation instantiates a specific connection plugin. First is the `MethodCountConnectionPluginFactory`. ```java import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.conf.PropertySet; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ha.plugins.IConnectionPlugin; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ha.plugins.IConnectionPluginFactory; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ha.plugins.ICurrentConnectionProvider; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.log.Log; /** * This class initializes the {@link MethodCountConnectionPlugin}. */ public class MethodCountConnectionPluginFactory implements IConnectionPluginFactory { @Override public IConnectionPlugin getInstance( ICurrentConnectionProvider currentConnectionProvider, PropertySet propertySet, IConnectionPlugin nextPlugin, Log logger) { logger.logInfo( "[MethodCountConnectionPluginFactory] ::: Creating a method count connection plugin"); return new MethodCountConnectionPlugin(nextPlugin, logger); } } ``` Next is the `ExecutionTimeConnectionPluginFactory`. ```java import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.conf.PropertySet; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ha.plugins.IConnectionPlugin; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ha.plugins.IConnectionPluginFactory; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.jdbc.ha.plugins.ICurrentConnectionProvider; import software.aws.rds.jdbc.mysql.shading.com.mysql.cj.log.Log; /** * This class initializes {@link ExecutionTimeConnectionPlugin}. */ public class ExecutionTimeConnectionPluginFactory implements IConnectionPluginFactory { @Override public IConnectionPlugin getInstance( ICurrentConnectionProvider currentConnectionProvider, PropertySet propertySet, IConnectionPlugin nextPlugin, Log logger) { logger.logInfo( "[ExecutionTimeConnectionPluginFactory] ::: Creating an execution time connection plugin"); return new ExecutionTimeConnectionPlugin(nextPlugin, logger); } } ``` ## Using the Custom Connection Plugins This main application is a simple JDBC application. It creates a connection to a MySQL database instance and executes a query with the several layers of connection plugins. ```java import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; /** * This is a simple application for creating and using custom connection plugins. */ public class SampleApplication { private static final String PREFIX = "jdbc:mysql:aws://"; private static final String CONNECTION_STRING = PREFIX + System.getenv("host"); private static final String USER = System.getenv("user"); private static final String PASSWORD = System.getenv("password"); private static final String QUERY = System.getenv("query"); public static void main(String[] args) throws SQLException { final Properties properties = new Properties(); properties.setProperty("user", USER); properties.setProperty("password", PASSWORD); properties.setProperty("logger", "StandardLogger"); final String methodCountConnectionPluginFactoryClassName = MethodCountConnectionPluginFactory.class.getName(); final String executionMeasurementPluginFactoryClassName = ExecutionTimeConnectionPluginFactory.class.getName(); // To use custom connection plugins, set the connectionPluginFactories to a // comma-separated string containing the fully-qualified class names of custom plugin // factories to use. properties.setProperty( "connectionPluginFactories", String.format("%s,%s", methodCountConnectionPluginFactoryClassName, executionMeasurementPluginFactoryClassName)); try (Connection conn = DriverManager.getConnection(CONNECTION_STRING, properties)) { try (Statement statement = conn.createStatement()) { try (ResultSet result = statement.executeQuery(QUERY)) { final int cols = result.getMetaData().getColumnCount(); while (result.next()) { for (int i = 1; i < cols; i++) { System.out.println(result.getString(i)); } } } } } } } ``` ## FAQ - Why does `IConnectionPlugin#execute(Class, String, Callable)` accept the invoked method and the Java class calling this method instead of the actual object? - The `execute` method accepts the invocation class and the method call, such as `execute(ResultSet.class, "getString", () -> rs.getString(1))` because the Java class and the method name already provides enough information about the invoked method. Not passing the actual object prevents modification of the object,and avoids side effects when executing SQL functions in connection plugins.





© 2015 - 2025 Weber Informatics LLC | Privacy Policy