引言
JEE作为建立在Java平台上的企业级应用解决方案,经过这些年不断发展,已经成为企业级开发的工业标准和首选平台。众多厂商如IBM,BEA和Oracle等都围绕该规范推出了相应的,功能强大的产品。JEE规范组中最受业界认同和取得最大成功的就是JEE Web层面规范,发展到今天,已经步入门户(Portal)的时代。
门户,简言之就是提供包括内容聚合、单点登陆、个性化定制和安全管理等服务的基础Web平台。众多JEE产品提供商基于JEE Web层技术推出了自己的Portal产品,著名的产品有IBM WebSphere Portal Server,BEA Weblogic Portal Server等。一直处于技术前沿的著名开源社区Apache,经过这几年的技术积累也形成了自己的门户项目组。该项目组目前已经初具规模,并且拥有了一定的用户群体,经受了一定的市场考验。
本文主要面向有一定JEE编程经验的Java开发者和试图构建自己的门户软件产品的产品经理,因为基于开源项目构建企业级的商用产品,已经在国外取得了许多成功案例。
回页首
名词解释
名词 | 解释 |
Portal | 门户,提供包括内容聚合、单点登陆、个性化定制和安全管理等服务的基础Web平台。 |
Portlet | Portlet是基于web的Java组件。它由Portlet容器管理,能够处理请求,产生动态内容。Portlet被Portal用作为可插拔的用户接口组件,为信息系统提供展现。由Portlet动态产生的内容也被叫做fragment。fragment是遵循某种规则的标记(例如:HTML, XHTML,WML),可与其他的fragment一起建立一个完整的文档。一般一个Portlet产生的内容和其他的Portlet产生的内容聚集在一起形成Portal网页。 |
Portlet Container | Portlet在Portlet容器中运行,Portlet容器为Portlet提供必需的运行环境。Portlet容器包含Portlet(组件)并且管理它们的生命周期,它也为Portlet的参数设置提供持久化的存储。Portlet 容器不是一个类似于 servlet 容器的独立容器。它是在 servlet 容器上通过扩展方式实现的,并重用 servlet容器提供的功能。从Portal的角度来看,Portlet Container是Portal平台所提供的众多服务之一。 |
JSR168,JSR286 | 由于越来越多的公司开发了各自的Portal组件和基于其的Portal产品(如Bea, IBM, Oracle, Sun, Sybase, Novell, SAP, Jetspeed, Vignette 等.这种互不兼容的接口实现不断带给程序提供商各种问题和麻烦, 为了解决这种问题, JCP发布了JSR168 (Java Specification Request), Portlet Specification, 用以提供不同Portal和Portlets之间的互用性。JSR 286是168规范的延伸,是目前最新标准规范,目前仍处在draft状态。 |
SSO Single | Sign-On,即单点登陆。当一个大系统中存在多个子系统时,用户只需要正确登陆其中任何一个子系统,就可以在各个子系统中来回自由切换和使用授予该用户权限的各种资源。一般可以分为两种类型:Web应用之间的单点登陆和门户Web应用和它所连接的后台系统之间的单点登陆。SSO是任何一个门户产品必须解决的问题,必须提供的服务。 |
WSRP | WSRP是OASIS组织的一个规范,它定义了远程门户网站的Web服务。通过Web Service将远程内容抓取到本地,最后通过本地内容聚合引擎展示出来。 |
回页首
Apache门户项目组整体架构
在引言中已经列举了Apache门户项目组的组成项目包括:Jetspeed-1/2,Bridges,Pluto,WSRP-4J和Graffito。由于Jetspeed-1和Jetspeed-2角色相同,下文中如果没有特别指出,所有Jetspeed都是指Jetspeed-2。
上图中粉红色包围部分为Apache门户项目,其它由土黄色包围部分为它们的依赖项目。通过上图可以很清楚看到,全部项目都构建在JEE Web Tier上,理论上只要支持Servlet 2.3或以上版本规范的Web容器,都可以作为Apache门户项目的基础平台,但Jetspeed官方其实仅仅声明Tomcat是其唯一支持的Web容器。另一块必要的依赖,是构建在O/R mapping项目Apache OJB之上的数据仓库,用于存放Portal系统信息和用户个性化配置(Profile)。
Portals Bridges项目其本质就是由一组类库构成的轻量级框架,通过该桥接器框架可以在门户上支持众多流行的Web框架,如上图括号中所列举。用户通过它可以很容易的将已有的基于这些流行Web框架的Web应用程序,通过少量的修改和配置,作为Portlet应用程序发布单元发布到Portal上。这个项目不但在Jetspeed上取得的成功,还被众多开源的,甚至商用的门户实现所使用,如JBoss Portal,GridSphere Portal,Stringbeans Portal,Vignette Application Portal,Apache Cocoon Portal和Jetspeed Portal。
Jetspeed项目是整个Apache Portal项目组的核心,它是一个功能完备的,易于扩展的企业级Portal实现,将在下面的文章中着重介绍它。
Pluto是Jetspeed默认的本地Portlet Container实现,它是一个完全符合JSR-168规范的Portlet容器实现,其前身为IBM捐赠的源代码,因此我们至今还能够在WebSphere Portal 5.1.1中看到它的身影。这里要注意本地的意思是指运行在该Portlet容器里的Portlet应用程序在物理上与Portal在同一个JVM进程中。
WSRP-4j是WSRP规范的JAVA实现,目前该项目还处在孵育状态,尚未吸引到足够多的开发者的兴趣。其实,我个人认为这是一个很有前途的技术发展方向,它可以提供类似Html IFrame这样速成的内容抓取能力。Jetspeed已经为WSRP-4j预留了远程Portlet Container的配置选项。
Graffito是用于构建内容管理应用程序的框架,从它自身的架构设计上来看应该是一个独立平台,但事实上该项目复用了大量Jetspeed的模块,并且其表现层为发布到Jetspeed上的几个Portlet应用程序,因此,我在上面的架构图中,将它放在了Jetspeed之上。该项目目前也处在孵育状态下,由于其该项目目前不太活跃,那几个Portlet应用程序都有些小问题。
回页首
企业级的门户实现--Jetspeed
产品特性
标准
体系架构
门户核心特性
门户管理
对Web框架的支持和例子Portlets
用户个性化
门户设计
门户开发工具
应用服务器
架构体系
本节将从Jetspeed和Spring的关系,运行时架构以及Jetspeed service架构这三方面详细介绍Jetspeed的架构体系。
Jetspeed和Spring
Jetspeed架构体系最大特点,也是其高度可订制的根基就是,它选用著名开源POJO框架Spring作为其底层实现。在项目之初,Jetspeed的开发者们也面临着Spring和PicoContainer的抉择,但事实证明当初的选择是正确的,因为随着Spring不断成长完善,Jetspeed的组件架构也跟着收益良多。从另一个角度来看,Jetspeed也可以作为利用Spring构建自己产品架构的经典范例,值得我们考察和学习。
下图简单描述了目前Jetspeed对Spring的依赖关系:
Beans BeanFactory and the ApplicationContext
Jetspeed主要使用了Spring最核心的IoC引擎BeanFactory和ApplicationContext,管理所有Jetspeed Components的生命周期和依赖关系,所有这些组件的Spring声明全部定义在名为assembly的文件夹中的XML文件里。如果第三方开发者认为默认的Jetspeed组件不足以满足要求,只要按照自己需求编写Jetspeed Component的Interface的实现类,然后修改Spring Bean XML定义,就可以轻易替换掉默认的实现。例如:
<beans default-lazy-init="false" default-dependency-check="none" default-autowire="no"> <!-- SEARCH COMPONENT --> <bean id="org.apache.jetspeed.search.SearchEngine" class="org.apache.jetspeed.search.lucene.SearchEngineImpl" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg index="0"> <value>${applicationRoot}/WEB-INF/search_index</value> </constructor-arg> <constructor-arg index="1"> <null /> </constructor-arg> <constructor-arg type="boolean"> <value>true</value> </constructor-arg> <constructor-arg> <ref bean="org.apache.jetspeed.search.HandlerFactory" /> </constructor-arg> </bean> </beans> |
Jetspeed在实现过程中遵循着面向接口编程的最佳实践,上图中的Bean id为org.apache.jetspeed.search.SearchEngine,事实上这是一个定义在核心jetspeed-api组件中的接口,org.apache.jetspeed.search.lucene.SearchEngineImpl为该接口的实现类,这个类定义在components/search组件中,后面的内容就是SearchEngineImpl的构造函数的输入参数,注意最后一个参数org.apache.jetspeed.search.HandlerFactory也是一个Java Interface的接口。Spring在实例化SearchEngine的时候,会首先分析它的构造函数参数是否已经全部满足条件(实例化),Spring会根据搜索bean id为org.apache.jetspeed.search.HandlerFactory的bean,如果已经实例化就直接注入到SearchEngineImpl的构造函数调用里;如果没有就实例化这个bean之后,再注入。
Apache OJB O/R Mappers
由于Spring对Apache OJB提供良好的支持,因此Jetspeed中与数据库相关的功能基本上都用过Spring的PersistenceBrokerDaoSupport实现。这些组件包括:Capablity、DatabasePageManager、PipeLine、Preferences、Profiler、Registry、Security、SSO等。O/R Mapping的信息定义在上面这些组件jar包中的JETSPEED-INF/ojb/%component name%_repository.xml文件中,其中%component name%需要用组建名称替代。
Declarative transaction management
在Jetspeed中,你找不到一行有关于数据库事务的代码,这是因为它采用了Spring的declarative transaction机制,下面一段XML定义了SSOProvider的事物管理:
<beans default-lazy-init="false" default-dependency-check="none" default-autowire="no"> <!-- SSO Implementation --> <bean id="PersistenceBrokerSSOProvider" class="org.apache.jetspeed.sso.impl.PersistenceBrokerSSOProvider" init-method="init" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg index="0"> <value>JETSPEED-INF/ojb/sso_repository.xml</value> </constructor-arg> </bean> <bean id="org.apache.jetspeed.sso.SSOProvider" parent="baseTransactionProxy" name="ssoProvider" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <property name="proxyInterfaces"> <value>org.apache.jetspeed.sso.SSOProvider</value> </property> <property name="target"> <ref bean="PersistenceBrokerSSOProvider" /> </property> <property name="transactionAttributes"> <props> <prop key="addSite*">PROPAGATION_REQUIRED</prop> <prop key="updateSite*">PROPAGATION_REQUIRED</prop> <prop key="removeSite">PROPAGATION_REQUIRED</prop> <prop key="addCredentialsForSite">PROPAGATION_REQUIRED</prop> <prop key="updateCredentialsForSite">PROPAGATION_REQUIRED</prop> <prop key="removeCredentialsForSite">PROPAGATION_REQUIRED</prop> <prop key="setRealmForSite">PROPAGATION_REQUIRED</prop> <prop key="*">PROPAGATION_SUPPORTS</prop> </props> </property> </bean> </beans> |
由上图可知,通过Spring 的Declarative Transaction机制,Jetspeed很轻易实现了细颗粒度的事物管理,用户可以很容易配置需要管理事务的方法,如"addSite*"、"updateSite*"和"removeSite"等等,其中"*"为通配符,详细信息见此处。
Spring MVC
由于Jetspeed对Spring的天生依赖,很自然Jetspeed也支持基于Spring MVC framework,详见Jetspeed自带的例子Portlet应用程序。
Jetspeed 组件架构启动过程
看了前面的介绍,你一定想知道Jetspeed是如何将基于Spring的组件架构和标准JEE Web Application架构融合在一起,本节将通过描述Jetspeed Web Application的启动过程来了解融合的细节。首先请看下图:
请点击这里查看Jetspeed Portal启动流程图的大图
由上图可知,Jetspeed Portal从JEE角度来看其实就是一个标准的Web应用程序,只不过在Servlet架构上引入了Component Manager的概念,然后用Spring实现了ComponentManager接口。因此如果你不满意Spring :,更换它也是有可能的。当Servlet被容器停止时,也会同时关闭SpringComponentManager。Servlet启动完毕后,所有通过Spring Bean XML定义POJO都被实例化了,除了那些指定了lazy init属性为true的Bean。
Runtime架构
JetspeedServlet
Jetspeed的运行时大环境是符合Servlet 2.3或以上规范的JEE Web容器,因此大家可以通过观察其web.xml了解或扩展其功能。下面是Jetspeed.war的web.xml:
<web-app> <display-name>Jetspeed-2 Enterprise Portal</display-name> <!-- Log4JConfigurator context-listener parameters --> <context-param> <param-name>log4j.config.file</param-name> <param-value>/WEB-INF/conf/Log4j.properties</param-value> </context-param> <context-param> <param-name>log4j.config.webApplicationRoot.key</param-name> <param-value>applicationRoot</param-value> </context-param> <filter> <filter-name>AJAXFilter</filter-name> <filter-class>org.apache.jetspeed.ajax.AJAXFilter </filter-class> </filter> <filter-mapping> <filter-name>AJAXFilter</filter-name> <url-pattern>*.ajax</url-pattern> </filter-mapping> <listener> <listener-class>org.apache.jetspeed.webapp.logging. Log4JConfigurator</listener-class> </listener> <listener> <listener-class>org.apache.jetspeed.engine.JetspeedServlet </listener-class> </listener> <servlet> <servlet-name>jetspeed</servlet-name> <servlet-class>org.apache.jetspeed.engine.JetspeedServlet </servlet-class> <init-param> <param-name>properties</param-name> <param-value>/WEB-INF/conf/jetspeed.properties</param-value> </init-param> <init-param> <param-name>applicationRoot</param-name> <param-value>webContext</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> - <!-- Define Velocity template compiler --> <servlet> <servlet-name>velocity</servlet-name> <servlet-class>org.apache.jetspeed.velocity. JetspeedVelocityViewServlet</servlet-class> <init-param> <param-name>org.apache.velocity.toolbox</param-name> <param-value>/WEB-INF/toolbox.xml</param-value> </init-param> <init-param> <param-name>org.apache.velocity.properties</param-name> <param-value>/WEB-INF/velocity.properties</param-value> </init-param> <init-param> <param-name>org.apache.jetspeed.cache.size</param-name> <param-value>50</param-value> </init-param> <init-param> <param-name>org.apache.jetspeed.cache.validation.interval </param-name> <param-value>10000</param-value> </init-param> <load-on-startup>10</load-on-startup> </servlet> <servlet> <servlet-name>LoginProxyServlet</servlet-name> <servlet-class>org.apache.jetspeed.login.LoginProxyServlet </servlet-class> </servlet> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>org.apache.jetspeed.login.LoginServlet </servlet-class> </servlet> <servlet> <servlet-name>LoginErrorServlet</servlet-name> <servlet-class>org.apache.jetspeed.login.LoginErrorServlet </servlet-class> </servlet> <servlet> <servlet-name>LoginRedirectorServlet</servlet-name> <servlet-class>org.apache.jetspeed.login.LoginRedirectorServlet </servlet-class> </servlet> <servlet> <servlet-name>LogoutServlet</servlet-name> <servlet-class>org.apache.jetspeed.login.LogoutServlet </servlet-class> </servlet> <servlet> <servlet-name>ManagerServlet</servlet-name> <servlet-class>org.apache.jetspeed.manager.ManagerServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/portal/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/portlet/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/jetspeed/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/fileserver/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/ajaxapi/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/desktop/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>jetspeed</servlet-name> <url-pattern>/action/*</url-pattern> </servlet-mapping> - <!-- Map *.vm files to Velocity --> <servlet-mapping> <servlet-name>velocity</servlet-name> <url-pattern>*.vm</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LoginProxyServlet</servlet-name> <url-pattern>/login/proxy</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login/login</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LoginErrorServlet</servlet-name> <url-pattern>/login/error</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LoginRedirectorServlet</servlet-name> <url-pattern>/login/redirector</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LogoutServlet</servlet-name> <url-pattern>/login/logout</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>ManagerServlet</servlet-name> <url-pattern>/manager/*</url-pattern> </servlet-mapping> <!-- The Usual Welcome File List --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- JNDI Db resource --> <resource-ref> <description>DB Connection</description> <res-ref-name>jdbc/jetspeed</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> <!-- Protect LogInRedirectory.jsp. This will require a login when called --> <security-constraint> <web-resource-collection> <web-resource-name>Login</web-resource-name> <url-pattern>/login/redirector</url-pattern> </web-resource-collection> <auth-constraint> <!-- the required portal user role name defined in: --> <!-- /WEB-INF/assembly/security-atn.xml --> <role-name>portal-user</role-name> </auth-constraint> </security-constraint> <!-- securing the ManagerServlet --> <security-constraint> <web-resource-collection> <web-resource-name>Manager</web-resource-name> <url-pattern>/manager/*</url-pattern> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> </security-constraint> <!-- Login configuration uses form-based authentication --> <login-config> <auth-method>FORM</auth-method> <realm-name>Jetspeed</realm-name> <form-login-config> <form-login-page>/login/login</form-login-page> <form-error-page>/login/error</form-error-page> </form-login-config> </login-config> </web-app> |
由于信息太多,这里只能选取重要的来解释。首先,请注意名为jetspeed的servlet,这就是前面一个小节里提到的入口servlet,它同时也是Portal runtime的入口,它被映射到几乎所有的URL Pattern。当来自客户端的Http请求满足这些Pattern时,jetspeed servlet将会触发如下图所示的处理流程:
请点击这里查看Jetspeed runtime execution process的大图
JetspeedServlet首先会通过RequestContextComponent为当前Http Request创建RequestContext实例,然后在这个context下调用engine的service方法。然后就会进入Pipeline的处理过程。
Pipeline
Jetspeed Pipeline实际上就是设计模式中常见的Chain of Responsibility模式的具体实现,其设计概念类似Servlet Filter,一个封装了HttpServletRequest和HttpServletResponse Object的Context在Pipeline中传递,每个valve都根据自己的需要从HttpServletRequest 对象中获取信息并将处理的结果写入context或HttpServletResponse对象,以传递给后面的valve使用。
这些Valve的定义和排序都是通过Spring Bean来配置的,定义文件为pipelines.xml,下面为该文件片断截取:
<bean id="securityValve" class="org.apache.jetspeed.security.impl.SecurityValveImpl" init-method="initialize" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg> <ref bean="org.apache.jetspeed.profiler.Profiler" /> </constructor-arg> <constructor-arg> <ref bean="org.apache.jetspeed.security.UserManager" /> </constructor-arg> <constructor-arg> <ref bean="PortalStatistics" /> </constructor-arg> </bean> |
<bean id="jetspeed-pipeline" class="org.apache.jetspeed.pipeline.JetspeedPipeline" init-method="initialize" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg> <value>JetspeedPipeline</value> </constructor-arg> <constructor-arg> <list> <ref bean="capabilityValve" /> <ref bean="portalURLValve" /> <ref bean="securityValve" /> <ref bean="localizationValve" /> <ref bean="passwordCredentialValve" /> <ref bean="loginValidationValve" /> <ref bean="profilerValve" /> <ref bean="containerValve" /> <ref bean="actionValve" /> <ref bean="DecorationValve" /> <ref bean="aggregatorValve" /> <ref bean="cleanUpValve" /> </list> </constructor-arg> </bean> |
<bean id="pipeline-map" class="java.util.HashMap" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg> <map> <entry key="/portlet"> <value>portlet-pipeline</value> </entry> <entry key="/portal"> <value>jetspeed-pipeline</value> </entry> <entry key="/ajaxapi"> <value>ajax-pipeline</value> </entry> <entry key="/login"> <value>jetspeed-pipeline</value> </entry> <entry key="/fileserver"> <value>fileserver-pipeline</value> </entry> <entry key="/desktop"> <value>desktop-pipeline</value> </entry> <entry key="/action"> <value>desktop-action-pipeline</value> </entry> </map> </constructor-arg> </bean> |
上面表格第一行定义了Security Valve,第二行定义了名为JetspeedPipeline的一个Pipeline,第三行定义了这些Pipeline对应的URL Pattern。门户开发者可以很容易的定义自己特有的Pipeline Valve(只需要实现org.apache.jetspeed.pipeline.valve.Valve接口,并在这个xml文件中定义它),或者改变现有Pipeline中valve执行顺序,甚至创建新的Pipeline,并把它映射到某个URL Pattern上。但这里需要注意的是新URL Pattern映射不能跟现有的重复,这是因为映射是通过Map数据结构实现。让我们再把注意力返回到图六,接下来的Container处理由一个竖线分隔,这是由于在Pipeline的aggregatorValve发生了cross context dispatch。需要注意的是Valve的实现类并不是Thread Safe的,开发者必须自己管理共享变量,最好就是不要定义对象成员变量,全部使用方法内部变量。
Jetspeed的设计者之所以要自己实现这种链式模式而不直接使用Filter的可能原因有二:其一,Filter是标准Servlet规范定义的接口,使用上肯定受规范限制,为了获取更大的控制权和灵活度需要自己的Pipeline。其二, Application Server,由于每种应用服务器可能对Filter的实现和配置方法上有异,为了实现一个Jetspeed Portal能够在各种应用服务器上发布并运行,因而必须自己实现类似Filter的功能。
Container
讲到这里,我们必须首先理清楚Portal和Portlet Container之间的关系。Portal并不等价于Portlet Container,一个企业级的门户实现,可以包含或者说支持多种Portlet Container同时运行,例如IBM WebSphere Portal就既包含兼容JSR-168规范的Portlet Container又包含了支持一些IBM特有功能属性的Portlet Container;BEA WebLogic也是采用相同策略,既有其旧有的基于Struts技术的Portlet Container,又支持JSR-168标准。随着JSR标准规范进一步深化完善,如JSR-286规范定稿,也许这些大的Portal厂商会逐渐放弃其旧的架构,完全拥抱标准。
对于Jetspeed而言,它同样关注的是门户本身的实现,而不是Portlet Container的实现,但由于Jetspeed-2完全抛弃Jetspeed-1的架构,而决定彻底拥抱标准,因此Jetspeed只有一个标准的Portlet Container实现,这就是Pluto项目。
Pluto的设计目标,其实既是一个完整的、自包含的轻量级门户,又是一个易于内置的Portlet Container。它不需要Jetspeed也能够单独工作,例如Apache唯一的应用服务器项目Geronimo就内置Pluto Portlet Container,作为它的Console实现平台。然而,这与Jetspeed的定位起了冲突。在2005年底的Apache Con上,门户项目组开发者齐聚一堂,就该问题作了深入的探讨,也许这两个项目在将来会更加关注于自身的技术领域。
Jetspeed所内置的Pluto版本为稳定的1.0.1版,它所提供的内置集成方式不如目前尚未正式发布的Pluto1.1版本丰富,因此Jetspeed采用了Servlet 规范中的Cross Context Dispatch机制,将之集成了起来,这就是为什么Runtime架构图上,两者要用竖线分隔开来,因为他们其实是两个完全不同的Web应用,有着不一样的Context设置,之间通过Cross Context Dispatch机制联系起来的。因此当门户开发者需要在其它应用服务器上部署Jetspeed门户时,必须注意开启Cross Context Dispatch机制,也同时要注意这种使用方式所带来的安全问题。例如Tomcat是通过设置文件%TOMCAT_ROOT%/conf/Catalina/localhost/jetspeed.xml(注意粉红色高亮字体):
<Context path="/jetspeed" docBase="jetspeed" crossContext="true"> <Realm className="org.apache.catalina.realm.JAASRealm" appName="Jetspeed" userClassNames="org.apache.jetspeed.security.impl.UserPrincipalImpl" roleClassNames="org.apache.jetspeed.security.impl.RolePrincipalImpl" useContextClassLoader="false" debug="0" /> <Resource name="jdbc/jetspeed" auth="Container" factory="org.apache.commons.dbcp.BasicDataSourceFactory" type="javax.sql.DataSource" username="" password="" driverClassName="org.apache.derby.jdbc.EmbeddedDriver" url="jdbc:derby:/tmp/j2" maxActive="100" maxIdle="30" maxWait="10000" /> <Valve className="org.apache.catalina.authenticator.FormAuthenticator" characterEncoding="UTF-8" /> </Context> |
在Pluto和Jetspeed相互配合下,通过JetspeedContainerServlet,最终执行控制权会交给Portlet的实现类。JetspeedContainerServlet就定义在Portlet应用程序所属的Web应用单位中,也就是说所有在Jetspeed中运行的Portlet Web应用都必须在Web.xml中包含JetspeedContainerServlet的定义。在Tomcat中,这是通过deploy-tool组件完成的,在其它应用服务器平台,很可能就要靠应用发布者手动添加了,需添加的信息包含:
<servlet> <servlet-name>JetspeedContainer</servlet-name> <display-name>Jetspeed Container</display-name> <description>MVC Servlet for Jetspeed Portlet Applications</description> <servlet-class>org.apache.jetspeed.container. JetspeedContainerServlet</servlet-class> <init-param> <param-name>contextName</param-name> <param-value>rss</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>JetspeedContainer</servlet-name> <url-pattern>/container/*</url-pattern> </servlet-mapping> <taglib> <taglib-uri>http://java.sun.com/portlet</taglib-uri> <taglib-location>/WEB-INF/tld/portlet.tld</taglib-location> </taglib> |
Jetspeed Portlet Extension Service
前面介绍了实现架构和运行时架构,接下来我们一起来看看Jetspeed为Portlet应用提供的Jetspeed Service架构。如果了解JSR-168规范的开发者就会知道,这个规范是基于Servlet 2.3规范基础上的一个简单扩展,因此并没有对Portlet开发提供任何特别的支持。因而每家Portal厂商都提供了自己的扩展。
Jetspeed提供的扩展方式跟很多厂商对Servlet规范的扩展一样,定义了一个名为jetspeed-portlet.xml的文件,作为标准的portlet.xml的扩展。只要你在打包发布portlet应用时将这个文件与portlet.xml放在一起,Jetspeed的发布程序就会自动读取这个文件,并根据其内容执行一系列的操作。它们的关系如同BEA Weblogic应用服务器里面的weblogic.xml与web.xml;JBoss应用服务器里面的jboss-web.xml与web.xml。
下面我们来看看这个文件的格式:
<portlet-app id="j2-admin" version="1.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" xmlns:js="http://portals.apache.org/jetspeed" xmlns:dc="http://www.purl.org/dc"> <js:services> <js:service name="ApplicationServerManager" /> <js:service name="DeploymentManager" /> <js:service name="EntityAccessor" /> <js:service name="GroupManager" /> <js:service name="PageManager" /> <js:service name="PermissionManager" /> <js:service name="PortalAdministration" /> <js:service name="PortletFactory" /> <js:service name="PortalAdministration" /> <js:service name="PortletRegistryComponent" /> <js:service name="PortalStatistics" /> <js:service name="Profiler" /> <js:service name="RoleManager" /> <js:service name="SearchComponent" /> <js:service name="SSO" /> <js:service name="UserManager" /> <js:service name="HeaderResource" /> </js:services> </portlet-app> |
跟据XML Element的名字,可以理解就是提供给j2-admin这个Portlet应用程序使用的一些Services。那么这些Services是怎么定义的呢?以UserManager这个服务为例,首先回到前面提到过的assemble目录下,找到jetspeed-services.xml和security-managers.xml,下面分别是它们的内容节选:
jetspeed-services.xml
<beans default-lazy-init="false" default-dependency-check="none" default-autowire="no"> <!-- Portlet Services --> <bean id="PortalServices" class="org.apache.jetspeed.services.JetspeedPortletServices" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg> <map> <entry key="SearchComponent"> <ref bean="org.apache.jetspeed.search.SearchEngine" /> </entry> <entry key="UserManager"> <ref bean="org.apache.jetspeed.security.UserManager" /> </entry> <entry key="PageManager"> <ref bean="org.apache.jetspeed.page.PageManager" /> </entry> </map> </constructor-arg> </bean> </beans> |
security-managers.xml
<beans default-lazy-init="false" default-dependency-check="none" default-autowire="no"> <bean id="org.apache.jetspeed.security.UserManager" class="org.apache.jetspeed.security.impl.UserManagerImpl" abstract="false" singleton="true" lazy-init="default" autowire="default" dependency-check="default"> <constructor-arg> <ref bean="org.apache.jetspeed.security.SecurityProvider" /> </constructor-arg> </bean> </beans> |
由上面的两个文件,我们可以清楚地看出UserManager是定义security-managers.xml文件中的,但是为了能够在portlet服务中引用它,还必须在jetspeed-services.xml中再次引用它的bean name,org.apache.jetspeed.services.JetspeedPortletServices封装了一个Map数据结构,Map中存放的就是服务名称和该服务POJO对象引用,这一切便利都是由Spring带来的。
下面是在Portlet的代码中使用该服务的例子:
protected UserManager userManager = null; public void init(PortletConfig config) throws PortletException { super.init(config); userManager = (UserManager) getPortletContext().getAttribute(CommonPortletServices. CPS_USER_MANAGER_COMPONENT); if (null == userManager) { throw new PortletException("Failed to find the User Manager on portlet initialization"); } } |
Jetspeed鼓励Portlet开发者在开发过程中,将公共的Service用Spring Bean的方式封装起来,然后添加到Jetspeed-services中,这样就可以在Portlet代码中轻松复用这些公共Service了,并且还可以利用Spring来管理这些Service的生命周期。事实上,Jetspeed自带管理界面Portlet应用程序就大量采用了这种技术。
还有一个需要注意的问题是,用户自定义的Service必须放到Tomcat的shared\lib下面去,以保证JAVA Classloader能够找到它。
Jetspeed核心组件简介
介绍了那么多Jetspeed架构方面的信息,下面我们一起来快速浏览一下Jetspeed Portal的核心组件。
Jetspeed-Api
路径:components/jetspeed-api
定义几乎所有的jetspeed-api interfaces,一般的开发者都使用这个组件中定义的接口进行二次开发。
Component Manager
路径:components/cm
Jetspeed组件管理器,通过接口org.apache.jetspeed.components.ComponentManager屏蔽了Spring的实现细节。可以通过实现该接口替换Spring。
Deploy-Tool
路径:components/deploy-tool
当Web Container为Tomcat时,通过该组件,读取已打包好的portlet应用程序中的portlet.xml和web.xml,检查是否包含JetspeedContainerServlet的定义,如果没有则修改web.xml加入这部分信息。
Id-Generator
路径:components/id-generator
用于生成全局唯一的portlet实例id。
Locator
路径:components/locator
提供定位门户资源的服务,资源包括:模板,Profiler等。
Page-Manager
路径:components/page-manager
对著名的门户结构描述文件-PSML(Portal Structure Markup Language),提供了Java对象模型映射,并且支持文本风格的PSML和数据库风格的PSML,以及PSML管理器。
Portal
路径:components/portal
实现绝大部分的jetspeed-api组件中定义的interface,是最核心的组件。
Preferences
路径:components/prefs
实现了Portlet属性偏好功能,提供将这些属性持久化到数据库的服务。
RDBMS
路径:components/rdbms
Jetspeed中所有与Apache OJB O/R Mapping框架有关的组建的基础组件。
Search
路径:components/search
提供整个门户资源的全文本搜索服务,具体实现依赖于Apache Lucene。
Security
路径:components/security
提供基于标准JAAS的认证服务,支持数据库和LDAP作为认证信息仓库。基于角色的授权服务,默认支持数据库作为权限仓库。
Single Sign-on
路径:components/sso
提供一个可扩展的单点登陆服务接口和一个简单的基于JAAS Subject的实现,该组件主要提供Portal门户与后台应用之间的单点登陆功能。
Statistics
路径:components/statistics
提供一个简单的访问请求统计服务的实现,支持将统计信息持久化到数据库。在Jetspeed-2管理界面中,还提供了专门的Portlet浏览这些统计信息。
回页首
总结
本文带读者浏览了Apache Portal项目组的所有成员,并着重介绍了Apache Jetspeed-2 Portal。希望能够使不了解门户技术的朋友对它有一个初步的认识,找到自己感兴趣的方向,继续深入研究;同时对那些试图在项目中使用开源软件的开发者,提供一些可以借鉴的信息。纵观目前开源软件中的门户实现,还没有哪一个社区能够提供像Apache Portal项目组这样完整的解决方案同时,还拥有如此友好的许可策略。只要去深入了解,开源软件往往能够给人们带来意外的惊喜。
参考资料