views:

1280

answers:

1

I am developing an Eclipse plug-in that fits a client-server model. Its a commercial project so we cannot re-distribute the JDBC drivers for the various databases we support with the plug-in.

So I developed a preference page to allow the user locate the jars and have a simple discovery mechanism that iterates through the classes in the jar files, loading each one to verify that it implements the java.sql.Driver interface. This all works great.

But the catch is that I am using Hibernate. And Hibernate uses Class.forName() to instantiate the JDBC driver.

If I try to use the following I get ClassNotFoundException.

public Object execute(final IRepositoryCallback callback)
{
    final DatabaseDriverClassLoader loader = new DatabaseDriverClassLoader(
        Activator.getDefault().getDatabaseDriverRegistry());
    final ClassLoader oldLoader = Thread.currentThread()
        .getContextClassLoader();
    try
    {
        Thread.currentThread().setContextClassLoader(loader);
        try
        {
            final SessionFactory sessionFactory = this.configuration
                .buildSessionFactory();
            if (sessionFactory != null)
            {
                final Session session = sessionFactory
                    .openSession();
                if (session != null)
                {
                    // CHECKSTYLE:OFF
                    try
                    // CHECKSTYLE:ON
                    {
                        return callback.doExecute(session);
                    }
                    finally
                    {
                        session.close();
                    }
                }
            }
            connection.close();
        }
        finally
        {
        }
    }
    // CHECKSTYLE:OFF
    catch (Exception e)
    // CHECKSTYLE:ON
    {
        RepositoryTemplate.LOG.error(e.getMessage(), e);
    }
    finally
    {
        Thread.currentThread().setContextClassLoader(oldLoader);
    }
    return null;
}

And if I try creating the driver myself as follows I get a SecurityException.

public Object execute(final IRepositoryCallback callback)
{
    final DatabaseDriverClassLoader loader = new DatabaseDriverClassLoader(
        Activator.getDefault().getDatabaseDriverRegistry());
    final ClassLoader oldLoader = Thread.currentThread()
        .getContextClassLoader();
    try
    {
        Thread.currentThread().setContextClassLoader(loader);
        final Class driverClass = loader.loadClass(this.connectionDriverClassName);
        final Driver driver = (Driver)driverClass.newInstance();
        DriverManager.registerDriver(driver);
        try
        {
            final Connection connection = DriverManager.getConnection(
                this.connectionUrl, this.connectionUsername,
                this.connectionPassword);
            final SessionFactory sessionFactory = this.configuration
                .buildSessionFactory();
            if (sessionFactory != null)
            {
                final Session session = sessionFactory
                    .openSession(connection);
                if (session != null)
                {
                    // CHECKSTYLE:OFF
                    try
                    // CHECKSTYLE:ON
                    {
                        return callback.doExecute(session);
                    }
                    finally
                    {
                        session.close();
                    }
                }
            }
            connection.close();
        }
        finally
        {
            DriverManager.deregisterDriver(driver);
        }
    }
    // CHECKSTYLE:OFF
    catch (Exception e)
    // CHECKSTYLE:ON
    {
        RepositoryTemplate.LOG.error(e.getMessage(), e);
    }
    finally
    {
        Thread.currentThread().setContextClassLoader(oldLoader);
    }
    return null;
}

EDIT: I am not sure it is the best option but I took the approach of implementing my own ConnectionProvider which allowed me instantiate the driver using Class.forName() and I then open the connection using Driver.connect() instead of DriverManager.getConnection(). Its pretty basic but I don't need connection pooling in my specific use case.

The configure() method was as follows:

public void configure(final Properties props)
{
    this.url = props.getProperty(Environment.URL);
    this.connectionProperties = ConnectionProviderFactory
        .getConnectionProperties(props);

    final DatabaseDriverClassLoader classLoader = new DatabaseDriverClassLoader(
        Activator.getDefault().getDatabaseDriverRegistry());

    final String driverClassName = props.getProperty(Environment.DRIVER);
    try
    {
        final Class driverClass = Class.forName(driverClassName, true,
            classLoader);
        this.driver = (Driver)driverClass.newInstance();
    }
    catch (ClassNotFoundException e)
    {
        throw new HibernateException(e);
    }
    catch (IllegalAccessException e)
    {
        throw new HibernateException(e);
    }
    catch (InstantiationException e)
    {
        throw new HibernateException(e);
    }
}

And the getConnection() method is as follows:

public Connection getConnection()
    throws SQLException
{
    return this.driver.connect(this.url, this.connectionProperties);
}
+4  A: 

Class.forName() in OSGi is a major pain. This is not really anyone's fault, just that both use class loaders which do not work the way the other's client is expecting (i.e. OSGi class loader doesn't work in the way that hibernate is expecting).

I think you can go one of a few ways, but the ones I can think of right now are:

  • the clean way, which is to package the JDBC drivers as OSGi bundles. Contribute the class as a service. You can do this with declarative services (probably better) or write an activator which you'll need to manage starting. When you're ready to get the driver, get the JDBCDriver service, and look for the class that you're interested in.
  • the less clean way, but will work for less effort than the first - use DynamicImport-Package to add the exported packages from your bundled drivers. This way, the client code can still see the class that it'll use, but it doesn't have to know about it until runtime. You may have to experiment with the package pattern, however, to cover all cases (that's why it's less clean).
  • the less OSGi way; which is to add your drivers to the eclipse classpath, and add the application parent classloader. You can add this: osgi.parentClassloader=app to your config.ini. This may not fit in with your deployment, especially if you haven't got control of the config.ini file.
  • the non-OSGi way, instead of using the context class loader, use the URLClassLoader. This will only work if you have a directory full of driver jars, or the user can directly or indirectly specify the location of the driver jar.
jamesh
I'll keep this advice in mind for other other projects. Unfortunately, these approaches amount to me repackaging and distributing the vendors' drivers with my tool. I have to avoid that due to licensing issues.
bmatthews68