背景知识:
最近项目组遇到一个问题就是改了一个new theme之后导致某些css文件不起作用了,这也激起了我的好奇心,让我有机会去研究下Liferay Dynamic CSS Filter的原理。
引入:
这个Filter 和一般的Filter一样,会配置在portal-web.xml中,并且声明了对于.css文件和.jsp资源文件请求时候会触发:
然后执行它的processFilter 方法:
protected void processFilter( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { Object parsedContent = getDynamicContent( request, response, filterChain); if (parsedContent == null) { processFilter( DynamicCSSFilter.class, request, response, filterChain); } else { if (parsedContent instanceof File) { ServletResponseUtil.write(response, (File)parsedContent); } else if (parsedContent instanceof String) { ServletResponseUtil.write(response, (String)parsedContent); } } }
调试场景:
比如当我们刚加载 liferay首页,因为上面有许多css资源文件,所以会自动触发这个调用,走入processFilter方法,而它会调用getDynamicContent()方法来获取jRuby解析Sass后的变成的普通css文件。这方法是我们这文章研究的重点。
getDynamicContent()的代码如下:
protected Object getDynamicContent( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { String requestURI = request.getRequestURI(); String requestPath = requestURI; String contextPath = request.getContextPath(); if (!contextPath.equals(StringPool.SLASH)) { requestPath = requestPath.substring(contextPath.length()); } String realPath = ServletContextUtil.getRealPath( _servletContext, requestPath); if (realPath == null) { return null; } realPath = StringUtil.replace( realPath, CharPool.BACK_SLASH, CharPool.SLASH); File file = new File(realPath); String cacheCommonFileName = getCacheFileName(request); File cacheContentTypeFile = new File( cacheCommonFileName + "_E_CONTENT_TYPE"); File cacheDataFile = new File(cacheCommonFileName + "_E_DATA"); if ((cacheDataFile.exists()) && (cacheDataFile.lastModified() >= file.lastModified())) { if (cacheContentTypeFile.exists()) { String contentType = FileUtil.read(cacheContentTypeFile); response.setContentType(contentType); } return cacheDataFile; } String dynamicContent = null; String content = null; try { if (realPath.endsWith(_CSS_EXTENSION) && file.exists()) { if (_log.isInfoEnabled()) { _log.info("Parsing SASS on CSS " + file); } content = FileUtil.read(file); dynamicContent = DynamicCSSUtil.parseSass( request, realPath, content); response.setContentType(ContentTypes.TEXT_CSS); FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS); } else if (realPath.endsWith(_JSP_EXTENSION) || !file.exists()) { if (_log.isInfoEnabled()) { _log.info("Parsing SASS on JSP or servlet " + realPath); } StringServletResponse stringResponse = new StringServletResponse(response); processFilter( DynamicCSSFilter.class, request, stringResponse, filterChain); CacheResponseUtil.setHeaders( response, stringResponse.getHeaders()); response.setContentType(stringResponse.getContentType()); content = stringResponse.getString(); dynamicContent = DynamicCSSUtil.parseSass( request, realPath, content); FileUtil.write( cacheContentTypeFile, stringResponse.getContentType()); } else { return null; } } catch (Exception e) { _log.error("Unable to parse SASS on CSS " + realPath, e); if (_log.isDebugEnabled()) { _log.debug(content); } response.setHeader( HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE); } if (dynamicContent != null) { FileUtil.write(cacheDataFile, dynamicContent); } else { dynamicContent = content; } return dynamicContent; }
我们附上调试信息:
从上述调试信息一目了然,主要是第5行获取当前请求的URI,从调试信息看,它的内容是:
html/portlet/login/css/main.css , 这个也正符合我们的猜想,因为当前请求的资源文件main.css符合CSS扩展名的模式,所以才被这个Dynamic CSS Filter所过滤到并且进入这个断点。
第16行获取这个资源文件的真实路径realPath(疑问1: 如何获取这个真实的path的? 答案在以后讨论中给出) ,这里给出的路径是
/app/Liferay/RI/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/webapps/ROOT/html/portlet/login/css/main.css
,这也正符合我们当初的设想,因为这个main.css我们的确是一年前把它手动复制到了该目录下。
然后第19行计算出这个文件对应的缓存base文件名,因为一个缓存文件总是由2部分组成,一个是内容类型文件,一个是数据文件,他们的各自名字都是由base名字加上指定后缀拼接而成。内容类型文件的名字是<cacheCommonFileName>_E_CONTENT_TYPE,而缓存数据文件的名字是<cacheCommonFileName>_E_DATA. (疑问2:如何计算得到这个缓存base文件名?答案也在后续讨论中给出) ,所以我们通过计算得到的缓存base文件名为:
/app/Liferay/RI/liferay-portal-6.1.0-ce-ga1/tomcat-7.0.23/temp/liferay/css/portal/623927847558055413
下面既然得到了缓存base文件名,并且依照后缀定则拼接了相应的内容类型文件名和数据文件名,那么下面的工作就是第20行和第21行在相应位置创建相应的File对象了。因为new File()按照我们对于java的语义,就是如果这个文件不存在,那么则创建新文件,如果存在,只File对象指向已知文件。
我们到服务器目录下看到了这个2个文件:
当缓存内容类型文件和缓存内容文件都固定下来后,下面就考虑到更新或者填入内容到这些文件了。
首先,从第35行开始,还是从原始的带有Sass的css文件入手:
if (realPath.endsWith(_CSS_EXTENSION) && file.exists()) { if (_log.isInfoEnabled()) { _log.info("Parsing SASS on CSS " + file); } content = FileUtil.read(file); dynamicContent = DynamicCSSUtil.parseSass( request, realPath, content); response.setContentType(ContentTypes.TEXT_CSS); FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS); }
它先判断原始文件是不是CSS扩展名的文件,如果是,那么就读取这个原始的css文件到一个字符串变量content中,见以下的调试信息:
然后调用DynamicCSSUtil的parseSass()方法吧这个带Sass的css文件解析成一个不带Sass的普通css文件,并且结果存放在dynamicContent变量中,比如上述content被解析后存放到的dynamicContent的值如下:
读者很容易看出这个新的样式文件是和原来不一样了,不仅仅是排版格式还有语法。
最后,把相应的内容写入到刚才最早的生成的内容类型文件和数据文件中。
源代码的第64行在内容类型文件(<cacheCommonFileName>_E_CONTENT_TYPE)中写入内容为 text/css:
FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
而第82行在数据文件中(<cacheCommonFileName>_E_DATA)写入刚才生成Sass解析后生成的普通css文件内容:
FileUtil.write(cacheContentTypeFile, stringResponse.getContentType()
(疑问3:这个写入过程细节是这样的呢?比如文件为空和文件中已经有内容各是如何处理的? 这个答案也在以后讨论中给出)
最后在第88行中返回的动态生成的普通css文件字符串。
现在我们返回到processFilter方法中,既然已经得到了Sass解析后生成的普通css字符串,所以最后就是把这个字符串返回到客户端,所以在processFilter()方法的行末:
ServletResponseUtil.write(response, (String)parsedContent);
这样我们访问页面时候就可以正确的看到和使用这里的样式了。
总结:
从非常宏观的角度,我们至少有以下几点收获:
(1)DynamicCssFilter的调用时机是在站点请求响应的资源文件的时候触发的。
(2)访问资源文件时,它会从原始含有Sass语法的css文件中获取原始内容,然后用jRuby引擎进行解析从而获得新的解析后的普通css文件。
(3)解析后的css文件总会最终被写入到缓存内容文件中,这个缓存数据文件的后缀总是_E_DATA,并且它总是对应一个内容类型文件,这个内容类型文件的格式总是_E_CONTENT_TYPE.
(4)解析后的文件会被服务器写到最终输出流中,从而你在浏览器中可以看到并且使用这个被解析后的普通css文件。
我们还留着几个疑点,会在接下来的文章中得到解决。