StockTradeServerへのコードの追加
このチュートリアルの目的上、StockTradeServerはきわめて単純です。株式に関する情報を要求するStockServiceと、取引を実行するOrderServiceという2つのサービスを作成し、公開します。
StockService
StockServiceが基本です。株式情報のリストを取得および設定するメソッドを作成します。
- 「stephenlum.services.stock」内に、株式情報を取得および設定する2つのメソッドを伴うインターフェイス
StockService
を作成します。
package stephenlum.services.stock; import java.util.List; import java.util.Map; import stephenlum.services.stock.dto.StockDTO; public interface StockService { public List<StockDTO> getStocks(List<String> tickerList); public void setStocks(Map<String, StockDTO> mapOfStocks); }
- パッケージ「stephenlum.services.stock.dto」内にデータ転送オブジェクト
StockDTO
を作成します。StockDTO
は回線を通じて提供されるため、Serializable
を実装する必要があります。以下のリストは、今回の例でStockDTO
用に作成した多くのプロパティを示しています。 - パッケージ「stephenlum.services.stock」内にクラス
StockServiceImpl
を作成します。このクラスではインターフェイスStockService
を実装します。クラスを作成したら、インターフェイスメソッドを実装します。 - 「applicationContext.xml」内でSpring Beansを宣言します。このファイル内で、
StockService
用のBeanと、StockDTO
のインターフェイスを宣言します。StockDTO
については、Microsoft、Sun、Oracle、およびIBMという4つの会社のインスタンスを作成します。
package stephenlum.services.stock.dto; import java.io.Serializable; /** * User: Stephen Lum */ public class StockDTO implements Serializable { private String tickerSymbol; private Double avgVol; private Double change; private String daysRange; private String fiftyTwoWeekRange; private Double lastTrade; private String marketCap; private Double volume; public StockDTO(String tickerSymbol, Double avgVol, Double change,String daysRange, String fiftyTwoWeekRange, Double lastTrade, String marketCap, Double volume) { this.tickerSymbol = tickerSymbol; this.avgVol = avgVol; this.change = change; this.daysRange = daysRange; this.fiftyTwoWeekRange = fiftyTwoWeekRange; this.lastTrade = lastTrade; this.marketCap = marketCap; this.volume = volume; } public Double getAvgVol() { return avgVol; } public Double getChange() { return change; } public String getDaysRange() { return daysRange; } public String getFiftyTwoWeekRange() { return fiftyTwoWeekRange; } public Double getLastTrade() { return lastTrade; } public String getMarketCap() { return marketCap; } public String getTickerSymbol() { return tickerSymbol; } public Double getVolume() { return volume; } }
package stephenlum.services.stock; import java.util.List; import java.util.Map; import java.util.ArrayList; import java.util.Iterator; import stephenlum.services.stock.dto.StockDTO; public class StockServiceImpl implements StockService { private Map mapOfStocks; public List<StockDTO> getStocks(List<String> tickerList) { List<StockDTO> resultList = new ArrayList<StockDTO>(); for (Iterator it = tickerList.listIterator(); it.hasNext();) { resultList.add((StockDTO)mapOfStocks.get(it.next())); } return resultList; } public void setStocks(Map<String, StockDTO> mapOfStocks) { this.mapOfStocks = mapOfStocks; } }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="stockService" class="stephenlum.services.stock.StockServiceImpl"> <property name="stocks"> <map> <entry key="msft" value-ref="msft"></entry> <entry key="sunw" value-ref="sunw"></entry> <entry key="orcl" value-ref="orcl"></entry> <entry key="ibm" value-ref="ibm"></entry> </map> </property> </bean> <bean id="msft" class="stephenlum.services.stock.dto.StockDTO"> <constructor-arg index="0"><!--ticker--> <value>MSFT</value> </constructor-arg> <constructor-arg index="1"><!--avgVol--> <value>75692500</value> </constructor-arg> <constructor-arg index="2"><!--change--> <value>-.16</value> </constructor-arg> <constructor-arg index="3"><!--daysRange--> <value>22.91 - 24.00</value> </constructor-arg> <constructor-arg index="4"><!--fiftyTwoWeekRange--> <value>23.03 - 28.38</value> </constructor-arg> <constructor-arg index="5"><!--lastTrade--> <value>22.99</value> </constructor-arg> <constructor-arg index="6"><!--marketCap--> <value>234.53B</value> </constructor-arg> <constructor-arg index="7"><!--volume--> <value>75692500</value> </constructor-arg> </bean> <bean id="sunw" class="stephenlum.services.stock.dto.StockDTO"> <constructor-arg index="0"><!--ticker--> <value>SUNW</value> </constructor-arg> <constructor-arg index="1"><!--avgVol--> <value>63942900</value> </constructor-arg> <constructor-arg index="2"><!--change--> <value>-.02</value> </constructor-arg> <constructor-arg index="3"><!--daysRange--> <value>4.56 - 4.70</value> </constructor-arg> <constructor-arg index="4"><!--fiftyTwoWeekRange--> <value>3.56 - 5.40</value> </constructor-arg> <constructor-arg index="5"><!--lastTrade--> <value>4.59</value> </constructor-arg> <constructor-arg index="6"><!--marketCap--> <value>16.05b</value> </constructor-arg> <constructor-arg index="7"><!--volume--> <value>66703285</value> </constructor-arg> </bean> <bean id="orcl" class="stephenlum.services.stock.dto.StockDTO"> <constructor-arg index="0"><!--ticker--> <value>ORCL</value> </constructor-arg> <constructor-arg index="1"><!--avgVol--> <value>47443800</value> </constructor-arg> <constructor-arg index="2"><!--change--> <value>-.07</value> </constructor-arg> <constructor-arg index="3"><!--daysRange--> <value>13.56 - 13.93</value> </constructor-arg> <constructor-arg index="4"><!--fiftyTwoWeekRange--> <value>11.75 - 15.21</value> </constructor-arg> <constructor-arg index="5"><!--lastTrade--> <value>13.70</value> </constructor-arg> <constructor-arg index="6"><!--marketCap--> <value>73.08B</value> </constructor-arg> <constructor-arg index="7"><!--volume--> <value>46390248</value> </constructor-arg> </bean> <bean id="ibm" class="stephenlum.services.stock.dto.StockDTO"> <constructor-arg index="0"><!--ticker--> <value>IBM</value> </constructor-arg> <constructor-arg index="1"><!--avgVol--> <value>5250390</value> </constructor-arg> <constructor-arg index="2"><!--change--> <value>-.38</value> </constructor-arg> <constructor-arg index="3"><!--daysRange--> <value>79.51 - 81.00</value> </constructor-arg> <constructor-arg index="4"><!--fiftyTwoWeekRange--> <value>73.45 - 89.94</value> </constructor-arg> <constructor-arg index="5"><!--lastTrade--> <value>80.28</value> </constructor-arg> <constructor-arg index="6"><!--marketCap--> <value>124.47B</value> </constructor-arg> <constructor-arg index="7"><!--volume--> <value>7019100</value> </constructor-arg> </bean> </beans>
Spring Remoting
シッククライアントからStockService
を呼び出せるようにするために、StockService
Beanを公開しなければなりません。それにはどうすればよいのでしょうか。まず考えられるのは、シッククライアント+アプリケーションサーバーのアーキテクチャを採用して、Spring Controllerを効率的に宣言し、このControllerにXMLファイルを返させるという方法です(サーブレットの応答OutputStreamに前述のXMLファイルを割り当てると考えてください)。また、StockService
をWebサービスとして公開するという方法もあります。どちらも間違いではありませんが、もっとよい方法があります。それがSpring Remotingです。
Spring Remotingを使用すると、Beanをサービスとして宣言し、多様なプロトコルを使って公開することができます。Spring Remotingが現在サポートしているのは、HTTP、Hessian、Burlap、RMI、またはJAX-RPC Webサービスです。それだけではなく、Spring Remotingでは、コードを修正せずにプロトコルを切り替えることができます。つまり、シッククライアントを構築してイントラネットに配備する場合に、RMIを使用できるのです。HTTPではなく、なぜRMIなのでしょうか。双方向通信であることに留意してください。RMIを使用すると、サーバーがクライアントに通知することができます。同様に、シックアプリケーションをネットワークの外部に配備してファイアウォールを経由させる場合も、同じサービスをHTTPで公開することができます(その場合も、コードの修正は必要ありません)。
それでは、Spring Remotingの利用方法を具体的に見ていきましょう。
- HTTPでStockServiceを公開するには、WebApplicationContext内にSpring Beanを宣言する必要があります。このためには、「springDispatcher-servlet.xml」に以下を追加します。
- このサービスをRMIでも公開します。「applicationContext.xml」に次のエントリを追加します。
<!-- httpInvoker exporter for the StockService --> <bean name="/stockServiceHttpInvoker" class="org.springframework.remoting.httpinvoker. HttpInvokerServiceExporter" lazy-init="false"> <property name="service"><ref bean="stockService"/></property> <property name="serviceInterface"> <value>stephenlum.services.stock.StockService</value> </property> </bean>
service
プロパティとserviceInterface
プロパティも定義する必要があります。また、個々のURLMappingHandler実装は宣言していないことに注意してください。そこで、Webアプリケーションは、Springの既定のBeanNameUrlMappingHandlerを使用します。<!-- rmi exporter for the StockService --> <bean name="/stockServiceRmi" class="org.springframework.remoting.rmi.RmiServiceExporter" lazy-init="false"> <property name="service"><ref bean="stockService"/></property> <property name="serviceName"><value>stockServiceRmi </value></property> <property name="serviceInterface"> <value>stephenlum.services.stock.StockService</value> </property> <property name="registryPort" value="1099"/> </bean>
Spring
クラスがRmiServiceExporterであることが唯一の相違です。serviceName
プロパティを宣言する必要があり、registryPort
はRMIの既定のポートである1099に宣言します。- ここで簡単なテストを作成します。[stocktradeserver]ノードの下に、「test」という名前の新しいソースフォルダを作成します。この新しいフォルダは、[src]と同じレベルにあることが必要です。
TestCase
を拡張して、StockServiceImplTest
という名前の新しいクラスを作成します。メソッドgetStocks(List<String> tickerList)
をテストするためのtestGetStocks()
というテストメソッドを作成します。これは例外をスローします。このメソッドでは、HTTPとRMIの両方の公開をテストします。- テストケース用のApplicationContextを作成します。「test」ディレクトリの下にファイル「applicationContext-test.xml」を作成し、以下を追加します。
- テストの準備はほぼ完了しました。SpringのSingletonBeanFactoryLocatorを使用するため、最後に「beanRefFactory.xml」を作成します。「test」ディレクトリの下にファイル「beanRefFactory.xml」を作成し、以下を追加します。
- テストを実行します。Eclipseで、ツールバーボタンの[Deploy MyEclipse J2EE project to Server]をクリックします(図10を参照)。
- Tomcatサーバーを開始します(図12を参照)。Tomcatが正常に開始するはずです。
- 最初のテストはブラウザを通じて行います。Webブラウザを開き、次のURLを入力します。
- JUnitテストを実行します。パッケージエクスプローラで、「test」ディレクトリの下にある
StockServiceImplTest
クラスにナビゲートします。クラスが確認できたら、それを右クリックし、[Run As]-[JUnit Test]を選択します(図13を参照)。
package stephenlum.services.stock; import junit.framework.Test; import junit.framework.TestSuite; import junit.framework.TestCase; import org.springframework.beans.factory.access. BeanFactoryLocator; import org.springframework.beans.factory.access. SingletonBeanFactoryLocator; import org.springframework.beans.factory.access. BeanFactoryReference; import java.util.List; import java.util.ArrayList; import java.util.Iterator; import stephenlum.services.stock.dto.StockDTO; /** * StockServiceImpl Tester. * * @author Stephen Lum * @version 1.0 */ public class StockServiceImplTest extends TestCase { public StockServiceImplTest(String name) { super(name); } public void setUp() throws Exception { super.setUp(); } public void tearDown() throws Exception { super.tearDown(); } public void testGetStocks() throws Exception { BeanFactoryLocator beanFactoryLocator = SingletonBeanFactoryLocator.getInstance(); BeanFactoryReference beanFactoryReference = beanFactoryLocator.useBeanFactory("ctx"); StockService stockServiceHttp = (StockService)beanFactoryReference .getFactory() .getBean("stockServiceHttpInvoker"); StockService stockServiceRmi = (StockService)beanFactoryReference .getFactory() .getBean("stockServiceRmi"); List tickerList = new ArrayList<String>(); tickerList.add("msft"); List stockList = stockServiceHttp.getStocks(tickerList); assertNotNull(stockList); for (Iterator it = stockList.listIterator(); it.hasNext();) { StockDTO stockDTO = (StockDTO)it.next(); assertNotNull(stockDTO.getTickerSymbol()); System.out.println("stockDTO\t" + stockDTO.getTickerSymbol()); } tickerList.clear(); tickerList.add("sunw"); stockList = stockServiceRmi.getStocks(tickerList); assertNotNull(stockList); for (Iterator it = stockList.listIterator(); it.hasNext();) { StockDTO stockDTO = (StockDTO)it.next(); assertNotNull(stockDTO.getTickerSymbol()); System.out.println("stockDTO\t" + stockDTO.getTickerSymbol()); } } public static Test suite() { return new TestSuite(StockServiceImplTest.class); } }
<beans> <bean id="stockServiceRmi" class="org.springframework.remoting.rmi. RmiProxyFactoryBean"> <property name="serviceUrl"> <value>rmi://localhost:1099/stockServiceRmi</value> </property> <property name="serviceInterface"> <value>stephenlum.services.stock.StockService</value> </property> <property name="cacheStub" value="true"/> <property name="lookupStubOnStartup" value="true"/> <property name="refreshStubOnConnectFailure" value="true"/> </bean> <bean id="stockServiceHttpInvoker" class="org.springframework.remoting.httpinvoker. HttpInvokerProxyFactoryBean"> <property name="serviceUrl"> <value> http://localhost:8080/stocktradeserver/service/ stockServiceHttpInvoker </value> </property> <property name="serviceInterface"> <value>stephenlum.services.stock.StockService</value> </property> </bean> </beans>
serviceUrl
プロパティのコンテキストは、「springDispatcher-servlet.xml」で宣言したserviceName
値と同じであることに注意してください。springDispatcher-servlet.xml serviceName="stockServiceRmi" applicationContext-test.xml serviceUrl="rmi://localhost:1099/stockServiceRmi"
<beans> <bean id="ctx" class="org.springframework.context.support. ClassPathXmlApplicationContext"> <constructor-arg> <list> <value>applicationContext-test.xml</value> </list> </constructor-arg> </bean> </beans>
http://localhost:8080/stocktradeserver/service/ stockServiceHttpInvoker
JUnitテストが正しく実行されます(図14を参照)。
次のようなエラーが表示されることがあります。
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: java.net.MalformedURLException: no protocol: Files/Apache ... ... ... Caused by: java.net.MalformedURLException: no protocol: Files/Apache at java.net.URL.(URL.java:567) ... ... ...
この場合は、RMIの部分を削除する(または起動しない)か、Tomcatのインストール先を、空白を含まないディレクトリに変更してください。
シッククライアントとアプリケーションサーバーとの通信
このチュートリアルでは、SpringとRMIを使用してサービスを公開する簡単な方法を紹介しました。Spring Remotingのおかげで、シッククライアントとアプリケーションサーバーの通信が、これまでより簡単になりました。コードを修正せずにプロトコルを変更できるSpringの柔軟性は、本当に優れています。私の知る限り、手動による方法はもちろん、他のどんなソリューションよりも優れています。このプロトコル交換は、HTTPやWebクライアントでは実現できないようなイベントをプッシュしたい場合に、特に便利です。