Liferay MinifierFilter的研究

 大家都知道,在Web应用程序中,为了节省网络开销,往往吧多个小的js文件整合成一个大的js文件,吧多个小的css文件整合成一个大的js文件,这样原本N次小文件的请求就可以合并成单次的网络请求。最典型的做这件事情的工具是大名鼎鼎的yui-compressor.

 

其实在Liferay中,我们为了达到合并css,js的目的,用了不同于yui-compressor的方法,这就是我们的主角 MinifierFilter.

 

既然是Filter,那么它肯定有filter-mapping,我们轻易的在liferay-web.xml中找到了Filter的定义和Filter的mapping.

  
  
  
  
  1. ... 
  2. <filter> 
  3.         <filter-name>Minifier Filter</filter-name> 
  4.         <filter-class>com.liferay.portal.servlet.filters.minifier.MinifierFilter</filter-class> 
  5.     </filter> 
  6. <filter> <filter-name>Minifier Filter - JSP</filter-name> <filter-class>com.liferay.portal.servlet.filters.minifier.MinifierFilter</filter-class> <init-param> <param-name>url-regex-pattern</param-name> <param-value>.+/(aui_lang|barebone|css|everything|main)\.jsp</param-value> </init-param> </filter>
  7.  
  8. ... 
  9. <filter-mapping> 
  10.         <filter-name>Minifier Filter</filter-name> 
  11.         <url-pattern>*.css</url-pattern> 
  12.     </filter-mapping> 
  13.     <filter-mapping> 
  14.         <filter-name>Minifier Filter</filter-name> 
  15.         <url-pattern>*.js</url-pattern> 
  16.     </filter-mapping> 
  17. <filter-mapping> <filter-name>Minifier Filter - JSP</filter-name> <url-pattern>*.jsp</url-pattern> </filter-mapping>
  18. ... 

所以,我们可以看到,当客户端对Liferay服务器上请求任意css或者javascript资源时候,都会被这个MinifierFilter所过滤,我们现在就来看下庐山真面目。

 

因为MinifierFilter最终实现了Filter接口,而doFilter方法在父类的父类BaseFilter中定义,这个doFilter仅仅是调用processFilter()方法,

  
  
  
  
  1. public void doFilter( 
  2.             ServletRequest servletRequest, ServletResponse servletResponse, 
  3.             FilterChain filterChain) 
  4.         throws IOException, ServletException { 
  5.  
  6.         try { 
  7.             HttpServletRequest request = (HttpServletRequest)servletRequest; 
  8.             HttpServletResponse response = (HttpServletResponse)servletResponse; 
  9.  
  10.             processFilter(request, response, filterChain); 
  11.         } 
  12.         catch (IOException ioe) { 
  13.             throw ioe; 
  14.         } 
  15.         catch (ServletException se) { 
  16.             throw se; 
  17.         } 

所以这就是我们的入口:

  
  
  
  
  1. protected void processFilter( 
  2.             HttpServletRequest request, HttpServletResponse response, 
  3.             FilterChain filterChain) 
  4.         throws Exception { 
  5.  
  6.         Object minifiedContent = getMinifiedContent( 
  7.             request, response, filterChain); 
  8.  
  9.         if (minifiedContent == null) { 
  10.             minifiedContent = getMinifiedBundleContent(request, response); 
  11.         } 
  12.  
  13.         if (minifiedContent == null) { 
  14.             processFilter(MinifierFilter.class, request, response, filterChain); 
  15.         } 
  16.         else { 
  17.             if (minifiedContent instanceof File) { 
  18.                 ServletResponseUtil.write(response, (File)minifiedContent); 
  19.             } 
  20.             else if (minifiedContent instanceof String) { 
  21.                 ServletResponseUtil.write(response, (String)minifiedContent); 
  22.             } 
  23.         } 
  24.     } 


首先,它会去执行06-07行的getMinifiedContent()方法,它会调用以下代码:

在getMinifiedContent()方法中,它会调用2个方法来分别最小化css和最小化js.

如下:

  
  
  
  
  1. protected Object getMinifiedContent( 
  2.             HttpServletRequest request, HttpServletResponse response, 
  3.             FilterChain filterChain) 
  4.         throws Exception { 
  5.  
  6.         .. 
  7.  
  8.        ..
  9.  
  10.         String minifiedContent = null
  11.  
  12.         if (realPath.endsWith(_CSS_EXTENSION)) { 
  13.             if (_log.isInfoEnabled()) { 
  14.                 _log.info("Minifying CSS " + file); 
  15.             } 
  16.  
  17.             minifiedContent = minifyCss(request, response, file); 
  18.  
  19.             response.setContentType(ContentTypes.TEXT_CSS); 
  20.  
  21.             FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS); 
  22.         } 
  23.         else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) { 
  24.             if (_log.isInfoEnabled()) { 
  25.                 _log.info("Minifying JavaScript " + file); 
  26.             } 
  27.  
  28.             minifiedContent = minifyJavaScript(file); 
  29.  
  30.             response.setContentType(ContentTypes.TEXT_JAVASCRIPT); 
  31.  
  32.             FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT); 
  33.         } 
  34.         else if (realPath.endsWith(_JSP_EXTENSION)) { 
  35.             if (_log.isInfoEnabled()) { 
  36.                 _log.info("Minifying JSP " + file); 
  37.             } 
  38.  
  39.             StringServletResponse stringResponse = new StringServletResponse( 
  40.                 response); 
  41.  
  42.             processFilter( 
  43.                 MinifierFilter.class, request, stringResponse, filterChain); 
  44.  
  45.             CacheResponseUtil.setHeaders(response, stringResponse.getHeaders()); 
  46.  
  47.             response.setContentType(stringResponse.getContentType()); 
  48.  
  49.             minifiedContent = stringResponse.getString(); 
  50.  
  51.             if (minifierType.equals("css")) { 
  52.                 minifiedContent = minifyCss( 
  53.                     request, response, realPath, minifiedContent); 
  54.             } 
  55.             else if (minifierType.equals("js")) { 
  56.                 minifiedContent = minifyJavaScript(minifiedContent); 
  57.             } 
  58.  
  59.             FileUtil.write( 
  60.                 cacheContentTypeFile, stringResponse.getContentType()); 
  61.         } 
  62.         else { 
  63.             return null
  64.         } 
  65.  
  66.         FileUtil.write(cacheDataFile, minifiedContent); 
  67.  
  68.         return minifiedContent; 
  69.     } 

 

minifyCSS:

从第12行可以看出,如果判断扩展名是.css,那么需要吧文件minify一下,并且设置content-type为text/css,最后把这个文件放入cacheContentTypeFile中。

我们来看下minifyCSS到底做了什么事情:

  
  
  
  
  1. protected String minifyCss( 
  2.             HttpServletRequest request, HttpServletResponse response, File file) 
  3.         throws IOException { 
  4.  
  5.         String content = FileUtil.read(file); 
  6.  
  7.         content = aggregateCss(file.getParent(), content); 
  8.  
  9.         return minifyCss(request, response, file.getAbsolutePath(), content); 
  10.     } 

从这里可以清楚的看出,

 

05行是先吧这个css文件的内容通过FileUtil读出来,其实这个FileUtil的读的方式会去除所有的换行,参见最终调用的FileImpl的read方法:

  
  
  
  
  1. public String read(File file, boolean raw) throws IOException { 
  2.         byte[] bytes = getBytes(file); 
  3.  
  4.         if (bytes == null) { 
  5.             return null
  6.         } 
  7.  
  8.         String s = new String(bytes, StringPool.UTF8); 
  9.  
  10.         if (raw) { 
  11.             return s; 
  12.         } 
  13.         else { 
  14.             return StringUtil.replace( 
  15.                 s, StringPool.RETURN_NEW_LINE, StringPool.NEW_LINE); 
  16.         } 
  17.     } 

然后把去除了所有换行的css文件的内容存入到变量content中。

 

07行会调用aggregateCSS来对这个css文件的内容做进一步处理,如何处理呢,我们看代码:

  
  
  
  
  1. public static String aggregateCss(String dir, String content) 
  2.         throws IOException { 
  3.  
  4.         StringBuilder sb = new StringBuilder(content.length()); 
  5.  
  6.         int pos = 0
  7.  
  8.         while (true) { 
  9.             int commentX = content.indexOf(_CSS_COMMENT_BEGIN, pos); 
  10.             int commentY = content.indexOf( 
  11.                 _CSS_COMMENT_END, commentX + _CSS_COMMENT_BEGIN.length()); 
  12.  
  13.             int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos); 
  14.             int importY = content.indexOf( 
  15.                 _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length()); 
  16.  
  17.             if ((importX == -1) || (importY == -1)) { 
  18.                 sb.append(content.substring(pos, content.length())); 
  19.  
  20.                 break
  21.             } 
  22.             else if ((commentX != -1) && (commentY != -1) && 
  23.                      (commentX < importX) && (commentY > importX)) { 
  24.  
  25.                 commentY += _CSS_COMMENT_END.length(); 
  26.  
  27.                 sb.append(content.substring(pos, commentY)); 
  28.  
  29.                 pos = commentY; 
  30.             } 
  31.             else { 
  32.                 sb.append(content.substring(pos, importX)); 
  33.  
  34.                 String importFileName = content.substring( 
  35.                     importX + _CSS_IMPORT_BEGIN.length(), importY); 
  36.  
  37.                 String importFullFileName = dir.concat(StringPool.SLASH).concat( 
  38.                     importFileName); 
  39.  
  40.                 String importContent = FileUtil.read(importFullFileName); 
  41.  
  42.                 if (importContent == null) { 
  43.                     if (_log.isWarnEnabled()) { 
  44.                         _log.warn( 
  45.                             "File " + importFullFileName + " does not exist"); 
  46.                     } 
  47.  
  48.                     importContent = StringPool.BLANK; 
  49.                 } 
  50.  
  51.                 String importDir = StringPool.BLANK; 
  52.  
  53.                 int slashPos = importFileName.lastIndexOf(CharPool.SLASH); 
  54.  
  55.                 if (slashPos != -1) { 
  56.                     importDir = StringPool.SLASH.concat( 
  57.                         importFileName.substring(0, slashPos + 1)); 
  58.                 } 
  59.  
  60.                 importContent = aggregateCss(dir + importDir, importContent); 
  61.  
  62.                 int importDepth = StringUtil.count( 
  63.                     importFileName, StringPool.SLASH); 
  64.  
  65.                 // LEP-7540 
  66.  
  67.                 String relativePath = StringPool.BLANK; 
  68.  
  69.                 for (int i = 0; i < importDepth; i++) { 
  70.                     relativePath += "../"
  71.                 } 
  72.  
  73.                 importContent = StringUtil.replace( 
  74.                     importContent, 
  75.                     new String[] { 
  76.                         "url('" + relativePath, 
  77.                         "url(\"" + relativePath, 
  78.                         "url(" + relativePath 
  79.                     }, 
  80.                     new String[] { 
  81.                         "url('[$TEMP_RELATIVE_PATH$]"
  82.                         "url(\"[$TEMP_RELATIVE_PATH$]"
  83.                         "url([$TEMP_RELATIVE_PATH$]" 
  84.                     }); 
  85.  
  86.                 importContent = StringUtil.replace( 
  87.                     importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK); 
  88.  
  89.                 sb.append(importContent); 
  90.  
  91.                 pos = importY + _CSS_IMPORT_END.length(); 
  92.             } 
  93.         } 
  94.  
  95.         return sb.toString(); 
  96.     } 

其实这段代码非常简单,它就是找出页面上所有的css注释 /* */,然后把这些注释去除,然后找出页面上@import(url=)的这种外部css文件,递归的调用aggregateCSS直到他们不含有外部引入标记,然后把这些文件的内容(已经被去除了注释,换行符等)插入到引入它们的css文件中。

 

minifyJavaScript:

从第23行可以看出,当遇到文件扩展名是.js时,它就会调用minifyJavaScript方法来最小化这个js文件,并且设置content-type为text/javascript,最后把minify之后的文件存入cacheContentTypeFile。

我们来看下minifyJavaScript到底做了什么事情:

  
  
  
  
  1. protected String minifyJavaScript(File file) throws IOException { 
  2.         String content = FileUtil.read(file); 
  3.  
  4.         return minifyJavaScript(content); 
  5.     } 

它首先还是利用FileUtil来去除换行(见minifyCSS部分对这个方法的讲解),然后对于已经没有换行符的js文件继续调用minifyJavaScript():

  
  
  
  
  1. protected String minifyJavaScript(String content) { 
  2.         return MinifierUtil.minifyJavaScript(content); 
  3.     } 

它又去调用MinifierUtil工具类方法来完成任务,最终执行任务的是MinifierUtil的_minifyJavaScript方法:

  
  
  
  
  1. private String _minifyJavaScript(String content) { 
  2.         UnsyncStringWriter unsyncStringWriter = new UnsyncStringWriter(); 
  3.  
  4.         try { 
  5.             JavaScriptCompressor javaScriptCompressor = 
  6.                 new JavaScriptCompressor( 
  7.                     new UnsyncStringReader(content), 
  8.                     new JavaScriptErrorReporter()); 
  9.  
  10.             javaScriptCompressor.compress( 
  11.                     unsyncStringWriter, _JS_LINE_BREAK, _JS_MUNGE, _JS_VERBOSE, 
  12.                     _JS_PRESERVE_ALL_SEMICOLONS, _JS_DISABLE_OPTIMIZATIONS); 
  13.         } 
  14.         catch (Exception e) { 
  15.             _log.error("JavaScript Minifier failed for\n" + content); 
  16.  
  17.             unsyncStringWriter.append(content); 
  18.         } 
  19.  
  20.         return unsyncStringWriter.toString(); 
  21.     } 

它会先创建一个JavaScriptCompressor对象,然后用它来压缩没有换行符的js文件,采用的方式是和yahoo的yui-compressor一样的方式,算法很复杂,没必要一行行看了。

 

minifyJSP:

从34行可以看到,它会先判断是jsp扩展名,当然了, 它也不会对所有的jsp都生效,它生效的jsp文件都在liferay-web.xml中的这个filter的<init-param>中,具体的就是

barebone.jsp,everything.jsp等因为满足init-param的正则表达式的pattern,所以会通过这个过滤器。从第47-56行可以看出他会吧原来cache的所有被minify处理过的css或者js内容再minify一下然后写入String变量,然后第59-60行利用FileUtil进一步去除换行符,然后把cacheContentFile的内容以最终请求的MIME格式来复写一遍。

 

当我们在文章一开始的MinifierFilter的processFilter()方法中执行了所有的getMinifiedContent调用后:

  
  
  
  
  1. Object minifiedContent = getMinifiedContent(  
  2.            request, response, filterChain);  

此时,这个Object minifiedContent的内容就不是null了。

 

现在我们来执行MinifierFilter的最后2个语句,它可以判断这个minifiedContent是个文件还是字符串,从而让其写入ServletResponse对象中并且返回给客户端,写入的方式是调用ServletResponseUtil工具类:

  
  
  
  
  1. else {  
  2.           if (minifiedContent instanceof File) {  
  3.               ServletResponseUtil.write(response, (File)minifiedContent);  
  4.           }  
  5.           else if (minifiedContent instanceof String) {  
  6.               ServletResponseUtil.write(response, (String)minifiedContent);  
  7.           }  
  8.       }  

 

由此大功告成,我们所有的css,js资源文件都得到了最小化,然后整合成单个文件.

 

 

高级话题:barebone.jsp和everything.jsp

事实上,Liferay启用了2个配置,一个只引入最少最需要的js文件,最终组合为barebone.jsp,一个是引入所有的js文件,最终组合为everything.jsp,他们可以自由切换,切换代码在top_js.jspf中:

  
  
  
  
  1. <c:choose> 
  2.     <c:when test="<%= themeDisplay.isThemeJsFastLoad() %>"> 
  3.         <c:choose> 
  4.             <c:when test="<%= themeDisplay.isThemeJsBarebone() %>"> 
  5.                 <script src="<%= HtmlUtil.escape(PortalUtil.getStaticResourceURL(request, themeDisplay.getPathJavaScript() + "/barebone.jsp", "minifierBundleId=javascript.barebone.files", javaScriptLastModified)) %>type="text/javascript"></script> 
  6.             </c:when> 
  7.             <c:otherwise> 
  8.                 <script src="<%= HtmlUtil.escape(PortalUtil.getStaticResourceURL(request, themeDisplay.getPathJavaScript() + "/everything.jsp", "minifierBundleId=javascript.everything.files", javaScriptLastModified)) %>type="text/javascript"></script> 
  9.             </c:otherwise> 
  10.         </c:choose> 
  11.     </c:when> 
  12.     <c:otherwise> 

这里可以看出来,切换主要去判断themeDisplay.isThemeJSBarebone,而这个配置在portal.properties中,比如我们服务器设置了javascript.barebone.enabled=true,则开启了barebone,则最后看情况可以有barebone.jsp可以有everything.jsp:

  
  
  
  
  1.     # Set this property to false to always load JavaScript files listed in the 
  2.     # property "javascript.everything.files"Set this to true to sometimes 
  3.     # load "javascript.barebone.files" and sometimes load 
  4.     # "javascript.everything.files"
  5.     # 
  6.     # The default logic is coded in com.liferay.portal.events.ServicePreAction 
  7.     # in such a way that unauthenticated users get the list of barebone 
  8.     # JavaScript files whereas authenticated users get both the list of barebone 
  9.     # JavaScript files and the list of everything JavaScript files. 
  10.     # 
  11.     javascript.barebone.enabled=true 

 

无论是barebone.jsp还是everything.jsp,他们的bundleId和读取目录都是预先是定好的:

  
  
  
  
  1.   # Input a list of comma delimited properties that are valid bundle ids for 
  2.   # the JavaScript minifier. 
  3.   # 
  4.   javascript.bundle.ids=\ 
  5.       javascript.barebone.files,\ 
  6.       javascript.everything.files 
  7.  
  8.   # 
  9.   # Define a bundle directory for each property listed in 
  10.   # "javascript.bundle.ids"
  11.   # 
  12.   javascript.bundle.dir[javascript.barebone.files]=/html/js 
  13.   javascript.bundle.dir[javascript.everything.files]=/html/js 
  14.  
  15.   # 

 

只不过barebone.jsp合并的js文件少,而everything.jsp文件合并全部的js文件:

barebone.jsp合并并且最小化哪些js文件呢?这也可以从portal.properties文件中找到答案:

  
  
  
  
  1. javascript.barebone.files=\ 
  2.        \ 
  3.        # 
  4.        # YUI core 
  5.        # 
  6.        \ 
  7.        aui/yui/yui.js,\ 
  8.        \ 
  9.        # 
  10.        # YUI modules 
  11.        # 
  12.        \ 
  13.        aui/anim-base/anim-base.js,\ 
  14.        aui/anim-color/anim-color.js,\ 
  15.        aui/anim-curve/anim-curve.js,\ 
  16. ... 

 

而everything.jsp合并并且最小化哪些js文件呢?它是由javascript.barebone.files 包含的所有js文件,外加如下列表的不在barebone中的文件:

  
  
  
  
  1.     # Specify the list of everything files (everything else not already in the 
  2.     # list of barebone files). 
  3.     # 
  4.     javascript.everything.files=\ 
  5.         \ 
  6.         # 
  7.         # YUI modules 
  8.         # 
  9.         \ 
  10.         aui/async-queue/async-queue.js,\ 
  11.         aui/cookie/cookie.js,\ 
  12.         aui/event-touch/event-touch.js,\ 
  13.         aui/querystring-stringify/querystring-stringify.js,\ 
  14.         \ 
  15.         # 
  16.         # Alloy modules 
  17.         # 
  18.         \ 
  19.         aui/aui-io/aui-io-plugin.js,\ 
  20.         aui/aui-io/aui-io-request.js,\ 
  21.         aui/aui-loading-mask/aui-loading-mask.js,\ 
  22.         aui/aui-parse-content/aui-parse-content.js,\ 
  23.         \ 
  24.         # 
  25.         # Liferay modules 
  26.         # 
  27.         \ 
  28.         liferay/address.js,\ 
  29.         liferay/dockbar.js,\ 
  30.         liferay/layout_configuration.js,\ 
  31.         liferay/layout_exporter.js,\ 
  32.         liferay/session.js,\ 
  33.         \ 
  34.         # 
  35.         # Deprecated JS 
  36.         # 
  37.         \ 
  38.         liferay/deprecated.js 

 

 

 

这样一分析下来,整个Liferay框架的静态资源加载文件就非常清晰了。

你可能感兴趣的:(liferay,MinifierFilter)