引入:
大家在Portlet 开发中经常用到<portlet:resourceURL>,而大体上都会去调用相应的serveResource()方法,这个过程虽然大家都清楚,但是能弄明白这个过程细节的,我相信全世界不超过100人,至少我去年就这个疑惑问了我们客户的liferay专家,她不能解释。后来去年团队里Danny问过我这个问题,我当时研究了一阵也走不通,所以一直搁置了。而现在,当我花了前面几天时间去研究了下liferay部署war包的细节后,我突然发现,这个问题我完全明白了。
调试分析:
其实,根据上文http://supercharles888.blog.51cto.com/609344/1286976的结论,在我们部署war包应用时候,对应的我们在war包中的xml文件并不是机械的复制到了webapps下面应用的部署目录,而是对于其中的xml文件进行了拆分和加内容。从上述文字结论我们知道,web.xml被添加了很多额外内容,然后被拆分为2个文件,1个是portal-web.xml文件,它包含了所有的除(Invoker Filter)以外的过滤器的定义,另外一个是web.xml,它添加了不少内容,而最重要的是,它会在web.xml中添加一段PortletServlet的定义。
所以,我们到服务器的webapps上面看下我们的应用部署目录下的web.xml,
发现它不再是原来的那个web.xml了,它有PortletServlet的定义(这里出于security考虑,我吧包名最前面部分去掉了):
所以,这个Portlet相当于一个桥接,它把本来隶属于Portal的一个个的Portlet地位提升上去,提升到一个一个Servlet,这样他们就可以独立的负责响应各种请求了。
而这个Servlet的mapping是:
现在,当我们页面上有个Search按钮。点击会触发如下的<portlet:resourceURL>:
我们可以看到,这个<portlet:resourceURL>标记会被liferay-portlet.tld所识别:
所以最终处理这个标记的类是ResourceURLTag和ResourceURLTei.
它会最终被处理类转为一个请求url:这个请求 url是:
撇开细节,最后因为它的请求url满足/logearchportlet/*这个url模式,
(你肯定会问,这个http://172.29.175.236:8080/web/guest/log-search?......这个url明显不匹配PortletServlet的模式/logsearchportlet/*嘛,因为PortletServlet的模式如下图所示:
那么,我们的请求url是如何进的这个portlet呢?关于这点,我想了整整2天,才想明白,我在后面的精华疑点解答中会提到)
所以它最终会走到PortletServlet方法中。
首先在第64行中从HttpServletRequest获得portletId:
然后在第66-70行分别从HttpServletRequest/Response中获取 PortletRequest和 PortletResponse对象,然后在第72行获取当前请求对应的请求的生命周期阶段LIFECYCLE_PHASE:
(疑点2:这里为什么portletRequest,portletResponse,lifecycle信息都在HttpServletRequest中,何时设置上去的,关于这个问题,参见精华疑点解答)
然后从第78-90行从portletRequest中PortletSession对象,并把PortalSession关联到PortletSession中。
然后第40行调用PortletUtilFilter.doFilter()方法:
它会根据当前lifecycle的值来判断吧PortletRequest转为何种请求类型:
因为我们从调试信息中看出,当前lifecycle是“RESOURCE_PHASE",所以它会吧PortletRequest转为ResourceRequest.然后在第71行继续调用filterChain的doFilter方法。
这次,它会去先把我们的portlet转为ResourceServingPortlet,这里是我们的LogSearchPortlet,再调用我们的LogSearchPortlet的serveResource方法:
而所有的我们在构造<portlet:resourceURL>时候附带的参数都会被封装在ResourceRequest
从以下截图中可以看出,<portlet:resourceURL>中的所有参数都会被添加到ResourceRequest中,一个不少:
而我们在portlet中的代码已经实现了serveResource方法,所以就可以正确的调用执行了。
精华疑点解答1:
我们的请求url:http://172.29.175.236:8080/web-guest/logsearch?.....是如何进入我们PortletServlet的url-pattern /logsearchportlet/*的。
这个问题很复杂,但是我们可以猜想,肯定在请求送达PortletServlet之前进行了若干预处理。我们知道,过滤器总是在Servlet之前执行的,而我们的请求,刚好可以符合 Invoker Filter的url-mapping .
而这个InvokerFilter,如果熟悉它的代码,会发现它其实会去按照每个Filter链上Filter的定义,依次去调用各个Filter的doFilter方法,当然了,这些Filter根据我们以前的研究内容,都是定义在liferay-web.xml中。
看到第一次,请求是/web/guest/logsearch,果然和我们的匹配,然后它会走一些filter,最后调用invokerFilterChain.doFilter(servletRequest,servletResponse).
第二次,我们进入这个方法时候,请求就变了,变为/c/portal/layout.
这里省略很多不重要步骤,因为我们在struts-config.xml中定义了/c/portal/layout的action-mapping,如下:
所以,会走到LayoutAction的execute()方法中:
它会在第244行调用重载的processLayout()方法:
然后在第663-665行它会去调用processPortletRequest方法:
跳过漫长的一段代码(和我们研究重点无关的代码),最终它在porcessPortletRequest的第899行,判断lifecycle是”RESOURCE_PHASE",所以进入这个分支:
再跳过N多不相关行,看到最后它会通过ServletRequest,ServletResponse 构造ResourceRequestImpl和ResourceResponseImpl对象,并且第936行通过ResourceRequestImpl创建并且封装一个ServiceContext对象,看右边的调试信息可以看到我们的请求url是封装在这个ServiceContext对象的。(_currentURL属性),然后我们把这个ServiceContext对象加到ThreadLocal列表中。
最后,在第941行调用InvokerPortlet的serveResource方法,它会最终调用InvokerPortletImpl的invoke()方法:
而我们访问invoke()方法时候,谜底终于揭开了,都给我睁大眼睛看清楚了:
原来,它会在第610行通过PortletConfigImpl获取Portlet的名字,我们获得是"logsearchportlet",然后把它拼接到后面的/invoke字符串就得到了这个path 为"/logsearchportlet/invoke",然后它创建一个RequestDispatcher对象用于转发请求,最后,如下图所示:
它会吧请求转发到path指定的/logsearchportlet/invoke, 而这个请求url显然是匹配/logsearchportlet/*的,所以就可以正确的进入到 PortletServlet了,于是这个问题得到圆满解决。
精华疑点解答2:
在PortletServlet服务于当前请求中的service方法中,为什么portletRequest,portletResponse,lifecycle信息都在HttpServletRequest中?
当我们分析完刚才整个过程中时,这个问题也迎刃而解了,参见InvokerPortletImpl的invoke()方法的第623行到第626行,
在创建好RequestDipatcher对象后,但是还没转发请求到/logsearchportlet/invoke 之前,它会去先获得HttpServletRequest对象,并且依次吧JAVAX_PORTLET_PORTLET,LIFECYCLE_PHASE,PORTLET_SERVLET_FILTER_CHAIN存入,这样在PortletServlet的service()方法中就可以正确取出这些信息并且处理了。
总结:
结束这文章时候,我真是非常开心,其实这个问题我已经想了半年没想通,不过今天终于想通了。本来这个问题是我们团队一个叫Danny的问我了,我当时研究了没解决,后来我在Liferay官网上挂了几个月没人能解答,真开心我还是靠自己的实力解决了。
(1)页面上用<portlet:resourceURL>对应的请求url最终会被映射到PortletServlet中进行处理,这个目的是吧 Portlet的对于请求处理能力的地位提升到Servlet级别,因为它现在可以接受 HttpServletRequest类型的请求了,这个PortletServlet会先从HttpServletRequest中获得portletId,portletRequest,portletResponse和lifecycle信息,然后根据lifecycle阶段信息,相应的吧PortletRequest转为何种请求类型,比如如果lifecycle是RESOURCE_PHASE,那么它会吧portletRequest转为ResourceRequest,它包含了<portlet:resourceURL>中所有附带参数。然后在doFilter方法中,它吧我们的portlet转为ResourceServingPortlet 并且调用serveResource()方法,于是可以就可以正确的调用我们在portlet应用层面定义的serveResource()方法了。
(2)这个PortletServlet并不是开始就定义在我们的项目打的war包中的,而是在部署war包到liferay部署目录后,liferay框架自己添加的一段代码,具体细节见上一篇文章:http://supercharles888.blog.51cto.com/609344/1286976
(3)但是最重要的一点是,我们的页面的<portlet:actionURL>并不直接对应到PortletServlet的url-mapping中,这也是困扰我半年多的问题。其实,它是先走到InvokerFilter中,然后在执行/c/portal/layout时候,它会走到struts框架,然后经过一系列漫长的调用,最终在InvokerPortletImpl的invoke()方法中得到解决了,它会生成一个新path类似 /<portlet-name>/invoke, 然后把所有portlet相关信息(包括portlet,lifecycle,filterchain)添加到HttpServletRequest对象中,并且新建一个RequestDispatcher吧当前请求转发到刚才的新path中,这样就可以让请求去匹配PortletServlet的url-pattern并且进入PortletSevlet了。