Eclipse RCPとSpringによる実用的なシッククライアントの構築はじめに世間ではWeb 2.0やRIA(Rich Internet Application)が大流行していますが、投資家向けに機能豊富なWebフロントエンドを構築したとしても、本当に必要なものがシッククライアント(thick-client)の機能である場合には役に立ちません。私の以前の投稿『シッククライアントを配備するためのJavaアプリケーションサーバー基盤の構築』では、従来のJava Webサーバーアーキテクチャを活用してシッククライアントを簡単に配備する方法を紹介しました。 しかし、RIAの流行を避けて実用的なシッククライアントソリューションを構築するためには、実際にどうしたらよいでしょうか。その答えとなるのがRCP(Rich Client Platform)です。RCPは、Javaデスクトップアプリケーションの世界にフレームワークの概念を持ち込むものです。J2EE/Java EEの世界には以前から数多くのフレームワークが存在し、そこでベストプラクティスの標準化が行われてきました(Spring、Struts、WebWorkなどが良い例です)。今ようやく、Javaデスクトップの世界でもその経験が生かされようとしています。 RCPは、アプリケーションのスケルトン/シェルと、このシェルに基づいて独自のアプリケーションを構築する際に利用できるモジュールベースのAPIを提供します。メニュー、ツールバー、各種ビューの作成など、基本的な部分はすべてRCPが処理してくれるので、一から作成する必要はありません。自動的に処理してくれる環境があるならば、わざわざ自分でフレームワークを動かす理由はないでしょう。 本稿では、前回の記事で構築したサーバーに接続するシッククライアントインターフェイスを構築する方法を解説します。Eclipse RCPベースのシッククライアントを構築し、その後でEclipse RCPとSpringを統合します。 必要な環境
補足説明1 Springを入手する
Spring Frameworkをまだ入手していない場合は、ダウンロードしてインストールしてください。私はSpring1.2.8を選びました。本稿の執筆時点ではこれが最新の安定したリリースでした。必要になるサードパーティライブラリが入っているので、Spring Framework with dependenciesをダウンロードすることをお勧めします。また、リソースとしてソースとjavadocもダウンロードしてください。ダウンロードし終えたら、ファイルを解凍してください。
Eclipse RCPを選ぶ理由さて、Javaに初めて触れる方や長いこと遠ざかっていた方のためにお伝えしますが、Eclipseの人気は史上最高です。人気だけでは成功が保証されないというものの、数百万人のユーザー、非常に活発なコミュニティ、高い評判、そしてIBMの支持は悪いものではありません。また、今も数多くのアプリケーションがEclipse RCPベースで開発されているので、手製のフレームワークよりもはるかにしっかりとしたテストが行われると考えてよいでしょう。 では、始めましょう。 新しいEclipseプラグインプロジェクトを作成する次の手順に従って、リッチクライアントアプリケーション用の新しいEclipseプラグインプロジェクトを作成します。
図1 新しいプラグインプロジェクト ![]() 図2 リッチクライアントアプリケーションの作成を選択する ![]() 図3 RCPプラグインプロジェクトウィザードの最後の画面 ![]() Eclipseプラグインに初めて触れる場合は、「plugin.xml」の一番下のタブをよく使うことを覚えておきましょう。[Overview]タブから分かるように、Eclipseリッチクライアントアプリケーションを実行/デバッグすることができます。
補足説明2 Eclipseによって生成されるクラス
Eclipseが自動的に生成するクラスは次のとおりです。
このクラスは事実上のJavaメインクラスです。アプリケーションを起動すると、最終的に
run(Object args)メソッドが呼び出されます。このクラスを使って、ToolbarとMenuBarに追加するためのコードをリンクします。
このクラスで実行する一番重要なことは、デフォルトにするパースペクティブを定義することです。この定義にはメソッド
getInitialWindowPerspectiveId()を使います。メソッド
createActionBarAdvisor()がApplicationActionBarAdvisorのインスタンスを作成します(ApplicationXXXAdvisorsという名前のクラスがたくさんありますね)。これはメインのEclipseプラグインクラスです。ここで作成しているのは、最終的にはEclipseのプラグインです。
これまでにEclipseの使用経験があり、Perspectiveが何かをご存知だとうれしいのですが、そうでなくても、Perspectiveが
IPerspectiveFactoryを実装することはお分かりでしょう。javadocには、「パースペクティブファクトリは初期ページレイアウトとページの可視アクションを生成する」と書かれています。Eclipse IDEでPerspectiveを実際に確認するには、[Debug]、[Java]、[Plug-in Development]、[Java]など、右下隅にあるさまざまなPerspectiveをクリックするだけで十分です。また、Eclipseのメニューで[Window]-[Open Perspective]を使う方法もあります。基本的に、パースペクティブは同じ機能を持つビューをグループにまとめるためのレイアウトです。ビューは基本的にコンポーネントを追加するパネル/パレットです。ビューを作成して1つの機能をカプセル化します。Eclipseでは、メニュー[Window]-[Show View]をクリックすると、IDEが提供しなければならないさまざまなビューが表示されます。例えば、Eclipseには、Package Explorer、Outline、JUnit、Antなどのビューがあります。
デフォルトのViewクラスをリファクタリングする このように、EclipseのウィザードはViewというクラスを自動的に作成しました。このままでは不便なので、次の手順に従って、このデフォルトの
「plugin.xml」でリファクタリングを更新する場合は手動で行わなければなりません。「plugin.xml」を開き、[plugin.xml]タブをクリックします。Viewという拡張を探して、次のように更新します。
name="ExplorerView" class="eclipseTradeClient.ExplorerView" id="EclipseTradeClient.explorerView"> 更新を終えたら、保存します(図7を参照)。
図7 リファクタリングの追加作業 - Plugin.XMLを手動で更新する ![]() 図8 [Eclipse Trade Client]画面 ![]() ![]() アプリケーションにSpring Remotingを追加する前回の記事で構築したStockTradeServerプロジェクトに要求を送るために、EclipseリッチクライアントにSpringを追加します。 まず、Eclipseプラグイン/RCPアプリケーションを開発しているときにサードパーティライブラリを追加する場合は、別のプラグインを使う方法をお勧めします。こうすれば、プロジェクトを作成するたびにサードパーティのjarを追加する必要がなく、プラグイン/RCPプロジェクトとサードパーティライブラリのプロジェクトとの依存関係を作成するだけで済みます。最も、これは言うほど簡単なことではありません。Eclipseのクラスローダーに慣れていない場合は、こちらを参照してください。要するに、プラグインの依存関係の中でクラスが互いを検出できるようにするために、2つの追加手順を実行する必要があります。
ID = SpringClient Class = springClient.SpringClientPlugin プラグインプロパティを入力し終えたら、[Finish]をクリックします。SpringClientプラグインプロジェクトの主な目的はSpringのライブラリとSpring関連のサービスクラスを保持することなので、テンプレートは不要です。
前にも述べたとおり、Eclipseのクラスローダーはたびたび問題を起こすことがあります。これを修正するため、[MANIFEST.MF]タブをクリックして次の行を追加します。
Eclipse-BuddyPolicy: registered 「src」ディレクトリの下にも「beanRefFactory.xml」という新しいファイルを作成し、次の行を追加します。
おや、と思った場合は正解です。これらのファイルはstocktradeserverプロジェクトのユニットテストで使用したものと同じSpringの構成ファイルで、名前を「applicationContext.xml」に変更しただけです。
この記事のダウンロードサンプルまたは前回の記事を参照し、stephenlum.services.stockの下にクラスStockService.javaをコピーします。次に、stephenlum.services.stock.dtoの下にクラスStockDTO.javaをコピーします(図11を参照)。
図11 実行後のパッケージエクスプローラ画面 ![]() package springClient; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.access.BeanFactoryLocator; import org.springframework.beans.factory.access.BeanFactoryReference; import org.springframework.beans.factory.access.SingletonBeanFactoryLocator; /** * The main plugin class to be used in the desktop. */ public class SpringClientPlugin extends AbstractUIPlugin { private BeanFactory beanFactory; //The shared instance. private static SpringClientPlugin plugin; /** * The constructor. */ public SpringClientPlugin() { plugin = this; BeanFactoryLocator beanFactoryLocator = SingletonBeanFactoryLocator.getInstance(); BeanFactoryReference beanFactoryReference = beanFactoryLocator.useBeanFactory("ctx"); beanFactory = beanFactoryReference.getFactory(); } /** * This method is called upon plug-in activation */ public void start(BundleContext context) throws Exception { super.start(context); } /** * This method is called when the plug-in is stopped */ public void stop(BundleContext context) throws Exception { super.stop(context); plugin = null; } /** * Returns the shared instance. */ public static SpringClientPlugin getDefault() { return plugin; } /** * Returns an image descriptor for the image file at the given * plug-in relative path. * * @param path the path * @return the image descriptor */ public static ImageDescriptor getImageDescriptor(String path) { return AbstractUIPlugin. imageDescriptorFromPlugin("SpringClient", path); } public BeanFactory getBeanFactory() { return beanFactory; } } 図12 必須プラグインとしてSpringClientを追加する ![]() 次に、Eclipseのバディポリシーに関してEclipseTradeClientマニフェストファイルにコードを追加する必要があります。ファイル「plugin.xml」で、[MANIFEST.MF]タブをクリックし、次のエントリを追加します。
Eclipse-RegisterBuddy: SpringClient 新しいWatchListViewを作成する これで、独自の
Java Package Name = eclipseTradeClient.views.watchlist View Class Name = WatchListView View Name = Watch List View View Category ID = EclipseTradeClient View Category Name = WatchList Category [Table Viewer]ボタンを選択したまま、[Add the view to the resource perspective]チェックボックスをオンにします(図14を参照)。[Next]をクリックします。
図14 Watch List Viewの[Main View Settings]画面 ![]() package eclipseTradeClient.views.watchlist; import java.text.NumberFormat; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.graphics.Image; import stephenlum.services.stock.dto.StockDTO; public class WatchListTableLabelProvider extends LabelProvider implements ITableLabelProvider { private static NumberFormat numberFormat = NumberFormat.getInstance(); public Image getColumnImage(Object element, int columnIndex) { return null; } public String getColumnText(Object element, int columnIndex) { if (element != null) { switch (columnIndex) { case 0: return ((StockDTO) element).getTickerSymbol(); case 1: return ((StockDTO) element).getLastTrade().toString(); case 2: return numberFormat. format(((StockDTO) element).getVolume()); case 3: return ((StockDTO) element).getDaysRange(); case 4: return numberFormat. format(((StockDTO) element).getAvgVol()); case 5: return ((StockDTO) element).getDaysRange(); case 6: return ((StockDTO) element).getFiftyTwoWeekRange(); case 7: return ((StockDTO) element).getMarketCap(); } } return ""; } } package eclipseTradeClient; import org.eclipse.ui.IPageLayout; import org.eclipse.ui.IPerspectiveFactory; import org.eclipse.ui.IFolderLayout; import eclipseTradeClient.views.WatchListView; public class Perspective implements IPerspectiveFactory { public void createInitialLayout(IPageLayout layout) { String editorArea = layout.getEditorArea(); layout.setEditorAreaVisible(false); layout.setFixed(false); layout.addStandaloneView(ExplorerView.ID, false, IPageLayout.LEFT, 0.25f, editorArea); IFolderLayout topLeft = layout.createFolder("TOP", IPageLayout.TOP, 0.50f, editorArea); layout.addView(WatchListView.ID, IPageLayout.BOTTOM, 0.25f, editorArea); } } リスト1 eclipseTradeClient.views.watchlist
package eclipseTradeClient.views.watchlist; import java.util.ArrayList; import java.util.List; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.ViewerSorter; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.ui.IActionBars; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IWorkbenchActionConstants; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.dialogs.ViewContentProvider; import org.eclipse.ui.part.ViewPart; import springClient.SpringClientPlugin; import stephenlum.services.stock.StockService; import stephenlum.services.stock.dto.StockDTO; public class WatchListView extends ViewPart { public static final String ID = "eclipseTradeClient.views.watchlist.WatchListView"; private TableViewer viewer; private Action getStocksAction; private Action clearTableAction; private Action doubleClickAction; class NameSorter extends ViewerSorter { } /** * The constructor. */ public WatchListView() { } /** * This is a callback that will allow us * to create the viewer and initialize it. */ public void createPartControl(Composite parent) { GridLayout gridLayout = new GridLayout(); gridLayout.numColumns = 8; gridLayout.marginHeight = 5; gridLayout.marginWidth = 5; parent.setLayout(gridLayout); GridData gridData = new GridData(); gridData.verticalAlignment = GridData.FILL; gridData.horizontalSpan = 3; gridData.grabExcessHorizontalSpace = true; gridData.grabExcessVerticalSpace = true; gridData.horizontalAlignment = GridData.FILL; viewer = new TableViewer( parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL); viewer.setContentProvider(new ViewContentProvider()); viewer.setLabelProvider(new WatchListTableLabelProvider()); viewer.setSorter(new NameSorter()); viewer.setInput(StockDTO.class); viewer.getControl().setLayoutData(gridData); Table table = viewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); TableColumn tickerSymbolColumn = new TableColumn(table, SWT.NONE); tickerSymbolColumn.setText("Ticker Symbol"); tickerSymbolColumn.setWidth(100); TableColumn lastTradeColumn = new TableColumn(table, SWT.NONE); lastTradeColumn.setText("Last Trade"); lastTradeColumn.setWidth(100); TableColumn volumeColumn = new TableColumn(table, SWT.NONE); volumeColumn.setText("Volume"); volumeColumn.setWidth(100); TableColumn daysrangeColumn = new TableColumn(table, SWT.NONE); daysrangeColumn.setText("Days Range"); daysrangeColumn.setWidth(100); TableColumn avgvolColumn = new TableColumn(table, SWT.NONE); avgvolColumn.setText("Avg Vol"); avgvolColumn.setWidth(100); TableColumn daysRangeColumn = new TableColumn(table, SWT.NONE); daysRangeColumn.setText("Days Range"); daysRangeColumn.setWidth(100); TableColumn fiftyTwoWeekColumn = new TableColumn(table, SWT.NONE); fiftyTwoWeekColumn.setText("52 Week Range"); fiftyTwoWeekColumn.setWidth(100); TableColumn marketCapColumn = new TableColumn(table, SWT.NONE); marketCapColumn.setText("Market Cap"); marketCapColumn.setWidth(100); makeActions(); hookContextMenu(); hookDoubleClickAction(); contributeToActionBars(); } private void hookContextMenu() { MenuManager menuMgr = new MenuManager("#PopupMenu"); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { WatchListView.this.fillContextMenu(manager); } }); Menu menu = menuMgr.createContextMenu(viewer.getControl()); viewer.getControl().setMenu(menu); getSite().registerContextMenu(menuMgr, viewer); } private void contributeToActionBars() { IActionBars bars = getViewSite().getActionBars(); fillLocalPullDown(bars.getMenuManager()); fillLocalToolBar(bars.getToolBarManager()); } private void fillLocalPullDown(IMenuManager manager) { manager.add(getStocksAction); manager.add(new Separator()); manager.add(clearTableAction); } private void fillContextMenu(IMenuManager manager) { manager.add(getStocksAction); manager.add(clearTableAction); // Other plug-ins can contribute there actions here manager.add(new Separator( IWorkbenchActionConstants.MB_ADDITIONS)); } private void fillLocalToolBar(IToolBarManager manager) { manager.add(getStocksAction); manager.add(clearTableAction); } private void makeActions() { getStocksAction = new Action() { public void run() { showMessage("Getting Watch List..."); StockService stockServiceHttp = (StockService)SpringClientPlugin.getDefault(). getBeanFactory().getBean("stockServiceHttpInvoker"); List<String> tickerList = new ArrayList<String>(); tickerList.add("msft"); tickerList.add("sunw"); tickerList.add("orcl"); tickerList.add("ibm"); List stockList = stockServiceHttp.getStocks(tickerList); viewer.add(stockList.toArray()); } }; getStocksAction.setText("Get Watch List"); getStocksAction.setToolTipText("Get Watch List"); getStocksAction.setImageDescriptor(PlatformUI.getWorkbench(). getSharedImages().getImageDescriptor( ISharedImages.IMG_OBJS_INFO_TSK)); clearTableAction = new Action() { public void run() { showMessage("Clear Watch List"); viewer.getTable().removeAll(); } }; clearTableAction.setText("Clear Watch List"); clearTableAction.setToolTipText("Clear Watch List"); clearTableAction.setImageDescriptor(PlatformUI.getWorkbench(). getSharedImages().getImageDescriptor( ISharedImages.IMG_OBJS_INFO_TSK)); doubleClickAction = new Action() { public void run() { ISelection selection = viewer.getSelection(); Object obj = ((IStructuredSelection)selection). getFirstElement(); showMessage("Double-click detected on "+obj.toString()); } }; } private void hookDoubleClickAction() { viewer.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(DoubleClickEvent event) { doubleClickAction.run(); } }); } private void showMessage(String message) { MessageDialog.openInformation( viewer.getControl().getShell(), "Watch List View", message); } /** * Passing the focus request to the viewer’s control. */ public void setFocus() { viewer.getControl().setFocus(); } } アプリケーションを実行するこれで、アプリケーションを実行する準備ができました。まだEclipseにstocktradeserverプロジェクトをインポートしていない場合は、次のようにしてインポートします。
図15 [Deploy MyEclipse J2EE Server]ボタン ![]() ドロップダウンリストにあるプロジェクトがstocktradeserverであることを確認します。[Add]をクリックし、サーバーとしてTomcat 5を選択して、[Finish]をクリックします。Successfully deployedというメッセージが表示されたら、[OK]をクリックします(図16を参照)。
図16 正しく配備されたStockTradeServer ![]() 次に、Tomcatサーバーを始動します(図17を参照)。Tomcatは正常に始動されるはずです。
図17 MyEclipseプラグインからTomcatを始動する ![]() お疲れさまでした。小さいEclipseリッチクライアントを構築し、Spring Remotingを使ってアプリケーションサーバーに接続することができました。 Eclipse RCPの豊富な機能を調べる今後はRCPベースの開発をぜひ検討してみてください。RCPにはそれだけの価値があります。次にEclipse RCPベースのシッククライアントを構築するときには、シッククライアントのGUIフレームワークを構築するために必要な基本部分のコードがかなり少なくなるでしょう。さらに、Spring Remotingをクライアント/サーバー通信のメカニズムとして利用すれば、プロトコルの切り替えが非常に簡単になるだけでなく、サーバーサイドでSpringのその他のメリットを利用することもできます。 著者紹介Stephen Lum(Stephen Lum)
ロンドンの投資銀行の上級開発者。Javaのプログラミング経験は7年に及ぶ。SunのJavaプログラマ認定資格とOracleの開発者認定資格のほか、CPAの資格を持つ。
|