S2JDBCでClassPath外のJDBCドライバを使う

表題の通りS2JDBCClassPathの通っていない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);
            }
        }
    }
}
変更したXADataSourceに差し替えるために"jdbc.dicon"ファイルを修正


これでContextClassLoaderを使ってドライバのインスタンスを生成するようになるはず。

呼び出し側の修正


加工したクラスローダを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

キタ━━━━(゚∀゚)━━━━ッ!!

なんか一応できたけど…


なんかブサイクすぎて納得行きません。なんかスゴイ間違ってる気がするッ…
よい方法あれば教えてください><