Liferay中使用portlet:resourceURL触发serveResource()方法调用的细节
引入:
大家在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了。
轉載于:https://blog.51cto.com/supercharles888/1287188
總結
以上是生活随笔為你收集整理的Liferay中使用portlet:resourceURL触发serveResource()方法调用的细节的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android中的BitMap(二)从网
- 下一篇: Magento安装后无法访问