本文介绍如何以外部war应用的方式向liferay portal集成portlet, 还是以sample-jsp-portlet为例来说明。
一,portlet如何被liferay portal调用?
在web-xml定义中,可以看到这样的一个servlet定义:
〈servlet〉
〈servlet-name〉sample_jsp_portlet〈/servlet-name〉
〈servlet-class〉com.liferay.portal.kernel.servlet.PortletServlet〈/servlet-class〉
〈init-param〉
〈param-name〉portlet-class〈/param-name〉
〈param-value〉com.sample.jsp.portlet.JSPPortlet〈/param-value〉
〈/init-param〉
〈load-on-startup〉0〈/load-on-startup〉
〈/servlet〉
〈servlet-mapping〉
〈servlet-name〉sample_jsp_portlet〈/servlet-name〉
〈url-pattern〉/sample_jsp_portlet/*〈/url-pattern〉
〈/servlet-mapping〉
在[Liferay中action的处理流程]一文中,分析了当portlet是一个war时,liferay portal将转发请求到war应用,代码如下:
CachePortlet
private void _invoke(
PortletRequest req, PortletResponse res, boolean action)
throws IOException, PortletException {
if (_portletConfig.isWARFile()) {
String path =
StringPool.SLASH + _portletConfig.getPortletName() + "/invoke";
RequestDispatcher rd =
_portletCtx.getServletContext().getRequestDispatcher(path);
// 一些初始化代码....
httpReq.setAttribute(WebKeys.JAVAX_PORTLET_PORTLET, _portlet);
rd.include(httpReq, httpRes);
}
else {
// 略...
}
}
在本文中portletName为sample_jsp_portlet,则path为/sample_jsp_portlet/invoke,这个url对应的servlet就是PortletServlet了,
这样liferay portlet就将请求转发到了sample-jsp-portlet应用。
这里要注意的是servlet-name与url-pattern的名称一定要保持一致!
// PortletServlet
public class PortletServlet extends HttpServlet {
public static final String JAVAX_PORTLET_PORTLET = "javax.portlet.portlet";
public static final String JAVAX_PORTLET_REQUEST = "javax.portlet.request";
public static final String JAVAX_PORTLET_RESPONSE = "javax.portlet.response";
public static final String PORTLET_CLASS_LOADER = "PORTLET_CLASS_LOADER";
public void service(HttpServletRequest req, HttpServletResponse res)
throws IOException, ServletException {
PortletRequest portletReq =
(PortletRequest)req.getAttribute(JAVAX_PORTLET_REQUEST);
PortletResponse portletRes =
(PortletResponse)req.getAttribute(JAVAX_PORTLET_RESPONSE);
Portlet portlet = (Portlet)req.getAttribute(JAVAX_PORTLET_PORTLET);
LiferayPortletSession portletSes =
(LiferayPortletSession)portletReq.getPortletSession();
portletSes.setHttpSession(req.getSession());
if (portletReq instanceof ActionRequest) {
ActionRequest actionReq = (ActionRequest)portletReq;
ActionResponse actionRes = (ActionResponse)portletRes;
portlet.processAction(actionReq, actionRes);
}
else {
RenderRequest renderReq = (RenderRequest)portletReq;
RenderResponse renderRes = (RenderResponse)portletRes;
portlet.render(renderReq, renderRes);
}
}
}
PortletServlet很简单,它根据PortletRequest的类型来决定是让portlet执行processAction还是render,
portlet由servlet定义的init-param参数指定,它必须继承自javax.portlet.GenericPortlet类, 这是jsr168规范所指定的,
本文中为com.sample.jsp.portlet.JSPPortlet。
// JSPPortlet.
public class JSPPortlet extends GenericPortlet {
public void init() throws PortletException {
editJSP = getInitParameter("edit-jsp");
helpJSP = getInitParameter("help-jsp");
viewJSP = getInitParameter("view-jsp");
}
public void doDispatch(RenderRequest req, RenderResponse res)
throws IOException, PortletException {
// 略...
}
public void doEdit(RenderRequest req, RenderResponse res)
throws IOException, PortletException {
// 略...
}
public void doHelp(RenderRequest req, RenderResponse res)
throws IOException, PortletException {
include(helpJSP, req, res);
}
public void doView(RenderRequest req, RenderResponse res)
throws IOException, PortletException {
include(viewJSP, req, res);
}
public void processAction(ActionRequest req, ActionResponse res)
throws IOException, PortletException {
}
protected void include(String path, RenderRequest req, RenderResponse res)
throws IOException, PortletException {
PortletRequestDispatcher prd =
getPortletContext().getRequestDispatcher(path);
if (prd == null) {
_log.error(path + " is not a valid include");
}
else {
prd.include(req, res);
}
}
}
这里一个很简单的portlet实现,它直接包含jsp文件。
二,portlet中的预定义对象.
portlet 页面上能使用的对象.
首先在portlet页面上加入如下tag定义,
DefineObjectsTag.doStartTag() {
ClassLoader contextClassLoader =
Thread.currentThread().getContextClassLoader();
try {
// 切换ClassLoader,对于采用War方式的portlet,这是必须的?
Thread.currentThread().setContextClassLoader(
PortalClassLoaderUtil.getClassLoader());
MethodWrapper methodWrapper = new MethodWrapper(
_TAG_CLASS, _TAG_DO_START_METHOD, new Object[] {pageContext});
MethodInvoker.invoke(methodWrapper);
}
catch (Exception e) {
throw new JspException(e);
}
finally {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
return SKIP_BODY;
}
private static final String _TAG_CLASS =
"com.liferay.portlet.DefineObjectsTagUtil";
private static final String _TAG_DO_START_METHOD = "doStartTag";
DefineObjectsTag.doStartTag(PageContext pageContext) {
ServletRequest req = pageContext.getRequest();
PortletConfigImpl portletConfig =
(PortletConfigImpl)req.getAttribute(WebKeys.JAVAX_PORTLET_CONFIG);
if (portletConfig != null) {
pageContext.setAttribute("portletConfig", portletConfig);
pageContext.setAttribute(
"portletName", portletConfig.getPortletName());
}
RenderRequest renderRequest =
(RenderRequest)req.getAttribute(WebKeys.JAVAX_PORTLET_REQUEST);
if (renderRequest != null) {
pageContext.setAttribute("renderRequest", renderRequest);
pageContext.setAttribute(
"portletPreferences", renderRequest.getPreferences());
pageContext.setAttribute(
"portletSession", renderRequest.getPortletSession());
}
RenderResponse renderResponse =
(RenderResponse)req.getAttribute(WebKeys.JAVAX_PORTLET_RESPONSE);
if (renderResponse != null) {
pageContext.setAttribute("renderResponse", renderResponse);
}
if (portletConfig.isWARFile() && ServerDetector.isWebLogic()) {
PortletSessionImpl portletSession =
(PortletSessionImpl)renderRequest.getPortletSession();
PortletSessionPool.put(
portletSession.getPortalSessionId(), pageContext.getSession());
}
}
从上面的代码可以看出,在portlet的页面上可以直接使用的对象有:
portletConfig // portlet配置.
portletName // portlet名称.
renderRequest // portlet request.
portletPreferences // portlet 首选项.
portletSession // portlet 会话.
renderResponse // portlet response.
三,portlets如何部署到liferay portal中?
在portlets应用的web.xml中加入如下listener定义:
加入了此listener后,当portlets应用启动时,将会向liferay portal注册此应用中的所有定义的portlet;
当portlets应用停止时,会向liferay portal取消所有定义的portlet, portlet定义当然是由portlet.xml文件给出。
PortletContextListener实现了ServletContextListener接口.
public class PortletContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
HotDeployUtil.fireDeployEvent(
new HotDeployEvent(
sce.getServletContext(),
Thread.currentThread().getContextClassLoader()));
}
public void contextDestroyed(ServletContextEvent sce) {
HotDeployUtil.fireUndeployEvent(
new HotDeployEvent(
sce.getServletContext(),
Thread.currentThread().getContextClassLoader()));
}
}
上面的两个方法直接调用HotDeployUtil的fireDeployEvent和fireUndeployEvent方法,HotDeployUtil是Liferay portal的热部署工具类,
下面来着重看看fireDeployEvent是如何执行portlet注册的,
public class HotDeployUtil {
// 构造一个实例,singlton模式.
private static HotDeployUtil _instance = new HotDeployUtil();
// 触发部署事件,通知HotDeployListener进行部署。
private synchronized void _fireDeployEvent(HotDeployEvent event) {
// 当_events不为空时,说明liferay portal还没启动,此时只是简单的保存event.
if (_events != null) {
_events.add(event);
return;
}
Iterator itr = _listeners.iterator();
// 遍历HotDeployListener,通知它们进行部署.
while (itr.hasNext()) {
HotDeployListener listener = (HotDeployListener)itr.next();
try {
listener.invokeDeploy(event);
} catch (HotDeployException hde) {
_log.error(StackTraceUtil.getStackTrace(hde));
}
}
}
// 在liferay portal启动后被调用,此设计的目的主要用于解决portals应用在liferay portal应用之前启动的情况。
private synchronized void _flushEvents() {
for (int i = 0; i < _events.size(); i++) {
HotDeployEvent event = (HotDeployEvent)_events.get(i);
Iterator itr = _listeners.iterator();
while (itr.hasNext()) {
HotDeployListener listener = (HotDeployListener)itr.next();
try {
listener.invokeDeploy(event);
}
catch (HotDeployException hde) {
_log.error(StackTraceUtil.getStackTrace(hde));
}
}
}
_events = null;
}
// 注册HotDeployListener
private void _registerListener(HotDeployListener listener) {
_listeners.add(listener);
}
private List _listeners;
private List _events;
}
这里就有个问题了,HotDeployListener是在哪里加入进来的了?答案是在liferay portal的初始化时加入的。
// liferay portal初始化
public class MainServlet extends ActionServlet {
public void init() {
// 其它初始化处理......
try {
// 执行全局启动事件.
EventsProcessor.process(PropsUtil.getArray(
PropsUtil.GLOBAL_STARTUP_EVENTS), true);
// 执行应用程序启动事件.
EventsProcessor.process(PropsUtil.getArray(
PropsUtil.APPLICATION_STARTUP_EVENTS),
new String[] {_companyId});
}
catch (Exception e) {
_log.error(e);
}
PortalInstances.init(_companyId);
}
}
EventsProcessor是liferay portal中的事件处理器,liferay portal中定义了一系列的事件点,
像上面的APPLICATION_STARTUP_EVENTS,就是应用程序启动事件点.
通过配置文件,就可以将我们的自定义事件加入到这些事件点里去执行了。
关于EventProcessor更详细的内容,将用另外一篇文章来详细介绍,这里只需要知道有两个事件被触发了。
// 全局启动事件。
public class GlobalStartupAction extends SimpleAction {
public void run(String[] ids) throws ActionException {
// JCR 处理...
// Portal initable
PortalInitableUtil.flushInitables();
// Hot deploy
// 注册layoutTemplate监听器.
HotDeployUtil.registerListener(new HotDeployLayoutTemplateListener());
// 注册portlet监听器.
HotDeployUtil.registerListener(new HotDeployPortletListener());
// 注册theme监听器.
HotDeployUtil.registerListener(new HotDeployThemeListener());
// 清理所有事件.
HotDeployUtil.flushEvents();
// Auto deploy 自动部署处理.....
}
// 部署Portlets
public class HotDeployPortletListener implements HotDeployListener {
public void invokeDeploy(HotDeployEvent event) throws HotDeployException {
String servletContextName = null;
try {
// Servlet context
ServletContext ctx = event.getServletContext();
servletContextName = ctx.getServletContextName();
// Company ids
String[] companyIds = StringUtil.split(
ctx.getInitParameter("company_id"));
if ((companyIds.length == 1) && (companyIds[0].equals("*"))) {
companyIds = PortalInstances.getCompanyIds();
}
// Initialize portlets, 初始化portlets
String[] xmls = new String[] {
Http.URLtoString(ctx.getResource("/WEB-INF/portlet.xml")),
Http.URLtoString(ctx.getResource(
"/WEB-INF/liferay-portlet.xml")),
Http.URLtoString(ctx.getResource("/WEB-INF/web.xml"))
};
List portlets = PortletLocalServiceUtil.initWAR(
servletContextName, xmls);
// Class loader, 类装载器
ClassLoader portletClassLoader = event.getContextClassLoader();
ctx.setAttribute(
PortletServlet.PORTLET_CLASS_LOADER, portletClassLoader);
// portlet context wrapper, portlet上下文包装器.
boolean strutsBridges = false;
Iterator itr1 = portlets.iterator();
while (itr1.hasNext()) {
Portlet portlet = (Portlet)itr1.next();
// 构造portlet实例.
Class portletClass = portletClassLoader.loadClass(
portlet.getPortletClass());
javax.portlet.Portlet portletInstance =
(javax.portlet.Portlet)portletClass.newInstance();
// 检查是否struts桥接portlet.
if (ClassUtil.isSubclass(portletClass,
StrutsPortlet.class.getName())) {
strutsBridges = true;
}
// 构造lucene indexer实例.
Indexer indexerInstance = null;
if (Validator.isNotNull(portlet.getIndexerClass())) {
indexerInstance = (Indexer)portletClassLoader.loadClass(
portlet.getIndexerClass()).newInstance();
}
// 构造scheduler实例.
Scheduler schedulerInstance = null;
if (Validator.isNotNull(portlet.getSchedulerClass())) {
schedulerInstance = (Scheduler)portletClassLoader.loadClass(
portlet.getSchedulerClass()).newInstance();
}
// 验证preferences.
PreferencesValidator prefsValidator = null;
if (Validator.isNotNull(portlet.getPreferencesValidator())) {
prefsValidator =
(PreferencesValidator)portletClassLoader.loadClass(
portlet.getPreferencesValidator()).newInstance();
try {
if (GetterUtil.getBoolean(PropsUtil.get(
PropsUtil.PREFERENCE_VALIDATE_ON_STARTUP))) {
prefsValidator.validate(
PortletPreferencesSerializer.fromDefaultXML(
portlet.getDefaultPreferences()));
}
}
catch (Exception e1) {
_log.warn(
"Portlet with the name " + portlet.getPortletId() +
" does not have valid default preferences");
}
}
// 资源处理.
Map resourceBundles = null;
if (Validator.isNotNull(portlet.getResourceBundle())) {
resourceBundles = CollectionFactory.getHashMap();
Iterator itr2 = portlet.getSupportedLocales().iterator();
while (itr2.hasNext()) {
String supportedLocale = (String)itr2.next();
Locale locale = new Locale(supportedLocale);
try {
ResourceBundle resourceBundle =
ResourceBundle.getBundle(
portlet.getResourceBundle(), locale,
portletClassLoader);
resourceBundles.put(
locale.getLanguage(), resourceBundle);
}
catch (MissingResourceException mre) {
_log.warn(mre.getMessage());
}
}
}
// 定制用户特性。
Map customUserAttributes = CollectionFactory.getHashMap();
Iterator itr2 =
portlet.getCustomUserAttributes().entrySet().iterator();
while (itr2.hasNext()) {
Map.Entry entry = (Map.Entry)itr2.next();
String attrCustomClass = (String)entry.getValue();
customUserAttributes.put(
attrCustomClass,
portletClassLoader.loadClass(
attrCustomClass).newInstance());
}
PortletContextWrapper pcw = new PortletContextWrapper(
portlet.getPortletId(), ctx, portletInstance,
indexerInstance, schedulerInstance, prefsValidator,
resourceBundles, customUserAttributes);
PortletContextPool.put(portlet.getPortletId(), pcw);
}
// Struts bridges
if (strutsBridges) {
ctx.setAttribute(
ServletContextProvider.STRUTS_BRIDGES_CONTEXT_PROVIDER,
new LiferayServletContextProvider());
}
// Portlet display,portlet展示处理,显示在portlet添加面板上位置.
String xml = Http.URLtoString(ctx.getResource(
"/WEB-INF/liferay-display.xml"));
PortletCategory newPortletCategory =
PortletLocalServiceUtil.getWARDisplay(servletContextName, xml);
for (int i = 0; i < companyIds.length; i++) {
String companyId = companyIds[i];
PortletCategory portletCategory =
(PortletCategory)WebAppPool.get(
companyId, WebKeys.PORTLET_CATEGORY);
if (portletCategory != null) {
portletCategory.merge(newPortletCategory);
}
else {
_log.error(
"Unable to register portlet for company " + companyId +
" because it does not exist");
}
}
// Variables
_vars.put(
servletContextName, new ObjectValuePair(companyIds, portlets));
if (_log.isInfoEnabled()) {
_log.info(
"Portlets for " + servletContextName +
" registered successfully");
}
}
catch (Exception e2) {
throw new HotDeployException(
"Error registering portlets for " + servletContextName, e2);
}
}
}
关于layoutTemplates和theme的部署这里就不列出了,请自行参考源代码。