![]() ![]() ![]() ![]() Jakarta Commonsを使ってJDKクラスを拡張する:パート3この記事のURLhttp://japan.internet.com/developer/20060322/26.html
著者:Narayanan A.R.
海外internet.com発の記事
はじめにJakarta Commonsは、さまざまなJakartaプロジェクトで使われている再利用可能なクラスの集まりです。これらのクラスは、独立したコンポーネントとして自分のJavaプロジェクトで利用することができます。今回はJakarta Commonsを紹介する3回シリーズの最終回にあたり、便利なコンポーネントをあと4つ取り上げ、サンプルアプリケーションを通じてその使い方を説明していきます。まだパート1とパート2を読んでいない方は、先にそちらをご覧ください。これらのサンプルはJakarta Commonsコンポーネントを例示するだけのものではなく、典型的なJavaプロジェクトで再利用できる有用な機能を盛り込んだ完全なアプリケーションです。 本稿では次のコンポーネントを取り上げます。
本稿には完全なソースコードが付属しており、各サンプルのテストケースをJUnitで起動することで実行できます。 著者注
Commonsコンポーネントのアーキテクチャと本稿で紹介するサンプルを理解するためには、オブジェクト指向プログラミング(OOP)とGang of Fourのデザインパターン(ChainおよびCommand)についての基本的知識が非常に役立ちます。
CLICLI(コマンドラインインターフェイス)コンポーネントは、コマンドラインアプリケーションの引数解析に大変重宝します。こうした引数解析コードを書く作業は、時間がかかって煩わしいものです。また、このコンポーネントを使えば、既存のCLIアプリケーションの機能拡張も容易になります。コードのリファクタリングにより、新しい機能を効率的に追加できます(Jakartaのサイトには、CLIの使い方の概要がわかりやすく記されています)。 本シリーズの第1回では、Commons Chainコンポーネントの解説のところで、ネットワークコマンドを実行するコマンドラインツールのサンプルを使用しました。Common CLIの説明にも同じサンプルアプリケーションを使います。このサンプルのソースコードは、ダウンロードサンプルのパッケージ「in.co.narayanan.commons.cli」に含まれています。メインクラス リスト1 サンプルアプリケーションで使用できるコマンドオプションの構文
// java CommandLine -user admin -password manager -ping {host} // java CommandLine -user admin -password manager // -ftp {host} -get {path_to_file} // java CommandLine -user admin -password manager // -ftp {host} -ls {path_to_file} このコードの処理レイヤには、CommonsのChainとCommandのパターンが使われています。コマンドラインのネットワークコマンドのそれぞれに、該当する処理を行うクラスが存在します。各クラスは連結してチェーンを形成します。コマンドが呼び出されると、まずチェーンの最初のコマンドに引数が与えられます。目的の処理を行うコマンドが見つかるまで、このチェーンに沿って実行が進みます。ここでの目標は、チェーン内の処理クラスがコンテキストオブジェクトを利用できるように、Commons CLIを使って引数の解析とコンテキストオブジェクトの作成を行うことです。 リスト2は、「in.co.narayanan.commons.cli.CommandLine.java」のコードの一部です。pingコマンドに渡された引数をCommons CLIを用いて解析する方法を示しています。 リスト 2 Commons CLIを利用したpingコマンドの引数解析
private void createPingCmdOptions() { // ping command // java CommandProcessor -user admin -password manager -ping {host} pingOptions = new Options(); Option user = createUserOption(); pingOptions.addOption(user); Option passwd = createPasswordOption(); pingOptions.addOption(passwd); Option ping = OptionBuilder.withArgName("ping") .hasArg() .isRequired() .withDescription("Ping a remote system") .create("ping"); pingOptions.addOption(ping); } このコードでは、個々のコマンドラインオプションを表す リスト3は、ftpコマンド用のCommons CLIのコードです。 リスト3 Commons CLIを利用したftpコマンドの引数解析
Option ftp = OptionBuilder.withArgName("ftp") .hasArg() .isRequired() .withDescription("File transfer protocol") .create("ftp"); ftpOptions.addOption(ftp); // For additional ftp commands like ls, put, and mget, // a OptionGroup needs to be created // to indicate the options are mutually exclusive Option get = OptionBuilder.withArgName("get") .hasArg() .withDescription("Get a file from the server") .create("get"); Option ls = OptionBuilder.withArgName("ls") .hasArg() .withDescription("List the folder contents in the server") .create("ls"); OptionGroup ftpArgs = new OptionGroup(); ftpArgs.addOption(get); ftpArgs.addOption(ls); ftpArgs.setRequired(true); ftpOptions.addOptionGroup(ftpArgs); このサンプルで扱うftpネットワークコマンドには、getとlsという相互排他的なオプションが含まれます。こうした相互排他的なオプションを表すために、リスト3では このコード全体において、Common CLIのフレームワークは次の処理を行っています。
リスト4は、パーサークラスの リスト4 パーサーによる解析の実行
public void process(String args[]) { // remaining code CommandLineParser parser = new BasicParser(); org.apache.commons.cli.CommandLine line = null; Context chainContext = null; // remaining code case PING : { try { line = parser.parse(pingOptions, args); chainContext = getPingContext(line); } catch (ParseException e) { System.out.println(e.toString()); HelpFormatter formatter = new HelpFormatter(); formatter.printHelp( "Ping options", pingOptions); } } break; // remaining code } private Context getPingContext( org.apache.commons.cli.CommandLine line) { String user = line.getOptionValue("user"); String passwd = line.getOptionValue("password"); String host = line.getOptionValue("ping"); return new CommandlineContext( user, passwd, new CLICommand("-ping", new String[] {host})); } 太字のコードは、パーサーの使い方を理解するにあたって非常に重要な部分です。最初に、パーサークラス Commons CLIのフレームワークは、単独のコマンドに対してのみオプションの解析を行うことができます。このサンプルと同じように、コマンドオプションのあるコマンドが複数存在するCLIアプリケーションの場合は、コマンドの種類を識別するために少し前処理を行う必要があります(リスト5を参照)。 リスト5 コマンドの識別
private int classifyCommand(String args[]) throws CommandLineException { if(args != null && args.length > 0) { for(String arg : args) { if(arg.equals("-ping")) { return PING; } if(arg.equals("-ftp")) { return FTP; } } } else { throw new CommandLineException( "Invalid command options. See usage."); } throw new CommandLineException( "Invalid command options. See usage"); } このサンプルアプリケーションでは、pingとftpは異なるコマンドであり、それぞれ異なるコマンドラインオプションを持っています。そのため、 Commons CLIはJavaのあらゆるCLIアプリケーションにとって不可欠なすばらしいAPIです このAPIを使えば、CLIアプリケーションの機能拡張にかかる時間を節約し、煩わしさを軽減できます。Jakarta Antプロジェクトでは、コマンドライン引数の処理にCommons CLIが利用されています。 VFSCommons Virtual File System(VFS)は、さまざまな種類のファイルシステムに統一された方法でアクセスするための抽象化レイヤを提供しています。このコンポーネントを使えば、1つまたは複数のファイルシステムに同時に接続できます。これはLinuxオペレーティングシステムのmount機能に似ています。 VFSがサポートするファイルシステムは次のとおりです。
サポートされているプロトコルURIの完全な構文についてはこちらを、高レベルでのこのAPIの用法についてはこちらをご覧ください。 このコンポーネントは、種類の異なるファイルに対してシームレスにアクセスする必要があるアプリケーションで非常に役立ちます。たとえば、デスクトップ検索ツールがその代表的な例です。ユーザーは、デスクトップ検索ツールを使って、さまざまな形式のファイルから特定のファイルまたはファイルコンテンツを検索します。Windows Explorerのような機能をJavaアプリケーションに組み込む場合にもこのコンポーネントが利用できます。 ここで紹介するサンプルアプリケーションでは、Commons VFSを利用して、フォルダだけでなくzipおよびjarファイルにも対応した検索を行います。このサンプルにユーザーインターフェイスはありませんが、テストケースを使ってその動作を確認できます。このサンプルとテストケースは「in.co.narayanan.commons.vfs」パッケージに含まれています。サンプルアプリケーションを実行するには、ソースアーカイブをダウンロードしてCommon VFSライブラリ作成用のAntビルドスクリプトを実行します。このAntスクリプトは、依存関係のある他のライブラリファイルもダウンロードできる優れモノです。 TCommons VFSを用いる基本的な考え方は、サポートされているファイルの種類ごとにプロバイダを作成して、 リスト6は、 リスト6 ファイルシステムマネージャの初期化
/** * Initialize the DefaultFileSystemManager to support * file, zip and jar providers. A virtual file system * is created and passed to the SearchableVirtualFileSystem * decorator class. * * @throws SearchException Error in initializing the file * FileSystemManager */ private void init() throws SearchException { defFileSysMgr = new DefaultFileSystemManager(); try { defFileSysMgr.addProvider( "file", new DefaultLocalFileProvider()); defFileSysMgr.addProvider( "zip", new ZipFileProvider()); defFileSysMgr.addProvider( "jar", new JarFileProvider()); defFileSysMgr.init(); // Create the virtual file system VirtualFileSystem vfs = (VirtualFileSystem)defFileSysMgr .createVirtualFileSystem("vfs://").getFileSystem(); searchableVfs = new SearchableVirtualFileSystem(vfs); } catch (FileSystemException e) { throw new SearchException( "Unable to initialize the FileSystemManager.", e); } } 太字の部分は、ローカルファイルシステム、zipファイル、jarファイルを検索するためのプロバイダを追加するコードです。このコードは リスト7は、テストケースクラス リスト7 検索ツールの使い方
/** * Adds the folder, zip, and a jar file to search * * @throws Exception Error in the test. */ public void testSearchInZips() throws Exception { SearchBuddy searchTool = new SearchBuddy(); searchTool.addSearchableZip("testroot.zip"); searchTool.addSearchableJar("testjar.jar"); searchTool.addSearchableFolder("."); System.out.println("Searching for news.txt"); searchTool.search("news", "txt"); System.out.println("Searching for Range.class"); searchTool.search("range", "class"); System.out.println("Searching for test.xml"); searchTool.search("test", "xml"); System.out.println("Searching for *.properties"); searchTool.search(null, "properties"); searchTool.close(); } 太字の行は、検索対象のzipファイルとjarファイルを追加する部分です。この リスト8は、 リスト8 zipファイルを仮想ファイルシステムにマウントするコード
/** * Mount a zip file to the searchable virtual * file system. * * @param pathToFolder Absolute or relative path to the zip file. * @throws SearchException Error while adding the zip file * to the virtual file system. */ public void addSearchableZip(String pathToZip) throws SearchException { File zipFile = new File(pathToZip); if(!zipFile.exists()) { throw new SearchException("Invalid zip file path"); } try { FileObject zipFileObject = defFileSysMgr.toFileObject(zipFile); searchableVfs.addJunction("/" + zipFile, defFileSysMgr.resolveFile("zip:" + zipFileObject + "!/")); } catch (FileSystemException e) { throw new SearchException( "Unable to add zip file to the virtual file system", e); } } このzipファイルのマウントは、 リスト9は、マウントされたファイルシステムを検索するコードです。 リスト9 検索処理
Class SearchBuddy /** * Delegate the search call to the Searchable virtual file * system decorator. * * @param fileNamePart Name of the file. * @param extension Extension to search * @throws SearchException Error from the VirtualFileSystem when searching for file. */ public void search(String fileNamePart, String extension) throws SearchException { searchableVfs.search(fileNamePart, extension); } Class SearchableVirtualFileSystem /** * Iterate the junctions to search for the given file name. * * @param fileNamePart File name to search * @param extension Extension to search * @throws SearchException */ public void search(String fileNamePart, String extension) throws SearchException { try { Iterator<String> searchPoints = junctions.iterator(); FileObject matchingFiles[]; while(searchPoints.hasNext()) { String searchPoint = searchPoints.next(); FileObject searchRoot = vfs.resolveFile(searchPoint); filter.setExtension(extension); filter.setFileNamePart(fileNamePart); matchingFiles = searchRoot.findFiles(filter); for(FileObject file : matchingFiles) { System.out.println("Result:" + file); } } } catch (FileSystemException e) { throw new SearchException("Search failed", e); } } この このサンプルアプリケーションのソースファイルには、説明用のコメントが十分に書かれているので、詳しくはそちらをご覧ください。 Common VFSのフレームワークをデスクトップおよびサーバーアプリケーションに用いれば、コンシューマプログラムとファイルタイプ固有のコードとを完全に分離できます。その結果、アプリケーションのモジュール性が高まり、開発が容易になります。 ConfigurationCommons Configurationは、プロパティファイルにアクセスするタイプの企業向けソフトウェアに役立つコンポーネントです。この機能によって、アプリケーションはさまざまな場所から読み込まれる設定情報を統一された方法で参照できます。 このAPIには、他にも便利な機能があります。
アプリケーションのモジュールコンテキストに Jakartaのサイトでは、このAPIが初歩的なサンプルと共にわかりやすく紹介されています。なお、プロパティを記憶域に自動保存する機能は、保存先がファイルの場合にのみサポートされます。 今回紹介するサンプルアプリケーションは、リモートのSNTP(Simple Network Time Protocol)サーバーとの間でシステム時刻の同期をとるものです。ただし、実際にSNTPサーバーに対して同期処理を行うメソッドは実装されておらず、Commons Configurationのデモ用スケルトンコードになっています。このサンプルの完全なソースコードは「in.co.narayanan.commons.config」パッケージに含まれています。 リスト10は、このサンプルアプリケーションで設定が必要なプロパティです。ここで、プロパティの内容を簡単に紹介しておきます。
リスト10 サンプルアプリケーションで使用する設定ファイル
application.properties syncintervalhours=12 enablesync=true #This property is set after the first run lastsync=<timestamp in milliseconds> sntpservers.xml <servers> <name>server1</name> <name>server2</name> <name>server3</name> </servers> リスト11は、Commons Configurationを利用して、このサンプルアプリケーションの設定ファイルにアクセスするためのインターフェイスです。 リスト11 サンプルアプリケーションの設定情報にアクセスするためのインターフェイス
public interface IConfiguration { void setStringConfig(String nameSpace, String key, String value); String getStringConfig(String nameSpace, String key); String[] getStringArrayConfig(String nameSpace, String key); void setBooleanConfig( String nameSpace, String key, boolean value); boolean getBooleanConfig(String nameSpace, String key); void setIntConfig(String nameSpace, String key, int value); int getIntConfig(String nameSpace, String key); } このインターフェイスによって、Commons Configuration APIの機能にアクセスしようとするアプリケーションのクラスを分離することができます。このインターフェイスには、文字列、整数型、論理型、文字列配列のプロパティを取得/設定するためのメソッドが用意されています。 リスト12のコードは、このインターフェイスを実装した リスト12 Commons Configuration APIを用いたプロパティの読み込み
/** * Get the config property value from configuration storage. The * properties are loaded if it is not already loaded for the * given namespace. * * @param nameSpace Name of the configuration group * @param key Unique key within the namespace * @return Sring value of the property. Null if the property * is not found or if the configuration cannot be loaded */ public String getStringConfig(String nameSpace, String key) { Configuration config = getConfiguration(nameSpace); if(config != null) { return config.getString(key); } return null; } private synchronized Configuration getConfiguration (String nameSpace) { Configuration config = configs.get(nameSpace); if(config == null) { try { if("application".equals(nameSpace)) { config = new PropertiesConfiguration( "application.properties"); } else if("sntpservers".equals(nameSpace)) { config = new XMLConfiguration( getClass().getResource("sntpservers.xml")); } configs.put(nameSpace, config); } catch (ConfigurationException e) { System.out.println("Unable to load the configuration:" + e.getMessage()); } } return config; } 太字の行は、取得メソッドが呼び出されたときにテキストファイルおよびXMLファイルからプロパティを読み込むコードです。 リスト13に、この設定を記憶域に再保存する方法を示します。 リスト13 テキストファイルへの設定の再保存
private synchronized void save(Configuration config) { if(config instanceof FileConfiguration) { try { // If reloading strategy is set, then the properties // doesn’t get saved ((FileConfiguration)config).save(); } catch (ConfigurationException e) { System.out.println("Config not saved. Error while saving." + e.getMessage()); } } else { System.out.println("Config not saved. Not supported."); } } 注意
私の環境では、リロードストラテジを
Configurationのインスタンスに設定しているときに、プロパティをテキストファイルに保存すると問題が生じました(プロパティファイルの用法の詳細については、こちらを参照)。リスト14に、アプリケーションのクラス内でこのコンテキストを通して設定プロパティにアクセスする方法を示します。 リスト14 アプリケーションのコンテキストを用いた設定情報へのアクセス
void syncTime() { if(shouldSyncNow()) { context.getLogger().log(Level.INFO, "Sync started"); sync(); recordSyncTime(); } else { context.getLogger().log(Level.INFO, "Sync skipped. Time not yet arrived"); } } private boolean shouldSyncNow() { boolean enableSync = context.getConfiguration() .getBooleanConfig("application", "enablesync"); context.getLogger().log(Level.INFO, "enablesync:" + enableSync); if(enableSync) { if(getCurrentTime() > readLastSync() + getInterval()) { return true; } } return false; } /** * Use SNTP API to sync the time. */ private void sync() { String servers[] = context.getConfiguration() .getStringArrayConfig("sntpservers", "name"); for(String server : servers) { context.getLogger().log(Level.INFO, "Time Server:" + server); } // Use the list of server to sync the system time. // sntp API for java can be used here context.getLogger().log(Level.INFO, "Syncing time.."); } このコードでは、必要なプロパティを取り出すために、ロガーへの参照とコンフィギュレーションクラスを取得しています。各プロパティの使い方については、 リスト15は、 リスト15 SyncTimeクラスの動作を検証するテストケースクラス
public class TestSyncTime extends TestCase { public void testSyncTime() { Logger consoleLogger = Logger.getAnonymousLogger(); ApplicationContext context = new ApplicationContext(); context.setLogger(consoleLogger); context.setConfiguration(new ApplicationConfiguration()); SyncTime sync = new SyncTime(context); sync.syncTime(); sync.syncTime(); } } このテストケースクラスでは、Javaのロガーとコンフィギュレーションクラスを初期化し、これらをアプリケーションコンテキストにセットして、 Commons Configurationを使えば、さまざまな場所で提供されている外部プロパティにアクセスできます。これは確かに便利なコンポーネントですが、企業向けソフトウェアの場合は、JNDIやデータベースを利用するディレクトリサーバーにプロパティを自動保存する必要があるため、改良の余地があるでしょう。しかし、単純なテキストファイルやXMLファイル上でプロパティを扱うのであれば、このフレームワークの利用を検討する価値があります。 PoolCommons Poolは、どんなオブジェクトでもプールできるすばらしいフレームワークです。JDBC接続、スレッド、業務用オブジェクト、JNDI接続のプールにも十分に役立つはずです。 サンプルアプリケーションではJNDI接続プールを利用します。このサンプルの実行にはJNDI互換のディレクトリサーバーが必要になります。また、このサンプルではActive Directory Application Mode(ADAM)を使用しますが、ADAMはこちらからダウンロードできます。それでは、このサンプルがCommons Poolを使ってどのようにJNDI接続をプールするのかを見ていきましょう。 リスト16は、JNDIの操作を行うメソッドを公開するインターフェイスです。 リスト16 JNDI操作用メソッドを公開するインターフェイス
public interface IJNDIConnection { /** * Execute the given query against the directory server. * * @param query JNDI query of the form ((objectclass=user)(cn=john)) * @return Search result * @throws NamingException Directory server error */ NamingEnumeration executeQuery(String query) throws NamingException; /** * Fetch the attributed of a given DN of an object. * * @param dn Distinguished name of the object * @return Attributes of an object * @throws NamingException Directory server error */ Attributes getAttributes(String dn) throws NamingException; } この リスト17 接続プールの使用方法
String host = "localhost"; String port = "389"; String binDN = "CN=guest,CN=Users,DC=narayanan,DC=co,DC=in"; String password = "guest"; String baseDN = "DC=narayanan,DC=co,DC=in"; JNDIConnectionManager manager = JNDIConnectionManager.getJNDIConnectionManager( host, port, binDN, password, baseDN); IJNDIConnection connection = manager.getConnection(); String query = "(objectclass=container)"; NamingEnumeration users = connection.executeQuery(query); while (users.hasMoreElements()) { System.out.println(users.next()); } このコードは、 リスト18 JNDIConnectionFactoryの実装
public class JNDIConnectionFactory implements PoolableObjectFactory { private JNDIConnectionInfo connInfo; public JNDIConnectionFactory(String host, String port, String bindDN, String password, String baseDN) { connInfo = new JNDIConnectionInfo( host, port, bindDN, password, baseDN); } public Object makeObject() throws Exception { Hashtable dirProps = connInfo.getJNDIConnectionProps(); DirContext dirContext = new InitialDirContext(dirProps); return dirContext; } public void destroyObject(Object context) throws Exception { DirContext dirContext = (DirContext)context; dirContext.close(); } // remaining methods // inner class JNDIConnectionInfo } 太字の行は、Commons Poolからの要求に応じて リスト19 プールから接続を取得するマネージャクラス
public class JNDIConnectionManager { // remaining declarations private GenericObjectPool pool; private PoolableObjectFactory factory; private JNDIConnectionManager( String host, String port, String bindDN, String password, String baseDN) { factory = new JNDIConnectionFactory( host, port, bindDN, password, baseDN); pool = new GenericObjectPool(factory); } // remaining code public IJNDIConnection getConnection() { try { return new JNDIConnection((DirContext)pool.borrowObject()); } catch (Exception e) { return null; } } public void returnConnection(IJNDIConnection connection) { try { pool.returnObject( ((JNDIConnection)connection).getContext()); } catch (Exception e) { System.out.println("Unable return the object back to pool"); } } } このコードでは、 Commons PoolはすべてのJavaアプリケーションで間違いなく役に立ちます。Javaアプリケーションではリソースのプールが共通の要件になっているためです。また、ニーズに合わせて まとめJakarta Commonsを紹介するシリーズの最終回では、次の内容を取り上げました。
これからJavaアプリケーションをデザインしたり開発したりするときには、有用なクラスを選んで適切に使用できることでしょう。 本シリーズの以前の記事著者紹介Narayanan A.R.(Narayanan A.R.)
テスト駆動開発、アジャイル方法論、Javaテクノロジ、およびデザインパターンの熱烈な擁護者。Javaテクノロジを使ったソフトウェアのデザインおよび開発に携わって数年になる。
|