2010年4月3日土曜日

GAE/J + BlazeDS + Flash Builder 環境の構築

今回は GAE/J(Google App Engine Java) と BlazeDS と flex (Adobe Flash Builder) の環境の構築方法についてまとめたいと思います。

基本情報

まずは関係する基本的な情報をおさえておきましょう。

Adobe Flash Builder の導入

Adobe Flex から、購入もしくは体験版をし取得してインストールします。(体験版は現時点では60日のライセンスになっています)

以下の動作検証は Adobe Flash Builder 4 の Standalone 版で行っています(が、どの版でも問題ないと思います)。

Adobe Flash Builder 4 の Standalone 版のベースは Eclipse 3.4(Ganymede) ではなく Eclipse 3.5(Galileo) のようです。


GAE/J(Google App Engine Java)環境の構築

Eclipse 3.5(Galileo) をベースとして、GAE/J(Google App Engine Java)環境を構築します。
  • Javaがインストールされていなければ http://java.sun.com/ から Java SE をダウンロードしてインストールします。
  • Eclipse Downloads から、Eclipse IDE for Java EE Developers をダウンロードしてインストール。(2010.04.03 時点では eclipse-jee-galileo-SR2-win32.zip)
  • Eclipse のGoogle 関連プラグインをインストール。Update Site は http://dl.google.com/eclipse/plugin/3.5。(eclipse が起動しない場合は eclipse.ini の -showsplash org.eclipse.platform を削除すると動くかもしれません。)

RPC疎通確認用アプリケーションの開発(サーバ側)

GAE/J環境にてRPC疎通確認用アプリケーションを以下のように作成します。
  • Web Application Project を作成
    • Project name : Server
    • Package : server
    • Google SDKs : Use Google App Engine のみチェック
  • 疎通確認用サービスの作成
    • server.EchoService クラスを作成
package server;

public class EchoService {
  public String echo(String text) {
    return "Server says: I received '" + text + "' from you";
  }
}
  • BlazeDS 関連ライブラリの導入
    • Downloads ページ内の Download the latest BlazeDS Release builds から Download the BlazeDS binary distribution を選択し、最新のリリース版のバイナリディストリビューションを取得します。
    • zip ファイルを展開し、blazeds.war 内の WEB-INF/lib フォルダ内の以下の jar ファイルを Server プロジェクトの war/WEB-INF/lib フォルダ以下にコピーします。
      • backport-util-concurrent.jar
      • flex-messaging-core.jar
      • flex-messaging-common.jar
      • flex-messaging-remoting.jar
  • BlazeDS 関連設定
    • WEB-INF/flex/remoting-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<service id="remoting-service" class="flex.messaging.services.RemotingService">

  <adapters>
    <adapter-definition id="java-object" class="flex.messaging.services.remoting.adapters.JavaAdapter" default="true" />
  </adapters>

  <destination id="echoServiceDestination">
    <properties>
      <source>server.EchoService</source>
      <scope>application</scope>
    </properties>
  </destination>

</service>

    • WEB-INF/flex/services-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<services-config>

  <services>
    <default-channels><channel ref="amf" /></default-channels>
    <service-include file-path="remoting-config.xml" />
  </services>

  <channels>
    <channel-definition id="amf" class="mx.messaging.channels.AMFChannel">
      <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint" />
    </channel-definition>
  </channels>

  <system>
    <!-- Workaround for GAE : java.lang.management.ManagementFactory is a restricted class. -->
    <manageable>false</manageable>
  </system>

</services-config>

    • WEB-INF/web.xml
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
         
  <!-- Http Flex Session attribute and binding listener support -->
  <listener>
    <listener-class>flex.messaging.HttpFlexSession</listener-class>
  </listener>

  <!-- MessageBroker Servlet -->
  <servlet>
    <display-name>MessageBrokerServlet</display-name>
    <servlet-name>MessageBrokerServlet</servlet-name>
    <servlet-class>flex.messaging.MessageBrokerServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <!-- GAE's sample servlet -->
  <servlet>
    <servlet-name>Server</servlet-name>
    <servlet-class>server.ServerServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>MessageBrokerServlet</servlet-name>
    <url-pattern>/messagebroker/*</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>Server</servlet-name>
    <url-pattern>/server</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>
</web-app>

    • WEB-INF/appengine-web.xml
<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
 <application></application>
 <version>1</version>
 
 <!-- Configure java.util.logging -->
 <system-properties>
  <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
 </system-properties>

    <!-- flex.messaging.HttpFlexSession uses session. -->
    <sessions-enabled>true</sessions-enabled>
     
</appengine-web-app>


疎通確認用アプリケーションの開発(クライアント側)

Adobe Flash Builder 4 にて疎通確認用アプリケーション(クライアント側)を以下のように作成します。
  • Flex Project を作成
    • Project name : Client
    • Application server type : J2EE
    • User remote object access service にチェックし BlazeDS を選択
    • Root folder : サーバ側プロジェクトの war フォルダのパス(例:C:\eclipse\workspace\Server\war)
    • Root URL : http://localhost:8888/
    • Context root : /
  •  RpcClient.mxml ファイルの編集
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%">
  <s:layout>
    <s:VerticalLayout horizontalAlign="center" paddingLeft="24" paddingBottom="24" paddingRight="24" paddingTop="24" />
  </s:layout>
  <fx:Declarations>
    <!-- Place non-visual elements (e.g., services, value objects) here -->
    <mx:RemoteObject id="remoteObject" destination="echoServiceDestination" result="resultHandler(event);" fault="faultHandler(event);" />
  </fx:Declarations>
  <fx:Script>
    <![CDATA[
      import mx.rpc.events.FaultEvent;
      import mx.rpc.events.ResultEvent;
      // Send the message in response to a Button click.
      private function echo():void {
        var text:String = ti.text;
        remoteObject.echo(text);
      }
      // Handle the recevied message.
      private function resultHandler(event:ResultEvent):void {
        ta.text += "Server responded: "+ event.result + "\n";
      }
      // Handle a message fault.
      private function faultHandler(event:FaultEvent):void {
        ta.text += "Received fault: " + event.fault + "\n";
      }
    ]]>
  </fx:Script>
  <mx:Label text="Enter a text for the server to echo" />
  <mx:TextInput id="ti" text="Hello World!" />
  <mx:Button label="Send" click="echo();" />
  <mx:TextArea id="ta" width="100%" height="100%" />
</s:Application>


ローカル環境での動作確認

ローカル環境で Google App Engine のサーバを起動し、http://localhost:8888/Client-debug/RpcClient.html にアクセスします。


GAE環境での動作確認

appengine-web.xml ファイルの application 要素に GAE の application-id を入れてデプロイし、http://<host:port>/Client-debug/RpcClient.html にアクセスします。

おそらく、ある確率で正常に動作すると思います。


GAE対策のパッチ当て

現状ではおそらく、ある確率で以下のようなエラーが出ることと思います。
Received fault: [RPC Fault faultString="Detected duplicate HTTP-based FlexSessions, generally due to the remote host disabling session cookies. Session cookies must be enabled to manage the client connection correctly." faultCode="Server.Processing.DuplicateSessionDetected" faultDetail="null"]

現在の筆者の環境では、2回失敗し1回成功するパターンの繰り返しになるようです。

この件の詳細はGAE/J + BlazeDS 環境の DuplicateSessionDetected 問題参照のこと。

Google App Engine のサポートに関しては BLZ-444 にて Feature Request が提出されていますが、BlazeDS 3 系は放置され、BlazeDS 4 から対応ようです。そのため、ここではソースレベルでパッチをあてることにより対応します。

まずは、問題箇所であるFlexSession クラスのソースを取得します。

これを、ローカルのクラス(flex.messaging.FlexSession)として配置します。

このままではコンパイルが通らないので、以下の jar ファイルをビルドパスに通します。(対象のjarファイルを右クリックして Build Path -> Add to Build Path)
  • WEB-INF\lib\flex-messaging-core.jar
  • WEB-INF\lib\flex-messaging-common.jar
  • WEB-INF\lib\backport-util-concurrent.jar

HttpFlexSession クラスに以下のメソッドを追加します。
@Override
public boolean equals(Object obj) {
 if (!FlexSession.class.isInstance(obj)) return false;
 return FlexSession.class.cast(obj).getId().equals(getId());
}

@Override
public int hashCode() {
 if (HttpFlexSession.class.isInstance(this) && HttpFlexSession.class.cast(this).httpSession != null) { // to avoid NPE.
  return getId().hashCode();
 } else {
  return super.hashCode();
 }
}

もしくは、FlexSession クラス以下に以下のメソッドを追加します。
@Override
public boolean equals(Object obj) {
 if (!FlexSession.class.isInstance(obj)) return false;
 return FlexSession.class.cast(obj).getId().equals(getId());
}

@Override
public int hashCode() {
 if (HttpFlexSession.class.isInstance(this) && HttpFlexSession.class.cast(this).httpSession != null) { // to avoid NPE.
  return getId().hashCode();
 } else {
  return super.hashCode();
 }
}

また、flex.messaging.client.FlexClient クラスの registerFlexSession(FlexSession) メソッドについて、以下のように修正します。(メモ
public void registerFlexSession(FlexSession session)
{
    if (sessions.addIfAbsent(session))
    {
        session.addSessionDestroyedListener(this);
        session.registerFlexClient(this);
    }
    // for session serialization
    else if (session.getFlexClients().isEmpty())
    {
        session.addSessionDestroyedListener(this);
        session.registerFlexClient(this);
    }
}

すべてビルドしなおし、GAEにデプロイすれば完了です。


メッセージング疎通確認用アプリケーションの開発

先ほど RPC の疎通確認用アプリケーションを作成したので、今度はメッセージングの疎通確認アプリケーションを作成します。

先ほどの Flex プロジェクトに、MessagingClient.mxml ファイルを作成します。
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
      xmlns:s="library://ns.adobe.com/flex/spark"
      xmlns:mx="library://ns.adobe.com/flex/mx" width="100%" height="100%" creationComplete="init()">
 <s:layout>
  <s:VerticalLayout horizontalAlign="center" paddingLeft="24" paddingBottom="24" paddingRight="24" paddingTop="24" />
 </s:layout>
 <fx:Declarations>
  <mx:Producer id="producer" destination="messageServiceDestination" acknowledge="acknowledgeHandler(event);" fault="messagingFaultHandler(event);"/>
  <mx:Consumer id="consumer" destination="messageServiceDestination" message="messageHandler(event);" fault="messagingFaultHandler(event);"/>
 </fx:Declarations>
 <fx:Script>
  <![CDATA[
   import mx.messaging.events.MessageAckEvent;
   import mx.messaging.events.MessageEvent;
   import mx.messaging.events.MessageFaultEvent;
   import mx.messaging.messages.AsyncMessage;
   import mx.rpc.events.FaultEvent;
   import mx.rpc.events.ResultEvent;
   
   // Initialize this component.
   private function init():void {
    consumer.subscribe();
   }
   
   // Send the message in response to a Button click.
   private function send():void {
    var text:String = ti.text;
    var message:AsyncMessage = new AsyncMessage();
    message.body = text;
    producer.send(message);
   }
   
   // Handle the received acknoledge.
   private function acknowledgeHandler(event:MessageAckEvent):void {
    if (!ackCheckBox.selected) return;
    ta.text += "[ Ack     ] "+ event.acknowledgeMessage + "\n";
   }
   // Handle the recevied message.
   private function messageHandler(event:MessageEvent):void {
    ta.text += "[ Message ] " + event.message.body + "\n";
   }
   
   // Handle a message fault.
   private function messagingFaultHandler(event:MessageFaultEvent):void {
    ta.text += "[ fault   ] "+ event.message.faultString + "\n";
   }
  ]]>
 </fx:Script>
 <mx:Label text="Enter a message" />
 <mx:TextInput id="ti" text="Hello World!" />
 <mx:Button label="Send" click="send();" />
 <mx:TextArea id="ta" width="100%" height="100%"  fontFamily="Courier New"/>
 <s:CheckBox id="ackCheckBox" label="show ack"/>
</s:Application>

WEB-INF/flex/messaging-config.xml ファイルの作成
<service id="message-service" class="flex.messaging.services.MessageService">

  <adapters>
    <adapter-definition id="actionscript" class="flex.messaging.services.messaging.adapters.ActionScriptAdapter" default="true" />
  </adapters>

  <destination id="messageServiceDestination">
  </destination>

</service>


services-config.xml ファイルに messaging-config.xml のインクルード設定をします。
<?xml version="1.0" encoding="UTF-8"?>
<services-config>

  <services>
    <default-channels><channel ref="amf" /></default-channels>
    <service-include file-path="remoting-config.xml" />
    <service-include file-path="messaging-config.xml" />
  </services>

  <channels>
    <channel-definition id="amf" class="mx.messaging.channels.AMFChannel">
      <endpoint url="http://{server.name}:{server.port}/{context.root}/messagebroker/amf" class="flex.messaging.endpoints.AMFEndpoint" />
      <properties>
        <polling-enabled>true</polling-enabled><!-- default is true (though the document doesn't say so) -->
      </properties>
    </channel-definition>
  </channels>

  <system>
    <!-- Workaround for GAE : java.lang.management.ManagementFactory is a restricted class. -->
    <manageable>false</manageable>
  </system>

</services-config>
polling-enabled は、現状の実装だとデフォルト値が true のようなので記述しなくても問題ないと思いますが、ドキュメントでは false と書かれているので、明示的に設定しておいたほうが無難かと思われます。

ローカル環境での動作確認

ローカル環境で Google App Engine のサーバを起動し、http://localhost:8888/Client-debug/MessagingClient.html にアクセスします。

付録:jar不足によるエラーの例
  • java.lang.ClassNotFoundException: flex.messaging.HttpFlexSession
    flex-messaging-core.jar が不足
  • java.lang.NoClassDefFoundError: flex/messaging/LocalizedException
    flex-messaging-common.jar が不足
  • java.lang.ClassNotFoundException: edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap
    backport-util-concurrent.jar が不足
  • javax.servlet.UnavailableException: Cannot create class of type 'flex.messaging.services.RemotingService'.
    flex-messaging-remoting.jar が不足

付録:WEB-INF/appengine-web.xml に <sessions-enabled>true</sessions-enabled> を付け忘れた場合の例外
java.lang.RuntimeException: Session support is not enabled in appengine-web.xml.  To enable sessions, put true in that file.  Without it, getSession() is allowed, but manipulation of sessionattributes is not.
 at com.google.apphosting.utils.jetty.StubSessionManager$StubSession.throwException(StubSessionManager.java:67)
 at com.google.apphosting.utils.jetty.StubSessionManager$StubSession.setAttribute(StubSessionManager.java:63)
 at flex.messaging.HttpFlexSession.getFlexSession(HttpFlexSession.java:236)
 at flex.messaging.MessageBrokerServlet.service(MessageBrokerServlet.java:257)
 at javax.servlet.http.HttpServlet.service(HttpServlet.java:806)
 at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
 at com.google.appengine.api.blobstore.dev.ServeBlobFilter.doFilter(ServeBlobFilter.java:51)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
 at com.google.apphosting.utils.servlet.TransactionCleanupFilter.doFilter(TransactionCleanupFilter.java:43)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
 at com.google.appengine.tools.development.StaticFileFilter.doFilter(StaticFileFilter.java:122)
 at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
 at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
 at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
 at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
 at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
 at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
 at com.google.apphosting.utils.jetty.DevAppEngineWebAppContext.handle(DevAppEngineWebAppContext.java:70)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
 at com.google.appengine.tools.development.JettyContainerService$ApiProxyHandler.handle(JettyContainerService.java:349)
 at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
 at org.mortbay.jetty.Server.handle(Server.java:326)
 at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
 at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.java:923)
 at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:547)
 at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:212)
 at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
 at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:409)
 at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)


付録:services-config.xml ファイルの endpoint の指定が不正な場合の例外の例
Received fault: [RPC Fault faultString="Send failed" faultCode="Client.Error.MessageSend" faultDetail="Channel.Security.Error error Error #2048: セキュリティサンドボックス侵害 : http://beyondseeker-mixi-01.appspot.com/BlazeDSTest.swf は http://localhost:8888/messagebroker/amf からデータを読み込めません。 url: 'http://localhost:8888/messagebroker/amf'"]

0 件のコメント:

コメントを投稿