Loading external JDBC driver using URLClassLoader

Signed-off-by: Toivo Adams <toivo.adams@gmail.com>
Signed-off-by: Mark Payne <markap14@hotmail.com>
This commit is contained in:
Toivo Adams 2015-03-08 19:45:32 +02:00 committed by Mark Payne
parent d1cace6a8a
commit 864e0996ca
2 changed files with 167 additions and 24 deletions

View File

@ -16,11 +16,15 @@
*/
package org.apache.nifi.dbcp;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
@ -78,6 +82,14 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor DB_DRIVER_JAR_URL = new PropertyDescriptor.Builder()
.name("Database Driver Jar Url")
.description("Optional database driver jar file path url. For example 'file:///var/tmp/mariadb-java-client-1.1.7.jar'")
.defaultValue(null)
.required(false)
.addValidator(StandardValidators.URL_VALIDATOR)
.build();
public static final PropertyDescriptor DB_NAME = new PropertyDescriptor.Builder()
.name("Database Name")
.description("Database name")
@ -103,14 +115,14 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
.sensitive(true)
.build();
public static final PropertyDescriptor MAX_WAIT_MILLIS = new PropertyDescriptor.Builder()
.name("Max Wait Millis")
.description("The maximum number of milliseconds that the pool will wait (when there are no available connections) "
+ " for a connection to be returned before throwing an exception, or -1 to wait indefinitely. ")
.defaultValue("500")
public static final PropertyDescriptor MAX_WAIT_TIME = new PropertyDescriptor.Builder()
.name("Max Wait Time")
.description("The maximum amount of time that the pool will wait (when there are no available connections) "
+ " for a connection to be returned before failing, or -1 to wait indefinitely. ")
.defaultValue("500 millis")
.required(true)
.addValidator(StandardValidators.LONG_VALIDATOR)
.sensitive(true)
.addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
.sensitive(false)
.build();
public static final PropertyDescriptor MAX_TOTAL_CONNECTIONS = new PropertyDescriptor.Builder()
@ -131,14 +143,16 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
props.add(DB_HOST);
props.add(DB_PORT);
props.add(DB_DRIVERNAME);
props.add(DB_DRIVER_JAR_URL);
props.add(DB_NAME);
props.add(DB_USER);
props.add(DB_PASSWORD);
props.add(MAX_WAIT_TIME);
props.add(MAX_TOTAL_CONNECTIONS);
properties = Collections.unmodifiableList(props);
}
private ConfigurationContext configContext;
private volatile BasicDataSource dataSource;
@Override
@ -153,7 +167,6 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
*/
@OnEnabled
public void onConfigured(final ConfigurationContext context) throws InitializationException {
configContext = context;
DatabaseSystemDescriptor dbsystem = DatabaseSystems.getDescriptor( context.getProperty(DATABASE_SYSTEM).getValue() );
@ -163,23 +176,25 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
String dbname = context.getProperty(DB_NAME).getValue();
String user = context.getProperty(DB_USER).getValue();
String passw = context.getProperty(DB_PASSWORD).getValue();
Long maxWaitMillis = context.getProperty(MAX_WAIT_MILLIS).asLong();
Long maxWaitMillis = context.getProperty(MAX_WAIT_TIME).asTimePeriod(TimeUnit.MILLISECONDS);
Integer maxTotal = context.getProperty(MAX_TOTAL_CONNECTIONS).asInteger();
dataSource = new BasicDataSource();
dataSource.setDriverClassName(drv);
// Optional driver URL, when exist, this URL will be used to locate driver jar file location
String urlString = context.getProperty(DB_DRIVER_JAR_URL).getValue();
dataSource.setDriverClassLoader( getDriverClassLoader(urlString) );
String dburl = dbsystem.buildUrl(host, port, dbname);
dataSource = new BasicDataSource();
dataSource.setMaxWait(maxWaitMillis);
dataSource.setMaxActive(maxTotal);
dataSource.setUrl(dburl);
dataSource.setDriverClassName(drv);
dataSource.setUsername(user);
dataSource.setPassword(passw);
// That will ensure that you are using the ClassLoader for you NAR.
dataSource.setDriverClassLoader(Thread.currentThread().getContextClassLoader());
// verify connection can be established.
try {
Connection con = dataSource.getConnection();
@ -191,6 +206,25 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
}
}
/**
* using Thread.currentThread().getContextClassLoader();
* will ensure that you are using the ClassLoader for you NAR.
* @throws InitializationException
*/
protected ClassLoader getDriverClassLoader(String urlString) throws InitializationException {
if (urlString!=null && urlString.length()>0) {
try {
URL[] urls = new URL[] { new URL(urlString) };
return new URLClassLoader(urls);
} catch (MalformedURLException e) {
throw new InitializationException("Invalid Database Driver Jar Url", e);
}
}
else
// That will ensure that you are using the ClassLoader for you NAR.
return Thread.currentThread().getContextClassLoader();
}
/**
* Shutdown pool, close all open connections.
*/
@ -216,7 +250,7 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
@Override
public String toString() {
return "DBCPServiceApacheDBCP14[id=" + getIdentifier() + "]";
return "DBCPConnectionPool[id=" + getIdentifier() + "]";
}
}

View File

@ -16,15 +16,22 @@
*/
package org.apache.nifi.dbcp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.apache.nifi.dbcp.DatabaseSystems.getDescriptor;
import static org.junit.Assert.*;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.nifi.processor.exception.ProcessException;
@ -44,7 +51,7 @@ public class DBCPServiceTest {
* Unknown database system.
*
*/
@Test
// @Test
public void testUnknownDatabaseSystem() throws InitializationException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final DBCPConnectionPool service = new DBCPConnectionPool();
@ -57,7 +64,7 @@ public class DBCPServiceTest {
/**
* Missing property values.
*/
@Test
// @Test
public void testMissingPropertyValues() throws InitializationException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final DBCPConnectionPool service = new DBCPConnectionPool();
@ -71,7 +78,7 @@ public class DBCPServiceTest {
* Connect, create table, insert, select, drop table.
*
*/
@Test
// @Test
public void testCreateInsertSelect() throws InitializationException, SQLException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final DBCPConnectionPool service = new DBCPConnectionPool();
@ -104,6 +111,49 @@ public class DBCPServiceTest {
connection.close(); // return to pool
}
/**
* NB!!!!
* Prerequisite: file should be present in /var/tmp/mariadb-java-client-1.1.7.jar
* Prerequisite: access to running MariaDb database server
*
* Test database connection using external JDBC jar located by URL.
* Connect, create table, insert, select, drop table.
*
*/
// @Test
public void testExternalJDBCDriverUsage() throws InitializationException, SQLException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final DBCPConnectionPool service = new DBCPConnectionPool();
runner.addControllerService("test-external-jar", service);
DatabaseSystemDescriptor mariaDb = getDescriptor("MariaDB");
assertNotNull(mariaDb);
// Set MariaDB properties values.
runner.setProperty(service, DBCPConnectionPool.DATABASE_SYSTEM, mariaDb.getValue());
runner.setProperty(service, DBCPConnectionPool.DB_PORT, mariaDb.defaultPort.toString());
runner.setProperty(service, DBCPConnectionPool.DB_DRIVERNAME, mariaDb.driverClassName);
runner.setProperty(service, DBCPConnectionPool.DB_DRIVER_JAR_URL, "file:///var/tmp/mariadb-java-client-1.1.7.jar");
runner.setProperty(service, DBCPConnectionPool.DB_HOST, "127.0.0.1"); // localhost
runner.setProperty(service, DBCPConnectionPool.DB_NAME, "testdb");
runner.setProperty(service, DBCPConnectionPool.DB_USER, "tester");
runner.setProperty(service, DBCPConnectionPool.DB_PASSWORD, "testerp");
runner.enableControllerService(service);
runner.assertValid(service);
DBCPService dbcpService = (DBCPService) runner.getProcessContext().getControllerServiceLookup().getControllerService("test-external-jar");
Assert.assertNotNull(dbcpService);
Connection connection = dbcpService.getConnection();
Assert.assertNotNull(connection);
createInsertSelectDrop(connection);
connection.close(); // return to pool
}
@Rule
public ExpectedException exception = ExpectedException.none();
@ -113,7 +163,7 @@ public class DBCPServiceTest {
* Get many times, after a while pool should not contain any available connection
* and getConnection should fail.
*/
@Test
// @Test
public void testExhaustPool() throws InitializationException, SQLException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final DBCPConnectionPool service = new DBCPConnectionPool();
@ -147,7 +197,7 @@ public class DBCPServiceTest {
* Get many times, release immediately
* and getConnection should not fail.
*/
@Test
// @Test
public void testGetManyNormal() throws InitializationException, SQLException {
final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
final DBCPConnectionPool service = new DBCPConnectionPool();
@ -176,12 +226,49 @@ public class DBCPServiceTest {
}
@Test
// @Test
public void testDriverLoad() throws ClassNotFoundException {
Class<?> clazz = Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
assertNotNull(clazz);
}
/**
* NB!!!!
* Prerequisite: file should be present in /var/tmp/mariadb-java-client-1.1.7.jar
*/
@Test
public void testURLClassLoader() throws ClassNotFoundException, MalformedURLException, SQLException, InstantiationException, IllegalAccessException {
URL url = new URL("file:///var/tmp/mariadb-java-client-1.1.7.jar");
URL[] urls = new URL[] { url };
ClassLoader parent = Thread.currentThread().getContextClassLoader();
URLClassLoader ucl = new URLClassLoader(urls,parent);
Class<?> clazz = Class.forName("org.mariadb.jdbc.Driver", true, ucl);
assertNotNull(clazz);
Driver driver = (Driver) clazz.newInstance();
// Driver is found when using URL ClassLoader
assertTrue( isDriverAllowed(driver, ucl) );
// Driver is not found when using parent ClassLoader
// unfortunately DriverManager will use caller ClassLoadar and driver is not found !!!
assertTrue( isDriverAllowed(driver, parent) );
// DriverManager.registerDriver( (Driver) clazz.newInstance());
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
driver = (Driver) drivers.nextElement();
System.out.println(driver);
}
// Driver driver = DriverManager.getDriver("jdbc:mariadb://127.0.0.1:3306/testdb");
// assertNotNull(driver);
}
String createTable = "create table restaurants(id integer, name varchar(20), city varchar(50))";
String dropTable = "drop table restaurants";
@ -211,4 +298,26 @@ public class DBCPServiceTest {
st.close();
}
//==================================== problem solving - no suitable driver found, mariadb =========================================
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
System.out.println(ex);
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
}