# jdbc treiber (h2) mit eigenem ClassLoader laden



## modestiny (24. Apr 2009)

Hi,

ich hab mal wieder eine Frage. Und zwar muß ich die JDBC-Treiber von h2 mittels eines eigenen ClassLoaders laden. Normalerweise wird das ja so gemacht:

```
import java.sql.*;
public class Test {
    public static void main(String[] a)
            throws Exception {
        Class.forName("org.h2.Driver");
        Connection conn = DriverManager.
            getConnection("jdbc:h2:~/test", "sa", "");
        // add application code here
        conn.close();
    }
}
```

Nun kann ich aber nicht den System-ClassLoader nehmen, da das Prog ein Installationtool ist, welches die h2-JAR in ein Verzeichnis installiert, in dem die spätere Anwendung (die ebenfalls installiert wird) läuft. Das Inst-Prog muß aber noch einige Tabellen in der DB erstellen und Einträge in diesen vornehmen. Somit ist zur Startzeit des Inst-Prog der spätere Installpath nicht bekannt, da dieser vom Benutzer gesetzt wird.

Nun habe ich mir gedacht, ich baue mir einen eigenen ClassLoader, der den Treiber lädt. Aber dies brachte neue Probleme mit sich. Erst wurden Klassen wie z.B. java.sql.Driver nicht mehr gefunden, die innerhalb des h2-jdbc-treibers referenziert werden. Dies konnte ich aber noch lösen, in dem ich bei nicht auffinden der Klasse innerhalb der h2-JAR den System-ClassLoader bemühe. Aber sobald ich den DriverManager bemühe eine Connection aufzubauen, sagt dieser das kein entsprechender Treiber gefunden werden konnte (no suitable driver...). Daraus schließe ich jetzt mal, das mein ClassLoader den Treiber nicht richtig initialisiert, so daß dieser sich beim DriverManager anmeldet.

Hier mal der Code meises ClassLoaders:

```
package WebSetup.Setup;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

public class JarClassLoader extends ClassLoader
{
	private JarFile jarFile;
	
	public JarClassLoader(File jarFile) throws IOException
	{
		this.jarFile = new JarFile(jarFile);
	}
	
	protected Class<?> findClass(String className)
	{
		Class<?> ret = null;
		
		try
		{
			String classString = className.replace(".", "/") + ".class";
			System.out.println("Try to find Class: " + classString);
			ZipEntry zipEntry = this.jarFile.getEntry(classString);
			if(zipEntry != null)
			{
				System.out.println("Class found...");
				InputStream in = this.jarFile.getInputStream(zipEntry);
				int b;
				ArrayList<Byte> array = new ArrayList<Byte>();
				while((b = in.read()) != -1)
				{
					array.add(new Byte((byte)b));
				}
				byte[] buffer = new byte[array.size()];
				for(int i = 0; i < buffer.length; i++)
				{
					buffer[i] = array.get(i).byteValue();
				}
				System.out.println("Class data length: " + buffer.length);
				ret = this.defineClass(className, buffer, 0, buffer.length);
				this.resolveClass(ret);
			}
			else
			{
				ret = Class.forName(className);
			}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
		catch (ClassNotFoundException e)
		{
			e.printStackTrace();
		}
		return(ret);
	}
	
	public Class<?> loadClass(String className)
	{
		Class<?> ret = this.findClass(className);
		return(ret);
	}
}
```

Außerdem fand ich noch etwas in den Sourcen des h2-Treibers, was die Initialisierung angeht, von dem ich aber weder wußte, daß es sowas gibt, noch wie sowas von der Funktion her abläuft.

```
/*
 * Copyright 2004-2009 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * ([url=http://h2database.com/html/license.html]License[/url]).
 * Initial Developer: H2 Group
 */
package org.h2;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Properties;

import org.h2.engine.Constants;
import org.h2.jdbc.JdbcConnection;
import org.h2.message.Message;
import org.h2.message.TraceSystem;

/**
 * The database driver. An application should not use this class directly. The
 * only thing the application needs to do is load the driver. This can be done
 * using Class.forName. To load the driver and open a database connection, use
 * the following code:
 *
 * <pre>
 * Class.forName(&quot;org.h2.Driver&quot;);
 * Connection conn = DriverManager.getConnection(
 *      &quot;jdbc:h2:&tilde;/test&quot;, &quot;sa&quot;, &quot;sa&quot;);
 * </pre>
 */
public class Driver implements java.sql.Driver {

    private static final Driver INSTANCE = new Driver();
    private static volatile boolean registered;

    static {
        load();
    }

    /**
     * Open a database connection.
     * This method should not be called by an application.
     * Instead, the method DriverManager.getConnection should be used.
     *
     * @param url the database URL
     * @param info the connection properties
     * @return the new connection
     */
    public Connection connect(String url, Properties info) throws SQLException {
        try {
            if (info == null) {
                info = new Properties();
            }
            if (!acceptsURL(url)) {
                return null;
            }
            return new JdbcConnection(url, info);
        } catch (Exception e) {
            throw Message.convert(e);
        }
    }

    /**
     * Check if the driver understands this URL.
     * This method should not be called by an application.
     *
     * @param url the database URL
     * @return if the driver understands the URL
     */
    public boolean acceptsURL(String url) {
        return url != null && url.startsWith(Constants.START_URL);
    }

    /**
     * Get the major version number of the driver.
     * This method should not be called by an application.
     *
     * @return the major version number
     */
    public int getMajorVersion() {
        return Constants.VERSION_MAJOR;
    }

    /**
     * Get the minor version number of the driver.
     * This method should not be called by an application.
     *
     * @return the minor version number
     */
    public int getMinorVersion() {
        return Constants.VERSION_MINOR;
    }

    /**
     * Get the list of supported properties.
     * This method should not be called by an application.
     *
     * @param url the database URL
     * @param info the connection properties
     * @return a zero length array
     */
    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
        return new DriverPropertyInfo[0];
    }

    /**
     * Check if this driver is compliant to the JDBC specification.
     * This method should not be called by an application.
     *
     * @return true
     */
    public boolean jdbcCompliant() {
        return true;
    }

    /**
     * INTERNAL
     */
    public static synchronized Driver load() {
        try {
            if (!registered) {
                registered = true;
                DriverManager.registerDriver(INSTANCE);
            }
        } catch (SQLException e) {
            TraceSystem.traceThrowable(e);
        }
        return INSTANCE;
    }

    /**
     * INTERNAL
     */
    public static synchronized void unload() {
        try {
            if (registered) {
                registered = false;
                DriverManager.deregisterDriver(INSTANCE);
            }
        } catch (SQLException e) {
            TraceSystem.traceThrowable(e);
        }
    }

}
```
Ich meine dieses einsame

```
static { load(); }
```
am Anfang der Klasse.

Wann wird sowas ausgeführt und wodurch?

Danke in Vorraus

Lars


----------



## mvitz (24. Apr 2009)

Ist jetzt keine direkte Antwort, sondern eher eine Idee.

h2 kann ja die DB auch als Datei speichern, wenn du dies eh machst, kannst du doch evtl. einfach die richtig initialisierte DB Datei mit installieren.

Das funktioniert natürlich nur, wenn das DB Schema nicht von irgendetwas abhängt, was erst während der Installation gesetzt wird.


----------



## modestiny (24. Apr 2009)

Tja und das ist leider der Fall.

Es werden informationen in die DB eingetragen, die erst bei der Installation gesammelt werden.

Gruß

Lars


----------



## Ebenius (24. Apr 2009)

modestiny hat gesagt.:


> Nun habe ich mir gedacht, ich baue mir einen eigenen ClassLoader, der den Treiber lädt.


Wieso? Was gibt's denn gegen den URLClassLoader einzuwenden?



modestiny hat gesagt.:


> Daraus schließe ich jetzt mal, das mein ClassLoader den Treiber nicht richtig initialisiert, so daß dieser sich beim DriverManager anmeldet.


Dieser Schluss ist falsch. Der DriverManager erlaubt ganz explizit nur Treiber die über den selben ClassLoader erreichbar sind, den auch der Caller benutzt. Zwei Lösungsvorschläge...

*Vorschlag 1:* (der einfache Weg) Mach Deine Connection einfach ohne DriverManager. Hier am Beispiel Oracle, weil ich kein MySQL habe: 
	
	
	
	





```
static Connection getOracleConnection(
      String jdbcURLString,
      Properties props) throws SQLException {
  final Driver driver = getOracleDriver();
  return driver.connect(jdbcURLString, props);
}

static Driver getOracleDriver() throws SQLException {
  File driverJARFile =
        new File("/opt/oracle/product/10gR2/jdbc/lib/ojdbc14.jar");
  URL driverJarUrl;
  try {
    driverJarUrl = driverJARFile.toURI().toURL();
  } catch (MalformedURLException ex) {
    throw new SQLException("Could not load JDBC Driver: " + driverJARFile,
          "IM003", ex);
  }

  String driverClassName = "oracle.jdbc.driver.OracleDriver";
  return loadDriver(driverJarUrl, driverClassName);
}

static Driver loadDriver(URL driverJARURL, String driverClassName)
      throws SQLException {
  try {
    final URLClassLoader cl =
          new URLClassLoader(new URL[] { driverJARURL });

    final Class<?> driverClass = Class.forName(driverClassName, true, cl);
    final Driver driver = (Driver) driverClass.newInstance();
    return driver;
  } catch (ClassNotFoundException ex) {
    throw new SQLException(
          "Could not load JDBC Driver: " + driverClassName, "IM003", ex);
  } catch (InstantiationException ex) {
    throw new SQLException(
          "Could not load JDBC Driver: " + driverClassName, "IM003", ex);
  } catch (IllegalAccessException ex) {
    throw new SQLException(
          "Could not load JDBC Driver: " + driverClassName, "IM003", ex);
  }
}
```

*Vorschlag 2:* (der Weg gefällt mir besser): Diese Klasse ist im Installationsprogramm enthalten: 
	
	
	
	





```
/* (@)JDBCDriverManager.java */

/* Copyright 2009 Sebastian Haufe

 * 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

       [url]http://www.apache.org/licenses/LICENSE-2.0[/url]

 * 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 com.ebenius;

import java.net.URL;
import java.net.URLClassLoader;
import java.sql.*;
import java.util.*;
import java.util.logging.Logger;

/**
 * A stub to load a JDBC Driver, working around the problem that the
 * {@link DriverManager DriverManager class} cannot load Drivers within an
 * alternative ClassLoader context.
 * 
 * @version 1.0
 * @author Sebastian Haufe
 */
public abstract class JDBCDriverManager {

  // -------------------------------------------------------------------------
  // Class members
  // -------------------------------------------------------------------------

  private static URL implURL;
  private static final String implClassName =
        JDBCDriverManager.class.getPackage().getName()
              + ".JDBCDriverStubImpl";

  private static boolean initialized = false;
  private static Collection<Stub> stubs =
        Collections.synchronizedCollection(new ArrayList<Stub>());

  static {
    try {
      Class.forName(implClassName);
      Logger.getLogger("JDBCDriverManager.class.getName()").severe(
            implClassName
                  + " is available per our class loader. "
                  + "Enhanced driver support will not work!");
    } catch (ClassNotFoundException ex) {
      // okay
    }
  }

  /**
   * Get the JDBC driver to the given URL.
   * 
   * @param url the URL
   * @return the driver
   * @throws SQLException if no such driver exists
   * @see DriverManager#getDriver(String)
   */
  public static Driver getDriver(String url) throws SQLException {
    /* try to get the driver from our context */
    SQLException storedException = null;
    try {
      return DriverManager.getDriver(url);
    } catch (SQLException ex) {
      storedException = ex;
    }

    /* find the driver by stubs */
    synchronized (stubs) {
      for (Stub stub : stubs) {
        try {
          return stub.getDriver(url);
        } catch (SQLException ex) {
          // ignore, we throw the stored exception if all attempts fail
        }
      }
    }

    throw storedException;
  }

  /**
   * Get a connection to the given JDBC URL with the given properties.
   * 
   * @param url the JDBC URL
   * @param info the properties
   * @return the connection
   * @throws SQLException if the connection cannot be established for any
   *           reason
   * @see DriverManager#getConnection(String, Properties)
   */
  public static Connection getConnection(String url, Properties info)
        throws SQLException {
    return getDriver(url).connect(url, info);
  }

  /**
   * Register a JDBC driver. The URL is used by an {@link URLClassLoader} to
   * load the driver.
   * 
   * @param driverResourceURL the URL to the driver (JAR file, directory, ...)
   * @param driverClassName the class name of the driver implementation to
   *          initialized
   * @throws SQLException if registering the driver fails for any reason
   * @throws IllegalStateException if this class is not initialized
   * @see #initialize(URL)
   */
  public static void registerDriver(
        URL driverResourceURL,
        String driverClassName) throws SQLException {
    if (!initialized) {
      throw new IllegalStateException("Stub not initialized!");
    }

    final URLClassLoader cl =
          new URLClassLoader(new URL[] { driverResourceURL, implURL });
    try {
      final Class<?> stubImplClass = Class.forName(implClassName, true, cl);
      stubs.add((Stub) stubImplClass.newInstance());
    } catch (Exception ex) {
      throw new SQLException("Could not load Driver Stub Impl: " + implURL,
            "IM003", ex);
    }

    /* let the driver register itself */
    try {
      Class.forName(driverClassName, true, cl);
    } catch (ClassNotFoundException ex) {
      throw new SQLException(
            "Could not load JDBC Driver: " + driverClassName, "IM003", ex);
    }
  }

  /**
   * Returns the URL to the stub implementation (JAR file or directory).
   * 
   * @return the URL &ndash; possibly {@code null} (if not yet initialized)
   * @see #initialize(URL)
   */
  public static URL getStubImplURL() {
    return implURL;
  }

  /**
   * Sets the URL to the stub implementation (JAR file or directory).
   * 
   * @param stubImplURL the URL to set
   */
  public static void initialize(URL stubImplURL) {
    synchronized (stubs) {
      if (stubImplURL == null) {
        throw new IllegalArgumentException( //
              "stubImplURL not allowed null"); //$NON-NLS-1$
      }

      JDBCDriverManager.implURL = stubImplURL;
      initialized = true;
    }
  }

  // -------------------------------------------------------------------------
  // Inner classes
  // -------------------------------------------------------------------------

  public interface Stub {

    /**
     * Loads the JDBC Driver to the given URL.
     * 
     * @param url the URL to load the JDBC driver for
     * @return the JDBC Driver
     * @throws SQLException if loading the driver fails for any reason
     */
    public abstract Driver getDriver(String url) throws SQLException;
  }
}
```

Diese Klasse liegt in einer extra JAR-Datei die *nicht* über den normalen ClassLoader verfügbar ist, aber im selben _package_ wie die andere Klasse: 
	
	
	
	





```
package com.ebenius;

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;

public final class JDBCDriverStubImpl implements JDBCDriverManager.Stub {

  /** Creates a new {@code JDBCDriverStubImpl}. */
  public JDBCDriverStubImpl() {}

  public Driver getDriver(String url) throws SQLException {
    return DriverManager.getDriver(url);
  }
}
```

Verwendet wird dann Vorschlag 2 so: 
	
	
	
	





```
String jdbcURLString = "jdbc:oracle:thin:@//workhorse:1521/MYDB";
final Properties p = new Properties();
p.setProperty("user", "XXXX");
p.setProperty("password", "YYYY");

JDBCDriverManager.initialize(new File(
      "/home/ebenius/workspace/Playground/jdbcdriverstubimpl.jar").toURI().toURL());
JDBCDriverManager.registerDriver(new File(
      "/opt/oracle/product/10gR2/jdbc/lib/ojdbc14.jar").toURI().toURL(),
      "oracle.jdbc.driver.OracleDriver");

final Connection conn = JDBCDriverManager.getConnection(jdbcURLString, p);
```



modestiny hat gesagt.:


> ```
> static { load(); }
> ```
> am Anfang der Klasse.


Das nennt sich _static initialization_. Diese Blöcke werden beim Initialisieren der Klasse ausgeführt; also frühestens direkt nach dem Laden der Klasse und spätestens direkt vor der ersten Verwendung der Klasse. Mehr dazu im Sun Java™ Tutorial: Initializing Fields

Ebenius


----------



## modestiny (24. Apr 2009)

Danke, das hat mir sehr weitergeholfen.

Gruß

Lars


----------

