S-JIS[2009-04-13/2009-04-23] 変更履歴
JavaでDBアクセスする為に使用するJDBCのDriverManagerの仕組みについて。
DriverManagerを使ってDBに接続するには、URLを指定してコネクションを取得する。
(実行時のクラスパスに必要なJDBCライブラリー(jarファイル)が入っているのが前提)
import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties;
String url = "jdbc:〜"; Properties info = new Properties(); info.put("user", "ユーザー"); info.put("password", "パスワード"); Connection conn = DriverManager.getConnection(url, info);
getConnection()は、DriverManagerが内部に保持している各JDBCドライバーに対し、順番にURLを渡してConnectionが取得できるかどうか試し、最初に取得できたコネクションを返している。
→ドライバー一覧の取得方法
→DriverManagerへのドライバーの登録方法
接続に使用可能なJDBCドライバー(DriverManagerに登録されているドライバー)一覧は、以下のようにして取得できる。
import java.sql.Driver; import java.util.Enumeration;
public static void dumpDrivers() { Enumeration<Driver> ed = DriverManager.getDrivers(); while (ed.hasMoreElements()) { Driver driver = ed.nextElement(); System.out.println(driver); } }
実行結果の例↓
sun.jdbc.odbc.JdbcOdbcDriver@e0e1c6 org.apache.derby.jdbc.AutoloadedDriver@1389e4 oracle.jdbc.driver.OracleDriver@157f0dc
JdbcOdbcDriverは、デフォルトで入っているっぽい。名前からしてODBCの為のドライバーか?
OracleDriverは、Oracleのドライバー。
AutoloadedDriverは、JavaDBの組み込み環境用ドライバー。実質的に使われるのはEmbeddedDriverのはずだが、DriverManagerにはこのドライバーが登録されている。
AutoloadedDriverはいわばダミーであり、JavaDBでは(getConnection()によって)接続するときに初めてEmbeddedDriverが用意されるらしい。
DriverManagerにJDBCドライバーを登録するには、JDBCドライバーのインスタンスを生成し、DriverManager.registerDriver()を呼び出す。
import java.sql.Driver; class SampleDriver implements Driver { 〜 }
Driver driver = new SampleDriver(); DriverManager.registerDriver(driver);
しかし通常のJDBCドライバーでは、この処理は隠蔽されている。
→JDBC3.0のドライバー登録方法
→JDBC4.0のドライバー登録方法
DriverManagerには、デバッグログの出力機能がある。
DriverManager.setLogWriter(new PrintWriter(System.out)); // DriverManager.setLogStream(System.out); ←こちらは非推奨になっている
これを設定した後で 例えばJDBCドライバーの登録処理を呼び出すと、以下のようなログが出力される。
registerDriver: driver[className=jp.hishidama.sample.jdbc.SampleDriver,jp.hishidama.sample.jdbc.SampleDriver@e0e1c6]
JDBC3.0(JDK1.4以降)のJDBCドライバーの登録方法は、以下のようになっている。
Class.forName("oracle.jdbc.driver.OracleDriver");
これは、リフレクションでクラス(oracle.jdbc.driver.OracleDriver)をロードする操作。
JDBCドライバーでは、クラスの中に静的初期化子で以下のような感じのロジックが書かれている。
(静的初期化子はクラスの中の「static { 〜 }
」という構文。いわばメソッド名や引数や戻り型の無いstaticメソッド)
package oracle.jdbc.driver; import java.sql.Driver; public class OracleDriver implements Driver { static { Driver driver = new OracleDriver(); DriverManager.registerDriver(driver); } 〜 }
静的初期化子は、クラスが初めて使われるときに一度だけ実行される。
なので、Class.forName()が実行された時点(クラスが初期化される時点)で実行される。
したがって、Class.forName()が実行された時点でJDBCドライバーがDriverManagerに登録されることになる。
なので、Class.forName()の代わりに、JDBCドライバークラスを操作してやりさえすればDriverManagerに登録されることになる。
import oracle.jdbc.driver.OracleDriver; 〜 new OracleDriver(); //○ newでインスタンス生成 OracleDriver.getCompileTime(); //○ staticメソッドの呼び出し Class c = OracleDriver.class; //× Classの取得のみ
インスタンスを生成すると、その前に一度だけ静的初期化子が実行されるので、DriverManagerに登録される。
staticメソッドを呼び出すと、その前に一度だけ静的初期化子が実行されるので、DriverManagerに登録される。
「.class」でクラスを取得するのは、基本的にはClass.forName()でクラスを取得するのと同じだが、これだけでは静的初期化子は実行されないらしい。なのでDriverManagerには登録されない。
これらの方法がClass.forName()に劣る点は、ソースの中にクラス名を直接書いているので、コンパイルする際にJDBCドライバーのライブラリー(jarファイル)が必要になること。
Class.forName()は文字列でクラス名を指定するので、コンパイルの際にはJDBCドライバーのライブラリーは必要ない。(実行時にあればよい)
JDBC4.0(JDK1.6以降)では、Class.forName()を実行する必要は無くなった。
JDBC4.0対応のJDBCライブラリー(jarファイル)では、META-INFの中にjava.sql.Driverというファイルが置かれている。
この中にJDBCドライバー名が書かれており、DriverManagerが初めて使用される直前(getConnection()やgetDrivers()の呼び出しの最初の処理)で読み込まれてドライバークラスが初期化される。[/2009-04-14]
したがってそこでドライバー登録の静的初期化子が実行され、DriverManagerに登録される。
>jar -tf derby.jar | findstr java.sql.Driver META-INF/services/java.sql.Driver >jar -xf derby.jar META-INF/services/java.sql.Driver >type META-INF\services\java.sql.Driver org.apache.derby.jdbc.AutoloadedDriver ←JDBCドライバーのクラス名が書かれている
JDBC3.0の方法と同様にClass.forName()等を実行してもよいのだが、上記の方法で既にクラスがロードされている為、静的初期化子はもう実行されない。
(つまり害は無いが意味も無い)
※JDBC4.0では、java.sql.Driverファイルの中にクラス名を書き、そのクラスの静的初期化子でドライバーを登録するようコーディングする。(つまり、どちらかだけではダメで、両方必要)
※なお、このjarファイル(java.sql.Driverファイル)の読み込みには、サービスプロバイダー機能が使われている。[2009-04-14]
前述の(一般的な)方法では、JDBCのライブラリー(jarファイル)が実行時のクラスパスに含まれているのが前提。
Javaで作られた汎用的なDBアクセスツールでは、ツール内でJDBCのライブラリー(jarファイル)やクラス名を登録し、そのままアクセスできるものがある。
クラスローダーを自分で用意すれば、jarファイルを実行時に読み込んでクラスをロードすることが出来るので、それをDriverManagerに登録すればよい。ように思えるのだが。
public static void registerByURLClassLoader() throws Exception { URL url = new File("C:/oracle/ora92/jdbc/lib/ojdbc14.jar").toURI().toURL(); URL[] urls = { url }; URLClassLoader loader = URLClassLoader.newInstance(urls); @SuppressWarnings("unchecked") Class<Driver> cd = (Class<Driver>) loader.loadClass("oracle.jdbc.driver.OracleDriver"); Driver driver = cd.newInstance(); DriverManager.registerDriver(driver); dumpDrivers(); }
登録処理は特にエラーにならずに終わるのだが、実際に接続しようとしたりドライバー一覧を表示したりしてみると、登録したはずのJDBCドライバーが出て来ない…。
実のところ、DriverManagerは、登録されたJDBCを使う際にそれがどこからロードされたものかをチェックしている。
で、システムクラスローダーでロードされたのでない場合(つまり実行時のクラスパスで指定されていない場合)は、そのドライバーは使用しないようになっている。(ドライバー一覧にも返さない)
なので、自前のクラスローダーでロードしたドライバークラスは使用できない。
(試してみたところでは、)システムクラスローダーの実体はURLClassLoaderであり、URLClassLoaderにはaddURL()という、後からURLを追加するメソッドがある。
ただ、これはprotectedメソッドなので、外からは呼べない。(Javaは(JavaScriptと違って)インスタンスに後からメソッドを追加することは出来ないし…)
アクセス権限を変えてやれば、リフレクションで無理矢理呼べなくもないが。
public static void registerBySystemClassLoader() throws Exception { URL url = new File("C:/oracle/ora92/jdbc/lib/ojdbc14.jar").toURI().toURL(); //JDBC3.0 // URL url = new File("C:/Program Files/Java/jdk1.6.0/db/lib/derby.jar").toURI().toURL(); //JDBC4.0 ClassLoader loader = ClassLoader.getSystemClassLoader(); Method m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); m.setAccessible(true); //protectedメソッドにアクセス許可 m.invoke(loader, url); // JDBC4.0のjarファイルの場合は、DriverManager自身の初期化前であれば // 明示的なClass.forName("ドライバー")等は不要 Class.forName("oracle.jdbc.driver.OracleDriver"); //JDBC3.0は必要 dumpDrivers(); }
DriverManagerがそれぞれのjarファイルからドライバーを読み出すのは、DriverManagerが初めて使われる直前(getDrivers()やgetConnection()が呼ばれた時)。[2009-04-14]
なので、JDBC4.0の場合、それより前にaddURL()しておけば、明示的なClass.forName()等は不要。
DriverManagerの初期化(jarファイルからのドライバーの読み込み及び登録)は一度しか行われないので、初期化後にaddURL()した場合は、JDBC4.0であってもClass.forName()等のドライバー登録処理が必要。
でもこの方法は、セキュリティー上、あまり良いやり方とは思えないよなー。
しかし実のところ、DBへの接続であるDriverManager.getConnection()は、内部でDriverクラスのconnect()を呼んでいるだけ。
なのでDriverManagerを使わずにDriverで直接接続すればいい^^;
public static Connection connectByDriver(String url, Properties info) throws Exception { URL fileUrl = new File("C:/Program Files/Java/jdk1.6.0/db/lib/derby.jar").toURI().toURL(); URL[] urls = { fileUrl }; URLClassLoader loader = URLClassLoader.newInstance(urls); @SuppressWarnings("unchecked") Class<Driver> cd = (Class<Driver>) loader.loadClass("org.apache.derby.jdbc.EmbeddedDriver"); Driver driver = cd.newInstance(); Connection conn = driver.connect(url, info); return conn; }
Driver#connect()は、渡されたURLがそのドライバーで扱えないものだった場合はnullを返す。
複数のドライバーの候補がある場合は、順次ドライバーのconnect()を呼び出し、nullでなかったらそれを使えばよい。
(URLの形式がそのドライバーに適合するものであって、でも存在しないパスだったりすると、SQLExceptionが発生する)
ただしこの方法の場合、システムローダーにJDBCドライバーのjarファイルが追加されている訳ではない為、そのjarファイルに入っているその他の(Driver以外の)クラスを別の箇所で使っていると、そちらではクラスがロードできなくてNoClassDefFoundErrorになってしまうので注意。
例えば以下のようなプログラムだと、ドライバーの動的ロード及びコネクション取得は成功するが、SubのクラスでTableName(JavaDBのライブラリーで定義されている)を使おうとしたところで例外が発生する。[2009-04-14]
(コンパイル時点ではderby.jarがクラスパスに入っている想定)
import java.sql.Connection; public static void main(String[] args) throws Exception { //JavaDBのドライバーの動的ロード Connection conn = connectByDriver( "jdbc:derby:C:/temp/javadb/sample1", null); System.out.println(conn.getMetaData()); conn.close(); Sub.test(); } |
import org.apache.derby.impl.sql.compile.TableName;
class Sub { static void test() { //Derbyの独自クラスTableNameを使用している TableName tname = new TableName(); tname.init("SYSIBM", "SYSDUMMY1"); System.out.println(tname); } } |
なお、この方法で一応問題なくDB接続できるのだが、JavaDBの場合、ドライバーインスタンスの変更後に(変更前まで接続していた)同一のDBにアクセスしようとした場合、エラーが発生する。[2009-04-23]
→JavaDBでクラスローダーを使ってDriverインスタンスを生成してDB接続する場合の問題点