はじめに
Jakarta Commonsは、さまざまなJakartaプロジェクトで使われている再利用可能なクラスの集まりです。これらのクラスは、独立したコンポーネントとして自分のJavaプロジェクトで利用することができます。今回はJakarta Commonsを紹介する3回シリーズの最終回にあたり、便利なコンポーネントをあと4つ取り上げ、サンプルアプリケーションを通じてその使い方を説明していきます。まだパート1とパート2を読んでいない方は、先にそちらをご覧ください。これらのサンプルはJakarta Commonsコンポーネントを例示するだけのものではなく、典型的なJavaプロジェクトで再利用できる有用な機能を盛り込んだ完全なアプリケーションです。
本稿では次のコンポーネントを取り上げます。
- CLI(コマンドラインインターフェイス)
- VFS(仮想ファイルシステム)
- Configuration
- Pool
本稿には完全なソースコードが付属しており、各サンプルのテストケースをJUnitで起動することで実行できます。
著者注
Commonsコンポーネントのアーキテクチャと本稿で紹介するサンプルを理解するためには、オブジェクト指向プログラミング(OOP)とGang of Fourのデザインパターン(ChainおよびCommand)についての基本的知識が非常に役立ちます。
CLI
CLI(コマンドラインインターフェイス)コンポーネントは、コマンドラインアプリケーションの引数解析に大変重宝します。こうした引数解析コードを書く作業は、時間がかかって煩わしいものです。また、このコンポーネントを使えば、既存のCLIアプリケーションの機能拡張も容易になります。コードのリファクタリングにより、新しい機能を効率的に追加できます(Jakartaのサイトには、CLIの使い方の概要がわかりやすく記されています)。
本シリーズの第1回では、Commons Chainコンポーネントの解説のところで、ネットワークコマンドを実行するコマンドラインツールのサンプルを使用しました。Common CLIの説明にも同じサンプルアプリケーションを使います。このサンプルのソースコードは、ダウンロードサンプルのパッケージ「in.co.narayanan.commons.cli」に含まれています。メインクラスCommandLineを起動すれば、このサンプルアプリケーションの引数が確認できます。リスト1は、このサンプルアプリケーションのコマンドラインオプションです。
リスト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);
}
このコードでは、個々のコマンドラインオプションを表すOptionクラスのインスタンスを作成して、Optionsクラスのインスタンスに追加していきます。OptionBuilderクラスは、一連の静的メソッドを通じてOptionクラスのインスタンスを作成する機能を持ちます。リスト2では、pingという名前の、引数を1つ取る必須オプションのインスタンスを作成しています。このインスタンスには、オプションの説明("Ping a remote system")も含まれています。OptionBuilderのwithArgNameメソッドに渡される文字列は、解析終了後にその引数の値を取り出すために使用されます(具体的な方法は後述)。OptionBuilderを使わずにOptionクラスのインスタンスを直接作成して、オプションのもっと細かい設定をすることもできます。
createUserOptionメソッドとcreatePasswordOptionメソッドは、それぞれコマンドラインのuser引数とpassword引数を表す新しいインスタンスを返します。今回のサンプルでは、別のコマンド用にOptionクラスのインスタンスを作成するときに、これらのメソッドを使用します。
リスト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ではOptionGroupクラスを使用しています。具体的には、OptionGroupクラスのsetRequiredメソッドを呼び出すときに、これらの相互排他的オプションが必須であることを示すtrueを渡しています。これにより、ユーザーはgetとlsのどちらのオプションを必ず入力しなければならなくなります。
このコード全体において、Common CLIのフレームワークは次の処理を行っています。
- コマンドライン引数の文字列配列を解析し、渡された値に基づいて各種
Optionクラスのインスタンスを作成する
- 検証を実施する(たとえば、必須の引数がユーザーから与えられなかった場合は例外をスローする)
- ユーザーから与えられた引数の数や種類が正しくない場合は用法メッセージを表示する
リスト4は、パーサークラスのparseメソッドを呼び出す例です。
リスト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}));
}
太字のコードは、パーサーの使い方を理解するにあたって非常に重要な部分です。最初に、パーサークラスBasicParserのインスタンスを作成しています。なお、Unix形式のCLIコマンドの場合はPosixParserを使用します。カスタムの要件によっては、org.apache.commons.cli.Parserクラスの拡張、またはorg.apache.commons.cli.CommandlineParserインターフェイスの実装を行うことでパーサーを一から作ることもできます。
parseメソッドを呼び出すと、コマンドラインオプションの値を取得するために利用するorg.apache.commons.cli.CommandLineへの参照が返されます。ここでは、この参照をgetPingContextメソッドに渡すことで、個々のオプション値を含んだCommandlineContextクラスのインスタンスを作成しています。このインスタンスを、以降の処理のためにチェーンに渡します。
parseメソッドによる引数解析の途中で問題が生じた場合は、Commons CLIは例外をスローします。この場合は、org.apache.commons.cli.HelpFormatterクラスによって、エラーを通知する書式設定済みのヘルプメッセージが表示されます。
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は異なるコマンドであり、それぞれ異なるコマンドラインオプションを持っています。そのため、Optionsクラスのインスタンスを1つ用意しただけでは、これらのコマンドライン引数を表現できません。コマンドの種類を識別したうえで、以降の解析に適切なOptionsクラスのインスタンスを用いることになります。
Commons CLIはJavaのあらゆるCLIアプリケーションにとって不可欠なすばらしいAPIです このAPIを使えば、CLIアプリケーションの機能拡張にかかる時間を節約し、煩わしさを軽減できます。Jakarta Antプロジェクトでは、コマンドライン引数の処理にCommons CLIが利用されています。
VFS
Commons Virtual File System(VFS)は、さまざまな種類のファイルシステムに統一された方法でアクセスするための抽象化レイヤを提供しています。このコンポーネントを使えば、1つまたは複数のファイルシステムに同時に接続できます。これはLinuxオペレーティングシステムのmount機能に似ています。
VFSがサポートするファイルシステムは次のとおりです。
- ローカルファイル ― ローカルのファイルおよびフォルダ(file://)
- zip、jar、tar、tgz、tbz2、gzip、bzip2 ― 多様な圧縮形式を透過的に処理可能(zip://、jar://、など)
- CIFS ― sambaサーバーまたはWindowsの共有ファイル(smb://)
- FTP ― FTPサーバー(ftp://)
- HTTPおよびHTTPS(http://)
- SFTP ― SSHまたはSCPサーバー(sftp://)
- テンポラリファイル(tmp://)
- WebDAV ― Web-based Distributed Authoring and Versioning(webdav://)
- クロスローダの提供するリソース ― ローディングクラスまたはクラスローダを使った他のリソース(res://)
サポートされているプロトコルURIの完全な構文についてはこちらを、高レベルでのこのAPIの用法についてはこちらをご覧ください。
このコンポーネントは、種類の異なるファイルに対してシームレスにアクセスする必要があるアプリケーションで非常に役立ちます。たとえば、デスクトップ検索ツールがその代表的な例です。ユーザーは、デスクトップ検索ツールを使って、さまざまな形式のファイルから特定のファイルまたはファイルコンテンツを検索します。Windows Explorerのような機能をJavaアプリケーションに組み込む場合にもこのコンポーネントが利用できます。
ここで紹介するサンプルアプリケーションでは、Commons VFSを利用して、フォルダだけでなくzipおよびjarファイルにも対応した検索を行います。このサンプルにユーザーインターフェイスはありませんが、テストケースを使ってその動作を確認できます。このサンプルとテストケースは「in.co.narayanan.commons.vfs」パッケージに含まれています。サンプルアプリケーションを実行するには、ソースアーカイブをダウンロードしてCommon VFSライブラリ作成用のAntビルドスクリプトを実行します。このAntスクリプトは、依存関係のある他のライブラリファイルもダウンロードできる優れモノです。in.co.narayanan.commons.vfs.TestSearchBuddyというJUnitテストケースクラスを実行するとサンプルアプリケーションが起動します。
TCommons VFSを用いる基本的な考え方は、サポートされているファイルの種類ごとにプロバイダを作成して、DefautFileSystemManagerのインスタンス(以降はマネージャとして参照されます)に追加するというものです。以降の処理では、managerのresolveFileメソッドを使って、該当するFileObjectのインスタンスへの参照を取得する必要があります。このmanagerとFileObjectには他にも役に立つメソッドがたくさんあるので、javadocを読んでおいて損はありません。以降では、検索ツールにおけるCommons VFS APIの使い方を説明します。
リスト6は、DefaultFileSystemManagerクラスの初期化に用いるin.co.narayanan.commons.vfs.SearchBuddyクラスのコードの一部です。
リスト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ファイルを検索するためのプロバイダを追加するコードです。このコードはVirtualFileSystemクラスのインスタンスを作成します。作成したインスタンスは他のファイルシステムのマウントに使用されます(この詳細とSearchableVirtualFileSystemクラスについては後述)。
リスト7は、テストケースクラスTestSearchBuddyの一部です。このコードは、サンプルアプリケーションを使ってどのようにファイル検索を行うかを示しています。
リスト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ファイルを追加する部分です。このsearchメソッドの呼び出し時には、検索するファイル名と拡張子を与えます。
リスト8は、SearchBuddyクラスのコードの一部です。このコードは、各種ファイルを仮想ファイルシステムにマウントする方法を示しています。
リスト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ファイルのマウントは、VirtualFileSystemクラスのaddJunctionメソッドを呼び出すことで行われます。この接合箇所(またはマウントポイント)はデコレータクラスSearchableVirtualFileSystemにキャッシュされ、後の検索処理で呼び出されます。
リスト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);
}
}
このsearchメソッドは、マウントされたファイルシステムに対して繰り返し処理を行い、ルートを特定して、FileObjectクラスのfindFilesメソッドを検索フィルタ付きで呼び出します。検索結果はコンソールに出力されます。このデコレータクラスSearchableVirtualFileSystemの主な役割は、マウントされたファイルシステムを記憶しておくことです。他のメソッドを呼び出す権限は、ラップされたVirtualFileSystemのインスタンスに委譲されます。
このサンプルアプリケーションのソースファイルには、説明用のコメントが十分に書かれているので、詳しくはそちらをご覧ください。
Common VFSのフレームワークをデスクトップおよびサーバーアプリケーションに用いれば、コンシューマプログラムとファイルタイプ固有のコードとを完全に分離できます。その結果、アプリケーションのモジュール性が高まり、開発が容易になります。
Configuration
Commons Configurationは、プロパティファイルにアクセスするタイプの企業向けソフトウェアに役立つコンポーネントです。この機能によって、アプリケーションはさまざまな場所から読み込まれる設定情報を統一された方法で参照できます。
このAPIには、他にも便利な機能があります。
- 各種のソース(JNDI、データベース、テキストファイル、XMLファイル、メモリ、システムプロパティ、アプレット、サーブレット初期化プロパティなど)に格納されている設定プロパティへの透過的アクセス
- リロードストラテジに基づくプロパティの自動リロード機能(このリロードストラテジはストラテジクラスの記述によってカスタマイズ可能)
- 変更されたプロパティの自動保存機能
- XPath風の構文を使ったXML設定情報へのアクセス
アプリケーションのモジュールコンテキストにorg.apache.commons.configuration.Configurationのインスタンスへの参照が含まれている場合は、アプリケーションに必要なプロパティをそのモジュール内のすべてのクラスで簡単に利用することができます。
Jakartaのサイトでは、このAPIが初歩的なサンプルと共にわかりやすく紹介されています。なお、プロパティを記憶域に自動保存する機能は、保存先がファイルの場合にのみサポートされます。
今回紹介するサンプルアプリケーションは、リモートのSNTP(Simple Network Time Protocol)サーバーとの間でシステム時刻の同期をとるものです。ただし、実際にSNTPサーバーに対して同期処理を行うメソッドは実装されておらず、Commons Configurationのデモ用スケルトンコードになっています。このサンプルの完全なソースコードは「in.co.narayanan.commons.config」パッケージに含まれています。
リスト10は、このサンプルアプリケーションで設定が必要なプロパティです。ここで、プロパティの内容を簡単に紹介しておきます。
syncintervalhours ― 同期の間隔を表す整数型プロパティ
enablesync ― 同期処理のオンオフを表す論理型プロパティ
<name> ― このツールが接続するSNTPサーバーの名前を表す文字列配列のプロパティ
lastsync ― 最後に成功した同期のタイムスタンプを表す文字列(同期完了のたびに記録される)
リスト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のコードは、このインターフェイスを実装したApplicationConfigurationクラスの一部です。
リスト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ファイルからプロパティを読み込むコードです。Configurationオブジェクトは、nameSpaceごとに内部マップに格納されます。これによって、プロパティどうしを混同することなく、設定情報の読み込みと格納が独立して行われます。種類の異なるプロパティが別々の場所から読み込まれる可能性がある場合にはorg.apache.commons.configuration.CompositeConfigurationクラスを使用できます(詳細については、こちらを参照)。
リスト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..");
}
このコードでは、必要なプロパティを取り出すために、ロガーへの参照とコンフィギュレーションクラスを取得しています。各プロパティの使い方については、in.co.narayanan.commons.config.SyncTimeクラスを調べるとよいでしょう。
リスト15は、SyncTimeクラスの動作を確認するためのテストケースクラスです。
リスト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のロガーとコンフィギュレーションクラスを初期化し、これらをアプリケーションコンテキストにセットして、SyncTimeクラスに依存関係を与えています。
Commons Configurationを使えば、さまざまな場所で提供されている外部プロパティにアクセスできます。これは確かに便利なコンポーネントですが、企業向けソフトウェアの場合は、JNDIやデータベースを利用するディレクトリサーバーにプロパティを自動保存する必要があるため、改良の余地があるでしょう。しかし、単純なテキストファイルやXMLファイル上でプロパティを扱うのであれば、このフレームワークの利用を検討する価値があります。
Pool
Commons 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;
}
このin.co.narayanan.commons.pool.JNDIConnectionクラスは、さきほどのインターフェイスを実装しており、javax.naming.directory.DirContextクラスを使って、要求されるJNDI操作を実行します。リスト17のコードは、プールされた接続をどのように取得して利用するかを示すテストケースクラスTestPoolの一部です。
リスト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());
}
このコードは、JNDIConnectionManagerを使って接続プールからIJNDIConnectionへの参照を取得したうえで、簡単な検索クエリを実行します。Commons Poolの利用では、org.apache.commons.pool.PoolableObjectFactoryインターフェイスの各メソッドを実装して、その参照をorg.apache.commons.pool.impl.GenericObjectPoolインターフェイスの実装に渡す、というのが基本的な手順です。ほとんどの場合は、org.apache.commons.pool.impl.GenericObjectPoolクラス ― ObjectPoolのデフォルト実装 ― で十分に間に合います。リスト18のコードは、Commons Poolによる新たなオブジェクトの要求時にJNDI接続を作成するJNDIConnectionFactoryの一部です。
リスト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からの要求に応じてDirContextの生成と破棄をどのように行うかを示しています。このObjectPoolを実装したクラスはmakeObjectメソッドとdestroyObjectメソッドを適切に呼び出します。リスト19は、JNDIConnectionManagerクラスの一部です。
リスト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");
}
}
}
このコードでは、JNDIConnectionFactoryの参照をGenericObjectPoolに渡すことでJNDI接続を作成しています。強調表示された行は、プールとの間でこの接続の取得と返却をどのように行うかを示しています。
Commons PoolはすべてのJavaアプリケーションで間違いなく役に立ちます。Javaアプリケーションではリソースのプールが共通の要件になっているためです。また、ニーズに合わせてGenericObjectPoolクラスをカスタマイズすることも可能です(詳細については、このクラスのjavadocを参照)。
まとめ
Jakarta Commonsを紹介するシリーズの最終回では、次の内容を取り上げました。
- Commonsのさまざまなコンポーネントが提供する豊富な機能
- ユーティリティクラスの特定のコンポーネントやメソッドの具体的な用途
- さまざまなAPIのパッケージとクラスの概要
これからJavaアプリケーションをデザインしたり開発したりするときには、有用なクラスを選んで適切に使用できることでしょう。
本シリーズの以前の記事
Narayanan A.R.(Narayanan A.R.)
テスト駆動開発、アジャイル方法論、Javaテクノロジ、およびデザインパターンの熱烈な擁護者。Javaテクノロジを使ったソフトウェアのデザインおよび開発に携わって数年になる。