CLOVER🍀

That was when it all began.

スタンドアロンでJNDIを使う

前回書いた、JPA Cache Storeの副産物です。これを書いた時、JPAにJTAとリソースのルックアップもできるように頑張ろうと思ったのですが、面倒過ぎてやめました…。

ただ、DataSourceくらいはJNDIルックアップできるようになったので、今度はJavaでちゃんと書きたいと思います。

参考にしたのは、このあたりです。

How can I bind a DataSource to an InitialContext for JUnit testing?
http://stackoverflow.com/questions/3461310/how-can-i-bind-a-datasource-to-an-initialcontext-for-junit-testing

Implementing standalone JPA JTA Hibernate application outside J2EE server using Infinispan 2nd level cache
https://docs.jboss.org/author/display/ISPN/Implementing+standalone+JPA+JTA+Hibernate+application+outside+J2EE+server+using+Infinispan+2nd+level+cache

Naming on JBoss
http://docs.jboss.org/jbossas/jboss4guide/r1/html/ch3.chapter.html

DataSourceの利用
http://www.techscore.com/tech/Java/JavaEE/JDBC/6-2/

というわけで、JNDIを使うのにはJBossのライブラリを使うことにしました。

Mavenの依存関係定義は、こんな感じです。

    <dependency>
      <groupId>jboss</groupId>
      <artifactId>jnp-client</artifactId>
      <version>4.2.2.GA</version>
    </dependency>
    <dependency>
      <groupId>jboss</groupId>
      <artifactId>jnpserver</artifactId>
      <version>4.2.2.GA</version>
    </dependency>
    <dependency>
      <groupId>org.jboss</groupId>
      <artifactId>jboss-common-core</artifactId>
      <version>2.2.22.GA</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.26</version>
    </dependency>

「jboss-common-core」というのは、コンパイルには必要ないのですが、ないと実行時にエラーになります…。MySQLのドライバが入っているのは、MySQLの提供するDataSourceを使用するためですね。

では、コードの方へ。以下のimport文を書いているものとします。

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.jnp.interfaces.NamingContext;
import org.jnp.server.Main;
import org.jnp.server.NamingServer;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

java.sqlパッケージのものは、DataSourceを使った後で一応SELECT文を投げて動作確認するためだけに使います。JNDIには直接関係ありません。

JNDIサーバのセットアップ。

    private static void setUpJndi() {
        try {
            NamingServer namingServer = new NamingServer();
            NamingContext.setLocal(namingServer);

            Main namingMain = new Main();
            namingMain.setInstallGlobalService(true);
            namingMain.setPort(-1);
            namingMain.start();
        } catch (NamingException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

続いて、InitialContextにDataSourceを登録しますが、bindしたInitialContextを使うかそうでないかで、設定が微妙に変わります…。

まずは、InitialContextを使い回すパターンの方から。

InitialContextの引数にjava.util.Propertiesを渡してインスタンスを生成し、JNDIリソースを作成します。

    private static InitialContext bindDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUrl("jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8");
        dataSource.setUser("kazuhira");
        dataSource.setPassword("password");

        Properties properties = new Properties();
        properties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
        properties.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");

        try {
            InitialContext context = new InitialContext(properties);
            context.createSubcontext("jdbc");
            context.bind("jdbc/mysql", dataSource);

            // java:comp/〜で書くなら、こんな感じ
            context.createSubcontext("java:comp");
            context.createSubcontext("java:comp/env");
            context.createSubcontext("java:comp/env/jdbc");
            context.bind("java:comp/env/jdbc/mysql", dataSource);

            return context;
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

InitialContext#createSubcontextで、ディレクトリを掘り進めていきます。最後に、bindしたInitialContextを返却するメソッドになっています。

続いて、上記メソッドが返却したInitialContextを引数に受けることを期待したメソッド。

    private static void lookupAndExecute(InitialContext context) {
        try {
            DataSource dataSource = (DataSource) context.lookup("jdbc/mysql");

            try (Connection conn = dataSource.getConnection();
                 PreparedStatement ps = conn.prepareStatement("SELECT * FROM user");
                 ResultSet rs = ps.executeQuery()) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();

                while (rs.next()) {
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = meta.getColumnName(i);
                        System.out.println(columnName + " = " + rs.getString(i));
                    }
                }                    
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

これで、DataSourceのルックアップとJDBCプログラミングが可能になりました。

続いて、bindしたInitialContextを使わないパターン。

先ほどは、InitialContextのコンストラクタに設定を渡していましたが、よく見かける

    InitialContext context = new InitialContext();
    DataSource dataSource = (DataSource) context.lookup("jdbc/mysql");

みたいなコードだと、そんな設定渡していませんからね。

事実、先ほどの例のJNDIの設定で、ルックアップしているコードをこのように変更すると

javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file:  java.naming.factory.initial

みたいなエラーを見ることになります。

なので、そのように実装しましょう。

DataSourceを登録するところは、このように実装します。

    private static void setUpDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUrl("jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8");
        dataSource.setUser("kazuhira");
        dataSource.setPassword("password");

        // システムプロパティで設定するか、「jndi.properties」を使用する
        // System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
        // System.setProperty(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");

        try {
            InitialContext context = new InitialContext();
            context.createSubcontext("jdbc");
            context.bind("jdbc/mysql", dataSource);

            // java:comp/〜で書くなら、こんな感じ
            context.createSubcontext("java:comp");
            context.createSubcontext("java:comp/env");
            context.createSubcontext("java:comp/env/jdbc");
            context.bind("java:comp/env/jdbc/mysql", dataSource);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

System.setPropertyしているところはコメントアウトしていますが、ここはコメントアウトを解除するか、「jndi.properites」という名前のファイルを用意して、同様の内容を定義します。

今回は、「jndi.properties」ファイルを用意することにしました。
src/main/resources/jndi.properties

### JBossNS properties
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
## ここは不要
## java.naming.provider.url=jnp://localhost:1099
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces

プロバイダのURLは要りません…。

すると、InitialContextを使い回さなくたり、別途設定しなくても動作するようになります。

    private static void lookupAndExecute() {
        try {
            InitialContext context = new InitialContext();
            DataSource dataSource = (DataSource) context.lookup("jdbc/mysql");

            try (Connection conn = dataSource.getConnection();
                 PreparedStatement ps = conn.prepareStatement("SELECT * FROM user");
                 ResultSet rs = ps.executeQuery()) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();

                while (rs.next()) {
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = meta.getColumnName(i);
                        System.out.println(columnName + " = " + rs.getString(i));
                    }
                }                    
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

簡単でしたが、スタンドアロンでJNDIを使うサンプルってことで。

最後に、作成したコード全体です。両方のパターンを書いていますが、同一リソースを登録するようになっているので、どちらか片方しか有効にできません。あしからず。
src/main/java/standalone/jndi/StandaloneJndiExample.java

package standalone.jndi;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.jnp.interfaces.NamingContext;
import org.jnp.server.Main;
import org.jnp.server.NamingServer;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

public class StandaloneJndiExample {
    public static void main(String[] args) {
        setUpJndi();

        // InitialContextを使い回す場合
        /*
        InitialContext context = bindDataSource();
        lookupAndExecute(context);
        */

        // bindしたInitialContextは使用しない場合
        setUpDataSource();
        lookupAndExecute();
    }

    private static void setUpJndi() {
        try {
            NamingServer namingServer = new NamingServer();
            NamingContext.setLocal(namingServer);

            Main namingMain = new Main();
            namingMain.setInstallGlobalService(true);
            namingMain.setPort(-1);
            namingMain.start();
        } catch (NamingException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static InitialContext bindDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUrl("jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8");
        dataSource.setUser("kazuhira");
        dataSource.setPassword("password");

        Properties properties = new Properties();
        properties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
        properties.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");

        try {
            InitialContext context = new InitialContext(properties);
            context.createSubcontext("jdbc");
            context.bind("jdbc/mysql", dataSource);

            // java:comp/〜で書くなら、こんな感じ
            context.createSubcontext("java:comp");
            context.createSubcontext("java:comp/env");
            context.createSubcontext("java:comp/env/jdbc");
            context.bind("java:comp/env/jdbc/mysql", dataSource);

            return context;
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

    private static void lookupAndExecute(InitialContext context) {
        try {
            DataSource dataSource = (DataSource) context.lookup("jdbc/mysql");

            try (Connection conn = dataSource.getConnection();
                 PreparedStatement ps = conn.prepareStatement("SELECT * FROM user");
                 ResultSet rs = ps.executeQuery()) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();

                while (rs.next()) {
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = meta.getColumnName(i);
                        System.out.println(columnName + " = " + rs.getString(i));
                    }
                }                    
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

    private static void setUpDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUrl("jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=utf8");
        dataSource.setUser("kazuhira");
        dataSource.setPassword("password");

        // システムプロパティで設定するか、「jndi.properties」を使用する
        // System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
        // System.setProperty(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");

        try {
            InitialContext context = new InitialContext();
            context.createSubcontext("jdbc");
            context.bind("jdbc/mysql", dataSource);

            // java:comp/〜で書くなら、こんな感じ
            context.createSubcontext("java:comp");
            context.createSubcontext("java:comp/env");
            context.createSubcontext("java:comp/env/jdbc");
            context.bind("java:comp/env/jdbc/mysql", dataSource);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }

    private static void lookupAndExecute() {
        try {
            InitialContext context = new InitialContext();
            DataSource dataSource = (DataSource) context.lookup("jdbc/mysql");

            try (Connection conn = dataSource.getConnection();
                 PreparedStatement ps = conn.prepareStatement("SELECT * FROM user");
                 ResultSet rs = ps.executeQuery()) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();

                while (rs.next()) {
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = meta.getColumnName(i);
                        System.out.println(columnName + " = " + rs.getString(i));
                    }
                }                    
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }
}