将Flex集成到Java EE应用程序的最佳实践

廖雪峰 / 文章 / ... / Reads: 1690 Edit

本文最早发表于IBM developerWorks:将Flex集成到Java EE应用程序的最佳实践

传统的Java EE应用程序通常使用某种MVC框架(例如,Struts)作为前端用户界面,随着Flex的兴起,基于RIA的客户端能够给用户带来更酷的界面,更短的响应时间,以及更接近于桌面应用程序的体验。本文将讲述如何将Flex集成至一个现有的Java EE应用程序中,以及如何应用最佳实践高效率地并行开发Java EE和Flex。

开发环境

本文的开发环境为Windows 7 Ultimate,Eclipse 3.4,Flex Builder 3。Java EE服务器使用Resin 3.2,当然,您也可以使用Tomcat等其他Java EE服务器。

现有的Java EE应用

假定我们已经拥有了一个管理雇员信息的Java EE应用,名为EmployeeMgmt-Server,结构如图所示:

jee-exist-proj

这是一个典型的Java EE应用,使用了流行的Spring框架。为了简化数据库操作,我们使用了内存数据库HSQLDB。对这个简单的应用,省略了DAO,直接在Façade中通过Spring的JdbcTemplate操作数据库。最后,EmployeeMgmt应用通过Servlet和JSP页面为用户提供前端界面:

jee-jsp

该界面为传统的HTML页面,用户每次点击某个链接都需要刷新页面。由于Employee Management系统更接近于传统的桌面应用程序,因此,用Flex重新编写界面会带来更好的用户体验。

集成BlazeDS

如何将Flex集成至该Java EE应用呢?现在,我们希望用Flex替换掉原有的Servlet和JSP页面,就需要让Flex和Java EE后端通信。Flex支持多种远程调用方式,包括HTTP,Web Services和AMF。不过,针对Java EE开发的服务器端应用,可以通过集成BlazeDS,充分利用AMF协议并能轻易与Flex前端交换数据,这种方式是JavaEE应用程序集成Flex的首选。

BlazeDS是Adobe LifeCycle Data Services的开源版本,遵循LGPL v3授权,可以免费使用。BlazeDS为Flex提供了基于AMF二进制协议的远程调用支持,其作用相当于Java的RMI。有了BlazeDS,通过简单的配置,一个Java接口就可以作为服务暴露给Flex,供其远程调用。

尽管现有的EmployeeMgmt应用程序已经有了Façade接口,但这个接口是暴露给Servlet使用的,最好能再为Flex定义另一个接口FlexService,并隐藏Java语言的特定对象:

public interface FlexService {

    Employee createEmployee(String name, String title, boolean gender, Date birth);

    void deleteEmployee(String id);

    Employee[] queryByName(String name);

    Employee[] queryAll();

}

现在,Java EE后端与Flex前端的接口已经定义好了,要完成Java EE后端的接口实现类非常容易,利用Spring强大的依赖注入功能,可以通过几行简单的代码完成:

public class FlexServiceImpl implements FlexService {

    private static final Employee[] EMPTY_EMPLOYEE_ARRAY = new Employee[0];

    private Facade facade;

    public void setFacade(Facade facade) {
        this.facade = facade;
    }

    public Employee createEmployee(String name, String title, boolean gender, Date birth) {
        return facade.createEmployee(name, title, gender, birth);
    }

    public void deleteEmployee(String id) {
        facade.deleteEmployee(id);
    }

    public Employee[] queryAll() {
        return facade.queryAll().toArray(EMPTY_EMPLOYEE_ARRAY);
    }

    public Employee[] queryByName(String name) {
        return facade.queryByName(name).toArray(EMPTY_EMPLOYEE_ARRAY);
    }

}

然后,我们将BlazeDS所需的jar包放至/WEB-INF/lib/。BlazeDS需要如下的jar:

  • backport-util-concurrent.jar
  • commons-httpclient.jar
  • commons-logging.jar
  • flex-messaging-common.jar
  • flex-messaging-core.jar
  • flex-messaging-proxy.jar
  • flex-messaging-remoting.jar

在web.xml中添加HttpFlexSession和Servlet映射。HttpFlexSession是BlazeDS提供的一个Listener,负责监听Flex远程调用请求,并进行一些初始化设置:

<listener>
    <listener-class>flex.messaging.HttpFlexSession</listener-class>
</listener>

MessageBrokerServlet是真正处理Flex远程调用请求的Servlet,我们需要将其映射到指定的URL:

<servlet>
    <servlet-name>messageBroker</servlet-name>
    <servlet-class>flex.messaging.MessageBrokerServlet</servlet-class>

    <init-param>
        <param-name>services.configuration.file</param-name>
        <param-value>/WEB-INF/flex/services-config.xml</param-value>
    </init-param>

    <load-on-startup>0</load-on-startup>
</servlet>

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

BlazeDS所需的所有配置文件均放在/WEB-INF/flex/目录下。BlazeDS将读取services-config.xml配置文件,该配置文件又引用了remoting-config.xml、proxy-config.xml和messaging-config.xml这3个配置文件,所以,一共需要4个配置文件。

由于BlazeDS需要将Java接口FlexService暴露给Flex前端,因此,我们在配置文件remoting-config.xml中将FlexService接口声明为一个服务:

<destination id="flexService">
    <properties>
        <source>org.expressme.employee.mgmt.flex.FlexServiceImpl</source>
        <scope>application</scope>
    </properties>
</destination>

服务名称通过destination的id属性指定,Flex前端通过该服务名称来进行远程调用。scope指定为application,表示该对象是一个全局对象。

然而,按照默认的声明,BlazeDS会去实例化FlexService对象。对于一个Java EE应用来说,通常这些服务对象都是被容器管理的(例如,Spring容器或EJB容器),更合适的方法是查找该服务对象而非直接实例化。因此,需要告诉BlazeDS通过Factory来查找指定的FlexService对象,修改配置如下:

<destination id="flexService">
    <properties>
        <factory>flexFactory</factory>
        <source>flexService</source>
        <scope>application</scope>
    </properties>
</destination>

现在,Flex如何才能通过BlazeDS调用FlexService接口呢?由于FlexService对象已经被Spring管理,因此,我们需要编写一个FlexFactory告诉BlazeDS如何找到Spring管理的FlexService的实例。flexFactory在services-config.xml中指定:

<factories>
    <factory id="flexFactory" class="org.expressme.employee.mgmt.flex.FlexFactoryImpl"/>
</factories>

FlexFactoryImpl实现了FlexFactory接口,该接口完成两件事情:

  1. 创建 FactoryInstance 对象;
  2. 通过 FactoryInstance 对象查找我们需要的FlexService。

因此,需要一个FactoryInstance的实现类,我们编写一个SpringFactoryInstance,以便从Spring的容器中查找FlexService:

class SpringFactoryInstance extends FactoryInstance {

    private Log log = LogFactory.getLog(getClass());

    SpringFactoryInstance(FlexFactory factory, String id, ConfigMap properties) {
        super(factory, id, properties);
    }

    public Object lookup() {
        ApplicationContext appContext = WebApplicationContextUtils.
                getRequiredWebApplicationContext(
                    FlexContext.getServletConfig().getServletContext()
        );
        String beanName = getSource();
        try {
            log.info("Lookup bean from Spring ApplicationContext: " + beanName);
            return appContext.getBean(beanName);
        }
        catch (NoSuchBeanDefinitionException nex) {
            ...
        }
        catch (BeansException bex) {
            ...
        }
        catch (Exception ex) {
            ...
        }
    }
}

FlexFactoryImpl负责实例化SpringFactoryInstance并通过SpringFactoryInstance的lookup()方法查找FlexService接口对象:

public class FlexFactoryImpl implements FlexFactory {

    private Log log = LogFactory.getLog(getClass());

    public FactoryInstance createFactoryInstance(String id, ConfigMap properties) {
        log.info("Create FactoryInstance.");
        SpringFactoryInstance instance = new SpringFactoryInstance(this, id, properties);
        instance.setSource(properties.getPropertyAsString(SOURCE, instance.getId()));
        return instance;
    }

    public Object lookup(FactoryInstance instanceInfo) {
        log.info("Lookup service object.");
        return instanceInfo.lookup();
    }

    public void initialize(String id, ConfigMap configMap) {
    }

}

以下是BlazeDS查找FlexService接口的过程:

  1. BlazeDS将首先创建FlexFactory的实例——FlexFactoryImpl;
  2. 当接收到Flex前端的远程调用请求时,BlazeDS通过FlexFactory创建FactoryInstance对象,并传入请求的Service ID。在这个应用程序中,被创建的FactoryInstance实际对象是SpringFactoryInstance;
  3. FactoryInstance的lookup()方法被调用,在SpringFactoryInstance中,首先查找Spring容器,然后,通过Bean的ID查找Bean,最终,FlexService接口的实例被返回。

注意到destination的id并没有写死在代码中,而是通过以下语句获得的:

properties.getPropertyAsString(SOURCE, instance.getId())

Property的SOURCE属性由BlazeDS读取XML配置文件获得:

<destination id="flexService">
    <properties>
        <factory>flexFactory</factory>
        <source>flexService</source>
        <scope>application</scope>
    </properties>
</destination>

如果您没有使用Spring框架,也不要紧,只需修改FactoryInstance的lookup()方法。例如,对于一个EJB来说,lookup()方法应该通过JNDI查找返回远程接口。无论应用程序结构如何,我们的最终目标是向BlazeDS返回一个FlexService的实例对象。

开发Flex客户端

首先安装Flex Builder 3,可以在Adobe的官方网站获得30天免费试用版。然后,打开Flex Builder 3,创建一个新的Flex Project,命名为EmployeeMgmt-Flex:

flex-proj

Flex Project需要指定Server端的配置文件地址:

flex-proj-2

因此,需要填入EmployeeMgmt-Server项目的web根目录,该目录下必须要存在/WEB-INF/flex/。点击“Validate Configuration”验证配置文件是否正确,只有通过验证后,才能继续。默认地,Flex Builder将会把生成的Flash文件放到EmployeeMgmt-Server项目的web/EmployeeMgmt-Flex-debug目录下。

一个Flex Project的目录结构如下:

flex-proj-structure

用Flex Builder做出漂亮的用户界面非常容易。Flex Builder提供了一个可视化的编辑器,通过简单的拖拽,一个毫无经验的开发人员也能够设计出漂亮的布局。如果熟悉一点XML的知识,编辑MXML也并非难事。我们设计的Employee Management系统界面的最终效果如下:

flex-ui-design

本文不打算讨论如何编写Flex界面,而是把重点放在如何实现远程调用。

为了能在Flex中实现远程调用,我们需要定义一个RemoteObject对象。可以通过ActionScript编码创建该对象,也可以直接在MXML中定义一个RemoteObject对象,并列出其所有的方法:

<mx:RemoteObject id="flexServiceRO" destination="flexService">
    <mx:method name="queryAll" result="handleQueryAll(result : ResultEvent)"/>
</mx:RemoteObject>

现在,就可以调用这个名为flexServiceRO的RemoteObject对象的方法了:

flexServiceRO.queryAll(function(result : ResultEvent) {
    var employees = result.result as Array;
});

运行该Flex Application,雇员信息已经被正确获取了:

flex-ui-in-browser

增强RemoteObject对象

通过RemoteObject进行调用虽然简单,但存在不少问题:首先,RemoteObject是一个Dynamic Class,Flex Builder的编译器无法替我们检查参数类型和参数个数,这样,在编写ActionScript代码时极易出错。此外,接口变动时(这种情况常常发生),需要重新修改RemoteObject的定义。此外,Flex团队需要一份随时修订的完整的FlexService接口文档才能工作。

因此,最好能使用强类型的RemoteObject接口,让Flex Builder的编译器及早发现错误。这个强类型的RemoteObject最好能通过Java EE应用的FlexService接口自动生成,这样,就无需再维护RemoteObject的定义。

为了能完成自动生成RemoteObject对象,我编写了一个Java2ActionScript的Ant任务来自动转换FlexService接口以及相关的所有JavaBean。JavaInterface2RemoteObjectTask完成一个Java接口对象到RemoteObject对象的转换。使用如下的Ant脚本:

<taskdef name="genactionscript" classname="org.expressme.ant.JavaBean2ActionScriptTask">
    <classpath refid="build-classpath" />
</taskdef>

<taskdef name="genremoteobject"
    classname="org.expressme.ant.JavaInterface2RemoteObjectTask">
    <classpath refid="build-classpath" />
</taskdef>

<genactionscript
    packageName="org.expressme.employee.mgmt"
    includes="Employee"
    orderByName="true"
    encoding="UTF-8"
    outputDir="${gen.dir}"
/>

<genremoteobject
    interfaceClass="org.expressme.employee.mgmt.flex.FlexService"
    encoding="UTF-8"
    outputDir="${gen.dir}"
    destination="flexService"
/>

转换后的FlexServiceRO类拥有Java接口对应的所有方法,每个方法均为强类型签名,并添加额外的两个可选的函数处理result和fault事件。例如,queryByName方法:

public function queryByName(arg1 : String, result : Function = null, fault : Function = null) : void {
    var op : AbstractOperation = ro.getOperation("queryByName");
    if (result!=null) {
        op.addEventListener(ResultEvent.RESULT, result);
    }
    if (fault!=null) {
        op.addEventListener(FaultEvent.FAULT, fault);
    }
    var f : Function = function() : void {
        op.removeEventListener(ResultEvent.RESULT, f);
        op.removeEventListener(FaultEvent.FAULT, f);
        if (result!=null) {
            op.removeEventListener(ResultEvent.RESULT, result);
        }
        if (fault!=null) {
            op.addEventListener(FaultEvent.FAULT, fault);
        }
    }
    op.addEventListener(ResultEvent.RESULT, f);
    op.addEventListener(FaultEvent.FAULT, f);
    op.send(arg1);
}

转换Java接口是通过Interface.as和InterfaceMethod.as两个模板文件完成的,此外,所有在Java EE后端和Flex之间传递的JavaBean对象也通过JavaBean2ActionScriptTask自动转换成对应的ActionScript类,这是通过Bean.as模板完成的。

有了Java类到ActionScript的自动转换,我们在编写ActionScript时,就能享受到编译器检查和ActionScript类方法的自动提示了:

flex-auto-complete

唯一的缺憾是通过反射读取FlexService接口时,我们失去了方法的参数名称,因此,FlexServiceRO的方法参数名只能变成arg1,arg2…… 等,要读取FlexService接口的方法参数名,只能通过解析Java源代码实现。

现在,Java EE后端开发团队和Flex前端开发团队只需协商定义好FlexService接口,然后,利用Java2ActionScript,Flex团队就得到了强类型的FlexServiceRO类,而Java EE团队则只需集中精力实现FlexService接口。

在开发的前期,甚至可以用硬编码的FlexService的实现类。每当FlexService变动时,只需再次运行Ant脚本,就可以获得最新的FlexServiceRO类。这样,两个团队都可以立刻开始工作,仅需要通过FlexService接口就可以完美地协同开发。

下载

Java EE工程源码:EmployeeMgmt-Server.zip

Flex工程源码:EmployeeMgmt-Flex.zip

Java2ActionScript工程源码:Java2ActionScript.zip

参考资料

Adobe Flex参考资料:查看Adobe Flex的文档集。

Flex开发入门(developerWorks,2009 年 1 月):本文介绍Flex开发的基础知识:包括如何搭建开发环境,如何调试,以及如何建立和部署简单的Flex项目。

集成Flex与Ajax应用程序(developerWorks,2008年7月):本文将介绍Adobe Flex Ajax Bridge (FABridge),这是让您可以采用轻松而一致的方法集成Ajax与Flex内容的代码库。

用Flex开发Google Map应用程序(developerWorks,2009年3月):介绍如何用Google Maps API for Flash来开发基于Flash的地图应用程序。

获得产品和技术

访问Flex产品页面

访问Adobe BlazeDS项目站点

下载Eclipse

下载Flex Builder

关于作者

廖雪峰,5年Java EE开发经验,对开源框架有深入研究,著有《Spring 2.0核心技术与最佳实践》一书,其官方博客是,可以通过askxuefeng@gmail.com与之联系。

Comments

Make a comment

Author: 廖雪峰

Publish at: ...

关注公众号不定期领红包:

加入知识星球社群:

关注微博获取实时动态: