S2JDBCでClassPath外のJDBCドライバを使う
表題の通りS2JDBCでClassPathの通っていないJarのJDBCドライバを使いたい。
なんでそんなことがしたいかというと、マルチDB対応のDB管理クライアントアプリケーションで
任意のJDBCドライバをユーザに選ばせる機能をつけたいから。
なのでJVM起動時にはJarの場所は分からないのです…
目標目指してコーディングしてみる。
まずは普通にS2JDBCを動かす
とりあえずS2JDBCが動くように環境を整える。
手っ取り早くするためにS2ContainerのダウンロードページからS2JDBCのチュートリアルをダウンロード。
このページのセットアップに従いインポートやらAntタスクやらソースの書き換えを行う。
デフォルトのままでHSQLDBの設定がしてあるから楽チン
その後動作確認のためにユニットテストを流してグリーンを確認!
ここまでできたらコーディング。
S2Containerを生成してJDBCManagerを取り出してEmployeeテーブルのデータをselectしてみる。
package examples; import java.util.List; import org.seasar.extension.jdbc.JdbcManager; import org.seasar.framework.container.S2Container; import org.seasar.framework.container.factory.S2ContainerFactory; import examples.entity.Employee; public class UsualMain { public static void main(String[] args) { // S2Containerを初期化 S2Container container = S2ContainerFactory.create("app.dicon"); container.init(); // JDBCManagerを取得 JdbcManager jdbcManager = (JdbcManager) container.getComponent("jdbcManager"); // Employeeレコードを取得 List<Employee> result = jdbcManager.from(Employee.class).getResultList(); for (Employee each : result) { System.out.println(each.name); } } }
Employeeテーブルには予め以下のデータが入っているので
id | name | job_type | salary | department_id | address_id | version |
---|---|---|---|---|---|---|
1 | 'ALLEN' | 1 | 1600 | 3 | 1 | 1 |
2 | 'WARD' | 1 | 1250 | 3 | 2 | 1 |
3 | 'JONES' | 2 | 2975 | 2 | 3 | 1 |
4 | 'MARTIN' | 1 | 1250 | 3 | 4 | 1 |
5 | 'BLAKE' | 2 | 2850 | 3 | 5 | 1 |
6 | 'CLARK' | 2 | 2450 | 1 | 6 | 1 |
7 | 'SCOTT' | 3 | 3000 | 2 | 7 | 1 |
8 | 'KING' | 4 | 5000 | 1 | 8 | 1 |
9 | 'TURNER' | 1 | 1500 | 3 | 9 | 1 |
10 | 'ADAMS' | 0 | 1100 | 2 | 10 | 1 |
11 | 'JAMES' | 0 | 950 | 3 | 11 | 1 |
12 | 'FORD' | 3 | 3000 | 2 | 12 | 1 |
13 | 'MILLER' | 0 | 1300 | 1 | 13 | 1 |
14 | 'SMITH' | 0 | 800 | 2 | 14 | 1 |
結果はこうなります
ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER SMITH
ちゃんとselectできてますね。予想通りです。
ここまでは普通のS2JDBCかと思います。
JDBCドライバをクラスパスから外してみる
次にJDBCドライバを含むJar"hsqldb-x.x.x.x.jar"をクラスパスから外して先程のコードを実行してみます。
Exception in thread "main" org.seasar.framework.beans.IllegalPropertyRuntimeException: [ESSR0059]クラス( org.seasar.extension.dbcp.impl.XADataSourceImpl)のプロパティ(driverClassName)の設定に失敗しました。 理由はorg.seasar.framework.exception.SIllegalArgumentException: [ESSR0098]クラス (org.seasar.extension.dbcp.impl.XADataSourceImpl)[sun.misc.Launcher$AppClassLoader@a90653]の型 (java.lang.String)[null]のプロパティ(driverClassName)に、型(java.lang.String)[null]の値(org.hsqldb.jdbcDriver) を設定できませんでした。対象のクラスは(org.seasar.extension.dbcp.impl.XADataSourceImpl) [sun.misc.Launcher$AppClassLoader@a90653]です。 (…略…) Caused by: java.lang.ClassNotFoundException: org.hsqldb.jdbcDriver at java.net.URLClassLoader$1.run(Unknown Source) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) at java.lang.ClassLoader.loadClass(Unknown Source) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Unknown Source) at org.seasar.framework.util.ClassUtil.forName(ClassUtil.java:98) ... 22 more
まぁ当然ClassNotFound例外が原因で落ちてしまいますよね。
これも当然。
JDBCのJarを含んだクラスローダを作ってS2に渡してみる
S2Containerにはクラスローダを渡して生成できる口がある。
S2ContainerFactory#create(java.lang.String, java.lang.ClassLoader)
ここにJDBCのJarを含んだクラスローダを突っ込んでやればうまいこと行くんじゃないの!?
と踏んで書いてみる。JDBCのJarはC:\test配下に置いてあるという前提。
package examples; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.List; import org.seasar.extension.jdbc.JdbcManager; import org.seasar.framework.container.S2Container; import org.seasar.framework.container.factory.S2ContainerFactory; import examples.entity.Employee; import static java.lang.String.format; public class CustomClassLoaderMain { static final File jarFile = new File("c:\\test\\hsqldb-1.8.0.1.jar"); public static void main(String[] args) throws Exception { URL jarURL = new URL(format("jar:%s!/", jarFile .getCanonicalFile() .toURI() .toURL() .toString())); URLClassLoader classLoader = new URLClassLoader( new URL[] { jarURL }, CustomClassLoaderMain.class.getClassLoader()); System.out.println(classLoader.loadClass("org.hsqldb.jdbcDriver")); // ロード可能か確認してみる S2Container container = S2ContainerFactory.create("app.dicon", classLoader); // Jarを追加したクラスローダ渡す container.init(); JdbcManager jdbcManager = (JdbcManager) container.getComponent("jdbcManager"); List<Employee> result = jdbcManager.from(Employee.class).getResultList(); for (Employee each : result) { System.out.println(each.name); } } }
結果は…
Exception in thread "main" org.seasar.framework.exception.SQLRuntimeException: [ESSR0072]SQLで例外 (SQL=[], Message=[No suitable driver found for jdbc:hsqldb:file:C:\Users\hoge\Desktop\s2jdbc-tutorial\build\classes/data/test], ErrorCode=0, SQLState=08001)が発生しました at org.seasar.extension.jdbc.util.DataSourceUtil.getConnection(DataSourceUtil.java:53) at org.seasar.extension.jdbc.manager.JdbcManagerImpl.getJdbcContext(JdbcManagerImpl.java:381) at org.seasar.extension.jdbc.query.AbstractSelect.getResultListInternal(AbstractSelect.java:223) at org.seasar.extension.jdbc.query.AbstractSelect.getResultList(AbstractSelect.java:172) at examples.CustomClassLoaderMain.main(CustomClassLoaderMain.java:61) Caused by: java.sql.SQLException: No suitable driver found for jdbc:hsqldb:file:C:\Users\hoge\Desktop\s2jdbc-tutorial\build\classes/data/test at java.sql.DriverManager.getConnection(Unknown Source) at java.sql.DriverManager.getConnection(Unknown Source) at org.seasar.extension.dbcp.impl.XADataSourceImpl.getXAConnection(XADataSourceImpl.java:168) at org.seasar.extension.dbcp.impl.XADataSourceImpl.getXAConnection(XADataSourceImpl.java:151) at org.seasar.extension.dbcp.impl.ConnectionPoolImpl.createConnection(ConnectionPoolImpl.java:446) at org.seasar.extension.dbcp.impl.ConnectionPoolImpl.checkOut(ConnectionPoolImpl.java:366) at org.seasar.extension.dbcp.impl.DataSourceImpl.getConnection(DataSourceImpl.java:59) at org.seasar.extension.jdbc.util.DataSourceUtil.getConnection(DataSourceUtil.java:51) ... 4 more
なんと例外…ダメだったか…orz
ログからするとC2Container自体は生成し終わって、前回よりも進んでる。
S2Container作るときに渡せるクラスローダーはコンポーネントを生成するときに使われるけど、
コンポーネント自身がロードするクラスには使われない(システムクラスローダーになるわけではない)
という理解でいいのだろうかなぁ?
XADataSourceをいぢる
このスタックトレースを元にS2のソースを見ていくとどうもXADataSourceImplクラスがDriverのConnectionを生成する役割のS2コンポーネントみたい。
こいつのJDBCドライバの生成でコケてるから、加工したクラスローダから生成するようにオーバーライドすればなんとかなりそう。
でもどうやったら加工したクラスローダをコンポーネントがうけとれるのか分かんない…(´・ω・`)
しょうがないので実行中のスレッドのget/setContextClassLoaderを介して無理やり受け渡してみる…
XADataSourceImplクラスをちょっと変更する。
加工されたクラスローダからDriverインスタンスをnewするところ以外はほとんどパクリ。
package examples; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; import javax.sql.XAConnection; import org.seasar.extension.dbcp.impl.XAConnectionImpl; import org.seasar.extension.dbcp.impl.XADataSourceImpl; import org.seasar.framework.log.Logger; import org.seasar.framework.util.StringUtil; public class MyXADataSourceImpl extends XADataSourceImpl { private Logger logger = Logger.getLogger(MyXADataSourceImpl.class); private Properties properties = new Properties(); @Override public void addProperty(String name, String value) { properties.put(name, value); } /* * (non-Javadoc) * * @see * org.seasar.extension.dbcp.impl.XADataSourceImpl#getXAConnection(java. * lang.String, java.lang.String) */ @Override public XAConnection getXAConnection(String user, String password) throws SQLException { Properties info = new Properties(); info.putAll(properties); if (StringUtil.isNotEmpty(user)) { info.put("user", user); } if (StringUtil.isNotEmpty(password)) { info.put("password", password); } int currentLoginTimeout = DriverManager.getLoginTimeout(); try { DriverManager.setLoginTimeout(getLoginTimeout()); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 加工されたクラスローダを取得 Driver driver = (Driver) classLoader.loadClass(getDriverClassName()).newInstance(); // 加工されたクラスローダからドライバのインスタンスを生成 Connection con = driver.connect(getURL(), info); return new XAConnectionImpl(con); } catch (Exception e) { throw new RuntimeException(e); } finally { try { DriverManager.setLoginTimeout(currentLoginTimeout); } catch (Exception e) { logger.log("ESSR0017", new Object[] { e.toString() }, e); } } } }
呼び出し側の修正
加工したクラスローダをContextClassLoaderとしてセットするようにする。
package examples; import static java.lang.String.format; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.List; import org.seasar.extension.jdbc.JdbcManager; import org.seasar.framework.container.S2Container; import org.seasar.framework.container.factory.S2ContainerFactory; import examples.entity.Employee; /** * @author ru * */ public class ContextClassLoaderMain { static final File jarFile = new File("c:\\test\\hsqldb-1.8.0.1.jar"); public static void main(String[] args) throws Exception { URL jarURL = new URL(format("jar:%s!/", jarFile .getCanonicalFile() .toURI() .toURL() .toString())); URLClassLoader classLoader = new URLClassLoader( new URL[] { jarURL }, CustomClassLoaderMain.class.getClassLoader()); Thread.currentThread().setContextClassLoader(classLoader); // 加工したクラスローダをコンテキストクラスローダとしてセット S2Container container = S2ContainerFactory.create("app.dicon"); container.init(); JdbcManager jdbcManager = (JdbcManager) container.getComponent("jdbcManager"); List<Employee> result = jdbcManager.from(Employee.class).getResultList(); for (Employee each : result) { System.out.println(each.name); } } }
実行
実行結果は…
ALLEN WARD JONES MARTIN BLAKE CLARK SCOTT KING TURNER ADAMS JAMES FORD MILLER SMITH
キタ━━━━(゚∀゚)━━━━ッ!!
なんか一応できたけど…
なんかブサイクすぎて納得行きません。なんかスゴイ間違ってる気がするッ…
よい方法あれば教えてください><